问题
Long post - please bear with me.
For a better understanding of what the goal is and what I have done so far, I have posted the code. Please let me know if any further information is required.
I have an image (as shown) and the goal is to correctly classify the number of positive (blue) and negative (purple) circles. I do not care about the semi-circles in the image. As shown in the image, there are 29 circles (excluding the semi circles) in which there 7 positive ones. But my code detects only 1 positive. Here is what I have done so far:
import cv2
import numpy as np
from matplotlib import pyplot as plt
from PIL import Image
import math
import cv2.cv as cv
# --------Read Images--------------------------
I = cv2.imread('input_image.jpg')
# -----------Apply Contrast---------------------
lab = cv2.cvtColor(I, cv2.COLOR_BGR2LAB) # Converting image to LAB Color model
l, a, b = cv2.split(lab) # Splitting the LAB image to different channels
clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8, 8)) # Applying CLAHE to L-channel
cl = clahe.apply(l)
limg = cv2.merge((cl, a, b)) # Merge the CLAHE enhanced L-channel with the a and b channel
localContrast = cv2.cvtColor(limg, cv2.COLOR_LAB2BGR) # Converting image from LAB Color model to RGB model
print("Local Contrast shape is", localContrast.shape)
print("Local Contrast shape is", type(localContrast))
cv2.imwrite('./Output/localContrast.jpg', localContrast)
# -------------Find Circles -----------------------
input_img = cv2.imread('./Output/localContrast.jpg') # Read Contrast Image
gray_img = cv2.cvtColor(input_img, cv2.COLOR_BGR2GRAY)
blur_img = cv2.medianBlur(gray_img, 7)
circles = cv2.HoughCircles(blur_img, cv.CV_HOUGH_GRADIENT, dp=1, minDist=20, param1=50, param2=30, minRadius=5,
maxRadius=36)
circles = np.uint16(np.around(circles))
no_of_circles = 0
radii = []
cx= []
cy = []
if circles is not None:
# convert the (x, y) coordinates and radius of the circles to integers
circles = np.round(circles[0, :]).astype("int")
no_of_circles = len(circles)
# loop over the (x, y) coordinates and radius of the circles
for (x,y,r) in circles:
radii.append(r)
cx.append(x)
cy.append(y)
centers = [cx, cy]
# draw the circle in the output image, then draw a rectangle
# corresponding to the center of the circle
cv2.circle(input_img, (x, y), r, (0, 0, 255), 2)
cv2.imwrite('/home/vr1019/Notebook/Output/circle_img.jpg', input_img)
print ('no of circles',no_of_circles)
The output is as shown in the image below: ('no of circles', 30)
Next, I calculated the intensity of each circle by taking the top 10 % of the pixel value (this is how I need to calculate the intensity). The idea is taken from createCirclesMask.m
def createCircleMask(localContrast, centers, radii):
radii = np.reshape(radii, (len(radii),1))
centers = np.asarray(centers)
centers = np.transpose(centers)
xdim = localContrast.shape[0]
ydim = localContrast.shape[1]
x = np.arange(0, xdim)
y = np.arange(0, ydim)
x = np.reshape(x, (1, len(x)))
y = np.reshape(y, (1, len(y)))
[xx,yy]= np.meshgrid(y, x)
xc = centers[:,0]
xc = np.reshape(xc, (len(xc),1))
yc = centers[:,1]
yc = np.reshape(yc, (len(yc),1))
circle_intensity = []
for ii in range(len(radii)):
r_square = np.square(radii)
var1= (np.square(y-xc[ii,0]))
var2 = np.square(np.transpose(x)-yc[ii,0])
cx,cy = np.where((var1 + var2)<r_square[ii])
i1 =[]
i2 =[]
i3 =[]
npixel = cx.shape[0]
for j in range(npixel):
i1.append(localContrast[cx[j],cy[j],0]);
localContrast[cx[j],cy[j],0] = 0;
i2.append(localContrast[cx[j],cy[j],1]);
localContrast[cx[j],cy[j],1] = 0;
i3.append(localContrast[cx[j],cy[j],2]);
localContrast[cx[j],cy[j],2] = 0;
s1= sorted(i1, reverse = True)
s2=sorted(i2, reverse = True)
s3=sorted(i3, reverse = True)
# top 10 percent intensity
m1 = np.asarray(s1[0:np.int(round(abs(len(s1)*0.1)))])
m2 = np.asarray(s1[0:np.int(round(abs(len(s2)*0.1)))])
m3 = np.asarray(s1[0:np.int(round(abs(len(s3)*0.1)))])
m = np.mean((m1+m2+m3)/3)
circle_intensity.append(m)
print("The len of circle_intensty is", len(circle_intensity))
return circle_intensity
and then plotting a histogram of circle_intensity gives:
I don't know what I am doing wrong. Could anyone help me out here? I looked online for the solution (like pyimagesearch or stackoverflow etc), but couldn't find what I was looking for.
回答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
来源:https://stackoverflow.com/questions/54576667/how-can-i-correctly-classify-the-number-of-positive-bright-color-circles-and-n