I have the following image containing a dartboard

After processing the image looks as follows:

In addition, I have a function that creates a theoretical dartboard:
import cv2
import numpy as np
def draw_dartboard():
IMG = np.ones((400, 400), 'uint8') * 255
center = (int(IMG.shape[0] // 2), int(IMG.shape[1] // 2))
size_dartboard = int(340)
r_board = int(170)
r_db = int(6.35)
r_sb = int(15.9)
r_doubles = int(162)
r_triples = int(99)
width_rings = int(8)
cv2.circle(IMG, center, r_doubles + width_rings, (0,0,0), -1)
cv2.circle(IMG, center, r_doubles, (255,255,255), -1)
cv2.circle(IMG, center, r_triples + width_rings, (0,0,0), -1)
cv2.circle(IMG, center, r_triples, (255,255,255), -1)
thetas_min = np.radians([(18 * t - 9) for t in range(20)])
thetas_max = np.radians([(18 * t + 9) for t in range(20)])
for idx, (theta_min, theta_max) in enumerate(zip(thetas_min, thetas_max)):
if (idx % 2) == 0:
x_min = int(center[0] + r_board * np.cos(theta_min))
y_min = int(center[1] + r_board * np.sin(theta_min))
x_max = int(center[0] + r_board * np.cos(theta_max))
y_max = int(center[1] + r_board * np.sin(theta_max))
cv2.fillPoly(IMG, np.array([(center, (x_min,y_min), (x_max,y_max))]), (0,0,0))
cv2.circle(IMG, center, r_sb, (0,0,0), -1)
return IMG
The output of this image looks as follows:

How can I “fit” the theoretical dartboard in the real image? Clearly, there is a mismatch in orientation and scale. What's the best way to do this?
You can register your dartboard image (i.e. source image) to the one you processed (i.e. destination image) by using affine transformations.
Here is my approach, and the outcome.
import cv2
import matplotlib.pyplot as plt
import numpy as np
# read images and remove matplotlib axes
src = cv2.imread('source.png',0)
src = src[20:-30,40:-20]
dest = cv2.imread('dest.png',0)
dest = dest[40:-40,40:-40]
# find matching points manually
dest_pts = np.array([[103,29],[215,13],[236,125]]).astype(np.float32) # x,y
src_pts = np.array([[19,175],[145,158],[176,284]]).astype(np.float32) #x,y
# calculate the affine transformation matrix
warp_mat = cv2.getAffineTransform(src_pts, dest_pts)
# get the registered source image
warp_dst = cv2.warpAffine(src, warp_mat, (dest.shape[1], dest.shape[0]))
fig,ax = plt.subplots(1,3)
ax[0].imshow(src,'gray')
ax[0].scatter(src_pts[:,0],src_pts[:,1],s=1,c='r')
ax[0].set_title('src')
ax[1].imshow(dest,'gray')
ax[1].scatter(dest_pts[:,0],dest_pts[:,1],s=1,c='r')
ax[1].set_title('dest')
ax[2].imshow(warp_dst,'gray')
ax[2].set_title('registered src')
plt.savefig('result.png')
fig, ax = plt.subplots(1)
ax.imshow(dest,'gray')
ax.imshow(warp_dst,cmap='jet',alpha=0.5)
plt.savefig('overlayed_result.png')
# plt.show()

In order to calculate affine transformation matrix, you will need 3 matching points on both images. I highlighted the points I chose on both images. FYI, you can develop a way to automate finding matching points, let us know in your question if you need that.

As you have already done the image processing, I will take it from there. So just to be clear, this is the image I will be working with (I cropped out the matplotlib axises, as I'm sure they aren't present in your actual image):

The concept is really simple:
Find the bounding box of the contour of the target.
With the bounding box, we can find the radius of the target by selecting the greatest among the dimensions (width and height) of the bounding box, and dividing it by 2.
With the radius of the target and the top-left corner coordinates of the target (returned when finding the bounding box of the target), we can find the center of the target with the expressions x + r and y + h - r.
With the radius of the target, you can scale your theoretical target accordingly, and with the center of the target, you can draw your theoretical target at the right coordinates.
Here is how the code goes, where Image.png is the above image. Note that I only draw one circle onto the image; the rest of them can be drawn on using the same way, with just some added scaling:
import cv2
import numpy as np
img = cv2.imread("Image.png")
img_processed = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
contours, _ = cv2.findContours(img_processed, cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE)
cnt = sorted(contours, key=cv2.contourArea)[-2]
x, y, w, h = cv2.boundingRect(cnt)
r = max(w, h) // 2
center_x = x + r
center_y = y + h - r
cv2.circle(img, (center_x, center_y), r, (0, 255, 0), 5)
cv2.imshow("Image", img)
cv2.waitKey(0)
cv2.destroyAllWindows()
Output:

Note that at this line:
cnt = sorted(contours, key=cv2.contourArea)[-2]
I am getting the contour with the second-greatest area, as the one with the greatest area would be the border of the image.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With