From 0075304d7000520527eb1886d2be1522d27cdf12 Mon Sep 17 00:00:00 2001 From: mattpetrucci <48534856+mattpetrucci@users.noreply.github.com> Date: Thu, 5 Jan 2023 11:10:25 -0800 Subject: [PATCH 1/4] add inputs for cameraModel and videoType Added inputs to computeAverageIntrinsics for camera model and video type. --- main_calcIntrinsics.py | 6 ++++-- utilsChecker.py | 20 ++++++++++++-------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/main_calcIntrinsics.py b/main_calcIntrinsics.py index 368c7aad..3312bbe7 100644 --- a/main_calcIntrinsics.py +++ b/main_calcIntrinsics.py @@ -26,6 +26,9 @@ saveIntrinsicsForDeployment = True deployedFolderNames = ['Deployed_720_60fps','Deployed'] # both folder names if want to keep the detailed folder + +cameraModel = "iPhoneTest" +videoType = ".mov" #can be .avi or other file formats # %% Paths to data folder for local testing. dataDir = os.path.join(getDataDirectory(),'Data') @@ -34,7 +37,6 @@ intrinsicComparisonFile = os.path.join(sessionDir,'intrinsicComparison.pkl') # %% Get checker parameters and filenames if they exist - # TODO this should come from the server API # Get checkerboard parameters from metadata. metadataPath = os.path.join(sessionDir,'sessionMetadata.yaml') @@ -61,7 +63,7 @@ # Compute average intrinsic values from multiple trials of same camera -CamParamsAverage, CamParamList, intrinsicComparisons, cameraModel = computeAverageIntrinsics(sessionDir,trials,CheckerBoardParams,nImages=50) +CamParamsAverage, CamParamList, intrinsicComparisons, cameraModel = computeAverageIntrinsics(sessionDir,trials,CheckerBoardParams,nImages=50,cameraModel=cameraModel,videoType=videoType) # Save intrinsics from first camera for deployement diff --git a/utilsChecker.py b/utilsChecker.py index ada0e724..f453edf9 100644 --- a/utilsChecker.py +++ b/utilsChecker.py @@ -194,24 +194,28 @@ def calcIntrinsics(folderName, CheckerBoardParams=None, filenames=['*.jpg'], return CamParams # %% -def computeAverageIntrinsics(session_path,trialIDs,CheckerBoardParams,nImages=25): +def computeAverageIntrinsics(session_path,trialIDs,CheckerBoardParams,nImages=25,cameraModel= None,videoType=".mov"): CamParamList = [] camModels = [] + trial_name = '' for trial_id in trialIDs: - resp = makeRequestWithRetry('GET', - API_URL + "trials/{}/".format(trial_id), - headers = {"Authorization": "Token {}".format(API_TOKEN)}) - trial = resp.json() - camModels.append(trial['videos'][0]['parameters']['model']) - trial_name = trial['name'] + if cameraModel is None: + resp = makeRequestWithRetry( + 'GET', API_URL + "trials/{}/".format(trial_id), + headers={"Authorization": "Token {}".format(API_TOKEN)}) + trial = resp.json() + camModels.append(trial['videos'][0]['parameters']['model']) + trial_name = trial['name'] + else: + camModels.append(cameraModel) if trial_name == 'null': trial_name = trial_id # Make directory (folder for trialname, intrinsics also saved there) video_dir = os.path.join(session_path,trial_name) os.makedirs(video_dir, exist_ok=True) - video_path = os.path.join(video_dir,trial_name + ".mov") + video_path = os.path.join(video_dir,trial_name + videoType) # Download video if not done if not os.path.exists(video_path): From fb68593058cd263c1f2b07f617cd5c5e9e19fe3d Mon Sep 17 00:00:00 2001 From: mattpetrucci <48534856+mattpetrucci@users.noreply.github.com> Date: Thu, 27 Aug 2026 10:45:35 -0600 Subject: [PATCH 2/4] Update line 199 to 'null' to ensure following if statement for trial_name gets triggered. --- utilsChecker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utilsChecker.py b/utilsChecker.py index f453edf9..7362b465 100644 --- a/utilsChecker.py +++ b/utilsChecker.py @@ -197,7 +197,7 @@ def calcIntrinsics(folderName, CheckerBoardParams=None, filenames=['*.jpg'], def computeAverageIntrinsics(session_path,trialIDs,CheckerBoardParams,nImages=25,cameraModel= None,videoType=".mov"): CamParamList = [] camModels = [] - trial_name = '' + trial_name = 'null' for trial_id in trialIDs: if cameraModel is None: From cb832bd1e3730e156b0f1b62b443b0b8e442b29c Mon Sep 17 00:00:00 2001 From: mattpetrucci <48534856+mattpetrucci@users.noreply.github.com> Date: Thu, 27 Aug 2026 12:37:11 -0600 Subject: [PATCH 3/4] Update per PR review. --- utilsChecker.py | 1 + 1 file changed, 1 insertion(+) diff --git a/utilsChecker.py b/utilsChecker.py index 7362b465..9adc98fd 100644 --- a/utilsChecker.py +++ b/utilsChecker.py @@ -209,6 +209,7 @@ def computeAverageIntrinsics(session_path,trialIDs,CheckerBoardParams,nImages=25 trial_name = trial['name'] else: camModels.append(cameraModel) + trial_name = trial_id if trial_name == 'null': trial_name = trial_id From 1ca052a2f6364d55fe2a6bc92c2beabe41f4b8af Mon Sep 17 00:00:00 2001 From: Callum Sherry Date: Fri, 28 Aug 2026 22:27:18 -0400 Subject: [PATCH 4/4] Add intrinsics regression tests --- tests/test_calibration.py | 137 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) diff --git a/tests/test_calibration.py b/tests/test_calibration.py index bfd9778d..cf2840f0 100644 --- a/tests/test_calibration.py +++ b/tests/test_calibration.py @@ -1,6 +1,8 @@ import os import shutil import sys +from pathlib import Path +from unittest.mock import Mock import cv2 import numpy as np @@ -48,6 +50,10 @@ 'production_success', 'utah_production_success.mov', ) +IPHONE_17E_FIXTURE_DIR = os.path.join( + REPO_DIR, + 'a8d94635-77db-4be4-ab7f-25da84c1a092', +) PRIMARY_SUCCESS_FIXTURES = [ ( @@ -418,3 +424,134 @@ def test_negative_fixtures_reject( assert calls['fallback'] > 0 assert fallback_flags assert all(flags & cv2.CALIB_CB_EXHAUSTIVE for flags in fallback_flags) + + +# ---- Intrinsics workflow regression tests ---- + +def make_intrinsics(focal_length): + return { + 'intrinsicMat': np.array([ + [focal_length, 0.0, 320.0], + [0.0, focal_length + 10.0, 240.0], + [0.0, 0.0, 1.0], + ]), + 'distortion': np.array([[0.1, -0.1, 0.01, 0.02, 0.03]]), + 'imageSize': np.array([[480.0], [640.0]]), + } + + +def test_intrinsics_api(tmp_path, monkeypatch): + video_url = 'https://example.test/trial-a.mov' + response = Mock() + response.json.return_value = { + 'name': 'null', + 'videos': [{ + 'video': video_url, + 'parameters': {'model': 'iPhone13,3'}, + }], + } + request = Mock(return_value=response) + download = Mock(side_effect=lambda _, path: Path(path).touch()) + extraction = Mock() + params = make_intrinsics(1000.0) + monkeypatch.setattr(utilsChecker, 'makeRequestWithRetry', request) + monkeypatch.setattr(utilsChecker, 'download_file', download) + monkeypatch.setattr(utilsChecker, 'video2Images', extraction) + monkeypatch.setattr(utilsChecker, 'calcIntrinsics', Mock(return_value=params)) + + average, _, _, model = utilsChecker.computeAverageIntrinsics( + str(tmp_path), ['trial-a'], DEFAULT_CHECKERBOARD_PARAMS + ) + + expected_path = os.path.join(tmp_path, 'trial-a', 'trial-a.mov') + request.assert_called_once() + download.assert_called_once_with(video_url, expected_path) + assert extraction.call_args.args[0] == expected_path + assert model == 'iPhone13,3' + np.testing.assert_allclose(average['intrinsicMat'], params['intrinsicMat']) + + +def test_intrinsics_local(tmp_path, monkeypatch): + trial_ids = ['capture-a', 'capture-b'] + paths = [] + for name in trial_ids: + path = tmp_path / name / f'{name}.avi' + path.parent.mkdir() + path.touch() + paths.append(str(path)) + + request = Mock() + download = Mock() + extraction = Mock() + monkeypatch.setattr(utilsChecker, 'makeRequestWithRetry', request) + monkeypatch.setattr(utilsChecker, 'download_file', download) + monkeypatch.setattr(utilsChecker, 'video2Images', extraction) + monkeypatch.setattr( + utilsChecker, + 'calcIntrinsics', + Mock(side_effect=[make_intrinsics(900.0), make_intrinsics(1100.0)]), + ) + + average, _, _, model = utilsChecker.computeAverageIntrinsics( + str(tmp_path), + trial_ids, + DEFAULT_CHECKERBOARD_PARAMS, + nImages=5, + cameraModel='ResearchCamera', + videoType='.avi', + ) + + request.assert_not_called() + download.assert_not_called() + assert [call.args[0] for call in extraction.call_args_list] == paths + assert model == 'ResearchCamera' + np.testing.assert_allclose(average['intrinsicMat'][0, 0], 1000.0) + + +# This fixture is shared separately and is not tracked in this repository, so +# this regression runs locally when available and skips elsewhere (including CI). +@pytest.mark.skipif( + not os.path.isdir(IPHONE_17E_FIXTURE_DIR), + reason='iPhone 17e calibration fixture is not available', +) +def test_intrinsics_iphone17e(): + average, captures, _, model = utilsChecker.computeAverageIntrinsics( + IPHONE_17E_FIXTURE_DIR, + ['iphone17e', 'iphone17e_1', 'iphone17e_2'], + {'dimensions': (11, 8), 'squareSize': 60}, + cameraModel='iPhone18,5', + videoType='.mov', + ) + expected = loadCameraParameters(os.path.join( + REPO_DIR, + 'CameraIntrinsics', + 'iPhone18,5', + 'Deployed', + 'cameraIntrinsics.pickle', + )) + + assert len(captures) == 3 + assert model == 'iPhone18,5' + for name in expected: + np.testing.assert_allclose(average[name], expected[name]) + + +# PR #281 currently raises UnboundLocalError here. Keep this skipped test as +# possible future coverage for clearer missing-local-video handling. +@pytest.mark.skip(reason='missing local videos are not handled by PR #281') +def test_intrinsics_missing(tmp_path, monkeypatch): + request = Mock() + download = Mock() + monkeypatch.setattr(utilsChecker, 'makeRequestWithRetry', request) + monkeypatch.setattr(utilsChecker, 'download_file', download) + + with pytest.raises(FileNotFoundError, match='capture-a\\.avi'): + utilsChecker.computeAverageIntrinsics( + str(tmp_path), + ['capture-a'], + DEFAULT_CHECKERBOARD_PARAMS, + cameraModel='ResearchCamera', + videoType='.avi', + ) + request.assert_not_called() + download.assert_not_called()