Remove all fallbacks: show errors, draw debug lines on calibration
- Remove try-except in CameraCalibrator — errors propagate to UI - Remove auto-load of saved calibrations — always start uncalibrated - Remove hardcoded "Base Setup" card with fake values - Remove addStereocameras/getCamParams dead code - Draw all detected Hough lines + corners on debug frame during calibration - Show debug images in calibration tab after attempt - Show error messages in UI instead of swallowing them Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
158
jetson/main.py
158
jetson/main.py
@@ -39,11 +39,9 @@ def auto_calibrate():
|
||||
compute camera pose, save to config.
|
||||
|
||||
Each camera sees one half of the court from the net position.
|
||||
We use the court lines visible in each frame to build correspondences.
|
||||
|
||||
For now: use a simple approach — detect the 4 most prominent lines
|
||||
(baseline, two sidelines, kitchen line) and map to known 3D coords.
|
||||
Returns error if court lines cannot be detected (no fallbacks).
|
||||
Detects court lines via Hough transform, finds 4 corners,
|
||||
then uses solvePnP to determine camera position.
|
||||
Returns debug images with detected lines drawn on them.
|
||||
"""
|
||||
results = {}
|
||||
|
||||
@@ -55,51 +53,93 @@ def auto_calibrate():
|
||||
|
||||
h, w = frame.shape[:2]
|
||||
side = 'left' if sensor_id == 0 else 'right'
|
||||
debug_frame = frame.copy()
|
||||
|
||||
# Try to detect court lines using edge detection + Hough
|
||||
corners_pixel = _detect_court_corners(frame, side)
|
||||
# Detect court lines — returns corners + debug info
|
||||
detection = _detect_court_corners(frame, side)
|
||||
|
||||
# Draw all detected Hough lines on debug frame
|
||||
if detection and detection.get('all_lines') is not None:
|
||||
for line in detection['all_lines']:
|
||||
x1, y1, x2, y2 = line[0]
|
||||
cv2.line(debug_frame, (x1, y1), (x2, y2), (50, 50, 50), 1)
|
||||
|
||||
# Draw classified lines
|
||||
if detection and detection.get('horizontals'):
|
||||
for line in detection['horizontals']:
|
||||
x1, y1, x2, y2 = line
|
||||
cv2.line(debug_frame, (x1, y1), (x2, y2), (0, 255, 255), 2) # yellow = horizontal
|
||||
if detection and detection.get('verticals'):
|
||||
for line in detection['verticals']:
|
||||
x1, y1, x2, y2 = line
|
||||
cv2.line(debug_frame, (x1, y1), (x2, y2), (255, 0, 255), 2) # magenta = vertical
|
||||
|
||||
# Draw selected 4 lines (top/bottom/left/right)
|
||||
if detection and detection.get('selected_lines'):
|
||||
sel = detection['selected_lines']
|
||||
colors = {'top': (0, 255, 0), 'bottom': (0, 200, 0),
|
||||
'left': (255, 128, 0), 'right': (200, 100, 0)}
|
||||
for name, line in sel.items():
|
||||
x1, y1, x2, y2 = line
|
||||
cv2.line(debug_frame, (x1, y1), (x2, y2), colors.get(name, (255, 255, 255)), 3)
|
||||
cv2.putText(debug_frame, name, (x1, y1 - 5),
|
||||
cv2.FONT_HERSHEY_SIMPLEX, 0.5, colors.get(name, (255, 255, 255)), 1)
|
||||
|
||||
# Encode debug frame
|
||||
_, jpeg = cv2.imencode('.jpg', debug_frame, [cv2.IMWRITE_JPEG_QUALITY, 85])
|
||||
debug_b64 = base64.b64encode(jpeg.tobytes()).decode('ascii')
|
||||
|
||||
corners_pixel = detection.get('corners') if detection else None
|
||||
|
||||
if corners_pixel is None:
|
||||
error_detail = detection.get('error', 'Unknown') if detection else 'No lines detected at all'
|
||||
results[str(sensor_id)] = {
|
||||
'ok': False,
|
||||
'error': f'Could not detect court lines for CAM {sensor_id}. '
|
||||
'Ensure court lines are clearly visible.'
|
||||
'error': f'CAM {sensor_id}: {error_detail}',
|
||||
'debug_image': debug_b64,
|
||||
}
|
||||
continue
|
||||
|
||||
# Draw corners on debug frame
|
||||
for i, corner in enumerate(corners_pixel):
|
||||
pt = (int(corner[0]), int(corner[1]))
|
||||
cv2.circle(debug_frame, pt, 8, (0, 0, 255), -1)
|
||||
cv2.putText(debug_frame, f'C{i}', (pt[0] + 10, pt[1]),
|
||||
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2)
|
||||
|
||||
# Re-encode with corners
|
||||
_, jpeg = cv2.imencode('.jpg', debug_frame, [cv2.IMWRITE_JPEG_QUALITY, 85])
|
||||
debug_b64 = base64.b64encode(jpeg.tobytes()).decode('ascii')
|
||||
|
||||
# Get known 3D coordinates for this half
|
||||
corners_3d = get_half_court_3d_points(side)
|
||||
|
||||
# Calibrate
|
||||
# Calibrate — no try/except, let errors propagate
|
||||
cal = CameraCalibrator()
|
||||
ok = cal.calibrate(
|
||||
cal.calibrate(
|
||||
np.array(corners_pixel, dtype=np.float32),
|
||||
corners_3d,
|
||||
w, h
|
||||
)
|
||||
|
||||
if ok:
|
||||
# Save to config
|
||||
cal_path = os.path.join(_args.calibration_dir,
|
||||
f'cam{sensor_id}_calibration.json')
|
||||
os.makedirs(os.path.dirname(cal_path), exist_ok=True)
|
||||
cal.save(cal_path)
|
||||
# Save to config
|
||||
cal_path = os.path.join(_args.calibration_dir,
|
||||
f'cam{sensor_id}_calibration.json')
|
||||
os.makedirs(os.path.dirname(cal_path), exist_ok=True)
|
||||
cal.save(cal_path)
|
||||
|
||||
state['calibrators'][sensor_id] = cal
|
||||
state['calibrators'][sensor_id] = cal
|
||||
|
||||
# Get camera position for 3D scene
|
||||
cam_pos = (-cal.rotation_matrix.T @ cal.translation_vec).flatten()
|
||||
# Get camera position for 3D scene
|
||||
cam_pos = (-cal.rotation_matrix.T @ cal.translation_vec).flatten()
|
||||
|
||||
results[str(sensor_id)] = {
|
||||
'ok': True,
|
||||
'camera_position': cam_pos.tolist(),
|
||||
'corners_pixel': corners_pixel.tolist() if isinstance(corners_pixel, np.ndarray)
|
||||
else corners_pixel,
|
||||
}
|
||||
print(f"[CAM {sensor_id}] Calibrated! Camera at "
|
||||
f"({cam_pos[0]:.1f}, {cam_pos[1]:.1f}, {cam_pos[2]:.1f})")
|
||||
else:
|
||||
results[str(sensor_id)] = {'ok': False, 'error': 'Calibration failed'}
|
||||
results[str(sensor_id)] = {
|
||||
'ok': True,
|
||||
'camera_position': cam_pos.tolist(),
|
||||
'debug_image': debug_b64,
|
||||
}
|
||||
print(f"[CAM {sensor_id}] Calibrated! Camera at "
|
||||
f"({cam_pos[0]:.1f}, {cam_pos[1]:.1f}, {cam_pos[2]:.1f})")
|
||||
|
||||
return results
|
||||
|
||||
@@ -107,7 +147,13 @@ def auto_calibrate():
|
||||
def _detect_court_corners(frame, side):
|
||||
"""Detect court corners from frame using edge detection.
|
||||
|
||||
Returns 4 corner points as numpy array, or None if detection fails.
|
||||
Returns dict with:
|
||||
corners: 4x2 numpy array or None
|
||||
all_lines: raw Hough lines
|
||||
horizontals: classified horizontal lines
|
||||
verticals: classified vertical lines
|
||||
selected_lines: the 4 lines used (top/bottom/left/right)
|
||||
error: description if detection failed
|
||||
"""
|
||||
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
|
||||
blur = cv2.GaussianBlur(gray, (5, 5), 0)
|
||||
@@ -118,7 +164,13 @@ def _detect_court_corners(frame, side):
|
||||
minLineLength=100, maxLineGap=20)
|
||||
|
||||
if lines is None or len(lines) < 4:
|
||||
return None
|
||||
n = 0 if lines is None else len(lines)
|
||||
return {
|
||||
'corners': None, 'all_lines': lines,
|
||||
'horizontals': [], 'verticals': [],
|
||||
'selected_lines': {},
|
||||
'error': f'Only {n} Hough lines found (need >= 4)',
|
||||
}
|
||||
|
||||
# Classify lines into horizontal and vertical
|
||||
horizontals = []
|
||||
@@ -134,20 +186,28 @@ def _detect_court_corners(frame, side):
|
||||
verticals.append(line[0])
|
||||
|
||||
if len(horizontals) < 2 or len(verticals) < 2:
|
||||
return None
|
||||
return {
|
||||
'corners': None, 'all_lines': lines,
|
||||
'horizontals': [h.tolist() for h in horizontals],
|
||||
'verticals': [v.tolist() for v in verticals],
|
||||
'selected_lines': {},
|
||||
'error': f'{len(horizontals)} horizontal, {len(verticals)} vertical lines (need >= 2 each)',
|
||||
}
|
||||
|
||||
# Cluster lines by position to find the dominant ones
|
||||
h_positions = sorted(horizontals, key=lambda l: (l[1] + l[3]) / 2)
|
||||
v_positions = sorted(verticals, key=lambda l: (l[0] + l[2]) / 2)
|
||||
|
||||
# Take the most separated horizontal pair (top and bottom court lines)
|
||||
top_line = h_positions[0]
|
||||
bottom_line = h_positions[-1]
|
||||
|
||||
# Take the most separated vertical pair (left and right sidelines)
|
||||
left_line = v_positions[0]
|
||||
right_line = v_positions[-1]
|
||||
|
||||
selected = {
|
||||
'top': top_line.tolist(), 'bottom': bottom_line.tolist(),
|
||||
'left': left_line.tolist(), 'right': right_line.tolist(),
|
||||
}
|
||||
|
||||
# Find intersections as corner points
|
||||
def line_intersection(l1, l2):
|
||||
x1, y1, x2, y2 = l1
|
||||
@@ -168,9 +228,22 @@ def _detect_court_corners(frame, side):
|
||||
]
|
||||
|
||||
if any(c is None for c in corners):
|
||||
return None
|
||||
return {
|
||||
'corners': None, 'all_lines': lines,
|
||||
'horizontals': [h.tolist() for h in horizontals],
|
||||
'verticals': [v.tolist() for v in verticals],
|
||||
'selected_lines': selected,
|
||||
'error': 'Lines are parallel — could not find all 4 corner intersections',
|
||||
}
|
||||
|
||||
return np.array(corners, dtype=np.float32)
|
||||
return {
|
||||
'corners': np.array(corners, dtype=np.float32),
|
||||
'all_lines': lines,
|
||||
'horizontals': [h.tolist() for h in horizontals],
|
||||
'verticals': [v.tolist() for v in verticals],
|
||||
'selected_lines': selected,
|
||||
'error': None,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -324,15 +397,10 @@ def main():
|
||||
|
||||
ring_buffer = FrameRingBuffer(max_seconds=args.buffer_seconds, fps=args.fps)
|
||||
|
||||
# Load calibrations if available
|
||||
# Start with empty calibrators — user must calibrate via UI
|
||||
os.makedirs(args.calibration_dir, exist_ok=True)
|
||||
for sensor_id in [0, 1]:
|
||||
cal = CameraCalibrator()
|
||||
cal_path = os.path.join(args.calibration_dir, f'cam{sensor_id}_calibration.json')
|
||||
if os.path.exists(cal_path):
|
||||
if cal.load(cal_path):
|
||||
print(f"[CAM {sensor_id}] Loaded calibration from {cal_path}")
|
||||
state['calibrators'][sensor_id] = cal
|
||||
state['calibrators'][sensor_id] = CameraCalibrator()
|
||||
|
||||
# Start camera readers
|
||||
cam_readers = {}
|
||||
|
||||
Reference in New Issue
Block a user