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:
Ruslan Bakiev
2026-03-22 14:27:53 +07:00
parent ee73aa80d8
commit e12edab19b
4 changed files with 178 additions and 229 deletions

View File

@@ -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 = {}