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
This commit is contained in:
David Murmann 2025-11-03 11:08:32 +01:00 • committed by Sergey Sharybin
parent 2d43ab553c
commit 11e8f332b5
13 changed files with 107 additions and 22 deletions

View file

@ -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<NukeCameraIntrinsics*>(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;
}

View file

@ -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;

View file

@ -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,

View file

@ -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<const NukeCameraIntrinsics*>(&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: {

View file

@ -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.
//

View file

@ -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;

View file

@ -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,

View file

@ -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")

View file

@ -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;

View file

@ -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),

View file

@ -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;

View file

@ -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. */

View file

@ -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);