Ejemplo n.º 1
0
def triangulate_nograd(intrinsics_data0,
                       intrinsics_data1,
                       rt_cam0_ref,
                       rt_cam0_ref_baseline,
                       rt_cam1_ref,
                       rt_ref_frame,
                       rt_ref_frame_baseline,
                       q,
                       lensmodel,
                       stabilize_coords=True):

    q = nps.atleast_dims(q, -3)

    rt01 = mrcal.compose_rt(rt_cam0_ref, mrcal.invert_rt(rt_cam1_ref))

    # all the v have shape (...,3)
    vlocal0 = \
        mrcal.unproject(q[...,0,:],
                        lensmodel, intrinsics_data0)
    vlocal1 = \
        mrcal.unproject(q[...,1,:],
                        lensmodel, intrinsics_data1)

    v0 = vlocal0
    v1 = \
        mrcal.rotate_point_r(rt01[:3], vlocal1)

    # The triangulated point in the perturbed camera-0 coordinate system.
    # Calibration-time perturbations move this coordinate system, so to get
    # a better estimate of the triangulation uncertainty, we try to
    # transform this to the original camera-0 coordinate system; the
    # stabilization path below does that.
    #
    # shape (..., 3)
    p_triangulated0 = \
        mrcal.triangulate_leecivera_mid2(v0, v1, rt01[3:])

    if not stabilize_coords:
        return p_triangulated0

    # Stabilization path. This uses the "true" solution, so I cannot do
    # this in the field. But I CAN do this in the randomized trials in
    # the test. And I can use the gradients to propagate the uncertainty
    # of this computation in the field
    #
    # Data flow:
    #   point_cam_perturbed -> point_ref_perturbed -> point_frames
    #   point_frames -> point_ref_baseline -> point_cam_baseline

    p_cam0_perturbed = p_triangulated0

    p_ref_perturbed = mrcal.transform_point_rt(rt_cam0_ref,
                                               p_cam0_perturbed,
                                               inverted=True)

    # shape (..., Nframes, 3)
    p_frames = \
        mrcal.transform_point_rt(rt_ref_frame,
                                 nps.dummy(p_ref_perturbed,-2),
                                 inverted = True)

    # shape (..., Nframes, 3)
    p_ref_baseline_all = mrcal.transform_point_rt(rt_ref_frame_baseline,
                                                  p_frames)

    # shape (..., 3)
    p_ref_baseline = np.mean(p_ref_baseline_all, axis=-2)

    # shape (..., 3)
    return mrcal.transform_point_rt(rt_cam0_ref_baseline, p_ref_baseline)
Ejemplo n.º 2
0
# in-place
Rt0_ref_copy = np.array(Rt0_ref)
Rt1_ref_copy = np.array(Rt1_ref)
Rt2 = mrcal.compose_Rt(Rt0_ref_copy, Rt1_ref_copy, out=Rt0_ref_copy)
confirm_equal(Rt2,
              compose_Rt(Rt0_ref, Rt1_ref),
              msg='compose_Rt result written in-place to Rt0')
Rt0_ref_copy = np.array(Rt0_ref)
Rt1_ref_copy = np.array(Rt1_ref)
Rt2 = mrcal.compose_Rt(Rt0_ref_copy, Rt1_ref_copy, out=Rt1_ref_copy)
confirm_equal(Rt2,
              compose_Rt(Rt0_ref, Rt1_ref),
              msg='compose_Rt result written in-place to Rt1')

rt2 = mrcal.compose_rt(rt0_ref, rt1_ref, out=out6)
confirm_equal(rt2, compose_rt(rt0_ref, rt1_ref), msg='compose_rt result')

# in-place
rt0_ref_copy = np.array(rt0_ref)
rt1_ref_copy = np.array(rt1_ref)
rt2 = mrcal.compose_rt(rt0_ref_copy, rt1_ref_copy, out=rt0_ref_copy)
confirm_equal(rt2,
              compose_rt(rt0_ref, rt1_ref),
              msg='compose_rt result written in-place to rt0')
rt0_ref_copy = np.array(rt0_ref)
rt1_ref_copy = np.array(rt1_ref)
rt2 = mrcal.compose_rt(rt0_ref_copy, rt1_ref_copy, out=rt1_ref_copy)
confirm_equal(rt2,
              compose_rt(rt0_ref, rt1_ref),
              msg='compose_rt result written in-place to rt1')
Ejemplo n.º 3
0
testdir = os.path.dirname(os.path.realpath(__file__))

# I import the LOCAL mrcal since that's what I'm testing
sys.path[:0] = f"{testdir}/..",
import mrcal
import scipy.interpolate
import testutils


model0 = mrcal.cameramodel(f"{testdir}/data/cam0.opencv8.cameramodel")
model1 = mrcal.cameramodel(model0)

for lensmodel in ('LENSMODEL_LATLON', 'LENSMODEL_PINHOLE'):
    # I create geometries to test. First off, a vanilla geometry for left-right stereo
    rt01 = np.array((0,0,0,  3.0, 0, 0))
    model1.extrinsics_rt_toref( mrcal.compose_rt(model0.extrinsics_rt_toref(),
                                                 rt01))

    az_fov_deg = 90
    el_fov_deg = 50
    models_rectified = \
        mrcal.rectified_system( (model0, model1),
                                az_fov_deg = az_fov_deg,
                                el_fov_deg = el_fov_deg,
                                pixels_per_deg_az = -1./8.,
                                pixels_per_deg_el = -1./4.,
                                rectification_model = lensmodel)
    az0 = 0.
    el0 = 0.

    try:
        mrcal.stereo._validate_models_rectified(models_rectified)
Ejemplo n.º 4
0
observations = observations[i, ...]
indices_frame_camintrinsics_camextrinsics = indices_frame_camintrinsics_camextrinsics[
    i, ...]
paths = [paths[_] for _ in i]

# reference models
models = [
    mrcal.cameramodel(m) for m in (
        f"{testdir}/data/cam0.opencv8.cameramodel",
        f"{testdir}/data/cam1.opencv8.cameramodel",
    )
]

lensmodel = models[0].intrinsics()[0]
intrinsics_data = nps.cat(models[0].intrinsics()[1], models[1].intrinsics()[1])
extrinsics_rt_fromref = mrcal.compose_rt(models[1].extrinsics_rt_fromref(),
                                         models[0].extrinsics_rt_toref())

imagersizes = nps.cat(models[0].imagersize(), models[1].imagersize())
# I now have the "right" camera parameters. I don't have the frames or points,
# but it's fine to just make them up. This is a regression test.
frames_rt_toref = linspace_shaped(3, 6)
frames_rt_toref[:, 5] += 5  # push them back

indices_point_camintrinsics_camextrinsics = \
    np.array(((0,1,-1),
              (1,0,-1),
              (1,1, 0),
              (2,0,-1),
              (2,1, 0)),
             dtype = np.int32)
Ejemplo n.º 5
0
testutils.confirm_equal(R_fit_vectors,
                        R,
                        eps=1e-2,
                        msg='Procrustes fit R (vectors)')

testutils.confirm_equal(mrcal.invert_Rt(
    mrcal.Rt_from_rt(mrcal.invert_rt(mrcal.rt_from_Rt(Rt)))),
                        Rt,
                        msg='Rt/rt and invert')

testutils.confirm_equal(mrcal.compose_Rt(Rt, mrcal.invert_Rt(Rt)),
                        nps.glue(np.eye(3), np.zeros((3, )), axis=-2),
                        msg='compose_Rt')

testutils.confirm_equal(mrcal.compose_rt(mrcal.rt_from_Rt(Rt),
                                         mrcal.invert_rt(
                                             mrcal.rt_from_Rt(Rt))),
                        np.zeros((6, )),
                        msg='compose_rt')

testutils.confirm_equal(mrcal.identity_Rt(),
                        nps.glue(np.eye(3), np.zeros((3, )), axis=-2),
                        msg='identity_Rt')

testutils.confirm_equal(mrcal.identity_rt(),
                        np.zeros((6, )),
                        msg='identity_rt')

testutils.confirm_equal(mrcal.transform_point_Rt(Rt, p),
                        Tp,
                        msg='transform_point_Rt')
Ejemplo n.º 6
0
def calibration_baseline(model,
                         Ncameras,
                         Nframes,
                         extra_observation_at,
                         object_width_n,
                         object_height_n,
                         object_spacing,
                         extrinsics_rt_fromref_true,
                         calobject_warp_true,
                         fixedframes,
                         testdir,
                         cull_left_of_center=False,
                         allow_nonidentity_cam0_transform=False,
                         range_to_boards=4.0):
    r'''Compute a calibration baseline as a starting point for experiments

This is a perfect, noiseless solve. Regularization IS enabled, and the returned
model is at the optimization optimum. So the returned models will not sit
exactly at the ground-truth.

NOTE: if not fixedframes: the ref frame in the returned
optimization_inputs_baseline is NOT the ref frame used by the returned
extrinsics and frames arrays. The arrays in optimization_inputs_baseline had to
be transformed to reference off camera 0. If the extrinsics of camera 0 are the
identity, then the two ref coord systems are the same. To avoid accidental bugs,
we have a kwarg allow_nonidentity_cam0_transform, which defaults to False. if
not allow_nonidentity_cam0_transform and norm(extrinsics_rt_fromref_true[0]) >
0: raise

This logic is here purely for safety. A caller that handles non-identity cam0
transforms has to explicitly say that

ARGUMENTS

- model: string. 'opencv4' or 'opencv8' or 'splined'

- ...

    '''

    if re.match('opencv', model):
        models_true = (
            mrcal.cameramodel(f"{testdir}/data/cam0.opencv8.cameramodel"),
            mrcal.cameramodel(f"{testdir}/data/cam0.opencv8.cameramodel"),
            mrcal.cameramodel(f"{testdir}/data/cam1.opencv8.cameramodel"),
            mrcal.cameramodel(f"{testdir}/data/cam1.opencv8.cameramodel"))

        if model == 'opencv4':
            # I have opencv8 models_true, but I truncate to opencv4 models_true
            for m in models_true:
                m.intrinsics(intrinsics=('LENSMODEL_OPENCV4',
                                         m.intrinsics()[1][:8]))
    elif model == 'splined':
        models_true = (
            mrcal.cameramodel(f"{testdir}/data/cam0.splined.cameramodel"),
            mrcal.cameramodel(f"{testdir}/data/cam0.splined.cameramodel"),
            mrcal.cameramodel(f"{testdir}/data/cam1.splined.cameramodel"),
            mrcal.cameramodel(f"{testdir}/data/cam1.splined.cameramodel"))
    else:
        raise Exception("Unknown lens being tested")

    models_true = models_true[:Ncameras]
    lensmodel = models_true[0].intrinsics()[0]
    Nintrinsics = mrcal.lensmodel_num_params(lensmodel)

    for i in range(Ncameras):
        models_true[i].extrinsics_rt_fromref(extrinsics_rt_fromref_true[i])

    if not allow_nonidentity_cam0_transform and \
       nps.norm2(extrinsics_rt_fromref_true[0]) > 0:
        raise Exception(
            "A non-identity cam0 transform was given, but the caller didn't explicitly say that they support this"
        )

    imagersizes = nps.cat(*[m.imagersize() for m in models_true])

    # These are perfect
    intrinsics_true = nps.cat(*[m.intrinsics()[1] for m in models_true])
    extrinsics_true_mounted = nps.cat(
        *[m.extrinsics_rt_fromref() for m in models_true])
    x_center = -(Ncameras - 1) / 2.

    # shapes (Nframes, Ncameras, Nh, Nw, 2),
    #        (Nframes, 4,3)
    q_true,Rt_ref_board_true = \
        mrcal.synthesize_board_observations(models_true,
                                            object_width_n, object_height_n, object_spacing,
                                            calobject_warp_true,
                                            np.array((0.,             0.,             0.,             x_center, 0,   range_to_boards)),
                                            np.array((np.pi/180.*30., np.pi/180.*30., np.pi/180.*20., 2.5,      2.5, range_to_boards/2.0)),
                                            Nframes)

    if extra_observation_at is not None:
        q_true_extra,Rt_ref_board_true_extra = \
            mrcal.synthesize_board_observations(models_true,
                                                object_width_n, object_height_n, object_spacing,
                                                calobject_warp_true,
                                                np.array((0.,             0.,             0.,             x_center, 0,   extra_observation_at)),
                                                np.array((np.pi/180.*30., np.pi/180.*30., np.pi/180.*20., 2.5,      2.5, extra_observation_at/10.0)),
                                                Nframes = 1)

        q_true = nps.glue(q_true, q_true_extra, axis=-5)
        Rt_ref_board_true = nps.glue(Rt_ref_board_true,
                                     Rt_ref_board_true_extra,
                                     axis=-3)

        Nframes += 1

    frames_true = mrcal.rt_from_Rt(Rt_ref_board_true)

    ############# I have perfect observations in q_true.
    # weight has shape (Nframes, Ncameras, Nh, Nw),
    weight01 = (np.random.rand(*q_true.shape[:-1]) + 1.) / 2.  # in [0,1]
    weight0 = 0.2
    weight1 = 1.0
    weight = weight0 + (weight1 - weight0) * weight01

    if cull_left_of_center:

        imagersize = models_true[0].imagersize()
        for m in models_true[1:]:
            if np.any(m.imagersize() - imagersize):
                raise Exception(
                    "I'm assuming all cameras have the same imager size, but this is false"
                )

        weight[q_true[..., 0] < imagersize[0] / 2.] /= 1000.

    # I want observations of shape (Nframes*Ncameras, Nh, Nw, 3) where each row is
    # (x,y,weight)
    observations_true = nps.clump(nps.glue(q_true,
                                           nps.dummy(weight, -1),
                                           axis=-1),
                                  n=2)

    # Dense observations. All the cameras see all the boards
    indices_frame_camera = np.zeros((Nframes * Ncameras, 2), dtype=np.int32)
    indices_frame = indices_frame_camera[:, 0].reshape(Nframes, Ncameras)
    indices_frame.setfield(nps.outer(np.arange(Nframes, dtype=np.int32),
                                     np.ones((Ncameras, ), dtype=np.int32)),
                           dtype=np.int32)
    indices_camera = indices_frame_camera[:, 1].reshape(Nframes, Ncameras)
    indices_camera.setfield(nps.outer(np.ones((Nframes, ), dtype=np.int32),
                                      np.arange(Ncameras, dtype=np.int32)),
                            dtype=np.int32)

    indices_frame_camintrinsics_camextrinsics = \
        nps.glue(indices_frame_camera,
                 indices_frame_camera[:,(1,)],
                 axis=-1)
    if not fixedframes:
        indices_frame_camintrinsics_camextrinsics[:, 2] -= 1

    ###########################################################################
    # p = mrcal.show_geometry(models_true,
    #                         frames          = frames_true,
    #                         object_width_n  = object_width_n,
    #                         object_height_n = object_height_n,
    #                         object_spacing  = object_spacing)
    # sys.exit()

    # I now reoptimize the perfect-observations problem. Without regularization,
    # this is a no-op: I'm already at the optimum. With regularization, this will
    # move us a certain amount (that the test will evaluate). Then I look at
    # noise-induced motions off this optimization optimum
    optimization_inputs_baseline = \
        dict( intrinsics                                = copy.deepcopy(intrinsics_true),
              points                                    = None,
              observations_board                        = observations_true,
              indices_frame_camintrinsics_camextrinsics = indices_frame_camintrinsics_camextrinsics,
              observations_point                        = None,
              indices_point_camintrinsics_camextrinsics = None,
              lensmodel                                 = lensmodel,
              calobject_warp                            = copy.deepcopy(calobject_warp_true),
              imagersizes                               = imagersizes,
              calibration_object_spacing                = object_spacing,
              verbose                                   = False,
              do_optimize_frames                        = not fixedframes,
              do_optimize_intrinsics_core               = False if model =='splined' else True,
              do_optimize_intrinsics_distortions        = True,
              do_optimize_extrinsics                    = True,
              do_optimize_calobject_warp                = True,
              do_apply_regularization                   = True,
              do_apply_outlier_rejection                = False)

    if fixedframes:
        # Frames are fixed: each camera has an independent pose
        optimization_inputs_baseline['extrinsics_rt_fromref'] = \
            copy.deepcopy(extrinsics_true_mounted)
        optimization_inputs_baseline['frames_rt_toref'] = copy.deepcopy(
            frames_true)
    else:
        # Frames are NOT fixed: cam0 is fixed as the reference coord system. I
        # transform each optimization extrinsics vector to be relative to cam0
        optimization_inputs_baseline['extrinsics_rt_fromref'] = \
            mrcal.compose_rt(extrinsics_true_mounted[1:,:],
                             mrcal.invert_rt(extrinsics_true_mounted[0,:]))
        optimization_inputs_baseline['frames_rt_toref'] = \
            mrcal.compose_rt(extrinsics_true_mounted[0,:], frames_true)

    mrcal.optimize(**optimization_inputs_baseline)

    models_baseline = \
        [ mrcal.cameramodel( optimization_inputs = optimization_inputs_baseline,
                             icam_intrinsics     = i) \
          for i in range(Ncameras) ]

    return                                                     \
        optimization_inputs_baseline,                          \
        models_true, models_baseline,                          \
        indices_frame_camintrinsics_camextrinsics,             \
        lensmodel, Nintrinsics, imagersizes,                   \
        intrinsics_true, extrinsics_true_mounted, frames_true, \
        observations_true,                                     \
        Nframes
Ejemplo n.º 7
0
Rt = mrcal.invert_Rt(Rt0_ref, out=out43)
confirm_equal(Rt, invert_Rt(Rt0_ref), msg='invert_Rt result')

rt = mrcal.invert_rt(rt0_ref, out=out6)
confirm_equal(rt, invert_rt(rt0_ref), msg='invert_rt result')

rt, drt_drt = mrcal.invert_rt(rt0_ref, get_gradients=True, out=(out6, out66))
drt_drt_ref = grad(invert_rt, rt0_ref)
confirm_equal(rt, invert_rt(rt0_ref), msg='invert_rt with grad result')
confirm_equal(drt_drt, drt_drt_ref, msg='invert_rt drt/drt result')

Rt2 = mrcal.compose_Rt(Rt0_ref, Rt1_ref, out=out43)
confirm_equal(Rt2, compose_Rt(Rt0_ref, Rt1_ref), msg='compose_Rt result')

rt2 = mrcal.compose_rt(rt0_ref, rt1_ref, out=out6)
confirm_equal(rt2, compose_rt(rt0_ref, rt1_ref), msg='compose_rt result')

# _compose_rt() is not excercised by the python library, so I explicitly test it
# here
rt2 = _poseutils._compose_rt(rt0_ref, rt1_ref, out=out6)
confirm_equal(rt2,
              compose_rt(rt0_ref, rt1_ref),
              msg='compose_rt result; calling _compose_rt() directly')

rt2,drt2_drt0,drt2_drt1 = \
    mrcal.compose_rt(rt0_ref, rt1_ref, get_gradients=True,
                     out = (out6, out66, out66a))

drt2_drt0_ref = grad(lambda rt0: compose_rt(rt0, rt1_ref), rt0_ref)
drt2_drt1_ref = grad(lambda rt1: compose_rt(rt0_ref, rt1), rt1_ref)
Ejemplo n.º 8
0
    print(f"Usage: {sys.argv[0]} model image yaw_deg what", file=sys.stderr)
    sys.exit(1)

# I want a pinhole model to cover the middle 1/3rd of my pixels
W, H = model.imagersize()
fit_points = \
    np.array((( W/3.,    H/3.),
              ( W*2./3., H/3.),
              ( W/3.,    H*2./3.),
              ( W*2./3., H*2./3.)))

model_pinhole = \
    mrcal.pinhole_model_for_reprojection(model,
                                         fit         = fit_points,
                                         scale_image = 0.5)

# yaw transformation: pure rotation around the y axis
rt_yaw = np.array((0., yaw_deg * np.pi / 180., 0, 0, 0, 0))

# apply the extra yaw transformation to my extrinsics
model_pinhole.extrinsics_rt_toref( \
    mrcal.compose_rt(model_pinhole.extrinsics_rt_toref(),
                     rt_yaw) )

mapxy = mrcal.image_transformation_map(model, model_pinhole, use_rotation=True)

image_transformed = mrcal.transform_image(image, mapxy)

cv2.imwrite(f'/tmp/narrow-{what}.jpg', image_transformed)
model_pinhole.write(f'/tmp/pinhole-narrow-yawed-{what}.cameramodel')
Ejemplo n.º 9
0
def _triangulate(# shape (Ncameras, Nintrinsics)
                 intrinsics_data,
                 # shape (Ncameras, 6)
                 rt_cam_ref,
                 # shape (Nframes,6),
                 rt_ref_frame, rt_ref_frame_true,
                 # shape (..., Ncameras, 2)
                 q,

                 lensmodel,
                 stabilize_coords,
                 get_gradients):

    if not ( intrinsics_data.ndim == 2 and intrinsics_data.shape[0] == 2 and \
             rt_cam_ref.shape == (2,6) and \
             rt_ref_frame.ndim == 2 and rt_ref_frame.shape[-1] == 6 and \
             q.shape[-2:] == (2,2 ) ):
        raise Exception("Arguments must have a consistent Ncameras == 2")

    # I now compute the same triangulation, but just at the un-perturbed baseline,
    # and keeping track of all the gradients
    rt0r = rt_cam_ref[0]
    rt1r = rt_cam_ref[1]

    if not get_gradients:

        rtr1          = mrcal.invert_rt(rt1r)
        rt01_baseline = mrcal.compose_rt(rt0r, rtr1)

        # all the v have shape (...,3)
        vlocal0 = \
            mrcal.unproject(q[...,0,:],
                            lensmodel, intrinsics_data[0])
        vlocal1 = \
            mrcal.unproject(q[...,1,:],
                            lensmodel, intrinsics_data[1])

        v0 = vlocal0
        v1 = \
            mrcal.rotate_point_r(rt01_baseline[:3], vlocal1)

        # p_triangulated has shape (..., 3)
        p_triangulated = \
            mrcal.triangulate_leecivera_mid2(v0, v1, rt01_baseline[3:])

        if stabilize_coords:

            # shape (..., Nframes, 3)
            p_frames_new = \
                mrcal.transform_point_rt(mrcal.invert_rt(rt_ref_frame),
                                         nps.dummy(p_triangulated,-2))

            # shape (..., Nframes, 3)
            p_refs = mrcal.transform_point_rt(rt_ref_frame_true, p_frames_new)

            # shape (..., 3)
            p_triangulated = np.mean(p_refs, axis=-2)

        return p_triangulated
    else:
        rtr1,drtr1_drt1r = mrcal.invert_rt(rt1r,
                                           get_gradients=True)
        rt01_baseline,drt01_drt0r, drt01_drtr1 = mrcal.compose_rt(rt0r, rtr1, get_gradients=True)

        # all the v have shape (...,3)
        vlocal0, dvlocal0_dq0, dvlocal0_dintrinsics0 = \
            mrcal.unproject(q[...,0,:],
                            lensmodel, intrinsics_data[0],
                            get_gradients = True)
        vlocal1, dvlocal1_dq1, dvlocal1_dintrinsics1 = \
            mrcal.unproject(q[...,1,:],
                            lensmodel, intrinsics_data[1],
                            get_gradients = True)

        v0 = vlocal0
        v1, dv1_dr01, dv1_dvlocal1 = \
            mrcal.rotate_point_r(rt01_baseline[:3], vlocal1,
                                 get_gradients=True)

        # p_triangulated has shape (..., 3)
        p_triangulated, dp_triangulated_dv0, dp_triangulated_dv1, dp_triangulated_dt01 = \
            mrcal.triangulate_leecivera_mid2(v0, v1, rt01_baseline[3:],
                                             get_gradients = True)

        shape_leading = dp_triangulated_dv0.shape[:-2]

        dp_triangulated_dq = np.zeros(shape_leading + (3,) + q.shape[-2:], dtype=float)
        nps.matmult( dp_triangulated_dv0,
                     dvlocal0_dq0,
                     out = dp_triangulated_dq[..., 0, :])
        nps.matmult( dp_triangulated_dv1,
                     dv1_dvlocal1,
                     dvlocal1_dq1,
                     out = dp_triangulated_dq[..., 1, :])

        Nframes = len(rt_ref_frame)

        if stabilize_coords:

            # shape (Nframes,6)
            rt_frame_ref, drtfr_drtrf = \
                mrcal.invert_rt(rt_ref_frame, get_gradients=True)

            # shape (Nframes,6)
            rt_true_shifted, _, drt_drtfr = \
                mrcal.compose_rt(rt_ref_frame_true, rt_frame_ref,
                                 get_gradients=True)

            # shape (..., Nframes, 3)
            p_refs,dprefs_drt,dprefs_dptriangulated = \
                mrcal.transform_point_rt(rt_true_shifted,
                                         nps.dummy(p_triangulated,-2),
                                         get_gradients = True)

            # shape (..., 3)
            p_triangulated = np.mean(p_refs, axis=-2)

            # I have dpold/dx. dpnew/dx = dpnew/dpold dpold/dx

            # shape (...,3,3)
            dpnew_dpold = np.mean(dprefs_dptriangulated, axis=-3)
            dp_triangulated_dv0  = nps.matmult(dpnew_dpold, dp_triangulated_dv0)
            dp_triangulated_dv1  = nps.matmult(dpnew_dpold, dp_triangulated_dv1)
            dp_triangulated_dt01 = nps.matmult(dpnew_dpold, dp_triangulated_dt01)
            dp_triangulated_dq   = nps.xchg(nps.matmult( dpnew_dpold,
                                                         nps.xchg(dp_triangulated_dq,
                                                                  -2,-3)),
                                            -2,-3)

            # shape (..., Nframes,3,6)
            dp_triangulated_drtrf = \
                nps.matmult(dprefs_drt, drt_drtfr, drtfr_drtrf) / Nframes
        else:
            dp_triangulated_drtrf = np.zeros(shape_leading + (Nframes,3,6), dtype=float)

        return \
            p_triangulated, \
            drtr1_drt1r, \
            drt01_drt0r, drt01_drtr1, \
            dvlocal0_dintrinsics0, dvlocal1_dintrinsics1, \
            dv1_dr01, dv1_dvlocal1, \
            dp_triangulated_dv0, dp_triangulated_dv1, dp_triangulated_dt01, \
            dp_triangulated_drtrf, \
            dp_triangulated_dq