Color Detection of PCR Plate using Python and OpenCV

Here’s how you can detect each well and it’s color in a PCR 96 well plate

Mir AbdulHaseeb
CodeX
5 min readJun 3, 2021

--

Photo by Louis Reed on Unsplash

OpenCV is an open source library aimed at computer vision. It has great features that make tasks such as object, and color detection simple and intuitive.

In order to detect each well’s color we need to first get each well’s position in the picture. So, our first goal is to detect each well in a PCR plate. Once we do that we can easily get the pixel colors inside those wells.

We will be working with these libraries:

  • numpy
  • KDTree
  • webcolors
  • cv2
import cv2
import numpy as np
import webcolors
from scipy.spatial import KDTree
PCR 96-Well plate

OpenCV let’s us easily detect objects and shapes with the help of its builtin functions. One such function is called Hough Circle Transform. Let’s see it in action.

# Convert to gray-scale and reduce noise
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
img_blur = cv2.medianBlur(gray, 5)
circles = cv2.HoughCircles(img_blur,
cv2.HOUGH_GRADIENT,
1,
img.shape[0]/64,
param1=200,
param2=10,
minRadius=14,
maxRadius=15
)
# Draw detected circles
if circles is not None:
circles = np.uint16(np.around(circles))
for i in circles[0, :96]:

# outer circle
## cv2.circle(image, center_coordinates, radius, color, thickness)
cv2.circle(img, (i[0], i[1]), i[2], (0, 0, 0), 2)

# rgb values
b = img[i[1], i[0], 0]
g = img[i[1], i[0], 1]
r = img[i[1], i[0], 2]

color_name = convert_rgb_to_color_name((r, g, b))

mapped_cell = {
"well_coordinates": (i[0], i[1]),
"well_color": color_name
}
mapped_cells_list.append(mapped_cell)

# inner circle
cv2.circle(img, (i[0], i[1]), 1, (0, 0, 255), 2)

cv2.HoughCircles() returns coordinates for the circles based on the parameters we provide. For instance minRadius, and maxRadius provide a range within which our circles should lie. The smaller the range the more accurate the result will be.

Once we get the circle coordinates, we can recreate the circles using cv2.circle() function. We are going to draw these circles on the original image

# cv2.circle(image, center_coordinates, radius, color, thickness)

Image after circles:

# View image in a window
cv2.imshow('Image',img)
cv2.waitKey(0)
cv2.destroyAllWindows()
Image with circles and center points

Now that we have successfully detected all the wells, we can get the rgb values of each center point and map it to color name. Note that we used convert_rgb_to_color_name() function. Here’s what it’s doing.

# Get Color names from RGB values
def convert_rgb_to_color_name(rgb_input):
hexnames = webcolors.css3_hex_to_names
names = []
positions = []
for hex, name in hexnames.items():
names.append(name)
positions.append(webcolors.hex_to_rgb(hex))
spacedb = KDTree(positions)querycolor = rgb_input
dist, index = spacedb.query(querycolor)
return names[index]

It takes the rgb values and returns the closest matched color name against it. I have written an article explained how it does that here.

This is what our mapped_cells_list is returning so far.

[{'well_coordinates': (164, 150), 'well_color': 'lightslategrey'},
{'well_coordinates': (72, 106), 'well_color': 'slategrey'},
{'well_coordinates': (208, 150), 'well_color': 'slategrey'},
{'well_coordinates': (210, 332), 'well_color': 'slategrey'},
.
.
.

These grey colors mean that the wells are empty. The colors are of the plate.

There is a slight problem with how OpenCV detects circles. It doesn’t do it in any specific order. It does it randomly or in best-matched-first order. For example if we are expecting 96 circles, the first circle will be the one that OpenCV’s algorithm is most confident about. So, if we are to label a well A1, A2 and so on, we need to know which well is which.

Let’s sort the wells in order that makes sense to us.

# Mapping Well names
sorted_outer_list = sorted(mapped_cells_list, key=lambda k: k['well_coordinates'])
for i in range(12):
sorted_inner_list = sorted(sorted_outer_list[8*i:8*(i+1)], key=lambda k: k['well_coordinates'][1])
for j in range(len(row_names)):
sorted_inner_list[j]['well_name'] = row_names[j]+ str(i+1)
sorted_final_list.append(sorted_inner_list[j])

Let’s iterate over our sorted list.

for d in sorted_final_list:
print(d)

Output:

{'well_coordinates': (72, 60), 'well_color': 'slategrey', 'well_name': 'A1'}
{'well_coordinates': (72, 106), 'well_color': 'slategrey', 'well_name': 'B1'}
{'well_coordinates': (74, 150), 'well_color': 'slategrey', 'well_name': 'C1'}
{'well_coordinates': (74, 196), 'well_color': 'slategrey', 'well_name': 'D1'}
{'well_coordinates': (74, 242), 'well_color': 'slategrey', 'well_name': 'E1'}
{'well_coordinates': (72, 286), 'well_color': 'lightslategrey', 'well_name': 'F1'}
{'well_coordinates': (72, 332), 'well_color': 'lightslategrey', 'well_name': 'G1'}
{'well_coordinates': (74, 376), 'well_color': 'slategrey', 'well_name': 'H1'}
{'well_coordinates': (118, 60), 'well_color': 'slategrey', 'well_name': 'A2'}
{'well_coordinates': (118, 106), 'well_color': 'slategrey', 'well_name': 'B2'}
{'well_coordinates': (118, 150), 'well_color': 'slategrey', 'well_name': 'C2'}
{'well_coordinates': (118, 196), 'well_color': 'slategrey', 'well_name': 'D2'}
{'well_coordinates': (118, 242), 'well_color': 'slategrey', 'well_name': 'E2'}
{'well_coordinates': (116, 288), 'well_color': 'lightslategrey', 'well_name': 'F2'}
{'well_coordinates': (118, 332), 'well_color': 'slategrey', 'well_name': 'G2'}
{'well_coordinates': (120, 376), 'well_color': 'slategrey', 'well_name': 'H2'}
.
.
.

Now we’re getting things in order. It’s still all grey-ish. Let’s put some colors in the wells and see if it recognizes them.

Image with colored wells

Output:

{'well_coordinates': (72, 60), 'well_color': 'red', 'well_name': 'A1'}
{'well_coordinates': (70, 108), 'well_color': 'darkviolet', 'well_name': 'B1'}
{'well_coordinates': (74, 150), 'well_color': 'blue', 'well_name': 'C1'}
{'well_coordinates': (74, 196), 'well_color': 'lime', 'well_name': 'D1'}
{'well_coordinates': (70, 240), 'well_color': 'blue', 'well_name': 'E1'}
{'well_coordinates': (72, 286), 'well_color': 'red', 'well_name': 'F1'}
{'well_coordinates': (72, 332), 'well_color': 'lime', 'well_name': 'G1'}
{'well_coordinates': (74, 376), 'well_color': 'blue', 'well_name': 'H1'}
{'well_coordinates': (118, 60), 'well_color': 'slategrey', 'well_name': 'A2'}
{'well_coordinates': (118, 106), 'well_color': 'yellow', 'well_name': 'B2'}
{'well_coordinates': (118, 150), 'well_color': 'darkviolet', 'well_name': 'C2'}
{'well_coordinates': (118, 196), 'well_color': 'lime', 'well_name': 'D2'}
{'well_coordinates': (116, 240), 'well_color': 'yellow', 'well_name': 'E2'}
{'well_coordinates': (116, 288), 'well_color': 'darkviolet', 'well_name': 'F2'}
{'well_coordinates': (118, 332), 'well_color': 'darkviolet', 'well_name': 'G2'}
{'well_coordinates': (120, 376), 'well_color': 'red', 'well_name': 'H2'}

Note: Some wells were left empty, hence the grey colors.

Complete code:

You can find the complete working code here.

Conclusion

It may look like we had to do a lot on our own, but most of the difficult work was made quite easy for us by OpenCV. You can take this code and modify it according to your requirements. Enjoy!

--

--