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 = {}
|
||||
|
||||
@@ -50,31 +50,27 @@ class CameraCalibrator:
|
||||
[0, 0, 1]
|
||||
], dtype=np.float32)
|
||||
|
||||
try:
|
||||
success, self.rotation_vec, self.translation_vec = cv2.solvePnP(
|
||||
court_corners_3d.astype(np.float32),
|
||||
court_corners_pixel.astype(np.float32),
|
||||
self.camera_matrix,
|
||||
None,
|
||||
flags=cv2.SOLVEPNP_ITERATIVE
|
||||
)
|
||||
if not success:
|
||||
return False
|
||||
success, self.rotation_vec, self.translation_vec = cv2.solvePnP(
|
||||
court_corners_3d.astype(np.float32),
|
||||
court_corners_pixel.astype(np.float32),
|
||||
self.camera_matrix,
|
||||
None,
|
||||
flags=cv2.SOLVEPNP_ITERATIVE
|
||||
)
|
||||
if not success:
|
||||
raise RuntimeError("solvePnP failed to find camera pose")
|
||||
|
||||
self.rotation_matrix, _ = cv2.Rodrigues(self.rotation_vec)
|
||||
self.rotation_matrix, _ = cv2.Rodrigues(self.rotation_vec)
|
||||
|
||||
# Build ground plane homography (Z=0)
|
||||
ground_2d = court_corners_3d[:, :2].astype(np.float32)
|
||||
self.homography = cv2.getPerspectiveTransform(
|
||||
court_corners_pixel[:4].astype(np.float32),
|
||||
ground_2d[:4]
|
||||
)
|
||||
# Build ground plane homography (Z=0)
|
||||
ground_2d = court_corners_3d[:, :2].astype(np.float32)
|
||||
self.homography = cv2.getPerspectiveTransform(
|
||||
court_corners_pixel[:4].astype(np.float32),
|
||||
ground_2d[:4]
|
||||
)
|
||||
|
||||
self.calibrated = True
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Calibration failed: {e}")
|
||||
return False
|
||||
self.calibrated = True
|
||||
return True
|
||||
|
||||
def pixel_to_ground(self, px: float, py: float) -> Optional[Tuple[float, float]]:
|
||||
"""Project pixel to ground plane (Z=0) using homography."""
|
||||
@@ -150,20 +146,16 @@ class CameraCalibrator:
|
||||
json.dump(data, f, indent=2)
|
||||
|
||||
def load(self, filepath: str) -> bool:
|
||||
try:
|
||||
with open(filepath, 'r') as f:
|
||||
data = json.load(f)
|
||||
self.camera_matrix = np.array(data['camera_matrix'], dtype=np.float32)
|
||||
self.rotation_vec = np.array(data['rotation_vector'], dtype=np.float32).reshape(3, 1)
|
||||
self.translation_vec = np.array(data['translation_vector'], dtype=np.float32).reshape(3, 1)
|
||||
self.rotation_matrix = np.array(data['rotation_matrix'], dtype=np.float32)
|
||||
if data.get('homography'):
|
||||
self.homography = np.array(data['homography'], dtype=np.float32)
|
||||
self.calibrated = True
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Failed to load calibration: {e}")
|
||||
return False
|
||||
with open(filepath, 'r') as f:
|
||||
data = json.load(f)
|
||||
self.camera_matrix = np.array(data['camera_matrix'], dtype=np.float32)
|
||||
self.rotation_vec = np.array(data['rotation_vector'], dtype=np.float32).reshape(3, 1)
|
||||
self.translation_vec = np.array(data['translation_vector'], dtype=np.float32).reshape(3, 1)
|
||||
self.rotation_matrix = np.array(data['rotation_matrix'], dtype=np.float32)
|
||||
if data.get('homography'):
|
||||
self.homography = np.array(data['homography'], dtype=np.float32)
|
||||
self.calibrated = True
|
||||
return True
|
||||
|
||||
|
||||
def get_half_court_3d_points(side: str) -> np.ndarray:
|
||||
|
||||
@@ -112,11 +112,16 @@ def api_calibration_trigger():
|
||||
if fn is None:
|
||||
return jsonify({'ok': False, 'error': 'Calibration not available'}), 500
|
||||
|
||||
import traceback
|
||||
try:
|
||||
result = fn()
|
||||
return jsonify({'ok': True, 'result': result})
|
||||
# Check if any camera failed
|
||||
any_ok = any(r.get('ok') for r in result.values())
|
||||
return jsonify({'ok': any_ok, 'result': result})
|
||||
except Exception as e:
|
||||
return jsonify({'ok': False, 'error': str(e)}), 500
|
||||
tb = traceback.format_exc()
|
||||
print(f"[CALIBRATION ERROR]\n{tb}")
|
||||
return jsonify({'ok': False, 'error': f'{type(e).__name__}: {e}'}), 500
|
||||
|
||||
|
||||
@app.route('/api/calibration/data')
|
||||
|
||||
@@ -332,19 +332,15 @@
|
||||
<div class="bottom-card"><img id="cal-cam1" alt="Camera 1"></div>
|
||||
<div class="bottom-card"><img id="cal-cam0" alt="Camera 0"></div>
|
||||
<div class="cam-card">
|
||||
<div class="cc-title">Base Setup</div>
|
||||
<div class="cc-item">Distance <b id="paramPosY">1.0</b>m</div>
|
||||
<div class="cc-item">Height <b id="paramPosZ">1.0</b>m</div>
|
||||
<div class="cc-item">Stereo <b id="paramStereo">6</b>cm</div>
|
||||
<div class="cc-item">Rotation <b id="paramAngle">15</b>°</div>
|
||||
<div class="cc-item">HFOV <b id="paramHfov">128</b>°</div>
|
||||
<div class="cc-item">Sensor <b>IMX219</b></div>
|
||||
<div class="cc-divider"></div>
|
||||
<div class="cc-title">Calibration</div>
|
||||
<button class="btn-calibrate" id="btnCalibrate" onclick="doCalibrate()">Calibrate</button>
|
||||
<div class="calibrate-status" id="calStatus">
|
||||
<span id="calStatusText">Not calibrated</span>
|
||||
</div>
|
||||
<div id="calError" style="color:#ff4444;font-size:8px;word-break:break-all;display:none"></div>
|
||||
</div>
|
||||
<div class="bottom-card" id="calDebug0" style="display:none"><img id="calDebugImg0" alt="CAM 0 debug"></div>
|
||||
<div class="bottom-card" id="calDebug1" style="display:none"><img id="calDebugImg1" alt="CAM 1 debug"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -411,15 +407,34 @@ if (activeTab !== 'camera') switchTab(activeTab);
|
||||
// ===================== Calibration =====================
|
||||
function doCalibrate() {
|
||||
var btn = document.getElementById('btnCalibrate');
|
||||
var errEl = document.getElementById('calError');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Calibrating...';
|
||||
errEl.style.display = 'none';
|
||||
|
||||
fetch('/api/calibration/trigger', { method: 'POST' })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
btn.disabled = false;
|
||||
|
||||
// Show debug images from calibration
|
||||
if (data.result) {
|
||||
for (var sid in data.result) {
|
||||
var r = data.result[sid];
|
||||
if (r.debug_image) {
|
||||
var dbgEl = document.getElementById('calDebug' + sid);
|
||||
var imgEl = document.getElementById('calDebugImg' + sid);
|
||||
if (dbgEl && imgEl) {
|
||||
imgEl.src = 'data:image/jpeg;base64,' + r.debug_image;
|
||||
dbgEl.style.display = 'block';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (data.ok) {
|
||||
btn.textContent = 'Re-calibrate';
|
||||
errEl.style.display = 'none';
|
||||
updateCalibrationStatus();
|
||||
|
||||
fetch('/api/calibration/data')
|
||||
@@ -427,19 +442,23 @@ function doCalibrate() {
|
||||
.then(function(camData) { addCamerasToScene(camData); });
|
||||
} else {
|
||||
btn.textContent = 'Calibrate';
|
||||
// Show errors
|
||||
var errors = [];
|
||||
if (data.result) {
|
||||
for (var sid in data.result) {
|
||||
if (!data.result[sid].ok) errors.push('CAM ' + sid + ': ' + data.result[sid].error);
|
||||
if (!data.result[sid].ok) errors.push(data.result[sid].error);
|
||||
}
|
||||
}
|
||||
alert('Calibration failed:\n' + (errors.join('\n') || data.error || 'Unknown error'));
|
||||
var msg = errors.join(' | ') || data.error || 'Unknown error';
|
||||
errEl.textContent = msg;
|
||||
errEl.style.display = 'block';
|
||||
}
|
||||
})
|
||||
.catch(function(e) {
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Calibrate';
|
||||
alert('Error: ' + e);
|
||||
errEl.textContent = String(e);
|
||||
errEl.style.display = 'block';
|
||||
});
|
||||
}
|
||||
|
||||
@@ -582,9 +601,6 @@ function initCourtScene() {
|
||||
|
||||
courtScene.add(new THREE.AmbientLight(0xffffff, 0.8));
|
||||
|
||||
// Physical camera marker: 1m from net, center, 1m height
|
||||
addStereocameras(courtScene, getCamParams());
|
||||
|
||||
// Load existing calibration cameras
|
||||
fetch('/api/calibration/data')
|
||||
.then(function(r) { return r.json(); })
|
||||
@@ -683,9 +699,6 @@ function initTrajectoryScene() {
|
||||
|
||||
trajScene.add(new THREE.AmbientLight(0xffffff, 0.8));
|
||||
|
||||
// Physical camera marker: 1m from net, center, 1m height
|
||||
addStereocameras(trajScene, getCamParams());
|
||||
|
||||
function animateTraj() {
|
||||
requestAnimationFrame(animateTraj);
|
||||
controls.update();
|
||||
@@ -702,115 +715,6 @@ function initTrajectoryScene() {
|
||||
});
|
||||
}
|
||||
|
||||
// ===================== Stereo camera rig with court coverage =====================
|
||||
// Track camera overlay objects for live update
|
||||
var camOverlayObjects = [];
|
||||
|
||||
function addStereocameras(scene, params) {
|
||||
params = params || {};
|
||||
var baseX = 6.7;
|
||||
var baseY = -(params.posY || 1);
|
||||
var baseZ = params.posZ || 1;
|
||||
var stereoGap = (params.stereo || 6) / 100;
|
||||
var camAngle = params.angle || 15;
|
||||
var hfov = params.hfov || 128;
|
||||
|
||||
// Remove old overlay objects
|
||||
camOverlayObjects.forEach(function(obj) { scene.remove(obj); });
|
||||
camOverlayObjects = [];
|
||||
|
||||
function addObj(obj) {
|
||||
scene.add(obj);
|
||||
camOverlayObjects.push(obj);
|
||||
}
|
||||
|
||||
var cam0x = baseX - stereoGap / 2;
|
||||
var cam1x = baseX + stereoGap / 2;
|
||||
|
||||
// Small camera bodies
|
||||
function drawCamBody(cx, color) {
|
||||
var body = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(0.12, 0.08, 0.08),
|
||||
new THREE.MeshBasicMaterial({ color: color })
|
||||
);
|
||||
body.position.set(cx, baseY, baseZ);
|
||||
addObj(body);
|
||||
}
|
||||
drawCamBody(cam0x, 0x44aaff);
|
||||
drawCamBody(cam1x, 0xff44aa);
|
||||
|
||||
// Pole
|
||||
var poleGeo = new THREE.BufferGeometry().setFromPoints([
|
||||
new THREE.Vector3(baseX, baseY, 0),
|
||||
new THREE.Vector3(baseX, baseY, baseZ)
|
||||
]);
|
||||
addObj(new THREE.Line(poleGeo, new THREE.LineBasicMaterial({ color: 0x666666 })));
|
||||
|
||||
// Court boundaries for clipping
|
||||
var courtMinX = 0, courtMaxX = 13.4, courtMinY = 0, courtMaxY = 6.1;
|
||||
var deg2rad = Math.PI / 180;
|
||||
var halfFov = hfov / 2;
|
||||
|
||||
function drawCoverage(cx, angleDeg, color) {
|
||||
var centerAngle = 90 + angleDeg;
|
||||
var leftAngle = centerAngle + halfFov;
|
||||
var rightAngle = centerAngle - halfFov;
|
||||
var ox = cx, oy = baseY;
|
||||
|
||||
// Cast ray, return farthest point on court boundary
|
||||
function castRay(aDeg) {
|
||||
var rad = aDeg * deg2rad;
|
||||
var dx = Math.cos(rad);
|
||||
var dy = Math.sin(rad);
|
||||
if (dy <= 0) return null;
|
||||
|
||||
// t where ray enters court (Y=0)
|
||||
var tEnter = (courtMinY - oy) / dy;
|
||||
// t where ray exits court
|
||||
var tExit = (courtMaxY - oy) / dy;
|
||||
// clip X bounds
|
||||
if (dx > 1e-9) tExit = Math.min(tExit, (courtMaxX - ox) / dx);
|
||||
else if (dx < -1e-9) tExit = Math.min(tExit, (courtMinX - ox) / dx);
|
||||
|
||||
if (tExit <= tEnter) return null;
|
||||
|
||||
var px = ox + dx * tExit;
|
||||
var py = oy + dy * tExit;
|
||||
px = Math.max(courtMinX, Math.min(courtMaxX, px));
|
||||
py = Math.max(courtMinY, Math.min(courtMaxY, py));
|
||||
return new THREE.Vector3(px, py, 0.015);
|
||||
}
|
||||
|
||||
// Camera actual position (behind court at Y=-1)
|
||||
var camOnCourt = new THREE.Vector3(cx, oy, 0.015);
|
||||
|
||||
// Far edge points
|
||||
var farPoints = [];
|
||||
var steps = 64;
|
||||
for (var i = 0; i <= steps; i++) {
|
||||
var a = rightAngle + (leftAngle - rightAngle) * (i / steps);
|
||||
var pt = castRay(a);
|
||||
if (pt) farPoints.push(pt);
|
||||
}
|
||||
if (farPoints.length < 2) return;
|
||||
|
||||
// Draw triangle fan from camera court position to far edge
|
||||
var mat = new THREE.MeshBasicMaterial({
|
||||
color: color, transparent: true, opacity: 0.35,
|
||||
side: THREE.DoubleSide, depthWrite: false
|
||||
});
|
||||
for (var i = 0; i < farPoints.length - 1; i++) {
|
||||
var triGeo = new THREE.BufferGeometry().setFromPoints([
|
||||
camOnCourt, farPoints[i], farPoints[i + 1]
|
||||
]);
|
||||
addObj(new THREE.Mesh(triGeo, mat));
|
||||
}
|
||||
}
|
||||
|
||||
drawCoverage(cam0x, -camAngle, 0x4488ff); // blue
|
||||
drawCoverage(cam1x, camAngle, 0xff44aa); // pink
|
||||
// overlap blends to purple naturally
|
||||
}
|
||||
|
||||
// ===================== Draw court lines =====================
|
||||
function drawCourtLines(scene) {
|
||||
@@ -983,26 +887,6 @@ setInterval(function() {
|
||||
}).catch(function() {});
|
||||
}, 1000);
|
||||
|
||||
// ===================== Camera params live update =====================
|
||||
function getCamParams() {
|
||||
return {
|
||||
posY: parseFloat(document.getElementById('paramPosY').textContent) || 1,
|
||||
posZ: parseFloat(document.getElementById('paramPosZ').textContent) || 1,
|
||||
stereo: parseFloat(document.getElementById('paramStereo').textContent) || 6,
|
||||
angle: parseFloat(document.getElementById('paramAngle').textContent) || 15,
|
||||
hfov: parseFloat(document.getElementById('paramHfov').textContent) || 128
|
||||
};
|
||||
}
|
||||
|
||||
function setCamParams(p) {
|
||||
document.getElementById('paramPosY').textContent = p.posY;
|
||||
document.getElementById('paramPosZ').textContent = p.posZ;
|
||||
document.getElementById('paramStereo').textContent = p.stereo;
|
||||
document.getElementById('paramAngle').textContent = p.angle;
|
||||
document.getElementById('paramHfov').textContent = p.hfov;
|
||||
if (courtSceneInitialized) addStereocameras(courtScene, p);
|
||||
if (trajSceneInitialized) addStereocameras(trajScene, p);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user