Как правильно классифицировать количество положительных (ярких) и отрицательных (темных) кругов на изображении

Длинный пост - пожалуйста, потерпите меня.

Для лучшего понимания того, какова цель и что я сделал до сих пор, я разместил код. Пожалуйста, дайте мне знать, если потребуется дополнительная информация.

У меня есть изображение (как показано) и цель состоит в том, чтобы правильно классифицировать количество положительных (синих) и отрицательных (фиолетовых) кругов. Меня не волнуют полукруги на изображении. Как показано на изображении, есть 29 кругов (исключая полукруги), в которых есть 7 положительных. Но мой код обнаруживает только 1 положительный результат. Вот что я сделал до сих пор:

input_image

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)

Вывод показан на изображении ниже: («количество кругов», 30)

circle_detected

Затем я рассчитал интенсивность каждого круга, взяв верхние 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 дает:

гистограмма Circle_Intensity

Я не знаю, что я делаю неправильно. Может ли кто-нибудь помочь мне здесь? Я искал в Интернете решение (например, pyimagesearch или stackoverflow и т. д.), но не смог найти то, что искал.


person Riya208    schedule 07.02.2019    source источник


Ответы (1)


У вас есть почти все права, если вы не беспокоитесь об одной неправильно классифицированной капле, частичных каплях, которые вообще не обнаруживаются, и (очевидно) неточном размере некоторых каплей.

Последняя проблема, которую нужно решить, — это получить разумный порог между яркими и темными пятнами. Один из способов сделать это — использовать адаптивный порог, например, метод Оцу или другие.

Ознакомьтесь с дополнительными пороговыми методами от scikit-learn. .

EDIT: обновлено, чтобы лучше соответствовать тому, что вы просили.


Вкратце, по сравнению с вашим кодом, я сделал следующие модификации:

  • Поместите весь код внутри функций (это помогает мне лучше рассуждать)
  • Я определил функцию повышения контрастности, но она не используется в коде (потому что я получал худшие результаты).
  • определить функцию, которая генерирует маски, связанные с кругами (обратите внимание, что эта функция будет доступна с немного другими параметрами в PyMRT - Отказ от ответственности: я являюсь его основным автором.)
  • пороговые значения кругов с использованием масок сверху и метода Оцу для определения оптимального порогового значения

(небольшое примечание: я сохранил входное изображение как blobs.jpg).

Вот как бы я это сделал, но я уверен, что есть возможность улучшить его надежность, настроив параметры.

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

Это приведет к следующему результату:

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

и соответствующее изображение:

circles_blobs.jpg

(и, конечно, вы можете адаптировать приведенный выше код, чтобы он лучше соответствовал вашим потребностям).

EDIT: добавлен код и рисунки.

Также можно построить гистограмму хороших/плохих результатов:

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

кружки_гистограмма

РЕДАКТИРОВАНИЕ: включены дополнительные гистограммы и гистограмма

person norok2    schedule 07.02.2019
comment
Здравствуйте, спасибо за ваш ответ. Итак, вы имеете в виду, что я должен сначала поставить порог, а затем выполнить функцию createCircleMask или до обнаружения круга? - person Riya208; 07.02.2019
comment
Нет, я имею в виду, что как только круги найдены и вычислена их средняя (или иная характеристика) интенсивность, вам понадобится порог (как у Оцу) для отделения положительного от отрицательного. - person norok2; 07.02.2019
comment
Я включил некоторый код и соответствующий вывод - person norok2; 08.02.2019
comment
@norok2: Это прекрасно! Большое спасибо за Вашу помощь. Я просмотрел код и попытался понять, как он работает. У меня есть пара вопросов (пожалуйста, простите, если некоторые из них глупы, я еще новичок) 1. Нужна ли функция defEnhance_contrast? Так как он нигде в коде не используется 2. Зачем нужен np.percentile? 3. Как получить одинаковый результат, т.е. Good_circles = 7 и Bad_circles = 22 в виде гистограммы? 4. Что нужно, если многословно:? Разве нельзя печатать без использования оператора if? Еще раз, большое спасибо! - person Riya208; 21.02.2019
comment
@ Riya208 Нет проблем. (1) Нет, в этом нет необходимости, я просто включил это, потому что это было в вашем коде; (2) np.percentile() реализует верхние 10% значения пикселя, как вы указали (дополнительную информацию см. В его документах); (3) Я не уверен, что понимаю вопрос histogram.. если вы хотите построить столбцы, вы можете использовать, например. matplotlib; (4) бит verbose присутствует только потому, что вы можете не захотеть печатать каждый раз, когда используете его. Наконец, вы можете проголосовать/принять ответ. :-) - person norok2; 21.02.2019
comment
@norok2: Я имею в виду, что хочу распределить плохие круги и хорошие круги. Какими (или как) должны быть входные данные для matplotlib.pyplot.hist или np.histogram? Потому что я не могу указать np.sum(values ​​› threshold в качестве входных данных, потому что (1) это дает мне ошибку (2) это даст мне распределение только хороших кругов, а не плохих кругов Я попробовал круги в качестве входных данных, но это также дает мне ошибку, потому что это 29 * 4 матрица и тем более круги не дадут мне распределения, так как хорошие круги получаются путем установки порога на значения (т.е. np.sum(значения › порог)). - person Riya208; 23.02.2019
comment
@ Riya208: Я думаю, что вы хотите на самом деле гистограмму хороших/плохих результатов. Но я также включил правильную гистограмму значений (я хочу использовать только values, а не circles). Обратите внимание, что я также изменил функцию, чтобы она возвращала также circles и threshold. - person norok2; 24.02.2019
comment
@norok2 Большое спасибо! Хотел бы я сделать больше, чем просто проголосовать и принять ответ! - person Riya208; 24.02.2019
comment
@ norok2 Мне просто интересно, почему положительные круги не имеют распределения Гаусса? Это потому, что точка данных над положительными значениями не масштабируется должным образом? Ясно, что отрицательные кружки (т. е. значение ниже порога) имеют гауссово распределение. Потому что я думал, что оба (положительные и отрицательные) должны иметь распределение Гаусса. - person Riya208; 26.02.2019
comment
@ Riya208: Это немного по теме ... в любом случае, я попытаюсь повторить: (1) негативный взгляд на меня, поскольку у них есть распределение Пуассона; (2) слишком мало положительных, чтобы сделать вывод о распределении на глаз; (3) лежащее в основе распределение в целом будет зависеть от физики процесса, который вы наблюдаете, и я не знаю, как было сгенерировано изображение, поэтому трудно сказать, чего ожидать. - person norok2; 27.02.2019