function rearrange for maintainability
This commit is contained in:
+402
-440
@@ -1,87 +1,41 @@
|
|||||||
|
"""
|
||||||
|
Sensor - Core sensor class for optical tactile sensing.
|
||||||
|
|
||||||
|
This module provides the Sensor class for capturing, processing, and analyzing
|
||||||
|
optical tactile sensor data with support for stitching, optical flow tracking,
|
||||||
|
and Helmholtz-Hodge decomposition.
|
||||||
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import cv2
|
import cv2
|
||||||
from .Stitcher import Stitcher
|
import time
|
||||||
from .tracker import Tracker
|
|
||||||
from .nHHD import *
|
|
||||||
import time
|
|
||||||
from scipy.interpolate import griddata
|
|
||||||
from enum import Enum, auto
|
from enum import Enum, auto
|
||||||
from typing import Optional, Tuple, Union, Any
|
from typing import Tuple
|
||||||
from .getDensity import getFilteredDensity
|
|
||||||
from . import configs
|
|
||||||
from threading import Thread, Event
|
from threading import Thread, Event
|
||||||
from queue import Queue, Empty
|
from queue import Queue, Empty
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
|
||||||
|
from .Stitcher import Stitcher
|
||||||
|
from .tracker import Tracker
|
||||||
|
from .nHHD import nHHD
|
||||||
|
from .getDensity import getFilteredDensity
|
||||||
|
from . import configs
|
||||||
|
|
||||||
|
|
||||||
class Sensor:
|
class Sensor:
|
||||||
"""
|
"""
|
||||||
Sensor helper that can decompose optical flow via Fourier methods.
|
Optical tactile sensor interface with deformation analysis.
|
||||||
Stores the decomposed components and global norms as attributes after compute().
|
|
||||||
|
Provides image capture, stitching, optical flow tracking, and force decomposition.
|
||||||
|
Supports both synchronous and asynchronous (pipelined) processing modes.
|
||||||
"""
|
"""
|
||||||
def __init__(self, vid_src, isstitch=False, verbose=True, cuda=True, config_name="prototype", cal_path=None,override=False, box=None):
|
|
||||||
"""
|
# =========================================================================
|
||||||
Initialize Sensor with video source and configuration.
|
# Info Enum (for read_info API)
|
||||||
|
# =========================================================================
|
||||||
Args:
|
|
||||||
vid_src: Video source (int for camera ID, str for file path)
|
|
||||||
isstitch: Whether to use stitching mode for multi-tile sensor (default: False)
|
|
||||||
verbose: Whether to print initialization logs (default: True)
|
|
||||||
cuda: Whether to use CUDA acceleration for optical flow & decomposition (default: True)
|
|
||||||
config_name: Name of the mdoel (default: "prototype")
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
import cupy as cp
|
|
||||||
import cupyx.scipy.signal as cp_signal
|
|
||||||
self.CUPY_AVAILABLE = True
|
|
||||||
except ImportError:
|
|
||||||
self.CUPY_AVAILABLE = False
|
|
||||||
|
|
||||||
# Initialize settings
|
|
||||||
if vid_src is None:
|
|
||||||
raise Exception("User must specify a video source.")
|
|
||||||
|
|
||||||
self.vid_src, self.isstitch, self.cuda, self.verbose, self.config_name, self.cal_path = vid_src, isstitch, cuda, verbose, config_name, cal_path
|
|
||||||
|
|
||||||
# Initialize state variables
|
|
||||||
self.stitch_valid = self.imgupdate = self.vupdate = False
|
|
||||||
self.stitch_calibration = []
|
|
||||||
self.img = self.flow = self.normal = self.shear = self.force = self.depth = self.n_boxes = self.img_filtered = None
|
|
||||||
self.fnormal=self.fshearx=self.fsheary=self.ftor=0.0
|
|
||||||
|
|
||||||
# Initialize Sensor Configuration
|
|
||||||
self.get_config()
|
|
||||||
|
|
||||||
# Initialize components
|
|
||||||
self.__stitcher = Stitcher(isstitch=isstitch, index=self.config_index)
|
|
||||||
self.tracker = Tracker(adaptive=True, cuda=cuda)
|
|
||||||
self.hhd = None
|
|
||||||
|
|
||||||
# Setup video capture
|
|
||||||
self.__open_video_source(override, box)
|
|
||||||
|
|
||||||
# Logging state
|
|
||||||
self.last_log_time = time.perf_counter()
|
|
||||||
self.fps = 0
|
|
||||||
|
|
||||||
# Async pipeline state (3-stage: Capture → Flow → HHD)
|
|
||||||
self._async_enabled = False
|
|
||||||
self._capture_thread = None
|
|
||||||
self._flow_thread = None
|
|
||||||
self._compute_thread = None
|
|
||||||
self._capture_queue = None
|
|
||||||
self._flow_queue = None
|
|
||||||
self._result_queue = None
|
|
||||||
self._stop_event = None
|
|
||||||
|
|
||||||
if self.verbose:
|
|
||||||
print(f" Video source: {vid_src}")
|
|
||||||
print(f" Stitch mode: {self.stitch_valid}")
|
|
||||||
print(f" CUDA acceleration: {self.cuda}")
|
|
||||||
print(f" Cupy available: {self.CUPY_AVAILABLE}")
|
|
||||||
print(f" OpenCV CUDA devices: {cv2.cuda.getCudaEnabledDeviceCount()}")
|
|
||||||
|
|
||||||
class info(Enum):
|
class info(Enum):
|
||||||
|
"""Data types available through read_info()."""
|
||||||
VRAW = auto()
|
VRAW = auto()
|
||||||
VNORMAL = auto()
|
VNORMAL = auto()
|
||||||
VSHEAR = auto()
|
VSHEAR = auto()
|
||||||
@@ -92,234 +46,273 @@ class Sensor:
|
|||||||
FSHEARY = auto()
|
FSHEARY = auto()
|
||||||
DEPTH = auto()
|
DEPTH = auto()
|
||||||
|
|
||||||
def __open_video_source(self, override, box):
|
# =========================================================================
|
||||||
|
# Initialization
|
||||||
|
# =========================================================================
|
||||||
|
|
||||||
|
def __init__(self, vid_src, isstitch=False, verbose=True, cuda=True,
|
||||||
|
config_name="prototype", cal_path=None, override=False, box=None):
|
||||||
|
"""
|
||||||
|
Initialize Sensor with video source and configuration.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
vid_src: Video source (int for camera ID, str for file path)
|
||||||
|
isstitch: Enable stitching mode for multi-tile sensor (default: False)
|
||||||
|
verbose: Print initialization logs (default: True)
|
||||||
|
cuda: Enable CUDA acceleration (default: True)
|
||||||
|
config_name: Configuration name (default: "prototype")
|
||||||
|
cal_path: Path to calibration file (default: None)
|
||||||
|
override: Skip auto-detection, use provided box (default: False)
|
||||||
|
box: Manual bounding boxes when override=True (default: None)
|
||||||
|
"""
|
||||||
|
# Check CUDA availability
|
||||||
|
try:
|
||||||
|
import cupy as cp
|
||||||
|
self.CUPY_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
self.CUPY_AVAILABLE = False
|
||||||
|
|
||||||
|
if vid_src is None:
|
||||||
|
raise Exception("User must specify a video source.")
|
||||||
|
|
||||||
|
# Store settings
|
||||||
|
self.vid_src = vid_src
|
||||||
|
self.isstitch = isstitch
|
||||||
|
self.cuda = cuda
|
||||||
|
self.verbose = verbose
|
||||||
|
self.config_name = config_name
|
||||||
|
self.cal_path = cal_path
|
||||||
|
|
||||||
|
# State variables
|
||||||
|
self.stitch_valid = False
|
||||||
|
self.imgupdate = False
|
||||||
|
self.vupdate = False
|
||||||
|
self.stitch_calibration = []
|
||||||
|
|
||||||
|
# Data variables
|
||||||
|
self.img = None
|
||||||
|
self.img_filtered = None
|
||||||
|
self.flow = None
|
||||||
|
self.normal = None
|
||||||
|
self.shear = None
|
||||||
|
self.depth = None
|
||||||
|
self.n_boxes = None
|
||||||
|
self.fnormal = self.fshearx = self.fsheary = self.ftor = 0.0
|
||||||
|
|
||||||
|
# Timing
|
||||||
|
self.last_log_time = time.perf_counter()
|
||||||
|
self.fps = 0
|
||||||
|
|
||||||
|
# Async pipeline state
|
||||||
|
self._async_enabled = False
|
||||||
|
self._capture_thread = None
|
||||||
|
self._flow_thread = None
|
||||||
|
self._compute_thread = None
|
||||||
|
self._capture_queue = None
|
||||||
|
self._flow_queue = None
|
||||||
|
self._result_queue = None
|
||||||
|
self._stop_event = None
|
||||||
|
|
||||||
|
# Load configuration
|
||||||
|
self._load_config()
|
||||||
|
|
||||||
|
# Initialize components
|
||||||
|
self._stitcher = Stitcher(isstitch=isstitch, index=self.config_index)
|
||||||
|
self.tracker = Tracker(adaptive=True, cuda=cuda)
|
||||||
|
self.hhd = None
|
||||||
|
|
||||||
# Setup video capture
|
# Setup video capture
|
||||||
self.cap = cv2.VideoCapture(self.vid_src)
|
self._open_video_source(override, box)
|
||||||
self.cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter.fourcc('M', 'J', 'P', 'G'))
|
|
||||||
self.cap.set(cv2.CAP_PROP_FPS, self.config_fps)
|
|
||||||
self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, self.config_resolution_raw[0])
|
|
||||||
self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, self.config_resolution_raw[1])
|
|
||||||
|
|
||||||
# Wait for first valid frame
|
|
||||||
if self.verbose:
|
|
||||||
print("Sensor connecting to video source...")
|
|
||||||
frame = None
|
|
||||||
if self.cap.isOpened():
|
|
||||||
while frame is None:
|
|
||||||
ret, frame = self.cap.read()
|
|
||||||
if not ret:
|
|
||||||
time.sleep(1)
|
|
||||||
else:
|
|
||||||
raise Exception("Failed to open video source.")
|
|
||||||
|
|
||||||
if override:
|
|
||||||
if box is not None:
|
|
||||||
self.__stitcher.isstitch = self.isstitch = False
|
|
||||||
self.__stitcher.boxes = box
|
|
||||||
else:
|
|
||||||
print("Override is True but no box provided.")
|
|
||||||
else:
|
|
||||||
# Initialize and calibrate
|
|
||||||
self.__find_stitch_boxes(frame, show=False)
|
|
||||||
self.__get_tiles(frame)
|
|
||||||
self.stitch_cal()
|
|
||||||
|
|
||||||
def get_config(self):
|
|
||||||
"""
|
|
||||||
Load and return sensor configuration from JSON file.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: Dictionary containing sensor configuration
|
|
||||||
"""
|
|
||||||
self.config = configs.load_config(self.config_name)
|
|
||||||
self.config_resolution_raw = self.config["camera"]["resolution_raw"]
|
|
||||||
self.config_fps = self.config["camera"]["fps"]
|
|
||||||
self.config_units = self.config["camera"]["units"]
|
|
||||||
self.config_resolution_stitch = self.config["camera"]["resolution_stitch"]
|
|
||||||
self.config_index = self.config["camera"]["index"]
|
|
||||||
self.config_pos_default = self.config["camera"]["pos_default"]
|
|
||||||
|
|
||||||
if self.verbose:
|
if self.verbose:
|
||||||
print("Config loaded: ", str(self.config_name))
|
print(f" Video source: {vid_src}")
|
||||||
print(" Resolution raw: ", str(self.config_resolution_raw))
|
print(f" Stitch mode: {self.stitch_valid}")
|
||||||
print(" Camera FPS: ", str(self.config_fps))
|
print(f" CUDA acceleration: {self.cuda}")
|
||||||
print(" Number of VisionUnits: ", str(self.config_units))
|
print(f" CuPy available: {self.CUPY_AVAILABLE}")
|
||||||
print(" Resolution after stitch: ", str(self.config_resolution_stitch))
|
print(f" OpenCV CUDA devices: {cv2.cuda.getCudaEnabledDeviceCount()}")
|
||||||
print(" Index for stitching: ", str(self.config_index))
|
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# Public API - Core Methods (Most Frequently Used)
|
||||||
|
# =========================================================================
|
||||||
|
|
||||||
def get_img(self, log_timing=False):
|
def get_img(self, log_timing=False):
|
||||||
"""
|
"""
|
||||||
Capture frame from video source, convert to grayscale, and perform stitching.
|
Capture and preprocess a frame.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
np.ndarray: Stitched grayscale image (contiguous array) or None if capture fails
|
np.ndarray: Processed grayscale image
|
||||||
"""
|
"""
|
||||||
ret, frame = self.cap.read()
|
ret, frame = self.cap.read()
|
||||||
if not ret:
|
if not ret:
|
||||||
print("Failed to get_img from video source.")
|
print("Failed to get_img from video source.")
|
||||||
exit(1)
|
return None
|
||||||
|
|
||||||
if log_timing:
|
if log_timing:
|
||||||
timings = {}
|
timings = {}
|
||||||
t_start = time.perf_counter()
|
t_start = time.perf_counter()
|
||||||
|
|
||||||
if log_timing:
|
# Convert to grayscale
|
||||||
t0 = time.perf_counter()
|
if log_timing: t0 = time.perf_counter()
|
||||||
gray_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
|
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
|
||||||
if log_timing:
|
if log_timing: timings['cvt_color'] = (time.perf_counter() - t0) * 1000
|
||||||
timings['cvt_color'] = (time.perf_counter() - t0) * 1000
|
|
||||||
|
|
||||||
if log_timing:
|
# Extract and stitch tiles
|
||||||
t0 = time.perf_counter()
|
if log_timing: t0 = time.perf_counter()
|
||||||
self.__stitcher.get_stitch_unit(gray_frame)
|
self._stitcher.get_stitch_unit(gray)
|
||||||
self.__get_tiles(gray_frame)
|
self._extract_tiles(gray)
|
||||||
if log_timing:
|
if log_timing: timings['get_tiles'] = (time.perf_counter() - t0) * 1000
|
||||||
timings['get_tiles'] = (time.perf_counter() - t0) * 1000
|
|
||||||
|
|
||||||
# Step 1: Optical flow tracking
|
if log_timing: t0 = time.perf_counter()
|
||||||
if log_timing:
|
img = np.ascontiguousarray(self._stitcher.stitch_rt_optimized())
|
||||||
t0 = time.perf_counter()
|
if log_timing: timings['stitch'] = (time.perf_counter() - t0) * 1000
|
||||||
img = np.ascontiguousarray(self.__stitcher.stitch_rt_optimized())
|
|
||||||
|
|
||||||
if log_timing:
|
|
||||||
timings['stitch'] = (time.perf_counter() - t0) * 1000
|
|
||||||
|
|
||||||
img_resized = cv2.resize(img, (self.config_resolution_stitch[0], self.config_resolution_stitch[1]), interpolation=cv2.INTER_CUBIC)
|
# Resize
|
||||||
|
img = cv2.resize(img, tuple(self.config_resolution_stitch), interpolation=cv2.INTER_CUBIC)
|
||||||
|
|
||||||
# Apply temporal noise reduction using exponential moving average
|
# Temporal filtering (noise reduction)
|
||||||
# Blend current frame with previous filtered frame to reduce temporal noise
|
alpha = 0.7
|
||||||
alpha = 0.7 # Weight for current frame (0.7 = 70% current, 30% previous)
|
|
||||||
if self.img_filtered is None:
|
if self.img_filtered is None:
|
||||||
# First frame: no previous frame to blend with
|
self.img_filtered = img.astype(np.float32)
|
||||||
self.img_filtered = img_resized.astype(np.float32)
|
|
||||||
else:
|
else:
|
||||||
# Temporal filtering: blend current frame with previous filtered frame
|
self.img_filtered = alpha * img + (1 - alpha) * self.img_filtered
|
||||||
self.img_filtered = alpha * img_resized.astype(np.float32) + (1 - alpha) * self.img_filtered
|
|
||||||
|
|
||||||
# Convert back to uint8
|
|
||||||
self.img = self.img_filtered.astype(np.uint8)
|
self.img = self.img_filtered.astype(np.uint8)
|
||||||
self.imgupdate = True
|
self.imgupdate = True
|
||||||
|
|
||||||
if log_timing:
|
if log_timing:
|
||||||
timings['get_img'] = (time.perf_counter() - t_start) * 1000
|
timings['total'] = (time.perf_counter() - t_start) * 1000
|
||||||
print("\n[get_img timing]")
|
self._print_timings("get_img", timings)
|
||||||
print(f" cvt_color: {timings['cvt_color']:7.2f} ms")
|
|
||||||
print(f" get_tiles: {timings['get_tiles']:7.2f} ms")
|
|
||||||
print(f" stitch: {timings['stitch']:7.2f} ms")
|
|
||||||
print(f" ─────────────────────────")
|
|
||||||
print(f" TOTAL: {timings['get_img']:7.2f} ms ({1000/timings['get_img']:.1f} FPS)")
|
|
||||||
|
|
||||||
# Store timings for external access
|
|
||||||
self._last_timings = timings
|
|
||||||
return self.img
|
return self.img
|
||||||
|
|
||||||
def compute_deformation(self, log_timing=False):
|
def compute_deformation(self, log_timing=False):
|
||||||
"""
|
"""
|
||||||
Track optical flow and decompose into force field.
|
Compute optical flow and decompose into force components.
|
||||||
|
|
||||||
Args:
|
|
||||||
log_timing: If True, print timing information for each computation step (default: False)
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
tuple: (flow, force) where:
|
np.ndarray: Optical flow field
|
||||||
- flow: Optical flow field (2D vector field)
|
|
||||||
- force: Decomposed force components from nHHD, ([normal, shear_x, shear_y, torque]). Note: torque is experimental.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if log_timing:
|
if log_timing:
|
||||||
timings = {}
|
timings = {}
|
||||||
t_start = time.perf_counter()
|
t_start = time.perf_counter()
|
||||||
|
|
||||||
# Step 1: Optical flow tracking
|
# Optical flow
|
||||||
if log_timing:
|
if log_timing: t0 = time.perf_counter()
|
||||||
t0 = time.perf_counter()
|
|
||||||
|
|
||||||
flow = self.tracker.track(self.img)
|
flow = self.tracker.track(self.img)
|
||||||
|
|
||||||
if log_timing:
|
|
||||||
timings['optical_flow'] = (time.perf_counter() - t0) * 1000
|
|
||||||
|
|
||||||
if hasattr(flow, 'download'):
|
if hasattr(flow, 'download'):
|
||||||
flow = flow.download()
|
flow = flow.download()
|
||||||
|
if log_timing: timings['optical_flow'] = (time.perf_counter() - t0) * 1000
|
||||||
|
|
||||||
|
# Initialize HHD solver
|
||||||
if self.hhd is None:
|
if self.hhd is None:
|
||||||
self.hhd = nHHD(grid=(flow.shape[0], flow.shape[1]), spacings=(18, 18))
|
self.hhd = nHHD(grid=flow.shape[:2], spacings=(18, 18))
|
||||||
|
|
||||||
if log_timing:
|
# Force computation
|
||||||
t0 = time.perf_counter()
|
if log_timing: t0 = time.perf_counter()
|
||||||
# Use GPU-accelerated force computation if CUDA is enabled
|
|
||||||
if self.cuda and self.CUPY_AVAILABLE:
|
if self.cuda and self.CUPY_AVAILABLE:
|
||||||
# Use the most optimized GPU method with custom kernel
|
_, forces = self.hhd.get_force_gpu_v2(flow)
|
||||||
flag, [self.fnormal,self.fshearx,self.fsheary,self.ftor] = self.hhd.get_force_gpu_v2(flow)
|
|
||||||
else:
|
else:
|
||||||
flag, [self.fnormal,self.fshearx,self.fsheary,self.ftor] = self.hhd.get_force(flow)
|
_, forces = self.hhd.get_force(flow)
|
||||||
# self.hhd.decompose(flow)
|
self.fnormal, self.fshearx, self.fsheary, self.ftor = forces
|
||||||
if log_timing:
|
if log_timing: timings['hhd_decompose'] = (time.perf_counter() - t0) * 1000
|
||||||
timings['hhd_decompose'] = (time.perf_counter() - t0) * 1000
|
|
||||||
|
|
||||||
self.imgupdate = False
|
|
||||||
self.vupdate = True
|
|
||||||
self.flow = flow
|
|
||||||
|
|
||||||
if log_timing:
|
|
||||||
t0 = time.perf_counter()
|
|
||||||
|
|
||||||
|
# Get components
|
||||||
self.normal = self.hhd.get_normal()
|
self.normal = self.hhd.get_normal()
|
||||||
self.shear = self.hhd.get_shear()
|
self.shear = self.hhd.get_shear()
|
||||||
|
|
||||||
# Step 8: Compute depth/density
|
|
||||||
if log_timing:
|
|
||||||
t0 = time.perf_counter()
|
|
||||||
|
|
||||||
self.depth = getFilteredDensity(self.flow, use_cuda=self.cuda)
|
# Depth computation
|
||||||
|
if log_timing: t0 = time.perf_counter()
|
||||||
|
self.depth = getFilteredDensity(flow, use_cuda=self.cuda)
|
||||||
|
if log_timing: timings['get_density'] = (time.perf_counter() - t0) * 1000
|
||||||
|
|
||||||
if log_timing:
|
# Update state
|
||||||
timings['get_density'] = (time.perf_counter() - t0) * 1000
|
self.flow = flow
|
||||||
|
self.imgupdate = False
|
||||||
|
self.vupdate = True
|
||||||
|
|
||||||
# Calculate FPS
|
# FPS calculation
|
||||||
self.fps = 1/((time.perf_counter() - self.last_log_time))
|
self.fps = 1.0 / (time.perf_counter() - self.last_log_time)
|
||||||
self.last_log_time = time.perf_counter()
|
self.last_log_time = time.perf_counter()
|
||||||
|
|
||||||
# Print timing summary
|
|
||||||
if log_timing:
|
if log_timing:
|
||||||
timings['total'] = (time.perf_counter() - t_start) * 1000
|
timings['total'] = (time.perf_counter() - t_start) * 1000
|
||||||
print("\n[compute_deformation timing]")
|
self._print_timings("compute_deformation", timings)
|
||||||
print(f" optical_flow: {timings['optical_flow']:7.2f} ms")
|
|
||||||
print(f" hhd_decompose: {timings['hhd_decompose']:7.2f} ms")
|
|
||||||
print(f" get_density: {timings['get_density']:7.2f} ms")
|
|
||||||
print(f" ─────────────────────────")
|
|
||||||
print(f" TOTAL: {timings['total']:7.2f} ms ({1000/timings['total']:.1f} FPS)")
|
|
||||||
|
|
||||||
# Store timings for external access
|
|
||||||
self._last_timings = timings
|
|
||||||
|
|
||||||
return self.flow
|
return self.flow
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
"""
|
||||||
|
Convenience method: get_img + compute_deformation in one call.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: All sensor data
|
||||||
|
"""
|
||||||
|
self.get_img()
|
||||||
|
self.compute_deformation()
|
||||||
|
return {
|
||||||
|
'img': self.img, 'flow': self.flow,
|
||||||
|
'normal': self.normal, 'shear': self.shear, 'depth': self.depth,
|
||||||
|
'fps': self.fps, 'fnormal': self.fnormal,
|
||||||
|
'fshearx': self.fshearx, 'fsheary': self.fsheary, 'ftor': self.ftor,
|
||||||
|
}
|
||||||
|
|
||||||
|
def read_info(self, *args) -> Tuple:
|
||||||
|
"""
|
||||||
|
Get sensor data by info type.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
*args: info enum values (e.g., sensor.info.FPS, sensor.info.FNORMAL)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple: Requested data in order
|
||||||
|
"""
|
||||||
|
if not self.vupdate:
|
||||||
|
print("Warning: Call compute_deformation() first.")
|
||||||
|
|
||||||
|
mapping = {
|
||||||
|
self.info.VRAW: lambda: self.flow if self.flow is not None else np.array([]),
|
||||||
|
self.info.VNORMAL: lambda: self.normal if self.normal is not None else np.array([]),
|
||||||
|
self.info.VSHEAR: lambda: self.shear if self.shear is not None else np.array([]),
|
||||||
|
self.info.FNORMAL: lambda: self.fnormal,
|
||||||
|
self.info.FSHEARX: lambda: self.fshearx,
|
||||||
|
self.info.FSHEARY: lambda: self.fsheary,
|
||||||
|
self.info.FPS: lambda: self.fps,
|
||||||
|
self.info.TIMESTAMP: lambda: time.strftime('%H:%M:%S'),
|
||||||
|
self.info.DEPTH: lambda: self.depth if self.depth is not None else np.array([]),
|
||||||
|
}
|
||||||
|
|
||||||
|
return tuple(mapping.get(arg, lambda: None)() for arg in args if isinstance(arg, self.info))
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
"""Reset optical flow tracker."""
|
||||||
|
self.tracker.reset()
|
||||||
|
|
||||||
|
def disconnect(self):
|
||||||
|
"""Release resources and stop async threads."""
|
||||||
|
if self._async_enabled:
|
||||||
|
self.disable_async()
|
||||||
|
if self.cap.isOpened():
|
||||||
|
self.cap.release()
|
||||||
|
if self.verbose:
|
||||||
|
print("Sensor disconnected.")
|
||||||
|
cv2.destroyAllWindows()
|
||||||
|
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
# Async/Pipelined Processing API (3-stage: Capture → Flow → HHD)
|
# Public API - Async Pipeline
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
|
|
||||||
def _queue_put(self, q, item):
|
|
||||||
"""Helper to put item in queue, dropping old if full."""
|
|
||||||
if q.full():
|
|
||||||
try: q.get_nowait()
|
|
||||||
except Empty: pass
|
|
||||||
q.put(item)
|
|
||||||
|
|
||||||
def enable_async(self, queue_size=2):
|
def enable_async(self, queue_size=2):
|
||||||
"""
|
"""
|
||||||
Enable 3-stage async pipeline for maximum throughput.
|
Enable 3-stage async pipeline: Capture → Flow → HHD.
|
||||||
|
|
||||||
Pipeline: [Capture] → [Optical Flow] → [HHD Compute]
|
Each stage runs in parallel for maximum throughput.
|
||||||
Each stage runs in parallel, overlapping all operations.
|
|
||||||
"""
|
"""
|
||||||
if self._async_enabled:
|
if self._async_enabled:
|
||||||
return
|
return
|
||||||
|
|
||||||
self._capture_queue = Queue(maxsize=queue_size) # img
|
self._capture_queue = Queue(maxsize=queue_size)
|
||||||
self._flow_queue = Queue(maxsize=queue_size) # (img, flow)
|
self._flow_queue = Queue(maxsize=queue_size)
|
||||||
self._result_queue = Queue(maxsize=queue_size) # final result
|
self._result_queue = Queue(maxsize=queue_size)
|
||||||
self._stop_event = Event()
|
self._stop_event = Event()
|
||||||
self._async_enabled = True
|
self._async_enabled = True
|
||||||
|
|
||||||
@@ -332,8 +325,169 @@ class Sensor:
|
|||||||
self._compute_thread.start()
|
self._compute_thread.start()
|
||||||
|
|
||||||
if self.verbose:
|
if self.verbose:
|
||||||
print("Async 3-stage pipeline enabled: Capture → Flow → HHD")
|
print("Async pipeline enabled: Capture → Flow → HHD")
|
||||||
|
|
||||||
|
def get_async(self, timeout=0.05):
|
||||||
|
"""
|
||||||
|
Get latest result from async pipeline (non-blocking).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict or None: Sensor data if available
|
||||||
|
"""
|
||||||
|
if not self._async_enabled:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
result = self._result_queue.get(timeout=timeout)
|
||||||
|
t = time.perf_counter()
|
||||||
|
result['fps'] = 1.0 / (t - self.last_log_time) if self.last_log_time else 0
|
||||||
|
self.last_log_time = t
|
||||||
|
|
||||||
|
# Sync instance variables
|
||||||
|
for k in ['img', 'flow', 'normal', 'shear', 'depth', 'fnormal', 'fshearx', 'fsheary', 'ftor', 'fps']:
|
||||||
|
setattr(self, k, result.get(k))
|
||||||
|
self.vupdate = True
|
||||||
|
return result
|
||||||
|
except Empty:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def disable_async(self):
|
||||||
|
"""Stop async pipeline."""
|
||||||
|
if not self._async_enabled:
|
||||||
|
return
|
||||||
|
self._stop_event.set()
|
||||||
|
for t in [self._capture_thread, self._flow_thread, self._compute_thread]:
|
||||||
|
if t: t.join(timeout=2.0)
|
||||||
|
self._async_enabled = False
|
||||||
|
self._capture_thread = self._flow_thread = self._compute_thread = None
|
||||||
|
self._capture_queue = self._flow_queue = self._result_queue = self._stop_event = None
|
||||||
|
if self.verbose:
|
||||||
|
print("Async pipeline disabled")
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# Public API - Configuration & Calibration
|
||||||
|
# =========================================================================
|
||||||
|
|
||||||
|
def get_config(self):
|
||||||
|
"""Return current configuration dict."""
|
||||||
|
return self.config
|
||||||
|
|
||||||
|
def stitch_cal(self):
|
||||||
|
"""
|
||||||
|
Load or create stitch calibration.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.ndarray: Stitched calibration image
|
||||||
|
"""
|
||||||
|
if self.cal_path is None:
|
||||||
|
self.cal_path = "./config/cal.npy"
|
||||||
|
|
||||||
|
if os.path.exists(self.cal_path):
|
||||||
|
print("Loading calibration from", self.cal_path)
|
||||||
|
self.stitch_calibration = np.load(self.cal_path, allow_pickle=True)
|
||||||
|
self._stitcher.stitch_calibration = self.stitch_calibration
|
||||||
|
return self._stitcher.stitch_rt_optimized()
|
||||||
|
else:
|
||||||
|
print("Creating new calibration...")
|
||||||
|
img, self.stitch_calibration = self._stitcher.stitch_cal()
|
||||||
|
os.makedirs("./config", exist_ok=True)
|
||||||
|
np.save(self.cal_path, self.stitch_calibration)
|
||||||
|
print("Calibration saved to", self.cal_path)
|
||||||
|
return img
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# Private - Initialization Helpers
|
||||||
|
# =========================================================================
|
||||||
|
|
||||||
|
def _load_config(self):
|
||||||
|
"""Load sensor configuration from JSON file."""
|
||||||
|
self.config = configs.load_config(self.config_name)
|
||||||
|
cam = self.config["camera"]
|
||||||
|
self.config_resolution_raw = cam["resolution_raw"]
|
||||||
|
self.config_fps = cam["fps"]
|
||||||
|
self.config_units = cam["units"]
|
||||||
|
self.config_resolution_stitch = cam["resolution_stitch"]
|
||||||
|
self.config_index = cam["index"]
|
||||||
|
self.config_pos_default = cam["pos_default"]
|
||||||
|
|
||||||
|
if self.verbose:
|
||||||
|
print(f"Config: {self.config_name}")
|
||||||
|
print(f" Resolution: {self.config_resolution_raw} → {self.config_resolution_stitch}")
|
||||||
|
print(f" FPS: {self.config_fps}, Units: {self.config_units}")
|
||||||
|
|
||||||
|
def _open_video_source(self, override, box):
|
||||||
|
"""Initialize video capture and calibration."""
|
||||||
|
self.cap = cv2.VideoCapture(self.vid_src)
|
||||||
|
self.cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter.fourcc('M', 'J', 'P', 'G'))
|
||||||
|
self.cap.set(cv2.CAP_PROP_FPS, self.config_fps)
|
||||||
|
self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, self.config_resolution_raw[0])
|
||||||
|
self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, self.config_resolution_raw[1])
|
||||||
|
|
||||||
|
if self.verbose:
|
||||||
|
print("Connecting to video source...")
|
||||||
|
|
||||||
|
if not self.cap.isOpened():
|
||||||
|
raise Exception("Failed to open video source.")
|
||||||
|
|
||||||
|
# Wait for first valid frame
|
||||||
|
frame = None
|
||||||
|
while frame is None:
|
||||||
|
ret, frame = self.cap.read()
|
||||||
|
if not ret:
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
if override:
|
||||||
|
if box is not None:
|
||||||
|
self._stitcher.isstitch = self.isstitch = False
|
||||||
|
self._stitcher.boxes = box
|
||||||
|
else:
|
||||||
|
print("Override=True but no box provided.")
|
||||||
|
else:
|
||||||
|
self._find_stitch_boxes(frame)
|
||||||
|
self._extract_tiles(frame)
|
||||||
|
self.stitch_cal()
|
||||||
|
|
||||||
|
def _find_stitch_boxes(self, img_src, show=False):
|
||||||
|
"""Detect vision tile boxes with camera stabilization."""
|
||||||
|
n_boxes, v_mean0, timeout = 0, 0, 5
|
||||||
|
start_time = cv2.getTickCount()
|
||||||
|
|
||||||
|
while n_boxes < self.config_units:
|
||||||
|
ret, frame = self.cap.read()
|
||||||
|
v_mean = np.sum(cv2.mean(frame))
|
||||||
|
|
||||||
|
if abs(v_mean - v_mean0) < 5:
|
||||||
|
n_boxes = self._stitcher.find_stitch_boxes(frame, show=show)
|
||||||
|
|
||||||
|
if (cv2.getTickCount() - start_time) / cv2.getTickFrequency() > timeout:
|
||||||
|
print("Timeout finding vision units.")
|
||||||
|
self._stitcher.isstitch = self.isstitch = False
|
||||||
|
break
|
||||||
|
v_mean0 = v_mean
|
||||||
|
|
||||||
|
self.n_boxes = n_boxes
|
||||||
|
self.stitch_valid = self.isstitch and n_boxes == self.config_units
|
||||||
|
|
||||||
|
status = "valid for stitching" if self.stitch_valid else "non-stitch mode"
|
||||||
|
print(f"Sensor: {status}")
|
||||||
|
return n_boxes
|
||||||
|
|
||||||
|
def _extract_tiles(self, img_src):
|
||||||
|
"""Extract tiles from source image."""
|
||||||
|
self._stitcher.get_stitch_unit(img_src)
|
||||||
|
self._stitcher.extract_tiles()
|
||||||
|
return True
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# Private - Async Workers
|
||||||
|
# =========================================================================
|
||||||
|
|
||||||
|
def _queue_put(self, q, item):
|
||||||
|
"""Put item in queue, dropping old if full."""
|
||||||
|
if q.full():
|
||||||
|
try: q.get_nowait()
|
||||||
|
except Empty: pass
|
||||||
|
q.put(item)
|
||||||
|
|
||||||
def _capture_worker(self):
|
def _capture_worker(self):
|
||||||
"""Stage 1: Capture and preprocess frames."""
|
"""Stage 1: Capture and preprocess frames."""
|
||||||
alpha = 0.7
|
alpha = 0.7
|
||||||
@@ -344,12 +498,11 @@ class Sensor:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
|
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
|
||||||
self._Sensor__stitcher.get_stitch_unit(gray)
|
self._stitcher.get_stitch_unit(gray)
|
||||||
self._Sensor__get_tiles(gray)
|
self._extract_tiles(gray)
|
||||||
img = np.ascontiguousarray(self._Sensor__stitcher.stitch_rt_optimized())
|
img = np.ascontiguousarray(self._stitcher.stitch_rt_optimized())
|
||||||
img = cv2.resize(img, tuple(self.config_resolution_stitch), interpolation=cv2.INTER_CUBIC)
|
img = cv2.resize(img, tuple(self.config_resolution_stitch), interpolation=cv2.INTER_CUBIC)
|
||||||
|
|
||||||
# Temporal filtering
|
|
||||||
if self.img_filtered is None:
|
if self.img_filtered is None:
|
||||||
self.img_filtered = img.astype(np.float32)
|
self.img_filtered = img.astype(np.float32)
|
||||||
else:
|
else:
|
||||||
@@ -358,7 +511,7 @@ class Sensor:
|
|||||||
self._queue_put(self._capture_queue, self.img_filtered.astype(np.uint8))
|
self._queue_put(self._capture_queue, self.img_filtered.astype(np.uint8))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if self.verbose: print(f"Capture error: {e}")
|
if self.verbose: print(f"Capture error: {e}")
|
||||||
|
|
||||||
def _flow_worker(self):
|
def _flow_worker(self):
|
||||||
"""Stage 2: Compute optical flow."""
|
"""Stage 2: Compute optical flow."""
|
||||||
while not self._stop_event.is_set():
|
while not self._stop_event.is_set():
|
||||||
@@ -367,15 +520,14 @@ class Sensor:
|
|||||||
flow = self.tracker.track(img)
|
flow = self.tracker.track(img)
|
||||||
if hasattr(flow, 'download'):
|
if hasattr(flow, 'download'):
|
||||||
flow = flow.download()
|
flow = flow.download()
|
||||||
|
|
||||||
self._queue_put(self._flow_queue, (img, flow))
|
self._queue_put(self._flow_queue, (img, flow))
|
||||||
except Empty:
|
except Empty:
|
||||||
continue
|
continue
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if self.verbose: print(f"Flow error: {e}")
|
if self.verbose: print(f"Flow error: {e}")
|
||||||
|
|
||||||
def _hhd_worker(self):
|
def _hhd_worker(self):
|
||||||
"""Stage 3: HHD decomposition and force computation."""
|
"""Stage 3: HHD decomposition."""
|
||||||
while not self._stop_event.is_set():
|
while not self._stop_event.is_set():
|
||||||
try:
|
try:
|
||||||
img, flow = self._flow_queue.get(timeout=0.1)
|
img, flow = self._flow_queue.get(timeout=0.1)
|
||||||
@@ -392,213 +544,23 @@ class Sensor:
|
|||||||
'img': img, 'flow': flow,
|
'img': img, 'flow': flow,
|
||||||
'normal': self.hhd.get_normal(), 'shear': self.hhd.get_shear(),
|
'normal': self.hhd.get_normal(), 'shear': self.hhd.get_shear(),
|
||||||
'depth': getFilteredDensity(flow, use_cuda=self.cuda),
|
'depth': getFilteredDensity(flow, use_cuda=self.cuda),
|
||||||
'fnormal': forces[0], 'fshearx': forces[1],
|
'fnormal': forces[0], 'fshearx': forces[1],
|
||||||
'fsheary': forces[2], 'ftor': forces[3],
|
'fsheary': forces[2], 'ftor': forces[3],
|
||||||
})
|
})
|
||||||
except Empty:
|
except Empty:
|
||||||
continue
|
continue
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if self.verbose: print(f"HHD error: {e}")
|
if self.verbose: print(f"HHD error: {e}")
|
||||||
|
|
||||||
def get_async(self, timeout=0.05):
|
|
||||||
"""Get latest result (non-blocking). Returns dict or None."""
|
|
||||||
if not self._async_enabled:
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
result = self._result_queue.get(timeout=timeout)
|
|
||||||
t = time.perf_counter()
|
|
||||||
result['fps'] = 1.0 / (t - self.last_log_time) if self.last_log_time else 0
|
|
||||||
self.last_log_time = t
|
|
||||||
|
|
||||||
# Update instance vars
|
|
||||||
for k in ['img', 'flow', 'normal', 'shear', 'depth', 'fnormal', 'fshearx', 'fsheary', 'ftor', 'fps']:
|
|
||||||
setattr(self, k, result.get(k))
|
|
||||||
self.vupdate = True
|
|
||||||
return result
|
|
||||||
except Empty:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def disable_async(self):
|
|
||||||
"""Stop async pipeline."""
|
|
||||||
if not self._async_enabled:
|
|
||||||
return
|
|
||||||
self._stop_event.set()
|
|
||||||
for t in [self._capture_thread, self._flow_thread, self._compute_thread]:
|
|
||||||
if t: t.join(timeout=2.0)
|
|
||||||
self._async_enabled = False
|
|
||||||
self._capture_thread = self._flow_thread = self._compute_thread = None
|
|
||||||
self._capture_queue = self._flow_queue = self._result_queue = self._stop_event = None
|
|
||||||
if self.verbose:
|
|
||||||
print("Async pipeline disabled")
|
|
||||||
|
|
||||||
def update(self):
|
|
||||||
"""Convenience: get_img + compute_deformation in one call."""
|
|
||||||
self.get_img()
|
|
||||||
self.compute_deformation()
|
|
||||||
return {
|
|
||||||
'img': self.img,
|
|
||||||
'flow': self.flow,
|
|
||||||
'normal': self.normal,
|
|
||||||
'shear': self.shear,
|
|
||||||
'depth': self.depth,
|
|
||||||
'fps': self.fps,
|
|
||||||
'fnormal': self.fnormal,
|
|
||||||
'fshearx': self.fshearx,
|
|
||||||
'fsheary': self.fsheary,
|
|
||||||
'ftor': self.ftor,
|
|
||||||
}
|
|
||||||
|
|
||||||
def read_info(self, *args: info) -> Tuple:
|
|
||||||
"""
|
|
||||||
Get sensor data according to specified info types.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
*args: Variable number of info enum values specifying desired data
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Tuple: Data in the order specified by args. Each element corresponds to:
|
|
||||||
- VRAW: Raw optical flow field (np.ndarray)
|
|
||||||
- VNORMAL: Normal component of flow (np.ndarray)
|
|
||||||
- VSHEAR: Shear component of flow (np.ndarray)
|
|
||||||
- FNORMAL: Normal force magnitude (float)
|
|
||||||
- FSHEAR: Shear force magnitude (float)
|
|
||||||
- FPS: Current frames per second (float)
|
|
||||||
- TIMESTAMP: Current timestamp string (str)
|
|
||||||
- DEPTH: Depth image (np.ndarray)
|
|
||||||
|
|
||||||
Example:
|
|
||||||
normal, shear, fps = sensor.read_info(sensor.info.VNORMAL, sensor.info.VSHEAR, sensor.info.FPS)
|
|
||||||
|
|
||||||
Note:
|
|
||||||
Must call compute_deformation() before reading flow/force data
|
|
||||||
"""
|
|
||||||
if not self.vupdate:
|
|
||||||
print("Warning: Update field first via compute_deformation() before reading flow/force info.")
|
|
||||||
|
|
||||||
results = []
|
|
||||||
for arg in args:
|
|
||||||
if not isinstance(arg, self.info):
|
|
||||||
print(f"Warning: Invalid argument type {type(arg)}, skipping")
|
|
||||||
continue
|
|
||||||
|
|
||||||
if arg == self.info.VRAW:
|
|
||||||
results.append(self.flow if self.flow is not None else np.array([]))
|
|
||||||
elif arg == self.info.VNORMAL:
|
|
||||||
results.append(self.normal if self.normal is not None else np.array([]))
|
|
||||||
elif arg == self.info.VSHEAR:
|
|
||||||
results.append(self.shear if self.shear is not None else np.array([]))
|
|
||||||
elif arg == self.info.FNORMAL:
|
|
||||||
results.append(self.fnormal)
|
|
||||||
elif arg == self.info.FSHEARX:
|
|
||||||
results.append(self.fshearx)
|
|
||||||
elif arg == self.info.FSHEARY:
|
|
||||||
results.append(self.fsheary)
|
|
||||||
elif arg == self.info.FPS:
|
|
||||||
results.append(self.fps)
|
|
||||||
elif arg == self.info.TIMESTAMP:
|
|
||||||
results.append(time.strftime('%H:%M:%S'))
|
|
||||||
elif arg == self.info.DEPTH:
|
|
||||||
results.append(self.depth if self.depth is not None else np.array([]))
|
|
||||||
return tuple(results) if results else ()
|
|
||||||
|
|
||||||
def reset(self):
|
|
||||||
self.tracker.reset()
|
|
||||||
|
|
||||||
def disconnect(self):
|
|
||||||
"""
|
|
||||||
Release video capture resources and stop async threads.
|
|
||||||
"""
|
|
||||||
# Stop async pipeline if running
|
|
||||||
if self._async_enabled:
|
|
||||||
self.disable_async()
|
|
||||||
|
|
||||||
if self.cap.isOpened():
|
|
||||||
self.cap.release()
|
|
||||||
if self.verbose:
|
|
||||||
print("Sensor disconnected.")
|
|
||||||
cv2.destroyAllWindows()
|
|
||||||
|
|
||||||
def __get_tiles(self, img_src):
|
|
||||||
"""
|
|
||||||
Extract individual tiles from source image for stitching.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
img_src: Source image (grayscale) containing multiple vision tiles
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True (always succeeds)
|
|
||||||
"""
|
|
||||||
self.__stitcher.get_stitch_unit(img_src)
|
|
||||||
self.__stitcher.extract_tiles()
|
|
||||||
return True
|
|
||||||
|
|
||||||
def __find_stitch_boxes(self, img_src, show=False):
|
# =========================================================================
|
||||||
"""
|
# Private - Utilities
|
||||||
Detect vision tile boxes in the frame, waiting for camera stabilization.
|
# =========================================================================
|
||||||
|
|
||||||
Args:
|
def _print_timings(self, name, timings):
|
||||||
img_src: Initial source image (not actively used, boxes detected from live capture)
|
"""Print timing breakdown."""
|
||||||
show: Whether to display detected boxes visually (default: False)
|
print(f"\n[{name} timing]")
|
||||||
|
for k, v in timings.items():
|
||||||
Returns:
|
if k != 'total':
|
||||||
int: Number of boxes detected (should be 15 for valid stitching)
|
print(f" {k}: {v:7.2f} ms")
|
||||||
"""
|
print(f" ─────────────────────────")
|
||||||
n_boxes, v_mean0, timeout = 0, 0, 5
|
print(f" TOTAL: {timings['total']:7.2f} ms ({1000/timings['total']:.1f} FPS)")
|
||||||
start_time = cv2.getTickCount()
|
|
||||||
|
|
||||||
# Wait for camera stabilization
|
|
||||||
while n_boxes < self.config_units:
|
|
||||||
ret, frame = self.cap.read()
|
|
||||||
v_mean = np.sum(cv2.mean(frame))
|
|
||||||
|
|
||||||
if abs(v_mean - v_mean0) < 5:
|
|
||||||
n_boxes = self.__stitcher.find_stitch_boxes(frame, show=show)
|
|
||||||
|
|
||||||
if (cv2.getTickCount() - start_time) / cv2.getTickFrequency() > timeout:
|
|
||||||
print("Timeout for finding valid vision units.")
|
|
||||||
self.__stitcher.isstitch = self.isstitch = False
|
|
||||||
break
|
|
||||||
v_mean0 = v_mean
|
|
||||||
|
|
||||||
self.n_boxes = n_boxes
|
|
||||||
self.stitch_valid = self.isstitch and n_boxes == self.config_units
|
|
||||||
|
|
||||||
if self.isstitch:
|
|
||||||
print("Sensor is valid for stithcing." if self.stitch_valid else "Sensor not valid to stitching, switching to non-stitch mode")
|
|
||||||
else:
|
|
||||||
print("Using non-stitch mode.")
|
|
||||||
|
|
||||||
return n_boxes
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def stitch_cal(self):
|
|
||||||
"""
|
|
||||||
Load or create stitch calibration for aligning multiple tiles.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
np.ndarray: Stitched image after calibration
|
|
||||||
|
|
||||||
Note:
|
|
||||||
- Loads calibration from ./config/cal.npy if exists
|
|
||||||
- Creates new calibration and saves to ./config/cal.npy if not found
|
|
||||||
- Calibration contains transformation parameters for tile alignment
|
|
||||||
"""
|
|
||||||
if self.cal_path is None:
|
|
||||||
self.cal_path = "./config/cal.npy"
|
|
||||||
cal_path = self.cal_path
|
|
||||||
|
|
||||||
if os.path.exists(cal_path):
|
|
||||||
print("Loading existing calibration from", cal_path)
|
|
||||||
self.stitch_calibration = np.load(cal_path, allow_pickle=True)
|
|
||||||
self.__stitcher.stitch_calibration = self.stitch_calibration
|
|
||||||
img_stitched_height = self.__stitcher.stitch_rt_optimized()
|
|
||||||
|
|
||||||
else:
|
|
||||||
print("Calibration file not found, creating new calibration...")
|
|
||||||
img_stitched_height, self.stitch_calibration = self.__stitcher.stitch_cal()
|
|
||||||
os.makedirs("./config", exist_ok=True)
|
|
||||||
np.save(cal_path, self.stitch_calibration)
|
|
||||||
print("Calibration saved to", cal_path)
|
|
||||||
|
|
||||||
return img_stitched_height
|
|
||||||
|
|||||||
Reference in New Issue
Block a user