I am relatively new to computer vision, and I am experimenting with extracting a clock/dial face from an image.
I managed to fit an ellipse onto the dial, and I wanted to fix the perspective such that the dial would face the camera directly.Essentially, I wanted to project an ellipse onto a circle.
I found online that homographies are normally used to accomplish this task.
I tried following the OpenCV tutorial on homographies, but I am having problems with the source poorly matching the destination.
Reading similar questions, a perfect projection seems impossible since perspective-projected circles are not really ellipses.However, I am not looking for a mathematically exact projection, but even the simplest cases I have seem to produce bad results (see example 4 in the images below).
Here are four example input images on which I annotated the ellipses (red), unit circles (blue), source points (yellow), and destination points (cyan) (note: the centers are annotated but are not used to compute the homography).Annotated input image
However, after applying the homography computed using OpenCV (I got the same exact results using Scikit), the ellipses are not projected well onto the unit circles.Homography results
I also tried using more than 4 points to compute the homography, but again, the results were identical.
Here is the code I used:
def persp_transform(orig): img = orig.copy() ellipses = find_ellipses(img) ellipse = ellipses[0] # Use the largest one center_x = ellipse[0] center_y = ellipse[1] center = np.array([center_x, center_y]) width = ellipse[2] height = ellipse[3] angle = ellipse[4] minor_semiaxis = min(width, height)/2 major_semiaxis = max(width, height)/2 # Translate the image to center the ellipse img_center = np.array([img.shape[0]//2, img.shape[1]//2]) translation = center - img_center M = np.float32([[1, 0, -translation[1]], [0, 1, -translation[0]]]) img = cv.warpAffine(img, M, (img.shape[1], img.shape[0])) # Draw ellipse before projection rr, cc = ellipse_perimeter(img_center[0], img_center[1], int(major_semiaxis), int(minor_semiaxis), orientation=angle, shape=img.shape) draw_ellipse(img, rr, cc, color=0, thickness=10) def sin(a): return np.sin(np.deg2rad(a)) def cos(a): return np.cos(np.deg2rad(a)) # Source points around the ellipse num_points = 5 rotation = np.array([[np.cos(angle), -np.sin(angle)], [np.sin(angle), np.cos(angle)]]) from_points = np.array([[major_semiaxis*sin(a), minor_semiaxis*cos(a)] for a in np.linspace(0, 360, num_points)]) @ rotation + img_center from_points = from_points.astype(np.float32) # Destination points around a centered circle to_radius = int((min(img.shape[:2]) / 2) * 0.8) to_points = np.array([[to_radius*sin(a), to_radius*cos(a)] for a in np.linspace(0, 360, num_points)]) @ rotation + img_center to_points = to_points.astype(np.float32) # Draw ellipse center and source points before projection cv.circle(img, (img_center[1], img_center[0]), 20, (255, 255, 0), -1) for fp in from_points: cv.circle(img, (int(fp[1]), int(fp[0])), 20, (255, 255, 0), -1) # Compute homography and project M, _ = cv.findHomography(from_points, to_points, cv.RANSAC, 5.0) img = cv.warpPerspective(img, M, (img.shape[1], img.shape[0])) # Draw target unit circle and destination points after projection cv.circle(img, (img_center[1], img_center[0]), to_radius, (0, 0, 255), 20) cv.circle(img, (img_center[1], img_center[0]), 20, (0, 255, 255), -1) for tp in to_points: cv.circle(img, (int(tp[1]), int(tp[0])), 20, (0, 255, 255), -1) return img
EDIT 1
Here are the 4 raw example images I used as input: GDrive Link
To fit the ellipses, I use the method proposed in AAMED, which can be found on GH here.
I manually played with the input parameters until I found satisfactory ones since I didn't need to find all the ellipses in the image, only the main ones.
My ellipse finding code looks like this:
def find_ellipses(orig, scale=0.3, theta_fsa=20, length_fsa=5.4, T_val=0.9): img = cv.resize(orig.copy(), None, fx=scale, fy=scale) gray_image = cv.cvtColor(img, cv.COLOR_BGR2GRAY) aamed = AAMED(img.shape[0]+1, img.shape[1]+1) aamed.setParameters(np.deg2rad(theta_fsa), length_fsa, T_val) ellipses = aamed.run_AAMED(gray_image) aamed.release() ellipses = sorted(ellipses, key=lambda e: ellipse_area(e), reverse=True) ellipses = [np.array(e) / scale for e in ellipses] return ellipses
Then I use skimage.draw.ellipse_perimeter(...)
to impose the largest ellipse on the image.
Edit 2
I tried to better visualize the homography and narrowed down the issue possibly to cv.warpPerspective(...)
.
Firstly, I color-coded each point pair to check that I was mapping the right pairs: Color-coded points
Then, I computed the homography and individually projected each source point using cv.perspectiveTransform(...)
without warping the image to see where they would end up, and they seemed to be projected properly: Projected points
However, when I then use cv.warpPerspective(...)
to project the image, the projection is inconsistent with the result from cv.perspectiveTransform(...)
: Wrong projection