Ejemplo n.º 1
0
def calibrate_relative_poses_interactive(image_sets, cameraMatrixs, distCoeffss, imageSizes, boardSizes, board_scales, board_rvecs, board_tvecs):
    """
    Make an estimate of the relative poses (as 4x4 projection matrices) between many cameras.
    Base these relative poses to the first camera.
    
    Each camera should be looking at its own chessboard,
    use different sizes of chessboards if a camera sees chessboard that are not associated with that camera.
    'board_scales' scales the chessboard-units to world-units.
    'board_rvecs' and 'board_tvecs' transform the rescaled local chessboard-coordinates to world-coordinates.
    
    The inverse of the reprojection error is used for weighting.
    """
    num_cams = len(image_sets)
    num_images = len(image_sets[0])
    reproj_error_max = 0
    
    # Preprocess object-points of the different boards
    board_objps = []
    for boardSize, board_scale, board_rvec, board_tvec in zip(
            boardSizes, board_scales, board_rvecs, board_tvecs ):
        objp = np.ones((np.prod(boardSize), 4))
        objp[:, 0:3] = calibration_tools.grid_objp(boardSize) * board_scale
        objp = objp.dot(trfm.P_from_R_and_t(cvh.Rodrigues(board_rvec), np.array(board_tvec).reshape(3, 1))[0:3, :].T)
        board_objps.append(objp)
    
    # Calculate all absolute poses
    Ps = np.zeros((num_images, num_cams, 4, 4))
    weights = np.zeros((num_images, 1, 1, 1))
    for i, images in enumerate(zip(*image_sets)):
        reproj_error = 0
        for c, (image, cameraMatrix, distCoeffs, imageSize, boardSize, board_objp) in enumerate(zip(
                images, cameraMatrixs, distCoeffss, imageSizes, boardSizes, board_objps )):
            img = cv2.imread(image)
            ret, corners = cvh.extractChessboardFeatures(img, boardSize)
            if not ret:
                print ("Error: Image '%s' didn't contain a chessboard of size %s." % (image, boardSize))
                return False, None
            
            # Draw and display the corners
            cv2.drawChessboardCorners(
                    img, boardSize, corners, ret )
            cv2.imshow("img", img)
            cv2.waitKey(100)
            
            ret, rvec, tvec = cv2.solvePnP(board_objp, corners, cameraMatrix, distCoeffs)
            Ps[i, c, :, :] = trfm.P_from_R_and_t(cvh.Rodrigues(rvec), tvec)
            reproj_error = max(reprojection_error(    # max: worst case
                    [board_objp], [corners], cameraMatrix, distCoeffs, [rvec], [tvec] )[1], reproj_error)
        reproj_error_max = max(reproj_error, reproj_error_max)
        weights[i] = 1. / reproj_error
    
    # Apply weighting on Ps, and rebase against first camera
    Ps *= weights / weights.sum()
    Ps = Ps.sum(axis=0)
    Pref_inv = trfm.P_inv(Ps[0, :, :])    # use first cam as reference
    return True, [P.dot(Pref_inv) for P in Ps], reproj_error_max
Ejemplo n.º 2
0
def main():
    global boardSize, filename_base_chessboards, filename_intrinsics, filename_distorted, filename_triangl_pose_est_left, filename_triangl_pose_est_right, filename_base_extrinsics, filenames_extra_chessboards, filenames_extra_intrinsics, extra_boardSizes, extra_board_scales, extra_board_rvecs, extra_board_tvecs, device_id
    boardSize = (8, 6)
    filename_base_chessboards = join_path(
        "data", "chessboards",
        "chessboard*.jpg")  # calibration images of the base camera
    filename_intrinsics = join_path("results", "camera_intrinsics.txt")
    filename_distorted = join_path(
        "data", "chessboards", "chessboard07.jpg")  # a randomly chosen image
    #filename_triangl_pose_est_left = join_path("data", "chessboards", "chessboard07.jpg")    # a randomly chosen image
    #filename_triangl_pose_est_right = join_path("data", "chessboards", "chessboard08.jpg")    # a randomly chosen image
    filename_triangl_pose_est_left = join_path(
        "data", "chessboards_and_nonplanar",
        "image-0001.jpeg")  # a randomly chosen image
    filename_triangl_pose_est_right = join_path(
        "data", "chessboards_and_nonplanar",
        "image-0056.jpeg")  # a randomly chosen image
    filename_base_extrinsics = join_path("results", "chessboards_extrinsic",
                                         "chessboard")
    filenames_extra_chessboards = (
        join_path(
            "data", "chessboards_front",
            "front-*.jpg"),  # sets of calibration images of extra cameras
        join_path("data", "chessboards_bottom", "bottom-*.jpg"))
    filenames_extra_intrinsics = (join_path("results",
                                            "camera_intrinsics_front.txt"),
                                  join_path("results",
                                            "camera_intrinsics_bottom.txt")
                                  )  # intrinsics of extra cameras
    extra_boardSizes = ((8, 6), (8, 6))
    extra_board_scales = (1., 1.)
    extra_board_rvecs = ((0., 0., 0.), (0., -pi / 2, 0.))
    extra_board_tvecs = ((0., 0., 0.), (6., 0., (1200. - 193.) / 27.6 + 1.))
    device_id = 1  # webcam

    nonplanar_left = nonplanar_right = np.zeros((0, 2))

    help_text = """\
    Choose between: (in order)
        1: prepare_object_points (required)
        2: calibrate_camera_interactive (required for "reprojection_error")
        3: save_camera_intrinsics
        4: load_camera_intrinsics (required)
        5: undistort_image
        6: reprojection_error
        7: triangl_pose_est_interactive
        8: realtime_pose_estimation (recommended)
        9: calibrate_relative_poses_interactive
        q: quit
    
    Info: Sometimes you will be prompted: 'someVariable [defaultValue]: ',
          in that case you can type a new value,
          or simply press ENTER to preserve the default value.
    """
    from textwrap import dedent
    print(dedent(help_text))

    inp = ""
    while inp.lower() != "q":
        inp = raw_input("\n: ").strip()

        if inp == "1":
            get_variable("boardSize", lambda x: eval("(%s)" % x))
            print()  # add new-line

            objp = calibration_tools.grid_objp(boardSize)

        elif inp == "2":
            get_variable("filename_base_chessboards")
            from glob import glob
            images = sorted(glob(filename_base_chessboards))
            print()  # add new-line

            reproj_error, cameraMatrix, distCoeffs, rvecs, tvecs, objectPoints, imagePoints, imageSize = \
                    calibrate_camera_interactive(images, objp, boardSize)
            print("cameraMatrix:\n", cameraMatrix)
            print("distCoeffs:\n", distCoeffs)
            print("reproj_error:", reproj_error)

            cv2.destroyAllWindows()

        elif inp == "3":
            get_variable("filename_intrinsics")
            print()  # add new-line

            calibration_tools.save_camera_intrinsics(filename_intrinsics,
                                                     cameraMatrix, distCoeffs,
                                                     imageSize)

        elif inp == "4":
            get_variable("filename_intrinsics")
            print()  # add new-line

            cameraMatrix, distCoeffs, imageSize = \
                    calibration_tools.load_camera_intrinsics(filename_intrinsics)

        elif inp == "5":
            get_variable("filename_distorted")
            img = cv2.imread(filename_distorted)
            print()  # add new-line

            img_undistorted, roi = calibration_tools.undistort_image(
                img, cameraMatrix, distCoeffs, imageSize)
            cv2.imshow("Original Image", img)
            cv2.imshow("Undistorted Image", img_undistorted)
            print("Press any key to continue.")
            cv2.waitKey()

            cv2.destroyAllWindows()

        elif inp == "6":
            mean_error, square_error = reprojection_error(
                objectPoints, imagePoints, cameraMatrix, distCoeffs, rvecs,
                tvecs)
            print("mean absolute error:", mean_error)
            print("square error:", square_error)

        elif inp == "7":
            print(triangl_pose_est_interactive.__doc__)

            get_variable("filename_triangl_pose_est_left")
            img_left = cv2.imread(filename_triangl_pose_est_left)
            get_variable("filename_triangl_pose_est_right")
            img_right = cv2.imread(filename_triangl_pose_est_right)
            print()  # add new-line

            nonplanar_left, nonplanar_right = \
                    triangl_pose_est_interactive(img_left, img_right, cameraMatrix, distCoeffs, imageSize, objp, boardSize, nonplanar_left, nonplanar_right)

            cv2.destroyAllWindows()

        elif inp == "8":
            print(realtime_pose_estimation.__doc__)

            get_variable("device_id", int)
            get_variable("filename_base_extrinsics")
            print()  # add new-line

            realtime_pose_estimation(device_id, filename_base_extrinsics,
                                     cameraMatrix, distCoeffs, objp, boardSize)

            cv2.destroyAllWindows()

        elif inp == "9":
            print(calibrate_relative_poses_interactive.__doc__)

            get_variable("filenames_extra_chessboards",
                         lambda x: eval("(%s)" % x))
            from glob import glob
            image_sets = [
                sorted(glob(images)) for images in filenames_extra_chessboards
            ]
            get_variable("filenames_extra_intrinsics",
                         lambda x: eval("(%s)" % x))
            cameraMatrixs, distCoeffss, imageSizes = zip(
                *map(calibration_tools.load_camera_intrinsics,
                     filenames_extra_intrinsics))
            get_variable("extra_boardSizes", lambda x: eval("(%s)" % x))
            get_variable("extra_board_scales", lambda x: eval("(%s)" % x))
            get_variable("extra_board_rvecs", lambda x: eval("(%s)" % x))
            get_variable("extra_board_tvecs", lambda x: eval("(%s)" % x))
            print()  # add new-line

            ret, Ps, reproj_error_max = \
                    calibrate_relative_poses_interactive(image_sets, cameraMatrixs, distCoeffss, imageSizes,
                                                         extra_boardSizes, extra_board_scales, extra_board_rvecs, extra_board_tvecs)
            if ret:
                print("Ps:")
                for P in Ps:
                    print(P)
                print("reproj_error_max:", reproj_error_max)
Ejemplo n.º 3
0
def calibrate_relative_poses_interactive(image_sets, cameraMatrixs,
                                         distCoeffss, imageSizes, boardSizes,
                                         board_scales, board_rvecs,
                                         board_tvecs):
    """
    Make an estimate of the relative poses (as 4x4 projection matrices) between many cameras.
    Base these relative poses to the first camera.
    
    Each camera should be looking at its own chessboard,
    use different sizes of chessboards if a camera sees chessboard that are not associated with that camera.
    'board_scales' scales the chessboard-units to world-units.
    'board_rvecs' and 'board_tvecs' transform the rescaled local chessboard-coordinates to world-coordinates.
    
    The inverse of the reprojection error is used for weighting.
    """
    num_cams = len(image_sets)
    num_images = len(image_sets[0])
    reproj_error_max = 0

    # Preprocess object-points of the different boards
    board_objps = []
    for boardSize, board_scale, board_rvec, board_tvec in zip(
            boardSizes, board_scales, board_rvecs, board_tvecs):
        objp = np.ones((np.prod(boardSize), 4))
        objp[:, 0:3] = calibration_tools.grid_objp(boardSize) * board_scale
        objp = objp.dot(
            trfm.P_from_R_and_t(cvh.Rodrigues(board_rvec),
                                np.array(board_tvec).reshape(3, 1))[0:3, :].T)
        board_objps.append(objp)

    # Calculate all absolute poses
    Ps = np.zeros((num_images, num_cams, 4, 4))
    weights = np.zeros((num_images, 1, 1, 1))
    for i, images in enumerate(zip(*image_sets)):
        reproj_error = 0
        for c, (image, cameraMatrix, distCoeffs, imageSize, boardSize,
                board_objp) in enumerate(
                    zip(images, cameraMatrixs, distCoeffss, imageSizes,
                        boardSizes, board_objps)):
            img = cv2.imread(image)
            ret, corners = cvh.extractChessboardFeatures(img, boardSize)
            if not ret:
                print(
                    "Error: Image '%s' didn't contain a chessboard of size %s."
                    % (image, boardSize))
                return False, None

            # Draw and display the corners
            cv2.drawChessboardCorners(img, boardSize, corners, ret)
            cv2.imshow("img", img)
            cv2.waitKey(100)

            ret, rvec, tvec = cv2.solvePnP(board_objp, corners, cameraMatrix,
                                           distCoeffs)
            Ps[i, c, :, :] = trfm.P_from_R_and_t(cvh.Rodrigues(rvec), tvec)
            reproj_error = max(
                reprojection_error(  # max: worst case
                    [board_objp], [corners], cameraMatrix, distCoeffs, [rvec],
                    [tvec])[1],
                reproj_error)
        reproj_error_max = max(reproj_error, reproj_error_max)
        weights[i] = 1. / reproj_error

    # Apply weighting on Ps, and rebase against first camera
    Ps *= weights / weights.sum()
    Ps = Ps.sum(axis=0)
    Pref_inv = trfm.P_inv(Ps[0, :, :])  # use first cam as reference
    return True, [P.dot(Pref_inv) for P in Ps], reproj_error_max
Ejemplo n.º 4
0
def triangl_pose_est_interactive(img_left, img_right, cameraMatrix, distCoeffs,
                                 imageSize, objp, boardSize, nonplanar_left,
                                 nonplanar_right):
    """ (TODO: remove debug-prints)
    Triangulation and relative pose estimation will be performed from LEFT to RIGHT image.
    
    Both images have to contain the whole chessboard,
    in order to make a decent estimation of the relative pose based on 'solvePnP'.
    
    Then the user can manually create matches between non-planar objects.
    If the user omits this step, only triangulation of the chessboard corners will be performed,
    and they will be compared to the real 3D points.
    Otherwise the coordinates of the triangulated points of the manually matched points will be printed,
    and a relative pose estimation will be performed (using the essential matrix),
    this pose estimation will be compared with the decent 'solvePnP' estimation.
    In that case you will be asked whether you want to mute the chessboard corners in all calculations,
    note that this requires at least 8 pairs of matches corresponding with non-planar geometry.
    
    During manual matching process,
    switching between LEFT and RIGHT image will be done in a zigzag fashion.
    To stop selecting matches, press SPACE.
    
    Additionally, you can also introduce predefined non-planar geometry,
    this is e.g. useful if you don't want to select non-planar geometry manually.
    """

    ### Setup initial state, and calc some very accurate poses to compare against with later

    K_left = cameraMatrix
    K_right = cameraMatrix
    K_inv_left = cvh.invert(K_left)
    K_inv_right = cvh.invert(K_right)
    distCoeffs_left = distCoeffs
    distCoeffs_right = distCoeffs

    # Extract chessboard features, together they form a set of 'planar' points
    ret_left, planar_left = cvh.extractChessboardFeatures(img_left, boardSize)
    ret_right, planar_right = cvh.extractChessboardFeatures(
        img_right, boardSize)
    if not ret_left or not ret_right:
        print("Chessboard is not (entirely) in sight, aborting.")
        return

    # Save exact 2D and 3D points of the chessboard
    num_planar = planar_left.shape[0]
    planar_left_orig, planar_right_orig = planar_left, planar_right
    objp_orig = objp

    # Calculate P matrix of left pose, in reality this one is known
    ret, rvec_left, tvec_left = cv2.solvePnP(objp, planar_left, K_left,
                                             distCoeffs_left)
    P_left = trfm.P_from_R_and_t(cvh.Rodrigues(rvec_left), tvec_left)

    # Calculate P matrix of right pose, in reality this one is unknown
    ret, rvec_right, tvec_right = cv2.solvePnP(objp, planar_right, K_right,
                                               distCoeffs_right)
    P_right = trfm.P_from_R_and_t(cvh.Rodrigues(rvec_right), tvec_right)

    # Load previous non-planar inliers, if desired
    if nonplanar_left.size and not raw_input(
            "Type 'no' if you don't want to use previous non-planar inliers? "
    ).strip().lower() == "no":
        nonplanar_left = map(tuple, nonplanar_left)
        nonplanar_right = map(tuple, nonplanar_left)
    else:
        nonplanar_left = []
        nonplanar_right = []

    ### Create predefined non-planar 3D geometry, to enable automatic selection of non-planar points

    if raw_input(
            "Type 'yes' if you want to create and select predefined non-planar geometry? "
    ).strip().lower() == "yes":

        # A cube
        cube_coords = np.array([(0., 0., 0.), (1., 0., 0.), (1., 1., 0.),
                                (0., 1., 0.), (0., 0., 1.), (1., 0., 1.),
                                (1., 1., 1.), (0., 1., 1.)])
        cube_coords *= 2  # scale
        cube_edges = np.array([(0, 1), (1, 2), (2, 3), (3, 0), (4, 5), (5, 6),
                               (6, 7), (7, 4), (0, 4), (1, 5), (2, 6), (3, 7)])

        # An 8-point circle
        s2 = 1. / np.sqrt(2)
        circle_coords = np.array([(1., 0., 0.), (s2, s2, 0.), (0., 1., 0.),
                                  (-s2, s2, 0.), (-1., 0., 0.), (-s2, -s2, 0.),
                                  (0., -1., 0.), (s2, -s2, 0.)])
        circle_edges = np.array([(i, (i + 1) % 8) for i in range(8)])

        # Position 2 cubes and 2 circles in the scene
        cube1 = np.array(cube_coords)
        cube1[:, 1] -= 1
        cube2 = np.array(cube_coords)
        cube2 = cvh.Rodrigues((0., 0., pi / 4)).dot(cube2.T).T
        cube2[:, 0] += 4
        cube2[:, 1] += 3
        cube2[:, 2] += 2
        circle1 = np.array(circle_coords)
        circle1 *= 2
        circle1[:, 1] += 5
        circle1[:, 2] += 2
        circle2 = np.array(circle_coords)
        circle2 = cvh.Rodrigues((pi / 2, 0., 0.)).dot(circle2.T).T
        circle2[:, 1] += 5
        circle2[:, 2] += 2

        # Print output to be used in Blender (see "calibrate_pose_visualization.blend")
        print()
        print("Cubes")
        print("edges_cube = \\\n", map(list, cube_edges))
        print("coords_cube1 = \\\n", map(list, cube1))
        print("coords_cube2 = \\\n", map(list, cube2))
        print()
        print("Circles")
        print("edges_circle = \\\n", map(list, circle_edges))
        print("coords_circle1 = \\\n", map(list, circle1))
        print("coords_circle2 = \\\n", map(list, circle2))
        print()

        color = rgb(0, 200, 150)
        for verts, edges in zip(
            [cube1, cube2, circle1, circle2],
            [cube_edges, cube_edges, circle_edges, circle_edges]):
            out_left = cvh.wireframe3DGeometry(img_left, verts, edges, color,
                                               rvec_left, tvec_left, K_left,
                                               distCoeffs_left)
            out_right = cvh.wireframe3DGeometry(img_right, verts, edges, color,
                                                rvec_right, tvec_right,
                                                K_right, distCoeffs_right)
            valid_match_idxs = [
                i for i, (pl, pr) in enumerate(zip(out_left, out_right))
                if 0 <= min(pl[0], pr[0]) <= max(pl[0], pr[0]) < imageSize[0]
                and 0 <= min(pl[1], pr[1]) <= max(pl[1], pr[1]) < imageSize[1]
            ]
            nonplanar_left += map(tuple,
                                  out_left[valid_match_idxs])  # concatenate
            nonplanar_right += map(tuple,
                                   out_right[valid_match_idxs])  # concatenate

        nonplanar_left = np.rint(nonplanar_left).astype(int)
        nonplanar_right = np.rint(nonplanar_right).astype(int)

    ### User can manually create matches between non-planar objects

    class ManualMatcher:
        def __init__(self, window_name, images, points):
            self.window_name = window_name
            self.images = images
            self.points = points
            self.img_idx = 0  # 0: left; 1: right
            cv2.namedWindow(window_name)
            cv2.setMouseCallback(window_name, self.onMouse)

        def onMouse(self, event, x, y, flags, userdata):
            if event == cv2.EVENT_LBUTTONDOWN:
                self.points[self.img_idx].append((x, y))

            elif event == cv2.EVENT_LBUTTONUP:
                # Switch images in a ping-pong fashion
                if len(self.points[0]) != len(self.points[1]):
                    self.img_idx = 1 - self.img_idx

        def run(self):
            print("Select your matches. Press SPACE when done.")
            while True:
                img = cv2.drawKeypoints(self.images[self.img_idx], [
                    cv2.KeyPoint(p[0], p[1], 7.)
                    for p in self.points[self.img_idx]
                ],
                                        color=rgb(0, 0, 255))
                cv2.imshow(self.window_name, img)
                key = cv2.waitKey(50) & 0xFF
                if key == ord(' '): break  # finish if SPACE is pressed

            num_points_diff = len(self.points[0]) - len(self.points[1])
            if num_points_diff:
                del self.points[num_points_diff < 0][-abs(num_points_diff):]
            print("Selected", len(self.points[0]), "pairs of matches.")

    # Execute the manual matching
    nonplanar_left, nonplanar_right = list(nonplanar_left), list(
        nonplanar_right)
    ManualMatcher("Select match-points of non-planar objects",
                  [img_left, img_right],
                  [nonplanar_left, nonplanar_right]).run()
    num_nonplanar = len(nonplanar_left)
    has_nonplanar = (num_nonplanar > 0)
    if 0 < num_nonplanar < 8:
        print("Warning: you've selected less than 8 pairs of matches.")
    nonplanar_left = np.array(nonplanar_left).reshape(num_nonplanar, 2)
    nonplanar_right = np.array(nonplanar_right).reshape(num_nonplanar, 2)

    mute_chessboard_corners = False
    if num_nonplanar >= 8 and raw_input(
            "Type 'yes' if you want to exclude chessboard corners from calculations? "
    ).strip().lower() == "yes":
        mute_chessboard_corners = True
        num_planar = 0
        planar_left = np.zeros((0, 2))
        planar_right = np.zeros((0, 2))
        objp = np.zeros((0, 3))
        print("Chessboard corners muted.")

    has_prev_triangl_points = not mute_chessboard_corners  # normally in this example, this should always be True, unless user forces False

    ### Undistort points => normalized coordinates

    allfeatures_left = np.concatenate((planar_left, nonplanar_left))
    allfeatures_right = np.concatenate((planar_right, nonplanar_right))

    allfeatures_nrm_left = cv2.undistortPoints(np.array([allfeatures_left]),
                                               K_left, distCoeffs_left)[0]
    allfeatures_nrm_right = cv2.undistortPoints(np.array([allfeatures_right]),
                                                K_right, distCoeffs_right)[0]

    planar_nrm_left, nonplanar_nrm_left = allfeatures_nrm_left[:planar_left.shape[
        0]], allfeatures_nrm_left[planar_left.shape[0]:]
    planar_nrm_right, nonplanar_nrm_right = allfeatures_nrm_right[:planar_right.shape[
        0]], allfeatures_nrm_right[planar_right.shape[0]:]

    ### Calculate relative pose using the essential matrix

    # Only do pose estimation if we've got 2D points of non-planar geometry
    if has_nonplanar:

        # Determine inliers by calculating the fundamental matrix on all points,
        # except when mute_chessboard_corners is True: only use nonplanar_nrm_left and nonplanar_nrm_right
        F, status = cv2.findFundamentalMat(
            allfeatures_nrm_left, allfeatures_nrm_right, cv2.FM_RANSAC,
            0.006 * np.amax(allfeatures_nrm_left),
            0.99)  # threshold from [Snavely07 4.1]
        # OpenCV BUG: "status" matrix is not initialized with zeros in some cases, reproducable with 2 sets of 8 2D points equal to (0., 0.)
        #   maybe because "_mask.create()" is not called:
        #   https://github.com/Itseez/opencv/blob/7e2bb63378dafb90063af40caff20c363c8c9eaf/modules/calib3d/src/ptsetreg.cpp#L185
        # Workaround to test on outliers: use "!= 1", instead of "== 0"
        print(status.T)
        inlier_idxs = np.where(status == 1)[0]
        print("Removed", allfeatures_nrm_left.shape[0] - inlier_idxs.shape[0],
              "outlier(s).")
        num_planar = np.where(inlier_idxs < num_planar)[0].shape[0]
        num_nonplanar = inlier_idxs.shape[0] - num_planar
        print("num chessboard inliers:", num_planar)
        allfeatures_left, allfeatures_right = allfeatures_left[
            inlier_idxs], allfeatures_right[inlier_idxs]
        allfeatures_nrm_left, allfeatures_nrm_right = allfeatures_nrm_left[
            inlier_idxs], allfeatures_nrm_right[inlier_idxs]
        if not mute_chessboard_corners:
            objp = objp[inlier_idxs[:num_planar]]
            planar_left, planar_right = allfeatures_left[:
                                                         num_planar], allfeatures_right[:
                                                                                        num_planar]
            planar_nrm_left, planar_nrm_right = allfeatures_nrm_left[:
                                                                     num_planar], allfeatures_nrm_right[:
                                                                                                        num_planar]
        nonplanar_nrm_left, nonplanar_nrm_right = allfeatures_nrm_left[
            num_planar:], allfeatures_nrm_right[num_planar:]

        # Calculate first solution of relative pose
        if allfeatures_nrm_left.shape[0] >= 8:
            F, status = cv2.findFundamentalMat(allfeatures_nrm_left,
                                               allfeatures_nrm_right,
                                               cv2.FM_8POINT)
            print("F:")
            print(F)
        else:
            print(
                "Error: less than 8 pairs of inliers found, I can't perform the 8-point algorithm."
            )
        #E = (K_right.T) .dot (F) .dot (K_left)    # "Multiple View Geometry in CV" by Hartley&Zisserman (9.12)
        E = F  # K = I because we already normalized the coordinates
        print("Correct determinant of essential matrix?",
              (abs(cv2.determinant(E)) <= 1e-7))
        w, u, vt = cv2.SVDecomp(E, flags=cv2.SVD_MODIFY_A)
        print(w)
        if ((w[0] < w[1] and w[0] / w[1]) or
            (w[1] < w[0] and w[1] / w[0]) or 0) < 0.7:
            print(
                "Essential matrix' 'w' vector deviates too much from expected")
        W = np.array([
            [0., -1., 0.],  # Hartley&Zisserman (9.13)
            [1., 0., 0.],
            [0., 0., 1.]
        ])
        R = (u).dot(W).dot(vt)  # Hartley&Zisserman result 9.19
        det_R = cv2.determinant(R)
        print("Coherent rotation?", (abs(det_R) - 1 <= 1e-7))
        if det_R - (
                -1
        ) < 1e-7:  # http://en.wikipedia.org/wiki/Essential_matrix#Showing_that_it_is_valid
            # E *= -1:
            vt *= -1  # svd(-E) = u * w * (-v).T
            R *= -1  # => u * W * (-v).T = -R
            print("det(R) == -1, compensated.")
        t = u[:, 2:3]  # Hartley&Zisserman result 9.19
        P = trfm.P_from_R_and_t(R, t)

        # Select right solution where the (center of the) 3d points are/is in front of both cameras,
        # when has_prev_triangl_points == True, we can do it a lot faster.

        test_point = np.ones((4, 1))  # 3D point to test the solutions with

        if has_prev_triangl_points:
            print("Using advantage of already triangulated points' position")
            print(P)

            # Select the closest already triangulated cloudpoint idx to the center of the cloud
            center_of_mass = objp.sum(axis=0) / objp.shape[0]
            center_objp_idx = np.argmin(
                ((objp - center_of_mass)**2).sum(axis=1))
            print("center_objp_idx:", center_objp_idx)

            # Select the corresponding image points
            center_imgp_left = planar_nrm_left[center_objp_idx]
            center_imgp_right = planar_nrm_right[center_objp_idx]

            # Select the corresponding 3D point
            test_point[0:3, :] = objp[center_objp_idx].reshape(3, 1)
            print("test_point:")
            print(test_point)
            test_point = P_left.dot(
                test_point
            )  # set the reference axis-system to the one of the left camera, note that are_points_in_front_of_left_camera is automatically True

            center_objp_triangl, triangl_status = iterative_LS_triangulation(
                center_imgp_left.reshape(1, 2), np.eye(4),
                center_imgp_right.reshape(1, 2), P)
            print("triangl_status:", triangl_status)

            if (center_objp_triangl).dot(test_point[0:3, 0:1]) < 0:
                P[0:3, 3:4] *= -1  # do a baseline reversal
                print(P, "fixed triangulation inversion")

            if P[0:3, :].dot(test_point)[
                    2, 0] < 0:  # are_points_in_front_of_right_camera is False
                P[0:3, 0:3] = (u).dot(W.T).dot(
                    vt)  # use the other solution of the twisted pair ...
                P[0:3, 3:4] *= -1  # ... and also do a baseline reversal
                print(P, "fixed camera projection inversion")

        elif num_nonplanar > 0:
            print(
                "Doing all ambiguity checks since there are no already triangulated points"
            )
            print(P)

            for i in range(4):  # check all 4 solutions
                objp_triangl, triangl_status = iterative_LS_triangulation(
                    nonplanar_nrm_left, np.eye(4), nonplanar_nrm_right, P)
                print("triangl_status:", triangl_status)
                center_of_mass = objp_triangl.sum(axis=0) / len(
                    objp_triangl
                )  # select the center of the triangulated cloudpoints
                test_point[0:3, :] = center_of_mass.reshape(3, 1)
                print("test_point:")
                print(trfm.P_inv(P_left).dot(test_point))

                if np.eye(3, 4).dot(test_point)[2, 0] > 0 and P[0:3, :].dot(
                        test_point
                )[2, 0] > 0:  # are_points_in_front_of_cameras is True
                    break

                if i % 2:
                    P[0:3, 0:3] = (u).dot(W.T).dot(
                        vt)  # use the other solution of the twisted pair
                    print(P, "using the other solution of the twisted pair")

                else:
                    P[0:3, 3:4] *= -1  # do a baseline reversal
                    print(P, "doing a baseline reversal")

        are_points_in_front_of_left_camera = (np.eye(
            3, 4).dot(test_point)[2, 0] > 0)
        are_points_in_front_of_right_camera = (P[0:3, :].dot(test_point)[2, 0]
                                               > 0)
        print("are_points_in_front_of_cameras?",
              are_points_in_front_of_left_camera,
              are_points_in_front_of_right_camera)
        if not (are_points_in_front_of_left_camera
                and are_points_in_front_of_right_camera):
            print("No valid solution found!")

        P_left_result = trfm.P_inv(P).dot(P_right)
        P_right_result = P.dot(P_left)

        print("P_left")
        print(P_left)
        print("P_rel")
        print(P)
        print("P_right")
        print(P_right)
        print(
            "=> error:",
            reprojection_error([objp_orig] * 2,
                               [planar_left_orig, planar_right_orig],
                               cameraMatrix, distCoeffs,
                               [rvec_left, rvec_right],
                               [tvec_left, tvec_right])[1])

        print("P_left_result")
        print(P_left_result)
        print("P_right_result")
        print(P_right_result)
        # We use "P_left" instead of "P_left_result" because the latter depends on the unknown "P_right"
        print(
            "=> error:",
            reprojection_error([objp_orig] * 2,
                               [planar_left_orig, planar_right_orig],
                               cameraMatrix, distCoeffs, [
                                   cvh.Rodrigues(P_left[0:3, 0:3]),
                                   cvh.Rodrigues(P_right_result[0:3, 0:3])
                               ], [P_left[0:3, 3], P_right_result[0:3, 3]])[1])

    ### Triangulate

    objp_result = np.zeros((0, 3))

    if has_prev_triangl_points:
        # Do triangulation of all points
        # NOTICE: in a real case, we should only use not yet triangulated points that are in sight
        objp_result, triangl_status = iterative_LS_triangulation(
            allfeatures_nrm_left, P_left, allfeatures_nrm_right, P_right)
        print("triangl_status:", triangl_status)

    elif num_nonplanar > 0:
        # We already did the triangulation during the pose estimation, but we still need to backtransform them from the left camera axis-system
        objp_result = trfm.P_inv(P_left).dot(
            np.concatenate((objp_triangl.T, np.ones((1, len(objp_triangl))))))
        objp_result = objp_result[0:3, :].T
        print(objp_triangl)

    print("objp:")
    print(objp)
    print(
        "=> error:",
        reprojection_error([objp_orig] * 2,
                           [planar_left_orig, planar_right_orig], cameraMatrix,
                           distCoeffs, [rvec_left, rvec_right],
                           [tvec_left, tvec_right])[1])

    print("objp_result of chessboard:")
    print(objp_result[:num_planar, :])
    if has_nonplanar:
        print("objp_result of non-planar geometry:")
        print(objp_result[num_planar:, :])
    if num_planar + num_nonplanar == 0:
        print("=> error: undefined")
    else:
        print(
            "=> error:",
            reprojection_error([objp_result] * 2,
                               [allfeatures_left, allfeatures_right],
                               cameraMatrix, distCoeffs,
                               [rvec_left, rvec_right],
                               [tvec_left, tvec_right])[1])

    ### Print total combined reprojection error

    # We only have both pose estimation and triangulation if we've got 2D points of non-planar geometry
    if has_nonplanar:
        if num_planar + num_nonplanar == 0:
            print("=> error: undefined")
        else:
            # We use "P_left" instead of "P_left_result" because the latter depends on the unknown "P_right"
            print(
                "Total combined error:",
                reprojection_error(
                    [objp_result] * 2, [allfeatures_left, allfeatures_right],
                    cameraMatrix, distCoeffs, [
                        cvh.Rodrigues(P_left[0:3, 0:3]),
                        cvh.Rodrigues(P_right_result[0:3, 0:3])
                    ], [P_left[0:3, 3], P_right_result[0:3, 3]])[1])
    """
    Further things we can do (in a real case):
        1. solvePnP() on inliers (including previously already triangulated points),
            or should we use solvePnPRansac() (in that case what to do with the outliers)?
        2. re-triangulate on new pose estimation
    """

    ### Print summary to be used in Blender to visualize (see "calibrate_pose_visualization.blend")

    print("Camera poses:")
    if has_nonplanar:
        print("Left")
        print_pose(rvec_left, tvec_left)
        print()
        print("Left_result")
        print_pose(cvh.Rodrigues(P_left_result[0:3, 0:3]), P_left_result[0:3,
                                                                         3:4])
        print()
        print("Right")
        print_pose(rvec_right, tvec_right)
        print()
        print("Right_result")
        print_pose(cvh.Rodrigues(P_right_result[0:3, 0:3]),
                   P_right_result[0:3, 3:4])
        print()
    else:
        print("<skipped: no non-planar objects have been selected>")
        print()

    print("Points:")
    print("Chessboard")
    print("coords = \\\n", map(list, objp_result[:num_planar, :]))
    print()
    if has_nonplanar:
        print("Non-planar geometry")
        print("coords_nonplanar = \\\n", map(list,
                                             objp_result[num_planar:, :]))
        print()

    ### Return to remember last manually matched successful non-planar imagepoints
    return nonplanar_left, nonplanar_right
Ejemplo n.º 5
0
def main():
    global boardSize, filename_base_chessboards, filename_intrinsics, filename_distorted, filename_triangl_pose_est_left, filename_triangl_pose_est_right, filename_base_extrinsics, filenames_extra_chessboards, filenames_extra_intrinsics, extra_boardSizes, extra_board_scales, extra_board_rvecs, extra_board_tvecs, device_id
    boardSize = (8, 6)
    filename_base_chessboards = join_path("data", "chessboards", "chessboard*.jpg")    # calibration images of the base camera
    filename_intrinsics = join_path("results", "camera_intrinsics.txt")
    filename_distorted = join_path("data", "chessboards", "chessboard07.jpg")    # a randomly chosen image
    #filename_triangl_pose_est_left = join_path("data", "chessboards", "chessboard07.jpg")    # a randomly chosen image
    #filename_triangl_pose_est_right = join_path("data", "chessboards", "chessboard08.jpg")    # a randomly chosen image
    filename_triangl_pose_est_left = join_path("data", "chessboards_and_nonplanar", "image-0001.jpeg")    # a randomly chosen image
    filename_triangl_pose_est_right = join_path("data", "chessboards_and_nonplanar", "image-0056.jpeg")    # a randomly chosen image
    filename_base_extrinsics = join_path("results", "chessboards_extrinsic", "chessboard")
    filenames_extra_chessboards = (join_path("data", "chessboards_front", "front-*.jpg"),    # sets of calibration images of extra cameras
                                   join_path("data", "chessboards_bottom", "bottom-*.jpg"))
    filenames_extra_intrinsics = (join_path("results", "camera_intrinsics_front.txt"),
                                  join_path("results", "camera_intrinsics_bottom.txt"))    # intrinsics of extra cameras
    extra_boardSizes = ((8, 6), (8, 6))
    extra_board_scales = (1., 1.)
    extra_board_rvecs = ((0., 0., 0.), (0., -pi/2, 0.))
    extra_board_tvecs = ((0., 0., 0.), (6., 0., (1200.-193.)/27.6+1.))
    device_id = 1    # webcam

    nonplanar_left = nonplanar_right = np.zeros((0, 2))

    help_text = """\
    Choose between: (in order)
        1: prepare_object_points (required)
        2: calibrate_camera_interactive (required for "reprojection_error")
        3: save_camera_intrinsics
        4: load_camera_intrinsics (required)
        5: undistort_image
        6: reprojection_error
        7: triangl_pose_est_interactive
        8: realtime_pose_estimation (recommended)
        9: calibrate_relative_poses_interactive
        q: quit
    
    Info: Sometimes you will be prompted: 'someVariable [defaultValue]: ',
          in that case you can type a new value,
          or simply press ENTER to preserve the default value.
    """
    from textwrap import dedent
    print (dedent(help_text))
    
    inp = ""
    while inp.lower() != "q":
        inp = raw_input("\n: ").strip()
        
        if inp == "1":
            get_variable("boardSize", lambda x: eval("(%s)" % x))
            print ()    # add new-line
            
            objp = calibration_tools.grid_objp(boardSize)
        
        elif inp == "2":
            get_variable("filename_base_chessboards")
            from glob import glob
            images = sorted(glob(filename_base_chessboards))
            print ()    # add new-line
            
            reproj_error, cameraMatrix, distCoeffs, rvecs, tvecs, objectPoints, imagePoints, imageSize = \
                    calibrate_camera_interactive(images, objp, boardSize)
            print ("cameraMatrix:\n", cameraMatrix)
            print ("distCoeffs:\n", distCoeffs)
            print ("reproj_error:", reproj_error)
            
            cv2.destroyAllWindows()
        
        elif inp == "3":
            get_variable("filename_intrinsics")
            print ()    # add new-line
            
            calibration_tools.save_camera_intrinsics(
                    filename_intrinsics, cameraMatrix, distCoeffs, imageSize )
        
        elif inp == "4":
            get_variable("filename_intrinsics")
            print ()    # add new-line
            
            cameraMatrix, distCoeffs, imageSize = \
                    calibration_tools.load_camera_intrinsics(filename_intrinsics)
        
        elif inp == "5":
            get_variable("filename_distorted")
            img = cv2.imread(filename_distorted)
            print ()    # add new-line
            
            img_undistorted, roi = calibration_tools.undistort_image(
                    img, cameraMatrix, distCoeffs, imageSize )
            cv2.imshow("Original Image", img)
            cv2.imshow("Undistorted Image", img_undistorted)
            print ("Press any key to continue.")
            cv2.waitKey()
            
            cv2.destroyAllWindows()
        
        elif inp == "6":
            mean_error, square_error = reprojection_error(
                    objectPoints, imagePoints, cameraMatrix, distCoeffs, rvecs, tvecs )
            print ("mean absolute error:", mean_error)
            print ("square error:", square_error)
        
        elif inp == "7":
            print (triangl_pose_est_interactive.__doc__)
            
            get_variable("filename_triangl_pose_est_left")
            img_left = cv2.imread(filename_triangl_pose_est_left)
            get_variable("filename_triangl_pose_est_right")
            img_right = cv2.imread(filename_triangl_pose_est_right)
            print ()    # add new-line
            
            nonplanar_left, nonplanar_right = \
                    triangl_pose_est_interactive(img_left, img_right, cameraMatrix, distCoeffs, imageSize, objp, boardSize, nonplanar_left, nonplanar_right)
            
            cv2.destroyAllWindows()
        
        elif inp == "8":
            print (realtime_pose_estimation.__doc__)
            
            get_variable("device_id", int)
            get_variable("filename_base_extrinsics")
            print ()    # add new-line
            
            realtime_pose_estimation(device_id, filename_base_extrinsics, cameraMatrix, distCoeffs, objp, boardSize)
            
            cv2.destroyAllWindows()
        
        elif inp == "9":
            print (calibrate_relative_poses_interactive.__doc__)
            
            get_variable("filenames_extra_chessboards", lambda x: eval("(%s)" % x))
            from glob import glob
            image_sets = [sorted(glob(images)) for images in filenames_extra_chessboards]
            get_variable("filenames_extra_intrinsics", lambda x: eval("(%s)" % x))
            cameraMatrixs, distCoeffss, imageSizes = zip(*map(
                    calibration_tools.load_camera_intrinsics, filenames_extra_intrinsics ))
            get_variable("extra_boardSizes", lambda x: eval("(%s)" % x))
            get_variable("extra_board_scales", lambda x: eval("(%s)" % x))
            get_variable("extra_board_rvecs", lambda x: eval("(%s)" % x))
            get_variable("extra_board_tvecs", lambda x: eval("(%s)" % x))
            print ()    # add new-line
            
            ret, Ps, reproj_error_max = \
                    calibrate_relative_poses_interactive(image_sets, cameraMatrixs, distCoeffss, imageSizes,
                                                         extra_boardSizes, extra_board_scales, extra_board_rvecs, extra_board_tvecs)
            if ret:
                print ("Ps:")
                for P in Ps: print (P)
                print ("reproj_error_max:", reproj_error_max)
Ejemplo n.º 6
0
def triangl_pose_est_interactive(img_left, img_right, cameraMatrix, distCoeffs, imageSize, objp, boardSize, nonplanar_left, nonplanar_right):
    """ (TODO: remove debug-prints)
    Triangulation and relative pose estimation will be performed from LEFT to RIGHT image.
    
    Both images have to contain the whole chessboard,
    in order to make a decent estimation of the relative pose based on 'solvePnP'.
    
    Then the user can manually create matches between non-planar objects.
    If the user omits this step, only triangulation of the chessboard corners will be performed,
    and they will be compared to the real 3D points.
    Otherwise the coordinates of the triangulated points of the manually matched points will be printed,
    and a relative pose estimation will be performed (using the essential matrix),
    this pose estimation will be compared with the decent 'solvePnP' estimation.
    In that case you will be asked whether you want to mute the chessboard corners in all calculations,
    note that this requires at least 8 pairs of matches corresponding with non-planar geometry.
    
    During manual matching process,
    switching between LEFT and RIGHT image will be done in a zigzag fashion.
    To stop selecting matches, press SPACE.
    
    Additionally, you can also introduce predefined non-planar geometry,
    this is e.g. useful if you don't want to select non-planar geometry manually.
    """
    
    ### Setup initial state, and calc some very accurate poses to compare against with later
    
    K_left = cameraMatrix
    K_right = cameraMatrix
    K_inv_left = cvh.invert(K_left)
    K_inv_right = cvh.invert(K_right)
    distCoeffs_left = distCoeffs
    distCoeffs_right = distCoeffs
    
    # Extract chessboard features, together they form a set of 'planar' points
    ret_left, planar_left = cvh.extractChessboardFeatures(img_left, boardSize)
    ret_right, planar_right = cvh.extractChessboardFeatures(img_right, boardSize)
    if not ret_left or not ret_right:
        print ("Chessboard is not (entirely) in sight, aborting.")
        return
    
    # Save exact 2D and 3D points of the chessboard
    num_planar = planar_left.shape[0]
    planar_left_orig, planar_right_orig = planar_left, planar_right
    objp_orig = objp
    
    # Calculate P matrix of left pose, in reality this one is known
    ret, rvec_left, tvec_left = cv2.solvePnP(
            objp, planar_left, K_left, distCoeffs_left )
    P_left = trfm.P_from_R_and_t(cvh.Rodrigues(rvec_left), tvec_left)
    
    # Calculate P matrix of right pose, in reality this one is unknown
    ret, rvec_right, tvec_right = cv2.solvePnP(
            objp, planar_right, K_right, distCoeffs_right )
    P_right = trfm.P_from_R_and_t(cvh.Rodrigues(rvec_right), tvec_right)
    
    # Load previous non-planar inliers, if desired
    if nonplanar_left.size and not raw_input("Type 'no' if you don't want to use previous non-planar inliers? ").strip().lower() == "no":
        nonplanar_left = map(tuple, nonplanar_left)
        nonplanar_right = map(tuple, nonplanar_left)
    else:
        nonplanar_left = []
        nonplanar_right = []
    
    
    ### Create predefined non-planar 3D geometry, to enable automatic selection of non-planar points
    
    if raw_input("Type 'yes' if you want to create and select predefined non-planar geometry? ").strip().lower() == "yes":
        
        # A cube
        cube_coords = np.array([(0., 0., 0.), (1., 0., 0.), (1., 1., 0.), (0., 1., 0.),
                                (0., 0., 1.), (1., 0., 1.), (1., 1., 1.), (0., 1., 1.)])
        cube_coords *= 2    # scale
        cube_edges = np.array([(0, 1), (1, 2), (2, 3), (3, 0),
                               (4, 5), (5, 6), (6, 7), (7, 4),
                               (0, 4), (1, 5), (2, 6), (3, 7)])
        
        # An 8-point circle
        s2 = 1. / np.sqrt(2)
        circle_coords = np.array([( 1.,  0., 0.), ( s2,  s2, 0.),
                                  ( 0.,  1., 0.), (-s2,  s2, 0.),
                                  (-1.,  0., 0.), (-s2, -s2, 0.),
                                  ( 0., -1., 0.), ( s2, -s2, 0.)])
        circle_edges = np.array([(i, (i+1) % 8) for i in range(8)])
        
        # Position 2 cubes and 2 circles in the scene
        cube1 = np.array(cube_coords)
        cube1[:, 1] -= 1
        cube2 = np.array(cube_coords)
        cube2 = cvh.Rodrigues((0., 0., pi/4)).dot(cube2.T).T
        cube2[:, 0] += 4
        cube2[:, 1] += 3
        cube2[:, 2] += 2
        circle1 = np.array(circle_coords)
        circle1 *= 2
        circle1[:, 1] += 5
        circle1[:, 2] += 2
        circle2 = np.array(circle_coords)
        circle2 = cvh.Rodrigues((pi/2, 0., 0.)).dot(circle2.T).T
        circle2[:, 1] += 5
        circle2[:, 2] += 2
        
        # Print output to be used in Blender (see "calibrate_pose_visualization.blend")
        print ()
        print ("Cubes")
        print ("edges_cube = \\\n", map(list, cube_edges))
        print ("coords_cube1 = \\\n", map(list, cube1))
        print ("coords_cube2 = \\\n", map(list, cube2))
        print ()
        print ("Circles")
        print ("edges_circle = \\\n", map(list, circle_edges))
        print ("coords_circle1 = \\\n", map(list, circle1))
        print ("coords_circle2 = \\\n", map(list, circle2))
        print ()
        
        color = rgb(0, 200, 150)
        for verts, edges in zip([cube1, cube2, circle1, circle2],
                                [cube_edges, cube_edges, circle_edges, circle_edges]):
            out_left = cvh.wireframe3DGeometry(
                    img_left, verts, edges, color, rvec_left, tvec_left, K_left, distCoeffs_left )
            out_right = cvh.wireframe3DGeometry(
                    img_right, verts, edges, color, rvec_right, tvec_right, K_right, distCoeffs_right )
            valid_match_idxs = [i for i, (pl, pr) in enumerate(zip(out_left, out_right))
                                    if 0 <= min(pl[0], pr[0]) <= max(pl[0], pr[0]) < imageSize[0] and 
                                        0 <= min(pl[1], pr[1]) <= max(pl[1], pr[1]) < imageSize[1]
                                ]
            nonplanar_left += map(tuple, out_left[valid_match_idxs])    # concatenate
            nonplanar_right += map(tuple, out_right[valid_match_idxs])    # concatenate
        
        nonplanar_left = np.rint(nonplanar_left).astype(int)
        nonplanar_right = np.rint(nonplanar_right).astype(int)
    
    
    ### User can manually create matches between non-planar objects
    
    class ManualMatcher:
        def __init__(self, window_name, images, points):
            self.window_name = window_name
            self.images = images
            self.points = points
            self.img_idx = 0    # 0: left; 1: right
            cv2.namedWindow(window_name)
            cv2.setMouseCallback(window_name, self.onMouse)
        
        def onMouse(self, event, x, y, flags, userdata):
            if event == cv2.EVENT_LBUTTONDOWN:
                self.points[self.img_idx].append((x, y))
            
            elif event == cv2.EVENT_LBUTTONUP:
                # Switch images in a ping-pong fashion
                if len(self.points[0]) != len(self.points[1]):
                    self.img_idx = 1 - self.img_idx
        
        def run(self):
            print ("Select your matches. Press SPACE when done.")
            while True:
                img = cv2.drawKeypoints(self.images[self.img_idx], [cv2.KeyPoint(p[0],p[1], 7.) for p in self.points[self.img_idx]], color=rgb(0,0,255))
                cv2.imshow(self.window_name, img)
                key = cv2.waitKey(50) & 0xFF
                if key == ord(' '): break    # finish if SPACE is pressed
            
            num_points_diff = len(self.points[0]) - len(self.points[1])
            if num_points_diff:
                del self.points[num_points_diff < 0][-abs(num_points_diff):]
            print ("Selected", len(self.points[0]), "pairs of matches.")
    
    # Execute the manual matching
    nonplanar_left, nonplanar_right = list(nonplanar_left), list(nonplanar_right)
    ManualMatcher("Select match-points of non-planar objects", [img_left, img_right], [nonplanar_left, nonplanar_right]).run()
    num_nonplanar = len(nonplanar_left)
    has_nonplanar = (num_nonplanar > 0)
    if 0 < num_nonplanar < 8:
        print ("Warning: you've selected less than 8 pairs of matches.")
    nonplanar_left = np.array(nonplanar_left).reshape(num_nonplanar, 2)
    nonplanar_right = np.array(nonplanar_right).reshape(num_nonplanar, 2)
    
    mute_chessboard_corners = False
    if num_nonplanar >= 8 and raw_input("Type 'yes' if you want to exclude chessboard corners from calculations? ").strip().lower() == "yes":
        mute_chessboard_corners = True
        num_planar = 0
        planar_left = np.zeros((0, 2))
        planar_right = np.zeros((0, 2))
        objp = np.zeros((0, 3))
        print ("Chessboard corners muted.")
    
    has_prev_triangl_points = not mute_chessboard_corners    # normally in this example, this should always be True, unless user forces False
    
    
    ### Undistort points => normalized coordinates
    
    allfeatures_left = np.concatenate((planar_left, nonplanar_left))
    allfeatures_right = np.concatenate((planar_right, nonplanar_right))
    
    allfeatures_nrm_left = cv2.undistortPoints(np.array([allfeatures_left]), K_left, distCoeffs_left)[0]
    allfeatures_nrm_right = cv2.undistortPoints(np.array([allfeatures_right]), K_right, distCoeffs_right)[0]
    
    planar_nrm_left, nonplanar_nrm_left = allfeatures_nrm_left[:planar_left.shape[0]], allfeatures_nrm_left[planar_left.shape[0]:]
    planar_nrm_right, nonplanar_nrm_right = allfeatures_nrm_right[:planar_right.shape[0]], allfeatures_nrm_right[planar_right.shape[0]:]
    
    
    ### Calculate relative pose using the essential matrix
    
    # Only do pose estimation if we've got 2D points of non-planar geometry
    if has_nonplanar:
    
        # Determine inliers by calculating the fundamental matrix on all points,
        # except when mute_chessboard_corners is True: only use nonplanar_nrm_left and nonplanar_nrm_right
        F, status = cv2.findFundamentalMat(allfeatures_nrm_left, allfeatures_nrm_right, cv2.FM_RANSAC, 0.006 * np.amax(allfeatures_nrm_left), 0.99)    # threshold from [Snavely07 4.1]
        # OpenCV BUG: "status" matrix is not initialized with zeros in some cases, reproducable with 2 sets of 8 2D points equal to (0., 0.)
        #   maybe because "_mask.create()" is not called:
        #   https://github.com/Itseez/opencv/blob/7e2bb63378dafb90063af40caff20c363c8c9eaf/modules/calib3d/src/ptsetreg.cpp#L185
        # Workaround to test on outliers: use "!= 1", instead of "== 0"
        print (status.T)
        inlier_idxs = np.where(status == 1)[0]
        print ("Removed", allfeatures_nrm_left.shape[0] - inlier_idxs.shape[0], "outlier(s).")
        num_planar = np.where(inlier_idxs < num_planar)[0].shape[0]
        num_nonplanar = inlier_idxs.shape[0] - num_planar
        print ("num chessboard inliers:", num_planar)
        allfeatures_left, allfeatures_right = allfeatures_left[inlier_idxs], allfeatures_right[inlier_idxs]
        allfeatures_nrm_left, allfeatures_nrm_right = allfeatures_nrm_left[inlier_idxs], allfeatures_nrm_right[inlier_idxs]
        if not mute_chessboard_corners:
            objp = objp[inlier_idxs[:num_planar]]
            planar_left, planar_right = allfeatures_left[:num_planar], allfeatures_right[:num_planar]
            planar_nrm_left, planar_nrm_right = allfeatures_nrm_left[:num_planar], allfeatures_nrm_right[:num_planar]
        nonplanar_nrm_left, nonplanar_nrm_right = allfeatures_nrm_left[num_planar:], allfeatures_nrm_right[num_planar:]
        
        # Calculate first solution of relative pose
        if allfeatures_nrm_left.shape[0] >= 8:
            F, status = cv2.findFundamentalMat(allfeatures_nrm_left, allfeatures_nrm_right, cv2.FM_8POINT)
            print ("F:")
            print (F)
        else:
            print ("Error: less than 8 pairs of inliers found, I can't perform the 8-point algorithm.")
        #E = (K_right.T) .dot (F) .dot (K_left)    # "Multiple View Geometry in CV" by Hartley&Zisserman (9.12)
        E = F    # K = I because we already normalized the coordinates
        print ("Correct determinant of essential matrix?", (abs(cv2.determinant(E)) <= 1e-7))
        w, u, vt = cv2.SVDecomp(E, flags=cv2.SVD_MODIFY_A)
        print (w)
        if ((w[0] < w[1] and w[0]/w[1]) or (w[1] < w[0] and w[1]/w[0]) or 0) < 0.7:
            print ("Essential matrix' 'w' vector deviates too much from expected")
        W = np.array([[0., -1., 0.],    # Hartley&Zisserman (9.13)
                      [1.,  0., 0.],
                      [0.,  0., 1.]])
        R = (u) .dot (W) .dot (vt)    # Hartley&Zisserman result 9.19
        det_R = cv2.determinant(R)
        print ("Coherent rotation?", (abs(det_R) - 1 <= 1e-7))
        if det_R - (-1) < 1e-7:    # http://en.wikipedia.org/wiki/Essential_matrix#Showing_that_it_is_valid
            # E *= -1:
            vt *= -1    # svd(-E) = u * w * (-v).T
            R *= -1     # => u * W * (-v).T = -R
            print ("det(R) == -1, compensated.")
        t = u[:, 2:3]    # Hartley&Zisserman result 9.19
        P = trfm.P_from_R_and_t(R, t)
        
        # Select right solution where the (center of the) 3d points are/is in front of both cameras,
        # when has_prev_triangl_points == True, we can do it a lot faster.
        
        test_point = np.ones((4, 1))    # 3D point to test the solutions with
        
        if has_prev_triangl_points:
            print ("Using advantage of already triangulated points' position")
            print (P)
            
            # Select the closest already triangulated cloudpoint idx to the center of the cloud
            center_of_mass = objp.sum(axis=0) / objp.shape[0]
            center_objp_idx = np.argmin(((objp - center_of_mass)**2).sum(axis=1))
            print ("center_objp_idx:", center_objp_idx)
            
            # Select the corresponding image points
            center_imgp_left = planar_nrm_left[center_objp_idx]
            center_imgp_right = planar_nrm_right[center_objp_idx]
            
            # Select the corresponding 3D point
            test_point[0:3, :] = objp[center_objp_idx].reshape(3, 1)
            print ("test_point:")
            print (test_point)
            test_point = P_left.dot(test_point)    # set the reference axis-system to the one of the left camera, note that are_points_in_front_of_left_camera is automatically True
            
            center_objp_triangl, triangl_status = iterative_LS_triangulation(
                    center_imgp_left.reshape(1, 2), np.eye(4),
                    center_imgp_right.reshape(1, 2), P )
            print ("triangl_status:", triangl_status)
            
            if (center_objp_triangl) .dot (test_point[0:3, 0:1]) < 0:
                P[0:3, 3:4] *= -1    # do a baseline reversal
                print (P, "fixed triangulation inversion")
            
            if P[0:3, :].dot(test_point)[2, 0] < 0:    # are_points_in_front_of_right_camera is False
                P[0:3, 0:3] = (u) .dot (W.T) .dot (vt)    # use the other solution of the twisted pair ...
                P[0:3, 3:4] *= -1    # ... and also do a baseline reversal
                print (P, "fixed camera projection inversion")
        
        elif num_nonplanar > 0:
            print ("Doing all ambiguity checks since there are no already triangulated points")
            print (P)
            
            for i in range(4):    # check all 4 solutions
                objp_triangl, triangl_status = iterative_LS_triangulation(
                        nonplanar_nrm_left, np.eye(4),
                        nonplanar_nrm_right, P )
                print ("triangl_status:", triangl_status)
                center_of_mass = objp_triangl.sum(axis=0) / len(objp_triangl)    # select the center of the triangulated cloudpoints
                test_point[0:3, :] = center_of_mass.reshape(3, 1)
                print ("test_point:")
                print (trfm.P_inv(P_left) .dot (test_point))
                
                if np.eye(3, 4).dot(test_point)[2, 0] > 0 and P[0:3, :].dot(test_point)[2, 0] > 0:    # are_points_in_front_of_cameras is True
                    break
                
                if i % 2:
                    P[0:3, 0:3] = (u) .dot (W.T) .dot (vt)   # use the other solution of the twisted pair
                    print (P, "using the other solution of the twisted pair")
                
                else:
                    P[0:3, 3:4] *= -1    # do a baseline reversal
                    print (P, "doing a baseline reversal")
        
        
        are_points_in_front_of_left_camera = (np.eye(3, 4).dot(test_point)[2, 0] > 0)
        are_points_in_front_of_right_camera = (P[0:3, :].dot(test_point)[2, 0] > 0)
        print ("are_points_in_front_of_cameras?", are_points_in_front_of_left_camera, are_points_in_front_of_right_camera)
        if not (are_points_in_front_of_left_camera and are_points_in_front_of_right_camera):
            print ("No valid solution found!")
        
        P_left_result = trfm.P_inv(P).dot(P_right)
        P_right_result = P.dot(P_left)
        
        print ("P_left")
        print (P_left)
        print ("P_rel")
        print (P)
        print ("P_right")
        print (P_right)
        print ("=> error:", reprojection_error(
                [objp_orig] * 2, [planar_left_orig, planar_right_orig],
                cameraMatrix, distCoeffs,
                [rvec_left, rvec_right],
                [tvec_left, tvec_right] )[1])
        
        print ("P_left_result")
        print (P_left_result)
        print ("P_right_result")
        print (P_right_result)
        # We use "P_left" instead of "P_left_result" because the latter depends on the unknown "P_right"
        print ("=> error:", reprojection_error(
                [objp_orig] * 2, [planar_left_orig, planar_right_orig],
                cameraMatrix, distCoeffs,
                [cvh.Rodrigues(P_left[0:3, 0:3]), cvh.Rodrigues(P_right_result[0:3, 0:3])],
                [P_left[0:3, 3], P_right_result[0:3, 3]] )[1])
    
    
    ### Triangulate
    
    objp_result = np.zeros((0, 3))
    
    if has_prev_triangl_points:
        # Do triangulation of all points
        # NOTICE: in a real case, we should only use not yet triangulated points that are in sight
        objp_result, triangl_status = iterative_LS_triangulation(
                allfeatures_nrm_left, P_left,
                allfeatures_nrm_right, P_right )
        print ("triangl_status:", triangl_status)
    
    elif num_nonplanar > 0:
        # We already did the triangulation during the pose estimation, but we still need to backtransform them from the left camera axis-system
        objp_result = trfm.P_inv(P_left) .dot (np.concatenate((objp_triangl.T, np.ones((1, len(objp_triangl))))))
        objp_result = objp_result[0:3, :].T
        print (objp_triangl)
    
    print ("objp:")
    print (objp)
    print ("=> error:", reprojection_error(
            [objp_orig] * 2,
            [planar_left_orig, planar_right_orig],
            cameraMatrix, distCoeffs, [rvec_left, rvec_right], [tvec_left, tvec_right] )[1])
    
    print ("objp_result of chessboard:")
    print (objp_result[:num_planar, :])
    if has_nonplanar:
        print ("objp_result of non-planar geometry:")
        print (objp_result[num_planar:, :])
    if num_planar + num_nonplanar == 0:
        print ("=> error: undefined")
    else:
        print ("=> error:", reprojection_error(
                [objp_result] * 2,
                [allfeatures_left, allfeatures_right],
                cameraMatrix, distCoeffs, [rvec_left, rvec_right], [tvec_left, tvec_right] )[1])
    
    
    ### Print total combined reprojection error
    
    # We only have both pose estimation and triangulation if we've got 2D points of non-planar geometry
    if has_nonplanar:
        if num_planar + num_nonplanar == 0:
            print ("=> error: undefined")
        else:
            # We use "P_left" instead of "P_left_result" because the latter depends on the unknown "P_right"
            print ("Total combined error:", reprojection_error(
                    [objp_result] * 2,
                    [allfeatures_left, allfeatures_right],
                    cameraMatrix, distCoeffs,
                    [cvh.Rodrigues(P_left[0:3, 0:3]), cvh.Rodrigues(P_right_result[0:3, 0:3])],
                    [P_left[0:3, 3], P_right_result[0:3, 3]] )[1])
    
    
    """
    Further things we can do (in a real case):
        1. solvePnP() on inliers (including previously already triangulated points),
            or should we use solvePnPRansac() (in that case what to do with the outliers)?
        2. re-triangulate on new pose estimation
    """
    
    
    ### Print summary to be used in Blender to visualize (see "calibrate_pose_visualization.blend")
    
    print ("Camera poses:")
    if has_nonplanar:
        print ("Left")
        print_pose(rvec_left, tvec_left)
        print ()
        print ("Left_result")
        print_pose(cvh.Rodrigues(P_left_result[0:3, 0:3]), P_left_result[0:3, 3:4])
        print ()
        print ("Right")
        print_pose(rvec_right, tvec_right)
        print ()
        print ("Right_result")
        print_pose(cvh.Rodrigues(P_right_result[0:3, 0:3]), P_right_result[0:3, 3:4])
        print ()
    else:
        print ("<skipped: no non-planar objects have been selected>")
        print ()
    
    print ("Points:")
    print ("Chessboard")
    print ("coords = \\\n", map(list, objp_result[:num_planar, :]))
    print ()
    if has_nonplanar:
        print ("Non-planar geometry")
        print ("coords_nonplanar = \\\n", map(list, objp_result[num_planar:, :]))
        print ()
    
    ### Return to remember last manually matched successful non-planar imagepoints
    return nonplanar_left, nonplanar_right