Beispiel #1
0
def hypothesis_board_corner_positions(icam_intrinsics=None,
                                      idx_inliers=None,
                                      **optimization_inputs):
    r'''Reports the 3D chessboard points observed by a camera at calibration time

SYNOPSIS

    model = mrcal.cameramodel("xxx.cameramodel")

    optimization_inputs = model.optimization_inputs()

    # shape (Nobservations, Nheight, Nwidth, 3)
    pcam = mrcal.hypothesis_board_corner_positions(**optimization_inputs)[0]

    i_intrinsics = \
      optimization_inputs['indices_frame_camintrinsics_camextrinsics'][:,1]

    # shape (Nobservations,1,1,Nintrinsics)
    intrinsics = nps.mv(optimization_inputs['intrinsics'][i_intrinsics],-2,-4)

    optimization_inputs['observations_board'][...,:2] = \
        mrcal.project( pcam,
                       optimization_inputs['lensmodel'],
                       intrinsics )

    # optimization_inputs now contains perfect, noiseless board observations

    x = mrcal.optimizer_callback(**optimization_inputs)[1]
    print(nps.norm2(x[:mrcal.num_measurements_boards(**optimization_inputs)]))
    ==>
    0

The optimization routine generates hypothetical observations from a set of
parameters being evaluated, trying to match these hypothetical observations to
real observations. To facilitate analysis, this routine returns these
hypothetical coordinates of the chessboard corners being observed. This routine
reports the 3D points in the coordinate system of the observing camera.

The hypothetical points are constructed from

- The calibration object geometry
- The calibration object-reference transformation in
  optimization_inputs['frames_rt_toref']
- The camera extrinsics (reference-camera transformation) in
  optimization_inputs['extrinsics_rt_fromref']
- The table selecting the camera and calibration object frame for each
  observation in
  optimization_inputs['indices_frame_camintrinsics_camextrinsics']

ARGUMENTS

- icam_intrinsics: optional integer specifying which intrinsic camera in the
  optimization_inputs we're looking at. If omitted (or None), we report
  camera-coordinate points for all the cameras

- idx_inliers: optional numpy array of booleans of shape
  (Nobservations,object_height,object_width) to select the outliers manually. If
  omitted (or None), the outliers are selected automatically: idx_inliers =
  observations_board[...,2] > 0. This argument is available to pick common
  inliers from two separate solves.

- **optimization_inputs: a dict() of arguments passable to mrcal.optimize() and
  mrcal.optimizer_callback(). We use the geometric data. This dict is obtainable
  from a cameramodel object by calling cameramodel.optimization_inputs()

RETURNED VALUE

- An array of shape (Nobservations, Nheight, Nwidth, 3) containing the
  coordinates (in the coordinate system of each camera) of the chessboard
  corners, for ALL the cameras. These correspond to the observations in
  optimization_inputs['observations_board'], which also have shape
  (Nobservations, Nheight, Nwidth, 3)

- An array of shape (Nobservations_thiscamera, Nheight, Nwidth, 3) containing
  the coordinates (in the camera coordinate system) of the chessboard corners,
  for the particular camera requested in icam_intrinsics. If icam_intrinsics is
  None: this is the same array as the previous returned value

- an (N,3) array containing camera-frame 3D points observed at calibration time,
  and accepted by the solver as inliers. This is a subset of the 2nd returned
  array.

- an (N,3) array containing camera-frame 3D points observed at calibration time,
  but rejected by the solver as outliers. This is a subset of the 2nd returned
  array.

    '''

    observations_board = optimization_inputs.get('observations_board')
    if observations_board is None:
        return Exception("No board observations available")

    indices_frame_camintrinsics_camextrinsics = \
        optimization_inputs['indices_frame_camintrinsics_camextrinsics']

    object_width_n = observations_board.shape[-2]
    object_height_n = observations_board.shape[-3]
    object_spacing = optimization_inputs['calibration_object_spacing']
    calobject_warp = optimization_inputs.get('calobject_warp')
    # shape (Nh,Nw,3)
    full_object = mrcal.ref_calibration_object(object_width_n, object_height_n,
                                               object_spacing, calobject_warp)
    frames_Rt_toref = \
        mrcal.Rt_from_rt( optimization_inputs['frames_rt_toref'] )\
        [ indices_frame_camintrinsics_camextrinsics[:,0] ]
    extrinsics_Rt_fromref = \
        nps.glue( mrcal.identity_Rt(),
                  mrcal.Rt_from_rt(optimization_inputs['extrinsics_rt_fromref']),
                  axis = -3 ) \
        [ indices_frame_camintrinsics_camextrinsics[:,2]+1 ]

    Rt_cam_frame = mrcal.compose_Rt(extrinsics_Rt_fromref, frames_Rt_toref)

    p_cam_calobjects = \
        mrcal.transform_point_Rt(nps.mv(Rt_cam_frame,-3,-5), full_object)

    # shape (Nobservations,Nheight,Nwidth)
    if idx_inliers is None:
        idx_inliers = observations_board[..., 2] > 0
    idx_outliers = ~idx_inliers

    if icam_intrinsics is None:
        return                                       \
            p_cam_calobjects,                        \
            p_cam_calobjects,                        \
            p_cam_calobjects[idx_inliers,      ...], \
            p_cam_calobjects[idx_outliers,     ...]

    # The user asked for a specific camera. Separate out its data

    # shape (Nobservations,)
    idx_observations = indices_frame_camintrinsics_camextrinsics[:,
                                                                 1] == icam_intrinsics

    idx_inliers[~idx_observations] = False
    idx_outliers[~idx_observations] = False

    return                                       \
        p_cam_calobjects,                        \
        p_cam_calobjects[idx_observations, ...], \
        p_cam_calobjects[idx_inliers,      ...], \
        p_cam_calobjects[idx_outliers,     ...]
Beispiel #2
0
out333 = base[:3, :3, :3, 3]
out343 = base[:3, 3:7, :3, 3]
out43 = base[2, 4:8, :3, 2]
out33 = base[2, 8:11, :3, 2]
out33a = base[3, :3, :3, 2]
out33b = base[3, 3:6, :3, 2]
out33c = base[3, 6:9, :3, 2]
out33d = base[4, :3, :3, 2]
out36 = base[6, :3, :6, 2]
out3 = base[1, 3, 6:9, 1]
out6 = base[1, 4, :6, 1]
out66 = base[5, 3:9, 3:9, 2]
out66a = base[6, 3:9, 3:9, 2]

confirm_equal(mrcal.identity_R(out=out33), np.eye(3), msg='identity_R')
confirm_equal(mrcal.identity_Rt(out=out43),
              nps.glue(np.eye(3), np.zeros((3, ), ), axis=-2),
              msg='identity_Rt')
confirm_equal(mrcal.identity_r(out=out3), np.zeros((3, )), msg='identity_r')
confirm_equal(mrcal.identity_rt(out=out6), np.zeros((6, )), msg='identity_rt')

################# rotate_point_R
y = \
    mrcal.rotate_point_R(R0_ref, x, out = out3)
confirm_equal(y,
              nps.matmult(x, nps.transpose(R0_ref)),
              msg='rotate_point_R result')

y, J_R, J_x = \
    mrcal.rotate_point_R(R0_ref, x, get_gradients=True,
                         out = (out3,out333,out33))
Beispiel #3
0
                          msg=f'Generated models pass validation ({lensmodel})')
    except:
        testutils.confirm(False,
                          msg=f'Generated models pass validation ({lensmodel})')

    Rt_cam0_rect = mrcal.compose_Rt( model0.extrinsics_Rt_fromref(),
                                     models_rectified[0].extrinsics_Rt_toref())
    Rt01_rectified = mrcal.compose_Rt( models_rectified[0].extrinsics_Rt_fromref(),
                                       models_rectified[1].extrinsics_Rt_toref())

    testutils.confirm_equal(models_rectified[0].intrinsics()[0], lensmodel,
                            msg=f'model0 has the right lensmodel ({lensmodel})')
    testutils.confirm_equal(models_rectified[1].intrinsics()[0], lensmodel,
                            msg=f'model1 has the right lensmodel ({lensmodel})')

    testutils.confirm_equal(Rt_cam0_rect, mrcal.identity_Rt(),
                            msg=f'vanilla stereo has a vanilla geometry ({lensmodel})')

    testutils.confirm_equal( Rt01_rectified[3,0],
                             nps.mag(rt01[3:]),
                             msg=f'vanilla stereo: baseline ({lensmodel})')

    Naz,Nel = models_rectified[0].imagersize()

    q0 = np.array(((Naz-1.)/2., (Nel-1.)/2.))
    v0   = mrcal.unproject(q0, *models_rectified[0].intrinsics(), normalize=True)

    if lensmodel == 'LENSMODEL_LATLON':
        v0_rect = mrcal.unproject_latlon(np.array((az0, el0)))
        # already normalized
        testutils.confirm_equal( v0_rect, v0,
Beispiel #4
0
def synthesize_board_observations(models,
                                  object_width_n,
                                  object_height_n,
                                  object_spacing,
                                  calobject_warp,
                                  rt_ref_boardcenter,
                                  rt_ref_boardcenter__noiseradius,
                                  Nframes,
                                  which='all-cameras-must-see-full-board'):
    r'''Produce synthetic chessboard observations

SYNOPSIS

    models = [mrcal.cameramodel("0.cameramodel"),
              mrcal.cameramodel("1.cameramodel"),]

    # shapes (Nframes, Ncameras, object_height_n, object_width_n, 2) and
    #        (Nframes, 4, 3)
    q,Rt_ref_boardref = \
        mrcal.synthesize_board_observations( \
          models,

          # board geometry
          10,12,0.1,None,

          # mean board pose and the radius of the added uniform noise
          rt_ref_boardcenter,
          rt_ref_boardcenter__noiseradius,

          # How many frames we want
          100,

          which = 'some-cameras-must-see-half-board')

    # q now contains the synthetic pixel observations, but some of them will be
    # out of view. I construct an (x,y,weight) observations array, as expected
    # by the optimizer, and I set the weight for the out-of-view points to -1 to
    # tell the optimizer to ignore those points


    # Set the weights to 1 initially
    # shape (Nframes, Ncameras, object_height_n, object_width_n, 3)
    observations = nps.glue(q,
                            np.ones( q.shape[:-1] + (1,) ),
                            axis = -1)

    # shape (Ncameras, 1, 1, 2)
    imagersizes = nps.mv( nps.cat(*[ m.imagersize() for m in models ]),
                          -2, -4 )

    observations[ np.any( q              < 0, axis=-1 ), 2 ] = -1.
    observations[ np.any( q-imagersizes >= 0, axis=-1 ), 2 ] = -1.

Given a description of a calibration object and of the cameras observing it,
produces perfect pixel observations of the objects by those cameras. We return a
dense observation array: every corner observation from every chessboard pose
will be reported for every camera. Some of these observations MAY be
out-of-view, depending on the value of the 'which' argument; see description
below. The example above demonstrates how to mark such out-of-bounds
observations as outliers to tell the optimization to ignore these.

The "models" provides the intrinsics and extrinsics.

The calibration objects are nominally have pose rt_ref_boardcenter in the
reference coordinate system, with each pose perturbed uniformly with radius
rt_ref_boardcenter__noiseradius. This is nonstandard since here I'm placing the
board origin at its center instead of the corner (as
mrcal.ref_calibration_object() does). But this is more appropriate to the usage
of this function. The returned Rt_ref_boardref transformation DOES use the
normal corner-referenced board geometry

Returns the point observations and the chessboard poses that produced these
observations.

ARGUMENTS

- models: an array of mrcal.cameramodel objects, one for each camera we're
  simulating. This is the intrinsics and the extrinsics. Ncameras = len(models)

- object_width_n:  the number of horizontal points in the calibration object grid

- object_height_n: the number of vertical points in the calibration object grid

- object_spacing: the distance between adjacent points in the calibration
  object. A square object is assumed, so the vertical and horizontal distances
  are assumed to be identical.

- calobject_warp: a description of the calibration board warping. None means "no
  warping": the object is flat. Otherwise this is an array of shape (2,). See
  the docs for ref_calibration_object() for the meaning of the values in this
  array.

- rt_ref_boardcenter: the nominal pose of the calibration object, in the
  reference coordinate system. This is an rt transformation from a
  center-referenced calibration object to the reference coordinate system

- rt_ref_boardcenter__noiseradius: the deviation-from-nominal for the chessboard
  pose for each frame. I add uniform noise to rt_ref_boardcenter, with each
  element sampled independently, with the radius given here.

- Nframes: how many frames of observations to return

- which: a string, defaulting to 'all-cameras-must-see-full-board'. Controls the
  requirements on the visibility of the returned points. Valid values:

  - 'all-cameras-must-see-full-board': We return only those chessboard poses
    that produce observations that are FULLY visible by ALL the cameras.

  - 'some-cameras-must-see-full-board': We return only those chessboard poses
    that produce observations that are FULLY visible by AT LEAST ONE camera.

  - 'all-cameras-must-see-half-board': We return only those chessboard poses
    that produce observations that are AT LEAST HALF visible by ALL the cameras.

  - 'some-cameras-must-see-half-board': We return only those chessboard poses
    that produce observations that are AT LEAST HALF visible by AT LEAST ONE
    camera.

RETURNED VALUES

We return a tuple:

- q: an array of shape (Nframes, Ncameras, object_height, object_width, 2)
  containing the pixel coordinates of the generated observations

- Rt_ref_boardref: an array of shape (Nframes, 4,3) containing the poses of the
  chessboards. This transforms the object returned by ref_calibration_object()
  to the pose that was projected, in the ref coord system

    '''

    # Can visualize results with this script:
    r'''
    r = np.array((30, 0, 0,), dtype=float) * np.pi/180.

    model = mrcal.cameramodel( intrinsics = ('LENSMODEL_PINHOLE',
                                             np.array((1000., 1000., 1000., 1000.,))),
                               imagersize = np.array((2000,2000)) )
    Rt_ref_boardref = \
        mrcal.synthesize_board_observations([model],
                                            5,20,0.1,None,
                                            nps.glue(r, np.array((0,0,3.)), axis=-1),
                                            np.array((0,0,0., 0,0,0)),
                                            1) [1]
    mrcal.show_geometry( models_or_extrinsics_rt_fromref = np.zeros((1,1,6), dtype=float),
                         frames_rt_toref                 = mrcal.rt_from_Rt(Rt_ref_boardref),
                         object_width_n                  = 20,
                         object_height_n                 = 5,
                         object_spacing                  = 0.1,
                         _set = 'xyplane 0',
                         wait = 1 )
    '''

    which_valid = (
        'all-cameras-must-see-full-board',
        'some-cameras-must-see-full-board',
        'all-cameras-must-see-half-board',
        'some-cameras-must-see-half-board',
    )

    if not which in which_valid:
        raise Exception(f"'which' argument must be one of {which_valid}")

    Ncameras = len(models)

    # I move the board, and keep the cameras stationary.
    #
    # Camera coords: x,y with pixels, z forward
    # Board coords:  x,y in-plane. z forward (i.e. back towards the camera)

    # The center of the board is at the origin (ignoring warping)
    board_center = \
        np.array(( (object_width_n -1)*object_spacing/2.,
                   (object_height_n-1)*object_spacing/2.,
                   0 ))

    # shape: (Nh,Nw,3)
    board_reference = \
        mrcal.ref_calibration_object(object_width_n,object_height_n,
                                     object_spacing,calobject_warp) - \
        board_center

    # Transformation from the board returned by ref_calibration_object() to
    # the one I use here. It's a shift to move the origin to the center of the
    # board
    Rt_boardref_origboardref = mrcal.identity_Rt()
    Rt_boardref_origboardref[3, :] = -board_center

    def get_observation_chunk():
        '''Make Nframes observations, and return them all, even the out-of-view ones'''

        # I compute the full random block in one shot. This is useful for
        # simulations that want to see identical poses when asking for N-1
        # random poses and when asking for the first N-1 of a set of N random
        # poses

        # shape (Nframes,6)
        randomblock = np.random.uniform(low=-1.0, high=1.0, size=(Nframes, 6))

        # shape(Nframes,4,3)
        Rt_ref_boardref = \
            mrcal.Rt_from_rt( rt_ref_boardcenter + randomblock * rt_ref_boardcenter__noiseradius )

        # shape = (Nframes, Nh,Nw,3)
        boards_ref = mrcal.transform_point_Rt(  # shape (Nframes, 1,1,4,3)
            nps.mv(Rt_ref_boardref, 0, -5),

            # shape ( Nh,Nw,3)
            board_reference)

        # I project full_board. Shape: (Nframes,Ncameras,Nh,Nw,2)
        q = \
          nps.mv( \
            nps.cat( \
              *[ mrcal.project( mrcal.transform_point_Rt(models[i].extrinsics_Rt_fromref(), boards_ref),
                                *models[i].intrinsics()) \
                 for i in range(Ncameras) ]),
                  0,1 )

        return q, Rt_ref_boardref

    def cull_out_of_view(q, Rt_ref_boardref, which):

        # q               has shape (Nframes,Ncameras,Nh,Nw,2)
        # Rt_ref_boardref has shape (Nframes,4,3)

        # I pick only those frames where at least one cameras sees the whole
        # board

        # shape (Nframes,Ncameras,Nh,Nw)
        mask_visible = (q[..., 0] >= 0) * (q[..., 1] >= 0)
        for i in range(Ncameras):
            W, H = models[i].imagersize()
            mask_visible[:,i,...] *= \
                (q[:,i,:,:,0] <= W-1) * \
                (q[:,i,:,:,1] <= H-1)

        # shape (Nframes, Ncameras)
        Nvisible = np.count_nonzero(mask_visible, axis=(-1, -2))

        Nh, Nw = q.shape[2:4]
        if which == 'all-cameras-must-see-full-board':
            iframe = np.all(Nvisible == Nh * Nw, axis=-1)
        elif which == 'some-cameras-must-see-full-board':
            iframe = np.any(Nvisible == Nh * Nw, axis=-1)
        elif which == 'all-cameras-must-see-half-board':
            iframe = np.all(Nvisible > Nh * Nw // 2, axis=-1)
        elif which == 'some-cameras-must-see-half-board':
            iframe = np.any(Nvisible > Nh * Nw // 2, axis=-1)
        else:
            raise Exception(
                "Unknown 'which' argument. This is a bug. I checked for the valid options at the top of this function"
            )

        # q               has shape (Nframes_inview,Ncameras,Nh*Nw,2)
        # Rt_ref_boardref has shape (Nframes_inview,4,3)
        return q[iframe, ...], Rt_ref_boardref[iframe, ...]

    # shape (Nframes_sofar,Ncameras,Nh,Nw,2)
    q = np.zeros((0, Ncameras, object_height_n, object_width_n, 2),
                 dtype=float)
    # shape (Nframes_sofar,4,3)
    Rt_ref_boardref = np.zeros((0, 4, 3), dtype=float)

    # I keep creating data, until I get Nframes-worth of in-view observations
    while True:
        q_here, Rt_ref_boardref_here = get_observation_chunk()

        q_here, Rt_ref_boardref_here = \
            cull_out_of_view(q_here, Rt_ref_boardref_here,
                             which)

        q = nps.glue(q, q_here, axis=-5)
        Rt_ref_boardref = nps.glue(Rt_ref_boardref,
                                   Rt_ref_boardref_here,
                                   axis=-3)
        if q.shape[0] >= Nframes:
            q = q[:Nframes, ...]
            Rt_ref_boardref = Rt_ref_boardref[:Nframes, ...]
            break

    return q, mrcal.compose_Rt(Rt_ref_boardref, Rt_boardref_origboardref)
Beispiel #5
0
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')

testutils.confirm_equal(mrcal.transform_point_rt(mrcal.rt_from_Rt(Rt), p),
                        Tp,
                        msg='transform_point_rt')
Beispiel #6
0
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
rectification_maps,cookie = \
    mrcal.stereo_rectify_prepare( (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.)
Rt_cam0_stereo = cookie['Rt_cam0_stereo']

testutils.confirm_equal(Rt_cam0_stereo,
                        mrcal.identity_Rt(),
                        msg='vanilla stereo has a vanilla geometry')

testutils.confirm_equal(cookie['baseline'],
                        nps.mag(rt01[3:]),
                        msg='vanilla stereo: baseline')

q0, q0x, q0y = mrcal.project(
    np.array(((0, 0, 1.), (1e-6, 0, 1.), (0, 1e-6, 1.))), *model0.intrinsics())

testutils.confirm_equal(cookie['pixels_per_deg_az'] * 8.,
                        (q0x - q0)[0] / 1e-6 * np.pi / 180.,
                        msg='vanilla stereo: correct az pixel density')

testutils.confirm_equal(cookie['pixels_per_deg_el'] * 4.,
                        (q0y - q0)[1] / 1e-6 * np.pi / 180.,
Beispiel #7
0
                            pixels_per_deg_el = -1./4.)
try:
    mrcal.stereo._validate_models_rectified(models_rectified)
    testutils.confirm(True,
                      msg='Generated models pass validation')
except:
    testutils.confirm(False,
                      msg='Generated models pass validation')

Rt_cam0_stereo = mrcal.compose_Rt( model0.extrinsics_Rt_fromref(),
                                   models_rectified[0].extrinsics_Rt_toref())
Rt01_rectified = mrcal.compose_Rt( models_rectified[0].extrinsics_Rt_fromref(),
                                   models_rectified[1].extrinsics_Rt_toref())
fxycxy = models_rectified[0].intrinsics()[1]

testutils.confirm_equal(Rt_cam0_stereo, mrcal.identity_Rt(),
                        msg='vanilla stereo has a vanilla geometry')

testutils.confirm_equal( Rt01_rectified[3,0],
                         nps.mag(rt01[3:]),
                         msg='vanilla stereo: baseline')

q0,q0x,q0y = mrcal.project( np.array(((0,      0,1.),
                                      (1e-6,   0,1.),
                                      (0,   1e-6, 1.))), *model0.intrinsics() )

testutils.confirm_equal(fxycxy[0] * np.pi/180. * 8.,
                        (q0x-q0)[0] / 1e-6 * np.pi/180.,
                        msg='vanilla stereo: correct az pixel density',
                        eps = 0.05)