How Can I Correctly Classify The Number Of Positive (bright Color) Circles And Negative (dark Color) Circles In The Image
Solution 1:
You got almost every right, if you are not worried about one mis-classified blob, the partial blobs not being detected at all, and the (apparently) inaccurate size for some of the blobs.
The last problem to solve is to get a sensible threshold between the bright and the dark blobs. One way to do so is to use an adaptive threshold, like e.g. the Otsu's method or others.
Have a look here for more threshold methods from scikit-learn.
EDIT: Updated to better match what you were asking.
Briefly, compared to your code, I have done the following modifications:
- Put all the code inside functions (it helps me reason better)
- I have defined a contrast enhancement function, but it is not used in the code (because I was getting worse results.)
- define a function that generates masks associated to the circles (note that this function would by available, with slightly different parameters, in PyMRT - Disclaimer: I am the main author of it.)
- threshold the circles using the masks from above and the Otsu method for determining the optimal threshold value
(minor note: I saved the input image as blobs.jpg
).
This is how I would have done it, but I am sure that there is room for improving its robustness by tweaking the parameters.
import numpy as np
import cv2
import matplotlib.pyplot as plt
from skimage.filters import threshold_otsu
# based on: https://stackoverflow.com/questions/46626267/how-to-generate-a-sphere-in-3d-numpy-array/46626448#46626448
def circle(shape, radius, position):
semisizes = (radius,) * 2
grid = [slice(-x0, dim - x0) for x0, dim in zip(position, shape)]
position = np.ogrid[grid]
arr = np.zeros(shape, dtype=float)
for x_i, semisize in zip(position, semisizes):
arr += (np.abs(x_i / semisize) ** 2)
return arr <= 1.0
def enhance_contrast(
in_img,
save_filepath=None):
"""Enhance contrast."""
lab_img = cv2.cvtColor(in_img, cv2.COLOR_BGR2LAB)
l_ch, a_ch, b_ch = cv2.split(lab_img)
# Applying CLAHE to L-channel
clahe_filter = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8, 8))
l_ch = clahe_filter.apply(l_ch)
out_img = cv2.merge((l_ch, a_ch, b_ch))
out_img = cv2.cvtColor(out_img, cv2.COLOR_LAB2BGR)
if save_filepath:
cv2.imwrite(save_filepath, out_img)
return out_img
def find_circles(
in_filepath,
out_filepath='circles_{in_filepath}',
enh_filepath='enh_{in_filepath}',
hough_circles_kws=(
('dp', 1), ('minDist', 15), ('param1', 30), ('param2', 30),
('minRadius', 5), ('maxRadius', 25)),
verbose=True):
"""Find circles in image."""
out_filepath = out_filepath.format(**locals())
enh_filepath = enh_filepath.format(**locals())
hough_circles_kws = dict(hough_circles_kws) if hough_circles_kws else {}
in_img = cv2.imread(in_filepath)
lab_img = cv2.cvtColor(in_img, cv2.COLOR_BGR2LAB)
l_ch, a_ch, b_ch = cv2.split(lab_img)
blur_l_ch = cv2.medianBlur(l_ch, 1)
circles = cv2.HoughCircles(blur_l_ch, cv2.HOUGH_GRADIENT, **hough_circles_kws)
if circles is not None:
values_img = l_ch
# compute means
if verbose:
print('Image size: ', values_img.shape)
circles = np.squeeze(circles)
values = []
for x0, y0, r in circles:
mask = circle(values_img.shape, r, (y0, x0))
values.append(np.percentile(values_img[mask], 90))
circles = np.concatenate((circles, np.array(values).reshape(-1, 1)), -1)
threshold = threshold_otsu(np.array(values))
if verbose:
print('Threshold: ', threshold)
# plot circles
for x0, y0, r, mean in circles:
if mean > threshold:
# good circles in green
cv2.circle(in_img, (int(x0), int(y0)), int(r), (0, 255, 0), 2)
else:
# bad circles in red
cv2.circle(in_img, (int(x0), int(y0)), int(r), (0, 0, 255), 2)
if verbose:
print('Circles:')
print(circles)
print('Num Circles: ', circles.shape[0])
print('Good Circles: ', np.sum(values > threshold))
if out_filepath:
cv2.imwrite(out_filepath.format(**locals()), in_img)
return out_filepath, circles, threshold
out_filepath, circles, threshold = find_circles('blobs.jpg')
This would generate the following output:
Image size: (230, 294)
Threshold: 96.1328125
Circles:
[[ 36.5 108.5 21.10000038 155.5 ]
[170.5 124.5 24.39999962 170. ]
[ 43.5 156.5 21.10000038 156.5 ]
[ 33.5 57.5 22.20000076 190. ]
[101.5 40.5 19.89999962 90. ]
[ 75.5 78.5 18.79999924 88. ]
[254.5 171.5 16.60000038 82. ]
[138.5 52.5 15.39999962 90. ]
[123.5 148.5 14.39999962 90. ]
[ 42.5 199.5 15.39999962 174. ]
[138.5 15.5 14.10000038 88. ]
[ 86.5 176.5 15.39999962 90. ]
[256.5 23.5 15.5 146. ]
[211.5 140.5 14.39999962 87. ]
[132.5 193.5 13.19999981 90.1 ]
[174.5 35.5 9.60000038 93. ]
[ 81.5 129.5 11. 93. ]
[223.5 54.5 9.60000038 87. ]
[177.5 75.5 13.19999981 146. ]
[214.5 195.5 11. 90. ]
[259.5 126.5 9.60000038 90. ]
[ 62.5 22.5 11. 96. ]
[220.5 98.5 9.60000038 89. ]
[263.5 77.5 12.10000038 84.1 ]
[116.5 101.5 9.60000038 92. ]
[170.5 177.5 11. 91. ]
[251.5 215.5 11. 91. ]
[167.5 215.5 11. 87. ]
[214.5 14.5 9.60000038 92. ]]
Num Circles: 29
Good Circles: 7
and the corresponding image:
(and of course you can adapt the code above to better fit your needs).
EDIT: Included some code and figures.
It is also possible to plot a barchart of good/bad results:
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
values = circles[:, -1]
data = [np.sum(values <= threshold), np.sum(values > threshold)]
labels = ['Bad', 'Good']
colors = ['red', 'green']
ax.bar(labels, data, color=colors)
plt.show()
Or to plot a complete histogram, e.g.:
fig, ax = plt.subplots()
hist, edges = np.histogram(values, bins=40)
widths = (edges[1:] - edges[:-1])
ax.bar(edges[:-1] + widths / 2, hist, widths) # plots the histogram
ax.axvline(x=threshold, color='black') # plots the threshold (optional)
plt.show()
EDIT: Included additional barplot and histogram
Post a Comment for "How Can I Correctly Classify The Number Of Positive (bright Color) Circles And Negative (dark Color) Circles In The Image"