From 11e8f332b50f2beda6c92c7eda0a39a401308be0 Mon Sep 17 00:00:00 2001 From: David Murmann Date: Mon, 3 Nov 2025 11:08:32 +0100 Subject: [PATCH] Motion Tracking: Add anamorphic coefficients to Nuke lens distortion model This patch extends the Nuke distortion model to support anamorphic lenses by introducing new anamorphic coefficients. - When the new coefficients are set to zero, the model behaves exactly as before. - The "Tangential Distortion" refine option is used to solve for these new parameters. - A test file is included, tracked from real anamorphic footage (footage itself cannot be shared unfortunately, I included a redistorted rendering of the markers, so that the file can be opened). - The test should show that other distortion models have higher error and fail to properly straighten curved lines. Pull Request: https://projects.blender.org/blender/blender/pulls/149035 --- intern/libmv/intern/camera_intrinsics.cc | 17 +++++++++--- intern/libmv/intern/camera_intrinsics.h | 1 + intern/libmv/libmv/simple_pipeline/bundle.cc | 4 +++ .../simple_pipeline/camera_intrinsics.cc | 26 ++++++++++++++++--- .../libmv/simple_pipeline/camera_intrinsics.h | 9 ++++++- .../simple_pipeline/distortion_models.cc | 12 ++++++++- .../libmv/simple_pipeline/distortion_models.h | 21 ++++++++++----- scripts/startup/bl_ui/space_clip.py | 5 +++- source/blender/blenkernel/intern/movieclip.cc | 5 ++++ source/blender/blenkernel/intern/tracking.cc | 12 +++++---- .../blenkernel/intern/tracking_util.cc | 4 +++ source/blender/makesdna/DNA_tracking_types.h | 1 + .../blender/makesrna/intern/rna_tracking.cc | 12 +++++++++ 13 files changed, 107 insertions(+), 22 deletions(-) diff --git a/intern/libmv/intern/camera_intrinsics.cc b/intern/libmv/intern/camera_intrinsics.cc index 6541a71354e..4740c3e5def 100644 --- a/intern/libmv/intern/camera_intrinsics.cc +++ b/intern/libmv/intern/camera_intrinsics.cc @@ -138,9 +138,15 @@ void libmv_cameraIntrinsicsUpdate( double k2 = libmv_camera_intrinsics_options->nuke_k2; if (nuke_intrinsics->k1() != k1 || nuke_intrinsics->k2() != k2) { - nuke_intrinsics->SetDistortion(k1, k2); + nuke_intrinsics->SetRadialDistortion(k1, k2); } + double p1 = libmv_camera_intrinsics_options->nuke_p1; + double p2 = libmv_camera_intrinsics_options->nuke_p2; + + if (nuke_intrinsics->p1() != p1 || nuke_intrinsics->p2() != p2) { + nuke_intrinsics->SetTangentialDistortion(p1, p2); + } break; } @@ -220,6 +226,8 @@ void libmv_cameraIntrinsicsExtractOptions( camera_intrinsics_options->distortion_model = LIBMV_DISTORTION_MODEL_NUKE; camera_intrinsics_options->nuke_k1 = nuke_intrinsics->k1(); camera_intrinsics_options->nuke_k2 = nuke_intrinsics->k2(); + camera_intrinsics_options->nuke_p1 = nuke_intrinsics->p1(); + camera_intrinsics_options->nuke_p2 = nuke_intrinsics->p2(); break; } @@ -353,8 +361,11 @@ static void libmv_cameraIntrinsicsFillFromOptions( NukeCameraIntrinsics* nuke_intrinsics = static_cast(camera_intrinsics); - nuke_intrinsics->SetDistortion(camera_intrinsics_options->nuke_k1, - camera_intrinsics_options->nuke_k2); + nuke_intrinsics->SetRadialDistortion(camera_intrinsics_options->nuke_k1, + camera_intrinsics_options->nuke_k2); + nuke_intrinsics->SetTangentialDistortion( + camera_intrinsics_options->nuke_p1, + camera_intrinsics_options->nuke_p2); break; } diff --git a/intern/libmv/intern/camera_intrinsics.h b/intern/libmv/intern/camera_intrinsics.h index 519eca2d3f5..b6d2c9d228e 100644 --- a/intern/libmv/intern/camera_intrinsics.h +++ b/intern/libmv/intern/camera_intrinsics.h @@ -34,6 +34,7 @@ typedef struct libmv_CameraIntrinsicsOptions { // Nuke distortion model. double nuke_k1, nuke_k2; + double nuke_p1, nuke_p2; // Brown-Conrady distortion model. double brown_k1, brown_k2, brown_k3, brown_k4; diff --git a/intern/libmv/libmv/simple_pipeline/bundle.cc b/intern/libmv/libmv/simple_pipeline/bundle.cc index 355c167d000..4c981af81b1 100644 --- a/intern/libmv/libmv/simple_pipeline/bundle.cc +++ b/intern/libmv/libmv/simple_pipeline/bundle.cc @@ -192,6 +192,8 @@ void InvertDistortionModelUsingIntrinsicsBlock( case DISTORTION_MODEL_NUKE: { const T& k1 = intrinsics_block[PackedIntrinsics::OFFSET_K1]; const T& k2 = intrinsics_block[PackedIntrinsics::OFFSET_K2]; + const T& p1 = intrinsics_block[PackedIntrinsics::OFFSET_P1]; + const T& p2 = intrinsics_block[PackedIntrinsics::OFFSET_P2]; InvertNukeDistortionModel(focal_length, focal_length, @@ -201,6 +203,8 @@ void InvertDistortionModelUsingIntrinsicsBlock( invariant_intrinsics->image_height(), k1, k2, + p1, + p2, image_x, image_y, normalized_x, diff --git a/intern/libmv/libmv/simple_pipeline/camera_intrinsics.cc b/intern/libmv/libmv/simple_pipeline/camera_intrinsics.cc index 107479896f8..f8312af1936 100644 --- a/intern/libmv/libmv/simple_pipeline/camera_intrinsics.cc +++ b/intern/libmv/libmv/simple_pipeline/camera_intrinsics.cc @@ -286,20 +286,28 @@ void DivisionCameraIntrinsics::Unpack( // Nuke model. NukeCameraIntrinsics::NukeCameraIntrinsics() : CameraIntrinsics() { - SetDistortion(0.0, 0.0); + SetRadialDistortion(0.0, 0.0); + SetTangentialDistortion(0.0, 0.0); } NukeCameraIntrinsics::NukeCameraIntrinsics(const NukeCameraIntrinsics& from) : CameraIntrinsics(from) { - SetDistortion(from.k1(), from.k2()); + SetRadialDistortion(from.k1(), from.k2()); + SetTangentialDistortion(from.p1(), from.p2()); } -void NukeCameraIntrinsics::SetDistortion(double k1, double k2) { +void NukeCameraIntrinsics::SetRadialDistortion(double k1, double k2) { parameters_[OFFSET_K1] = k1; parameters_[OFFSET_K2] = k2; ResetLookupGrids(); } +void NukeCameraIntrinsics::SetTangentialDistortion(double p1, double p2) { + parameters_[OFFSET_P1] = p1; + parameters_[OFFSET_P2] = p2; + ResetLookupGrids(); +} + void NukeCameraIntrinsics::ApplyIntrinsics(double normalized_x, double normalized_y, double* image_x, @@ -312,6 +320,8 @@ void NukeCameraIntrinsics::ApplyIntrinsics(double normalized_x, image_height(), k1(), k2(), + p1(), + p2(), normalized_x, normalized_y, image_x, @@ -330,6 +340,8 @@ void NukeCameraIntrinsics::InvertIntrinsics(double image_x, image_height(), k1(), k2(), + p1(), + p2(), image_x, image_y, normalized_x, @@ -341,12 +353,16 @@ void NukeCameraIntrinsics::Pack(PackedIntrinsics* packed_intrinsics) const { packed_intrinsics->SetK1(k1()); packed_intrinsics->SetK2(k2()); + + packed_intrinsics->SetP1(p1()); + packed_intrinsics->SetP2(p2()); } void NukeCameraIntrinsics::Unpack(const PackedIntrinsics& packed_intrinsics) { CameraIntrinsics::Unpack(packed_intrinsics); - SetDistortion(packed_intrinsics.GetK1(), packed_intrinsics.GetK2()); + SetRadialDistortion(packed_intrinsics.GetK1(), packed_intrinsics.GetK2()); + SetTangentialDistortion(packed_intrinsics.GetP1(), packed_intrinsics.GetP2()); } // Brown model. @@ -484,6 +500,8 @@ std::ostream& operator<<(std::ostream& os, const CameraIntrinsics& intrinsics) { static_cast(&intrinsics); PRINT_NONZERO_COEFFICIENT(nuke_intrinsics, k1); PRINT_NONZERO_COEFFICIENT(nuke_intrinsics, k2); + PRINT_NONZERO_COEFFICIENT(nuke_intrinsics, p1); + PRINT_NONZERO_COEFFICIENT(nuke_intrinsics, p2); break; } case DISTORTION_MODEL_BROWN: { diff --git a/intern/libmv/libmv/simple_pipeline/camera_intrinsics.h b/intern/libmv/libmv/simple_pipeline/camera_intrinsics.h index 5a73086f82d..848b3b58872 100644 --- a/intern/libmv/libmv/simple_pipeline/camera_intrinsics.h +++ b/intern/libmv/libmv/simple_pipeline/camera_intrinsics.h @@ -389,6 +389,8 @@ class NukeCameraIntrinsics : public CameraIntrinsics { enum { OFFSET_K1, OFFSET_K2, + OFFSET_P1, + OFFSET_P2, // This defines the size of array which we need to have in order // to store all the coefficients. @@ -404,9 +406,14 @@ class NukeCameraIntrinsics : public CameraIntrinsics { double k1() const { return parameters_[OFFSET_K1]; } double k2() const { return parameters_[OFFSET_K2]; } + double p1() const { return parameters_[OFFSET_P1]; } + double p2() const { return parameters_[OFFSET_P2]; } // Set radial distortion coeffcients. - void SetDistortion(double k1, double k2); + void SetRadialDistortion(double k1, double k2); + + // Set tangential distortion coeffcients. + void SetTangentialDistortion(double p1, double p2); // Apply camera intrinsics to the normalized point to get image coordinates. // diff --git a/intern/libmv/libmv/simple_pipeline/distortion_models.cc b/intern/libmv/libmv/simple_pipeline/distortion_models.cc index 4556e3ceaf9..34e210a12ad 100644 --- a/intern/libmv/libmv/simple_pipeline/distortion_models.cc +++ b/intern/libmv/libmv/simple_pipeline/distortion_models.cc @@ -333,6 +333,8 @@ struct ApplyNukeIntrinsicsCostFunction { const int image_height, const double k1, const double k2, + const double p1, + const double p2, const double expected_normalized_x, const double expected_normalized_y) : focal_length_x_(focal_length_x), @@ -343,6 +345,8 @@ struct ApplyNukeIntrinsicsCostFunction { image_height_(image_height), k1_(k1), k2_(k2), + p1_(p1), + p2_(p2), expected_normalized_x_(expected_normalized_x), expected_normalized_y_(expected_normalized_y) {} @@ -357,6 +361,8 @@ struct ApplyNukeIntrinsicsCostFunction { image_height_, k1_, k2_, + p1_, + p2_, image_coordinate(0), image_coordinate(1), &actual_normalized_x, @@ -373,7 +379,7 @@ struct ApplyNukeIntrinsicsCostFunction { double principal_point_y_; int image_width_; int image_height_; - double k1_, k2_; + double k1_, k2_, p1_, p2_; double expected_normalized_x_, expected_normalized_y_; }; @@ -385,6 +391,8 @@ void ApplyNukeDistortionModel(const double focal_length_x, const int image_height, const double k1, const double k2, + const double p1, + const double p2, const double normalized_x, const double normalized_y, double* image_x, @@ -406,6 +414,8 @@ void ApplyNukeDistortionModel(const double focal_length_x, image_height, k1, k2, + p1, + p2, normalized_x, normalized_y); Solver::SolverParameters params; diff --git a/intern/libmv/libmv/simple_pipeline/distortion_models.h b/intern/libmv/libmv/simple_pipeline/distortion_models.h index 5fe9fee8d54..7f9017feee7 100644 --- a/intern/libmv/libmv/simple_pipeline/distortion_models.h +++ b/intern/libmv/libmv/simple_pipeline/distortion_models.h @@ -143,14 +143,16 @@ void InvertNukeDistortionModel(const T& focal_length_x, const int image_height, const T& k1, const T& k2, + const T& p1, + const T& p2, const T& image_x, const T& image_y, T* normalized_x, T* normalized_y) { - // According to the documentation: + // According to the documentation for the anamorphic model: // - // xu = xd / (1 + k0 * rd^2 + k1 * rd^4) - // yu = yd / (1 + k0 * rd^2 + k1 * rd^4) + // xu = xd / (1 + k0 * rd^2 + k1 * rd^4 + k2 * yd^2) + // yu = yd / (1 + k0 * rd^2 + k1 * rd^4 + k3 * xd^2) // // Legend: // (xd, yd) are the distorted cartesian coordinates, @@ -160,6 +162,8 @@ void InvertNukeDistortionModel(const T& focal_length_x, // the k-values are the distortion coefficients. // // The coordinate systems are relative to the distortion centre. + // Nukes k0, k1, k2, k3 correspond to Blenders k1, k2, p1, p2. + // The spherical model is identical except p1 and p2 are both zero. const int max_image_size = std::max(image_width, image_height); const double max_half_image_size = max_image_size * 0.5; @@ -173,11 +177,12 @@ void InvertNukeDistortionModel(const T& focal_length_x, const T xd = (image_x - principal_point_x) / max_half_image_size; const T yd = (image_y - principal_point_y) / max_half_image_size; - T rd2 = xd * xd + yd * yd; + T xd2 = xd * xd; + T yd2 = yd * yd; + T rd2 = xd2 + yd2; T rd4 = rd2 * rd2; - T r_coeff = T(1) / (T(1) + k1 * rd2 + k2 * rd4); - T xu = xd * r_coeff; - T yu = yd * r_coeff; + T xu = xd / (T(1) + k1 * rd2 + k2 * rd4 + p1 * yd2); + T yu = yd / (T(1) + k1 * rd2 + k2 * rd4 + p2 * xd2); *normalized_x = xu * max_half_image_size / focal_length_x; *normalized_y = yu * max_half_image_size / focal_length_y; @@ -197,6 +202,8 @@ void ApplyNukeDistortionModel(const double focal_length_x, const int image_height, const double k1, const double k2, + const double p1, + const double p2, const double normalized_x, const double normalized_y, double* image_x, diff --git a/scripts/startup/bl_ui/space_clip.py b/scripts/startup/bl_ui/space_clip.py index de19d355d99..874cac24bc9 100644 --- a/scripts/startup/bl_ui/space_clip.py +++ b/scripts/startup/bl_ui/space_clip.py @@ -550,7 +550,7 @@ class CLIP_PT_tools_solve(CLIP_PT_tracking_panel, Panel): col.prop(settings, "refine_intrinsics_radial_distortion", text="Radial Distortion") row = col.row() - row.active = (camera.distortion_model == 'BROWN') + row.active = (camera.distortion_model in ('BROWN', 'NUKE')) row.prop(settings, "refine_intrinsics_tangential_distortion", text="Tangential Distortion") col = layout.column(align=True) @@ -940,6 +940,9 @@ class CLIP_PT_tracking_lens(Panel): col = layout.column(align=True) col.prop(camera, "nuke_k1") col.prop(camera, "nuke_k2") + col.separator() + col.prop(camera, "nuke_p1") + col.prop(camera, "nuke_p2") elif camera.distortion_model == 'BROWN': col = layout.column(align=True) col.prop(camera, "brown_k1") diff --git a/source/blender/blenkernel/intern/movieclip.cc b/source/blender/blenkernel/intern/movieclip.cc index f6773ed1e51..2dfffad5b34 100644 --- a/source/blender/blenkernel/intern/movieclip.cc +++ b/source/blender/blenkernel/intern/movieclip.cc @@ -682,6 +682,7 @@ struct MovieClipCache { float polynomial_k[3]; float division_k[2]; float nuke_k[2]; + float nuke_p[2]; float brown_k[4]; float brown_p[2]; short distortion_model; @@ -1114,6 +1115,9 @@ static bool check_undistortion_cache_flags(const MovieClip *clip) if (!equals_v2v2(&camera->nuke_k1, cache->postprocessed.nuke_k)) { return false; } + if (!equals_v2v2(&camera->nuke_p1, cache->postprocessed.nuke_p)) { + return false; + } if (!equals_v4v4(&camera->brown_k1, cache->postprocessed.brown_k)) { return false; @@ -1224,6 +1228,7 @@ static void put_postprocessed_frame_to_cache( copy_v3_v3(cache->postprocessed.polynomial_k, &camera->k1); copy_v2_v2(cache->postprocessed.division_k, &camera->division_k1); copy_v2_v2(cache->postprocessed.nuke_k, &camera->nuke_k1); + copy_v2_v2(cache->postprocessed.nuke_p, &camera->nuke_p1); copy_v4_v4(cache->postprocessed.brown_k, &camera->brown_k1); copy_v2_v2(cache->postprocessed.brown_p, &camera->brown_p1); cache->postprocessed.undistortion_used = true; diff --git a/source/blender/blenkernel/intern/tracking.cc b/source/blender/blenkernel/intern/tracking.cc index f81aaec11ca..934b683dd44 100644 --- a/source/blender/blenkernel/intern/tracking.cc +++ b/source/blender/blenkernel/intern/tracking.cc @@ -2214,7 +2214,8 @@ bool BKE_tracking_camera_distortion_equal(const MovieTrackingCamera *a, case TRACKING_DISTORTION_MODEL_DIVISION: return a->division_k1 == b->division_k1 && a->division_k2 == b->division_k2; case TRACKING_DISTORTION_MODEL_NUKE: - return a->nuke_k1 == b->nuke_k1 && a->nuke_k2 == b->nuke_k2; + return a->nuke_k1 == b->nuke_k1 && a->nuke_k2 == b->nuke_k2 && a->nuke_p1 == b->nuke_p1 && + a->nuke_p2 == b->nuke_p2; case TRACKING_DISTORTION_MODEL_BROWN: return a->brown_k1 == b->brown_k1 && a->brown_k2 == b->brown_k2 && a->brown_k3 == b->brown_k3 && a->brown_k4 == b->brown_k4 && @@ -2240,10 +2241,11 @@ uint64_t BKE_tracking_camera_distortion_hash(const MovieTrackingCamera *camera) float2(camera->principal_point), float2(camera->division_k1, camera->division_k2)); case TRACKING_DISTORTION_MODEL_NUKE: - return get_default_hash(camera->distortion_model, - float2(camera->pixel_aspect, camera->focal), - float2(camera->principal_point), - float2(camera->nuke_k1, camera->nuke_k2)); + return get_default_hash( + camera->distortion_model, + float2(camera->pixel_aspect, camera->focal), + float2(camera->principal_point), + float4(camera->nuke_k1, camera->nuke_k2, camera->nuke_p1, camera->nuke_p2)); case TRACKING_DISTORTION_MODEL_BROWN: return get_default_hash( float2(camera->pixel_aspect, camera->focal), diff --git a/source/blender/blenkernel/intern/tracking_util.cc b/source/blender/blenkernel/intern/tracking_util.cc index 517970ac640..35cce8d0ff6 100644 --- a/source/blender/blenkernel/intern/tracking_util.cc +++ b/source/blender/blenkernel/intern/tracking_util.cc @@ -420,6 +420,8 @@ static void distortion_model_parameters_from_tracking( camera_intrinsics_options->distortion_model = LIBMV_DISTORTION_MODEL_NUKE; camera_intrinsics_options->nuke_k1 = camera->nuke_k1; camera_intrinsics_options->nuke_k2 = camera->nuke_k2; + camera_intrinsics_options->nuke_p1 = camera->nuke_p1; + camera_intrinsics_options->nuke_p2 = camera->nuke_p2; return; case TRACKING_DISTORTION_MODEL_BROWN: camera_intrinsics_options->distortion_model = LIBMV_DISTORTION_MODEL_BROWN; @@ -463,6 +465,8 @@ static void distortion_model_parameters_from_options( camera->distortion_model = TRACKING_DISTORTION_MODEL_NUKE; camera->nuke_k1 = camera_intrinsics_options->nuke_k1; camera->nuke_k2 = camera_intrinsics_options->nuke_k2; + camera->nuke_p1 = camera_intrinsics_options->nuke_p1; + camera->nuke_p2 = camera_intrinsics_options->nuke_p2; return; case LIBMV_DISTORTION_MODEL_BROWN: camera->distortion_model = TRACKING_DISTORTION_MODEL_BROWN; diff --git a/source/blender/makesdna/DNA_tracking_types.h b/source/blender/makesdna/DNA_tracking_types.h index 27b3dece088..dcc9ef84845 100644 --- a/source/blender/makesdna/DNA_tracking_types.h +++ b/source/blender/makesdna/DNA_tracking_types.h @@ -68,6 +68,7 @@ typedef struct MovieTrackingCamera { /* Nuke distortion model coefficients */ float nuke_k1, nuke_k2; + float nuke_p1, nuke_p2; /* Brown-Conrady distortion model coefficients */ /** Brown-Conrady radial distortion. */ diff --git a/source/blender/makesrna/intern/rna_tracking.cc b/source/blender/makesrna/intern/rna_tracking.cc index 77b31f51e56..0c1ff7b51a0 100644 --- a/source/blender/makesrna/intern/rna_tracking.cc +++ b/source/blender/makesrna/intern/rna_tracking.cc @@ -1293,6 +1293,18 @@ static void rna_def_trackingCamera(BlenderRNA *brna) RNA_def_property_ui_text(prop, "K2", "Second coefficient of second order Nuke distortion"); RNA_def_property_update(prop, NC_MOVIECLIP | NA_EDITED, "rna_tracking_flushUpdate"); + prop = RNA_def_property(srna, "nuke_p1", PROP_FLOAT, PROP_NONE); + RNA_def_property_clear_flag(prop, PROP_ANIMATABLE); + RNA_def_property_ui_range(prop, -10, 10, 0.1, 3); + RNA_def_property_ui_text(prop, "P1", "First coefficient of tangential Nuke distortion"); + RNA_def_property_update(prop, NC_MOVIECLIP | NA_EDITED, "rna_tracking_flushUpdate"); + + prop = RNA_def_property(srna, "nuke_p2", PROP_FLOAT, PROP_NONE); + RNA_def_property_clear_flag(prop, PROP_ANIMATABLE); + RNA_def_property_ui_range(prop, -10, 10, 0.1, 3); + RNA_def_property_ui_text(prop, "P2", "Second coefficient of tangential Nuke distortion"); + RNA_def_property_update(prop, NC_MOVIECLIP | NA_EDITED, "rna_tracking_flushUpdate"); + /* Brown-Conrady distortion parameters */ prop = RNA_def_property(srna, "brown_k1", PROP_FLOAT, PROP_NONE); RNA_def_property_clear_flag(prop, PROP_ANIMATABLE);