Files
orisys-sdk/src/orisys/tracker.py
T
2026-09-12 19:36:36 +08:00

389 lines
17 KiB
Python

import cv2
import numpy as np
import time
class Tracker:
def __init__(self,
adaptive,
cuda,
config_tracker=None,
lower_threshold=12.0,
upper_threshold=18.0,
loop_closure_interval=1,
# dis_preset='medium',
verbose=False):
# Check if OpenCV is built with CUDA support
# if cuda and not cv2.cuda.getCudaEnabledDeviceCount():
# # print("Warning: CUDA requested but OpenCV not built with CUDA support or no CUDA device found. Falling back to CPU.")
# cuda = False
self.cuda = cuda
self.verbose = verbose
self.__loop_closure_interval = loop_closure_interval
self.__frame_count = 0
dis_preset = config_tracker["dis_preset"]
loop_closure_interval = config_tracker["loop_closure_interval"]
self.__algorithm = config_tracker["algorithm"]
# Conditionally import torch only if RAFT algorithm is used
self.torch = None
if self.__algorithm == "raft":
import torch
self.torch = torch
if self.__algorithm == "raft" and (not self.cuda or not self.torch.cuda.is_available()):
self.__algorithm = "dis"
if self.verbose:
print("No CUDA available, falling back to OPENCV DIS optical flow")
if self.__algorithm == "raft":
### RAFT
self.__raft_device = "cuda"
print(f"RAFT device: {self.__raft_device}")
self.__raft_model = self.setup_simple_raft()
self.__raft_ref_frame = None
self.__raft_ref_tensor = None
elif self.__algorithm == "dis":
# DIS method - optimize for speed
if dis_preset == 'ultrafast':
preset = cv2.DISOPTICAL_FLOW_PRESET_ULTRAFAST
elif dis_preset == 'fast':
preset = cv2.DISOPTICAL_FLOW_PRESET_FAST
else:
preset = cv2.DISOPTICAL_FLOW_PRESET_MEDIUM
self.__tracking_inst = cv2.DISOpticalFlow_create(preset)
# Aggressive optimization for CPU
self.__tracking_inst.setFinestScale(config_tracker["dis_mode"]) # Skip finest pyramid level (2x faster)
# self.__tracking_inst.setUseSpatialPropagation(False)
# self.__tracking_inst.setUseMeanNormalization(False)
# self.__tracking_inst.setGradientDescentIterations(16) #16
# self.__tracking_inst.setPatchSize(12) # 8
# self.__tracking_inst.setPatchStride(5) # 3
# self.__tracking_inst.setVariationalRefinementIterations(5)#5
# else:
# # CUDA flow method with stream support
# self.__tracking_inst = cv2.cuda.FarnebackOpticalFlow_create()
# # Create CUDA stream for async operations
# self.__cuda_stream = cv2.cuda.Stream()
self.__fine_base_frame_queue = []
self.__fine_flow_queue = []
self.__prev_fine_flow = None
self.__adaptive = adaptive
self.__lower_threshold = config_tracker["lower_threshold"] # error lower than threshold will close the loop
self.__upper_threshold = config_tracker["upper_threshold"] # error greater than threshold will add new reference frame
def _prepare_valid_mask(self, valid_mask, shape, erode_iters=1):
if valid_mask is None:
return None
mask = np.asarray(valid_mask, dtype=np.uint8)
if mask.shape != shape:
mask = cv2.resize(mask, (shape[1], shape[0]), interpolation=cv2.INTER_NEAREST)
if erode_iters > 0:
kernel = np.ones((3, 3), dtype=np.uint8)
mask = cv2.erode(mask, kernel, iterations=erode_iters)
return mask.astype(bool)
def apply_valid_mask(self, flow, valid_mask, erode_iters=1):
"""Zero flow outside the valid UV domain (in place when safe).
Multiplying by a 0/1 mask is a single streaming pass, whereas
``flow.copy()`` followed by a boolean fancy-index assignment costs a
full copy plus a gather/scatter over the masked-out pixels.
Note: a masked-out *negative* component becomes ``-0.0`` rather than
``+0.0``. The two compare, sum and magnitude identically; only the raw
bit pattern of those zeros differs from the previous implementation.
"""
if flow is None or valid_mask is None:
return flow
mask = self._prepare_valid_mask(valid_mask, flow.shape[:2], erode_iters=erode_iters)
if not getattr(self, "_Tracker__adaptive", False):
# The non-adaptive path hands back the DIS state buffer, which feeds
# the next frame as its initial guess; never mutate it.
flow_out = flow.copy()
flow_out[~mask] = 0
return flow_out
if not flow.flags.writeable:
flow = flow.copy()
mask_f = mask.astype(np.float32, copy=False)[:, :, None]
np.multiply(flow, mask_f, out=flow)
return flow
def track(self, frame, valid_mask=None):
"""Main tracking function that dispatches to CUDA or CPU implementation"""
if self.__algorithm == "raft":
flow = self.track_raft(frame.copy())
elif self.__algorithm == "dis":
if frame.ndim == 3:
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
flow = self.track_cpu(frame.copy(), valid_mask=valid_mask)
else:
flow = None
return self.apply_valid_mask(flow, valid_mask)
def track_cpu(self, frame, valid_mask=None):
"""CPU-based optical flow tracking"""
frame_height, frame_width = frame.shape[:2]
if self.__adaptive:
if not self.__fine_base_frame_queue:
self.__fine_base_frame_queue.append(frame)
# Pre-compute meshgrid for remap operations
x = np.linspace(0, frame_width - 1, frame_width, dtype='float32')
y = np.linspace(0, frame_height - 1, frame_height, dtype='float32')
self.map = np.array(np.meshgrid(x, y)).transpose((1, 2, 0))
# Pre-allocate with correct dtype
self.__fine_flow_queue.append(
np.zeros((frame_height, frame_width, 2), dtype=np.float32))
self.__prev_fine_flow = np.zeros(
(frame_height, frame_width, 2), dtype=np.float32)
return self.__fine_flow_queue[-1]
else:
self.__frame_count += 1
# Only check loop closure periodically (expensive operation)
if self.__frame_count % self.__loop_closure_interval == 0:
self.__checkLoopClosure(frame, valid_mask=valid_mask)
# CPU optical flow computation
self.__prev_fine_flow = self.__tracking_inst.calc(
self.__fine_base_frame_queue[-1], frame,
self.__prev_fine_flow)
# Direct numpy addition is faster than cv2.add
final_flow = self.__fine_flow_queue[-1] + self.__prev_fine_flow
# Use fine flow for error estimation
error = self.__getForwardBackwardError(
self.__fine_base_frame_queue[-1], frame,
self.__prev_fine_flow, valid_mask=valid_mask)
if error > self.__upper_threshold:
self.__fine_base_frame_queue.append(frame)
self.__fine_flow_queue.append(final_flow)
self.__prev_fine_flow = np.zeros(
(frame_height, frame_width, 2), dtype=np.float32)
return final_flow
else:
if not self.__fine_base_frame_queue:
self.__fine_base_frame_queue.append(frame)
self.__prev_fine_flow = self.__tracking_inst.calc(
self.__fine_base_frame_queue[-1], frame,
self.__prev_fine_flow)
return self.__prev_fine_flow
# def track_2dto3d(self, flow):
# decompose_obj = nHHD(grid=(flow.shape[0], flow.shape[1]), spacings=(18, 18))
# flag,output = decompose_obj.get_force(flow, verbose=0)
# return output
def __getForwardBackwardError(self, I0, I1, flow, valid_mask=None):
# if self.cuda:
# flow_map = cv2.cuda.add(self.map, flow)
# map_x, map_y = cv2.cuda.split(flow_map)
# z = cv2.cuda.remap(I1,
# map_x,
# map_y,
# interpolation=cv2.INTER_CUBIC,
# borderMode=cv2.BORDER_REPLICATE)
# t = I0.clone()
# t = t.convertTo(rtype=cv2.CV_32FC1, dst=t)
# z = z.convertTo(rtype=cv2.CV_32FC1, dst=z)
# rev = cv2.cuda.absSum(cv2.cuda.subtract(
# t, z))[0] / t.size()[0] / t.size()[1]
# else:
# Optimized CPU path - avoid redundant type conversions
flow_map = self.map + flow
z = cv2.remap(I1,
flow_map[:, :, 0],
flow_map[:, :, 1],
interpolation=cv2.INTER_CUBIC, # INTER_LINEAR faster than INTER_CUBIC
borderMode=cv2.BORDER_REPLICATE)
# Use in-place operations when possible
if I0.dtype != np.float32:
diff = z.astype('float32') - I0.astype('float32')
else:
diff = z - I0
diff_abs = np.abs(diff)
mask = self._prepare_valid_mask(valid_mask, diff_abs.shape, erode_iters=0)
if mask is not None and np.any(mask):
rev = float(diff_abs[mask].mean())
else:
rev = float(np.mean(diff_abs))
return rev
def __checkLoopClosure(self, frame, valid_mask=None): # update (prune) the frame queue with loop closure detection
for i in range(len(self.__fine_base_frame_queue) - 2, -1, -1):
f = self.__tracking_inst.calc(
self.__fine_base_frame_queue[i], frame, None)
error = self.__getForwardBackwardError(
self.__fine_base_frame_queue[i], frame, f, valid_mask=valid_mask)
if error < self.__lower_threshold:
self.__fine_base_frame_queue = self.__fine_base_frame_queue[:i+1]
self.__fine_flow_queue = self.__fine_flow_queue[:i + 1]
return True
return False
def reset(self):
if self.__algorithm == "raft":
self.__raft_ref_frame = None
self.__raft_ref_tensor = None
elif self.__algorithm == "dis":
self.__fine_base_frame_queue = []
self.__fine_flow_queue = []
self.__prev_fine_flow = None
self.__frame_count = 0
def getFrameQueueLength(self):
return len(self.__fine_base_frame_queue)
### RAFT ###
def setup_simple_raft(self):
"""
Setup a simpler version using available models with performance optimizations
"""
import torchvision.transforms as T
from torchvision.models.optical_flow import raft_large
# Load model and move to GPU
model = raft_large(pretrained=True, progress=False).to(self.__raft_device)
model = model.eval()
if self.verbose:
print(f"RAFT model loaded on {self.__raft_device}")
return model
def _preprocess_frame_to_tensor(self, frame):
"""Efficiently convert numpy frame to RAFT-ready tensor"""
# Ensure contiguous memory for faster tensor conversion
if not frame.flags['C_CONTIGUOUS']:
frame = np.ascontiguousarray(frame)
# Upload to device as uint8 first to save bandwidth (4x less data than float32)
tensor = self.torch.from_numpy(frame).permute(2, 0, 1)
# Pin memory for faster CPU-GPU transfer (similar to DataLoader pin_memory=True)
if self.__raft_device == 'cuda':
# Pin memory before transferring to GPU (must be on CPU first)
if not tensor.is_pinned():
tensor = tensor.pin_memory()
tensor = tensor.to(self.__raft_device, non_blocking=True)
else:
tensor = tensor.to(self.__raft_device)
# Convert to float and normalize on device
tensor = tensor.float() / 255.0
# Normalize to [-1, 1]
tensor = (tensor - 0.5) * 2.0
# Add batch dimension
tensor = tensor.unsqueeze(0)
return tensor
def track_raft(self, frame, gpu = False):
# No need to copy if we just read
# frame is expected to be (H, W, 3) or (H, W)
if frame.ndim == 2:
frame = cv2.cvtColor(frame, cv2.COLOR_GRAY2RGB)
elif frame.shape[2] == 3:
# Assume BGR input from OpenCV
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
if self.__raft_ref_frame is None:
self.__raft_ref_frame = frame
self.__raft_ref_tensor = self._preprocess_frame_to_tensor(frame)
prev_tensor = self.__raft_ref_tensor
curr_tensor = self._preprocess_frame_to_tensor(frame)
if self.verbose:
t0 = time.perf_counter()
with self.torch.inference_mode():
if self.__raft_device == 'cuda':
with self.torch.amp.autocast('cuda'):
flow = self.__raft_model(prev_tensor, curr_tensor)
else:
flow = self.__raft_model(prev_tensor, curr_tensor)
predicted_flows = flow[-1]
if self.verbose:
t1 = time.perf_counter()
print(f"RAFT time: {t1 - t0}")
# Optimize tensor->numpy conversion: contiguous() ensures optimal memory layout
flow_np = predicted_flows[0].contiguous().cpu().numpy()
# Transpose to (H, W, 2) format expected by draw_arrows
flow_np = flow_np.transpose(1, 2, 0) # (2, H, W) -> (H, W, 2)
return flow_np
# def track_cuda(self, frame):
# """CUDA-accelerated optical flow tracking"""
# frame_height, frame_width = frame.shape[:2]
# frame = cv2.cuda_GpuMat(frame)
# if self.__adaptive:
# if not self.__fine_base_frame_queue:
# self.__fine_base_frame_queue.append(frame)
# # Pre-compute meshgrid for remap operations
# x = np.linspace(0, frame_width - 1, frame_width, dtype='float32')
# y = np.linspace(0, frame_height - 1, frame_height, dtype='float32')
# self.map = np.array(np.meshgrid(x, y)).transpose((1, 2, 0))
# cuMat = cv2.cuda_GpuMat()
# cuMat.upload(self.map)
# self.map = cuMat
# self.__fine_flow_queue.append(
# cv2.cuda_GpuMat(frame.size(), cv2.CV_32FC2, (0.0, 0.0)))
# self.__prev_fine_flow = cv2.cuda_GpuMat(
# frame.size(), cv2.CV_32FC2, (0.0, 0.0))
# return self.__fine_flow_queue[-1]
# else:
# self.__frame_count += 1
# # Only check loop closure periodically (expensive operation)
# if self.__frame_count % self.__loop_closure_interval == 0:
# self.__checkLoopClosure(frame)
# # Use CUDA stream for async optical flow computation
# self.__prev_fine_flow = self.__tracking_inst.calc(
# self.__fine_base_frame_queue[-1], frame,
# self.__prev_fine_flow, self.__cuda_stream)
# final_flow = cv2.cuda.add(self.__fine_flow_queue[-1],
# self.__prev_fine_flow,
# stream=self.__cuda_stream)
# # Use fine flow for error estimation
# error = self.__getForwardBackwardError(
# self.__fine_base_frame_queue[-1], frame,
# self.__prev_fine_flow)
# if error > self.__upper_threshold:
# self.__fine_base_frame_queue.append(frame)
# self.__fine_flow_queue.append(final_flow)
# self.__prev_fine_flow = None
# return final_flow
# else:
# if not self.__fine_base_frame_queue:
# self.__fine_base_frame_queue.append(frame)
# self.__prev_fine_flow = self.__tracking_inst.calc(
# self.__fine_base_frame_queue[-1], frame,
# self.__prev_fine_flow, self.__cuda_stream)
# return self.__prev_fine_flow