如何正确分类图像中正(亮色)圆圈和负(深色)圆圈的数量

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

长post - 请多多包涵

为了更好地理解目标是什么以及到目前为止我做了什么,我 post 编辑了代码。如果需要任何进一步的信息,请告诉我。

我有一张图像(如图所示),目标是正确分类正(蓝色)和负(紫色)圆圈的数量我不关心图像中的半圆。如图所示,一共有29个圆(不包括半圆),其中有7个正圆。但我的代码只检测到 1 个阳性。这是我到目前为止所做的:

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)

输出如下图所示:('no of circles', 30)

接下来,我通过取像素值的前 10% 来计算每个圆的强度(这就是我需要计算强度的方式)。 思路取自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

然后绘制 circle_intensity 的直方图给出:

我不知道我做错了什么。有人可以帮我吗?我在网上寻找解决方案(如 pyimagesearch 或 Whosebug 等),但找不到我要找的东西。

如果您不担心一个错误分类的 blob、部分 blob 根本没有被检测到以及某些 blob 的(显然)大小不准确,那么您几乎答对了。

最后一个要解决的问题是在亮斑点和暗斑点之间获得一个合理的阈值。一种方法是使用自适应阈值,例如Otsu's method 或其他人。

在此处查看更多来自 scikit-learn 的 threshold methods

编辑:已更新以更好地匹配您的要求。


简而言之,与您的代码相比,我做了以下修改:

  • 将所有代码放在函数中(这有助于我更好地推理)
  • 我已经定义了一个对比度增强函数,但它没有在代码中使用(因为我的结果越来越差。)
  • 定义一个生成与圆相关的掩码的函数(请注意,此函数将可用,参数略有不同,在 PyMRT - 免责声明:我是它的主要作者。)
  • 使用上面的蒙版和确定最佳阈值的 Otsu 方法对圆圈进行阈值处理

(小注:我将输入图像保存为blobs.jpg)。

这就是我的做法,但我确信通过调整参数可以提高其稳健性。

import numpy as np
import cv2
import matplotlib.pyplot as plt

from skimage.filters import threshold_otsu


# based on: 
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')

这将生成以下输出:

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

以及对应的图片:

(当然,您可以调整上面的代码以更好地满足您的需求)。

编辑:包括一些代码和数字。

也可以绘制 good/bad 个结果的条形图:

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()

或者绘制完整的直方图,例如:

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()

编辑:包括额外的条形图和直方图