I am working on a closed-loop alignment system for an optical fiber using a microscope (Dino-Lite). The goal is to track the XY coordinates of the fiber tip with high precision to provide feedback to a piezo-stage controller.
The Setup:
-
Hardware: 3-axis piezo controller + Dino-Lite Edge microscope.
-
Software: Python 3.x + OpenCV using the
calcOpticalFlowPyrLK(Lucas-Kanade) algorithm. -
Environment: Microscopy images with specular reflections on the fiber surface and semi-transparent edges.
The Problem: My current implementation (code below) works for smooth movements, but I am facing three main issues:
-
Drift: Over time, the tracking points tend to “slide” along the body of the fiber rather than staying locked on the very end of the tip.
-
Specular Glints: Reflections on the glass surface often create “false features” that Lucas-Kanade tries to track, destabilizing the motion vector.
-
Motion Inconsistency: When the fiber moves backwards or changes direction quickly, the tracker sometimes loses the tip and stays “stuck” on a background texture.
I have implemented a CLAHE (Contrast Limited Adaptive Histogram Equalization) pre-processing step and a median displacement filter to reject outliers, but the sub-pixel precision is still not robust enough for long-term alignment.
My Current Implementation:
Python
import cv2
import numpy as np
from typing import Optional, Tuple
class PuntaTracker:
"""
Precision tracker for optical fiber alignment.
Optimized for microscopy:
- Resists focus changes and background noise.
- Sub-pixel precision to avoid vibrations.
- Motion coherence filter (ignores background motion).
"""
def __init__(
self,
box_sizes=(12, 16, 20, 24),
search_radius=16,
max_corners=8,
quality_level=0.06,
min_distance=5,
block_size=7,
lk_win_size=(21, 21),
lk_max_level=3,
lk_max_iter=30,
lk_eps=0.01,
min_points=3,
max_jump_px=40.0,
redetect_min_points=3,
):
self.box_sizes = list(box_sizes)
self.search_radius = int(search_radius)
self.max_corners = int(max_corners)
self.quality_level = float(quality_level)
self.min_distance = float(min_distance)
self.block_size = int(block_size)
self.lk_params = dict(
winSize=lk_win_size,
maxLevel=int(lk_max_level),
criteria=(cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, int(lk_max_iter), float(lk_eps)),
)
self.min_points = int(min_points)
self.redetect_min_points = int(redetect_min_points)
self.max_jump_px = float(max_jump_px)
self.active = False
self.prev_gray = None
self.points = None
self.leader_idx = None
self.last_tip: Optional[Tuple[int, int]] = None
self.last_bbox: Optional[Tuple[int, int, int, int]] = None
self.frame_count = 0
self.clahe = cv2.createCLAHE(clipLimit=2.5, tileGridSize=(8, 8))
def _preprocess(self, frame_bgr):
gray = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2GRAY)
gray = cv2.GaussianBlur(gray, (5, 5), 0)
return self.clahe.apply(gray)
def _detect_features_in_roi(self, gray, bbox):
H, W = gray.shape[:2]
x, y, w, h = self._clamp_bbox(bbox[0], bbox[1], bbox[2], bbox[3], W, H)
roi = gray[y : y + h, x : x + w]
if roi.size == 0: return None, None
pts = cv2.goodFeaturesToTrack(
roi, maxCorners=self.max_corners, qualityLevel=self.quality_level,
minDistance=self.min_distance, blockSize=self.block_size
)
if pts is not None and len(pts) >= self.min_points:
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 40, 0.001)
pts = cv2.cornerSubPix(roi, pts, (5, 5), (-1, -1), criteria)
pts[:, 0, 0] += x
pts[:, 0, 1] += y
return pts.astype(np.float32), (x, y, w, h)
return None, None
def update(self, frame_bgr):
if not self.active or self.points is None or self.prev_gray is None:
return None, None, "need_click"
self.frame_count += 1
gray = self._preprocess(frame_bgr)
next_pts, st, _ = cv2.calcOpticalFlowPyrLK(self.prev_gray, gray, self.points, None, **self.lk_params)
if next_pts is None or st is None or np.count_nonzero(st) < self.min_points:
return self._attempt_redetect(gray, "lost")
good_new = next_pts[st == 1].reshape(-1, 2)
good_old = self.points[st == 1].reshape(-1, 2)
if len(good_new) >= 2:
displacements = good_new - good_old
median_disp = np.median(displacements, axis=0)
diff = np.linalg.norm(displacements - median_disp, axis=1)
mask = diff < 3.5
if np.count_nonzero(mask) >= self.min_points:
good_new = good_new[mask]
else:
return self._attempt_redetect(gray, "unstable_motion")
ref_point = good_new[np.argmin(np.sum((good_new - self.last_tip)**2, axis=1))]
new_tip = (int(ref_point[0]), int(ref_point[1]))
jump = np.linalg.norm(np.array(new_tip) - np.array(self.last_tip))
if jump > self.max_jump_px:
return self._attempt_redetect(gray, "jump_detected")
self.last_tip = new_tip
self.points = good_new.reshape(-1, 1, 2).astype(np.float32)
self.prev_gray = gray
return self.last_tip, self.last_bbox, "ok"
def _attempt_redetect(self, gray, reason):
if self._redetect(gray):
return self.last_tip, self.last_bbox, "recovered"
self.active = False
return None, None, reason
def _clamp_bbox(self, x0, y0, w, h, W, H):
x0 = max(0, min(x0, W - 1))
y0 = max(0, min(y0, H - 1))
w = max(10, min(w, W - x0))
h = max(10, min(h, H - y0))
return x0, y0, w, h
Questions:
-
Is there a better alternative to
goodFeaturesToTrackfor objects with low contrast and transparency like an optical fiber? -
How can I improve the “lock” on the tip to prevent the points from sliding along the fiber body (drift)?
-
Would a Template Matching approach or an Edge-Centroid algorithm be more stable than Lucas-Kanade for this specific microscopy use case?
Any advice or references to similar micro-tracking projects would be greatly appreciated!
