From 1fe83bae4e2ced33dd07f38a9d3f4b839615b94f Mon Sep 17 00:00:00 2001 From: Callum Sherry Date: Mon, 13 Jul 2026 11:29:47 -0700 Subject: [PATCH] Improve calibration errors --- main.py | 18 +++++++++++++++--- utils.py | 44 ++++++++++++++++++++++++++++++++++++++------ utilsChecker.py | 15 ++++++++++++--- 3 files changed, 65 insertions(+), 12 deletions(-) diff --git a/main.py b/main.py index 42e6af0f..e4ffec4f 100644 --- a/main.py +++ b/main.py @@ -212,12 +212,19 @@ def main(sessionName, trialName, trial_id, cameras_to_use=['all'], cameraDirectories[camName] = os.path.join(sessionDir, 'Videos', pathCam) cameraModels[camName] = sessionMetadata['iphoneModel'][camName] + cameraMapping = sessionMetadata.get('cameraMapping', {}) # Get cameras' intrinsics and extrinsics. # Load parameters if saved, compute and save them if not. CamParamDict = {} loadedCamParams = {} for camName in cameraDirectories: + cameraLabel = cameraMapping.get(camName, {}).get('phone_label') + calibrationFailurePrefix = ( + "Calibration failed for {}.".format(cameraLabel) + if cameraLabel else + "Calibration failed for at least one camera." + ) camDir = cameraDirectories[camName] lidarIntrinsicPath = os.path.join(camDir, 'InputMedia', trialName, 'camera_matrix.csv') @@ -254,7 +261,10 @@ def main(sessionName, trialName, trial_id, cameras_to_use=['all'], # webapp will give you the opportunity to compute them. else: - exception = "Intrinsics don't exist for your camera model. OpenCap supports all iOS devices released in 2018 or later: https://www.opencap.ai/get-started." + if cameraLabel: + exception = "Intrinsics don't exist for the camera model used by {}. OpenCap supports all iOS devices released in 2018 or later: https://www.opencap.ai/get-started.".format(cameraLabel) + else: + exception = "Intrinsics don't exist for your camera model. OpenCap supports all iOS devices released in 2018 or later: https://www.opencap.ai/get-started." raise Exception(exception, exception) # Extrinsics ################################################## @@ -281,9 +291,11 @@ def main(sessionName, trialName, trial_id, cameras_to_use=['all'], useSecondExtrinsicsSolution = useSecondExtrinsicsSolution) except Exception as e: if len(e.args) == 2: # specific exception - raise Exception(e.args[0], e.args[1]) + exception = "{} {}".format( + calibrationFailurePrefix, e.args[0]) + raise Exception(exception, e.args[1]) elif len(e.args) == 1: # generic exception - exception = "Camera calibration failed. Verify your setup and try again. Visit https://www.opencap.ai/best-pratices to learn more about camera calibration and https://www.opencap.ai/troubleshooting for potential causes for a failed calibration." + exception = "{} Verify your setup and try again. Visit https://www.opencap.ai/best-pratices to learn more about camera calibration and https://www.opencap.ai/troubleshooting for potential causes for a failed calibration.".format(calibrationFailurePrefix) raise Exception(exception, traceback.format_exc()) loadedCamParams[camName] = False diff --git a/utils.py b/utils.py index 962aa74b..396dd72f 100644 --- a/utils.py +++ b/utils.py @@ -528,6 +528,22 @@ def downloadVideosFromServer(session_id,trial_id, isDocker=True, os.makedirs(session_path, exist_ok=True) trial = getTrialJson(trial_id) + session = getSessionJson(session_id) + apiCameraMap = (session.get('meta') or {}).get('camera_map') or {} + + def getCameraIndex(video, fallbackIndex, usedIndices): + device_id = video["device_id"].replace('-', '').upper() + camera_label = apiCameraMap.get(device_id, {}).get('camera_label', '') + if camera_label.startswith('Cam'): + try: + camera_index = int(camera_label.replace('Cam', '')) + if camera_index not in usedIndices: + return camera_index + except ValueError: + pass + while fallbackIndex in usedIndices: + fallbackIndex += 1 + return fallbackIndex if trial_name is None: trial_name = trial['name'] @@ -542,7 +558,10 @@ def downloadVideosFromServer(session_id,trial_id, isDocker=True, if not benchmark: if not os.path.exists(os.path.join(session_path, "Videos", 'mappingCamDevice.pickle')): mappingCamDevice = {} - for k, video in enumerate(trial["videos"]): + usedIndices = set() + for i, video in enumerate(trial["videos"]): + k = getCameraIndex(video, i, usedIndices) + usedIndices.add(k) os.makedirs(os.path.join(session_path, "Videos", "Cam{}".format(k), "InputMedia", trial_name), exist_ok=True) video_path = os.path.join(session_path, "Videos", "Cam{}".format(k), "InputMedia", trial_name, trial_id + ".mov") downloadVideoFile(video, video_path, trial_id, @@ -584,11 +603,24 @@ def downloadVideosFromServer(session_id,trial_id, isDocker=True, else: # subject parameters will be entered when capturing static pose session_desc = getMetadataFromServer(session_id) - # Load iPhone models. - phoneModel= [] - for i,video in enumerate(trial["videos"]): - phoneModel.append(video['parameters']['model']) - session_desc['iphoneModel'] = {'Cam' + str(i) : phoneModel[i] for i in range(len(phoneModel))} + # Load camera labels and iPhone models. + phoneModel = {} + cameraMapping = {} + for video in trial["videos"]: + device_id = video["device_id"].replace('-', '').upper() + if device_id not in mappingCamDevice: + continue + camName = 'Cam' + str(mappingCamDevice[device_id]) + phoneModel[camName] = video.get('parameters', {}).get('model', 'unknown') + phone_label = apiCameraMap.get(device_id, {}).get('phone_label') + if phone_label: + cameraMapping[camName] = { + 'phone_label': phone_label, + 'device_id': video["device_id"], + } + session_desc['iphoneModel'] = phoneModel + if cameraMapping: + session_desc['cameraMapping'] = cameraMapping # Save metadata. with open(sessionYamlPath, 'w') as file: diff --git a/utilsChecker.py b/utilsChecker.py index 15a3185c..e4d7ca7f 100644 --- a/utilsChecker.py +++ b/utilsChecker.py @@ -452,6 +452,7 @@ def calcExtrinsics(imageFileName, CameraParams, CheckerBoardParams, # 3D points real world coordinates. Assuming z=0 objectp3d = generate3Dgrid(CheckerBoardParams) + detectedCheckerBoardParams = CheckerBoardParams # Load and resize image - remember calibration image res needs to be same as all processing image = cv2.imread(imageFileName) @@ -510,16 +511,24 @@ def calcExtrinsics(imageFileName, CameraParams, CheckerBoardParams, # Using ACCURACY|LARGER without EXHAUSTIVE to keep the fallback reasonably fast. corners2_from_sb = False if not ret: - ret_sb, corners_sb, _ = cv2.findChessboardCornersSBWithMeta( + ret_sb, corners_sb, meta_sb = cv2.findChessboardCornersSBWithMeta( grayColor, CheckerBoardParams['dimensions'], cv2.CALIB_CB_ACCURACY | cv2.CALIB_CB_LARGER | cv2.CALIB_CB_EXHAUSTIVE) if ret_sb: + detectedDimensions = meta_sb.shape[::-1] + checkerCopy = copy.copy(CheckerBoardParams) + checkerCopy['dimensions'] = detectedDimensions + detectedCheckerBoardParams = checkerCopy + objectp3d = generate3Dgrid(detectedCheckerBoardParams) ret = True corners, orderingSuccess, orderingError = ensureCornerOrdering( - grayColor, corners_sb, CheckerBoardParams['dimensions'], + grayColor, corners_sb, detectedCheckerBoardParams['dimensions'], squareResolution=2) if orderingSuccess: corners2_from_sb = True + if tuple(detectedDimensions) != tuple(CheckerBoardParams['dimensions']): + print('Detected checkerboard dimensions {} instead of input dimensions {}.'.format( + detectedDimensions, CheckerBoardParams['dimensions'])) else: print('Rejected SB checkerboard detection: ' + orderingError) ret = False @@ -544,7 +553,7 @@ def calcExtrinsics(imageFileName, CameraParams, CheckerBoardParams, # For testing: Draw and display the corners # image = cv2.drawChessboardCorners(image, - # CheckerBoardParams['dimensions'], + # detectedCheckerBoardParams['dimensions'], # corners2, ret) # Draw small dots instead # Choose dot size based on size of squares in pixels