How can I correctly classify the number of positive (bright color) circles and negative (dark color) circles in the image

久未见 提交于 2019-12-20 07:13:26

问题


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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!