Remove Dagster pipeline and redesign calibration flow UI

This commit is contained in:
Ruslan Bakiev
2026-03-26 09:28:49 +07:00
parent f14249dec9
commit a3b57c5742
21 changed files with 455 additions and 2215 deletions

View File

@@ -34,6 +34,19 @@ _cam_readers = {}
_args = None
def _init_calibration_steps():
"""Initialize ordered calibration step status map for UI."""
return {
'green_mask': {'status': 'pending', 'detail': 'Not started'},
'line_segments': {'status': 'pending', 'detail': 'Not started'},
'merged_lines': {'status': 'pending', 'detail': 'Not started'},
'intersections': {'status': 'pending', 'detail': 'Not started'},
'template_match': {'status': 'pending', 'detail': 'Not started'},
'camera_pose': {'status': 'pending', 'detail': 'Not started'},
'overlay': {'status': 'pending', 'detail': 'Not started'},
}
def auto_calibrate():
"""Calibrate cameras by detecting court line intersections and matching
them to the known pickleball court template.
@@ -51,9 +64,21 @@ def auto_calibrate():
results = {}
for sensor_id, reader in _cam_readers.items():
steps = _init_calibration_steps()
frame = reader.grab()
if frame is None:
results[str(sensor_id)] = {'ok': False, 'error': 'No frame available'}
steps['green_mask'] = {'status': 'fail', 'detail': 'No frame available'}
steps['line_segments'] = {'status': 'fail', 'detail': 'Calibration aborted'}
steps['merged_lines'] = {'status': 'fail', 'detail': 'Calibration aborted'}
steps['intersections'] = {'status': 'fail', 'detail': 'Calibration aborted'}
steps['template_match'] = {'status': 'fail', 'detail': 'Calibration aborted'}
steps['camera_pose'] = {'status': 'fail', 'detail': 'Calibration aborted'}
steps['overlay'] = {'status': 'fail', 'detail': 'Calibration aborted'}
results[str(sensor_id)] = {
'ok': False,
'error': f'CAM {sensor_id}: No frame available',
'steps': steps,
}
continue
h, w = frame.shape[:2]
@@ -68,6 +93,13 @@ def auto_calibrate():
green_pct = np.count_nonzero(green_mask) / (w * h) * 100
if green_pct < 5:
steps['green_mask'] = {'status': 'fail', 'detail': f'Green area too small: {green_pct:.1f}%'}
steps['line_segments'] = {'status': 'fail', 'detail': 'Calibration aborted'}
steps['merged_lines'] = {'status': 'fail', 'detail': 'Calibration aborted'}
steps['intersections'] = {'status': 'fail', 'detail': 'Calibration aborted'}
steps['template_match'] = {'status': 'fail', 'detail': 'Calibration aborted'}
steps['camera_pose'] = {'status': 'fail', 'detail': 'Calibration aborted'}
steps['overlay'] = {'status': 'fail', 'detail': 'Calibration aborted'}
cv2.putText(debug_frame, f"FAILED: Green area too small ({green_pct:.0f}%)",
(10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2)
_, jpeg = cv2.imencode('.jpg', debug_frame, [cv2.IMWRITE_JPEG_QUALITY, 85])
@@ -75,13 +107,19 @@ def auto_calibrate():
'ok': False,
'error': f'CAM {sensor_id}: Green area too small ({green_pct:.0f}%)',
'debug_image': base64.b64encode(jpeg.tobytes()).decode('ascii'),
'steps': steps,
}
continue
steps['green_mask'] = {'status': 'ok', 'detail': f'Green area: {green_pct:.1f}%'}
# Step 2: Detect white line segments on court
white_segments = _detect_white_lines_on_court(frame, green_mask)
for x1, y1, x2, y2 in white_segments:
cv2.line(debug_frame, (x1, y1), (x2, y2), (255, 255, 0), 1)
if white_segments:
steps['line_segments'] = {'status': 'ok', 'detail': f'{len(white_segments)} segments detected'}
else:
steps['line_segments'] = {'status': 'fail', 'detail': '0 segments detected'}
# Step 3: Merge into distinct lines
merged = _merge_line_segments(white_segments)
@@ -89,28 +127,55 @@ def auto_calibrate():
p1, p2 = m['p1'], m['p2']
cv2.line(debug_frame, (int(p1[0]), int(p1[1])),
(int(p2[0]), int(p2[1])), (0, 165, 255), 2)
if merged:
steps['merged_lines'] = {'status': 'ok', 'detail': f'{len(merged)} merged lines'}
else:
steps['merged_lines'] = {'status': 'fail', 'detail': '0 merged lines'}
# Step 4: Find intersections
intersections = _find_line_intersections(merged, w, h)
for ix, iy in intersections:
cv2.circle(debug_frame, (int(ix), int(iy)), 6, (0, 0, 255), -1)
if intersections:
steps['intersections'] = {'status': 'ok', 'detail': f'{len(intersections)} intersections'}
else:
steps['intersections'] = {'status': 'fail', 'detail': '0 intersections'}
cv2.putText(debug_frame,
f"{len(white_segments)} segs -> {len(merged)} lines -> {len(intersections)} pts",
(10, h - 15), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 255), 1)
template = get_half_court_intersections(side)
quad = _find_green_quad(green_mask, w * h)
if len(intersections) < 4:
# Fallback: try green quad + both mappings
quad = _find_green_quad(green_mask, w * h)
if quad is not None:
cal, cam_pos, mapping_info = _calibrate_from_quad(quad, side, w, h)
if cal is not None:
steps['template_match'] = {
'status': 'ok',
'detail': 'Low intersections, used quad fallback mapping'
}
_finalize_calibration(
cal, cam_pos, sensor_id, side, debug_frame, w, h,
results, mapping_info, len(intersections), len(merged)
cal=cal,
cam_pos=cam_pos,
sensor_id=sensor_id,
side=side,
debug_frame=debug_frame,
results=results,
method_info=mapping_info,
steps=steps,
n_segments=len(white_segments),
n_lines=len(merged),
n_intersections=len(intersections),
n_points_matched=0,
)
continue
steps['template_match'] = {'status': 'fail', 'detail': 'Not enough intersections for template fit'}
steps['camera_pose'] = {'status': 'fail', 'detail': 'solvePnP skipped'}
steps['overlay'] = {'status': 'fail', 'detail': 'No calibration output'}
cv2.putText(debug_frame, f"FAILED: Need 4+ intersections, got {len(intersections)}",
(10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2)
_, jpeg = cv2.imencode('.jpg', debug_frame, [cv2.IMWRITE_JPEG_QUALITY, 85])
@@ -118,14 +183,11 @@ def auto_calibrate():
'ok': False,
'error': f'CAM {sensor_id}: {len(intersections)} intersections (need 4+) from {len(merged)} lines',
'debug_image': base64.b64encode(jpeg.tobytes()).decode('ascii'),
'steps': steps,
}
continue
# Step 5: Match intersections to court template
template = get_half_court_intersections(side)
# Try matching with green quad seeded homography
quad = _find_green_quad(green_mask, w * h)
match = _match_intersections_to_template(
intersections, template, side, quad, w, h
)
@@ -135,12 +197,29 @@ def auto_calibrate():
if quad is not None:
cal, cam_pos, mapping_info = _calibrate_from_quad(quad, side, w, h)
if cal is not None:
steps['template_match'] = {
'status': 'ok',
'detail': 'Template match weak, used quad fallback mapping'
}
_finalize_calibration(
cal, cam_pos, sensor_id, side, debug_frame, w, h,
results, mapping_info, len(intersections), len(merged)
cal=cal,
cam_pos=cam_pos,
sensor_id=sensor_id,
side=side,
debug_frame=debug_frame,
results=results,
method_info=mapping_info,
steps=steps,
n_segments=len(white_segments),
n_lines=len(merged),
n_intersections=len(intersections),
n_points_matched=0,
)
continue
steps['template_match'] = {'status': 'fail', 'detail': 'Could not match intersections to template'}
steps['camera_pose'] = {'status': 'fail', 'detail': 'solvePnP skipped'}
steps['overlay'] = {'status': 'fail', 'detail': 'No calibration output'}
cv2.putText(debug_frame, "FAILED: Could not match court pattern",
(10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2)
_, jpeg = cv2.imencode('.jpg', debug_frame, [cv2.IMWRITE_JPEG_QUALITY, 85])
@@ -148,9 +227,16 @@ def auto_calibrate():
'ok': False,
'error': f'CAM {sensor_id}: Could not match court pattern',
'debug_image': base64.b64encode(jpeg.tobytes()).decode('ascii'),
'steps': steps,
}
continue
n_matched = len(match['image_points'])
steps['template_match'] = {
'status': 'ok',
'detail': f'Matched {n_matched}/{len(template)} template points'
}
# Draw matched points
for name, (px, py) in match['image_points'].items():
cv2.circle(debug_frame, (int(px), int(py)), 10, (0, 255, 0), 2)
@@ -162,8 +248,22 @@ def auto_calibrate():
pts_3d = np.array(list(match['world_points'].values()), dtype=np.float32)
cal = CameraCalibrator()
cal.calibrate(pts_2d, pts_3d, w, h)
try:
cal.calibrate(pts_2d, pts_3d, w, h)
except RuntimeError as exc:
steps['camera_pose'] = {'status': 'fail', 'detail': str(exc)}
steps['overlay'] = {'status': 'fail', 'detail': 'No calibration output'}
_, jpeg = cv2.imencode('.jpg', debug_frame, [cv2.IMWRITE_JPEG_QUALITY, 85])
results[str(sensor_id)] = {
'ok': False,
'error': f'CAM {sensor_id}: solvePnP failed ({exc})',
'debug_image': base64.b64encode(jpeg.tobytes()).decode('ascii'),
'steps': steps,
}
continue
cam_pos = (-cal.rotation_matrix.T @ cal.translation_vec).flatten()
method_info = f"{n_matched} intersections matched"
# Sanity check
if cam_pos[2] < 0 or cam_pos[2] > 10:
@@ -173,19 +273,39 @@ def auto_calibrate():
cal2, cam_pos2, info = _calibrate_from_quad(quad, side, w, h)
if cal2 is not None:
cal, cam_pos = cal2, cam_pos2
method_info = f"{method_info}; fallback={info}"
n_matched = len(match['image_points'])
_finalize_calibration(
cal, cam_pos, sensor_id, side, debug_frame, w, h,
results, f"{n_matched} intersections matched", len(intersections), len(merged)
cal=cal,
cam_pos=cam_pos,
sensor_id=sensor_id,
side=side,
debug_frame=debug_frame,
results=results,
method_info=method_info,
steps=steps,
n_segments=len(white_segments),
n_lines=len(merged),
n_intersections=len(intersections),
n_points_matched=n_matched,
)
return results
def _finalize_calibration(cal, cam_pos, sensor_id, side, debug_frame, w, h,
results, method_info, n_intersections, n_lines):
def _finalize_calibration(cal, cam_pos, sensor_id, side, debug_frame,
results, method_info, steps, n_segments, n_lines,
n_intersections, n_points_matched):
"""Save calibration and build result dict."""
steps['camera_pose'] = {
'status': 'ok',
'detail': f'Camera pose solved: X={cam_pos[0]:.2f}, Y={cam_pos[1]:.2f}, Z={cam_pos[2]:.2f}m'
}
steps['overlay'] = {
'status': 'ok',
'detail': f'Geometry projected with {n_points_matched} matched points'
}
cv2.putText(debug_frame,
f"CAM{sensor_id}: X={cam_pos[0]:.2f} Y={cam_pos[1]:.2f} Z={cam_pos[2]:.2f}m",
(10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)
@@ -208,7 +328,12 @@ def _finalize_calibration(cal, cam_pos, sensor_id, side, debug_frame, w, h,
'camera_position': cam_pos.tolist(),
'debug_image': base64.b64encode(jpeg.tobytes()).decode('ascii'),
'matched_lines_3d': matched_lines_3d,
'points_matched': n_intersections,
'points_matched': n_points_matched,
'intersections_detected': n_intersections,
'lines_detected': n_lines,
'segments_detected': n_segments,
'method': method_info,
'steps': steps,
}
print(f"[CAM {sensor_id}] Calibrated! Camera at "
f"({cam_pos[0]:.2f}, {cam_pos[1]:.2f}, {cam_pos[2]:.2f}) via {method_info}")