Remove Dagster pipeline and redesign calibration flow UI
This commit is contained in:
159
jetson/main.py
159
jetson/main.py
@@ -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}")
|
||||
|
||||
Reference in New Issue
Block a user