Кофе beans алгоритм разделения

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

coffee beans image
(источник: beucher на cmm.ensmp.fr)

Я работаю на самом деле не с кофейными зернами, но с кофейными зернами это легче описать. Это подзадача в моей задаче подсчета всех присутствующих людей и подсчета людей, пересекающих какую-то воображаемую линию из видео наблюдения супермаркета. Я извлек движущиеся объекты в бинарную маску, и теперь мне нужно как-то разделить их.

Два многообещающих алгоритма, которые кто-то упомянул в комментариях:

  • Wathershed + distancetransofrm + маркировка. Это, вероятно, ответ на этот вопрос, как я выразился (разделение бобов).
  • Отслеживание движущихся объектов из видеопоследовательности (как называется этот алгоритм?). Он может отслеживать перекрывающиеся объекты. Это более многообещающий алгоритм и, вероятно, именно то, что мне нужно для решения стоящей передо мной задачи (перемещение людей).

Ответ 1

Этот подход представляет собой отключение от ответа mmgp, в котором подробно объясняется , как работает алгоритм водораздела. Поэтому, если вам нужно какое-то объяснение того, что делает код, проверьте его ответ.

Код может воспроизводиться с целью улучшения скорости обнаружения. Вот он:

import sys
import cv2
import numpy
from scipy.ndimage import label

def segment_on_dt(a, img):
    border = cv2.dilate(img, None, iterations=3)
    border = border - cv2.erode(border, None)
    cv2.imwrite("border.png", border)

    dt = cv2.distanceTransform(img, 2, 5)    
    dt = ((dt - dt.min()) / (dt.max() - dt.min()) * 255).astype(numpy.uint8)
    _, dt = cv2.threshold(dt, 135, 255, cv2.THRESH_BINARY)
    cv2.imwrite("dt_thres.png", dt)    

border (слева), dt (справа):

enter image description hereenter image description here

    lbl, ncc = label(dt)
    lbl = lbl * (255/ncc)      
    # Completing the markers now. 
    lbl[border == 255] = 255

    lbl = lbl.astype(numpy.int32)
    cv2.imwrite("label.png", lbl)

LBL

enter image description here

    cv2.watershed(a, lbl)

    lbl[lbl == -1] = 0
    lbl = lbl.astype(numpy.uint8)
    return 255 - lbl

# Application entry point
img = cv2.imread("beans.png")
if img == None:
    print("!!! Failed to open input image")
    sys.exit(0)

# Pre-processing.
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)    
_, img_bin = cv2.threshold(img_gray, 128, 255, cv2.THRESH_OTSU | cv2.THRESH_BINARY_INV)
cv2.imwrite("img_bin.png", img_bin)

img_bin = cv2.morphologyEx(img_bin, cv2.MORPH_OPEN, numpy.ones((3, 3), dtype=int))
cv2.imwrite("img_bin_morphoEx.png", img_bin)

img_bin (слева) до и после (справа) операции морфологии:

enter image description hereenter image description here

result = segment_on_dt(img, img_bin)
cv2.imwrite("result.png", result)

result[result != 255] = 0
result = cv2.dilate(result, None)
img[result == 255] = (0, 0, 255)
cv2.imwrite("output.png", img)

результат (слева) сегментации водоразделов, за которым следует вывод (справа):

enter image description hereenter image description here

Ответ 2

Ниже представлен подход к поиску центра каждого bean. Анализируя центральное положение сегментированных объектов в кадрах в разное, но последовательное время, можно отслеживать их. Сохранение визуальных профилей или анализ его пути может повысить точность алгоритма отслеживания в ситуациях, когда объект пересекает другой или существует некоторое перекрытие.

Я использовал Marvin Image Processing Framework и Java.

Поиск подхода центра

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

Thresholding

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

Erosion small kernel

Используя большую матрицу ядра, я могу отделить большие, а маленькие исчезнут, как показано ниже.

enter image description here

Объединяя два результата - удаляя центральные точки, которые находятся слишком близко и, вероятно, из того же bean - я получил результат, представленный ниже.

enter image description here

Даже не имея реального сегмента каждого bean, используя центральные позиции, можно подсчитывать и отслеживать их. Центры также могут быть использованы для поиска каждого сегмента bean.

Исходный код

Исходный код находится на Java, но алгоритмы обработки изображений, используемые в решении, предоставляются большинством фреймворков.


EDIT: Я отредактировал исходный код, чтобы сохранить изображения каждого шага. Исходный код можно оптимизировать, удалив эти этапы отладки и создав методы для повторного использования кода. Некоторые объекты и списки были созданы только для демонстрации тезисов и также могут быть удалены.
import static marvin.MarvinPluginCollection.floodfillSegmentation;
import static marvin.MarvinPluginCollection.thresholding;
import marvin.image.MarvinColorModelConverter;
import marvin.image.MarvinImage;
import marvin.image.MarvinSegment;
import marvin.io.MarvinImageIO;
import marvin.math.MarvinMath;
import marvin.plugin.MarvinImagePlugin;
import marvin.util.MarvinPluginLoader;

public class CoffeeBeansSeparation {

    private MarvinImagePlugin erosion = MarvinPluginLoader.loadImagePlugin("org.marvinproject.image.morphological.erosion.jar");

    public CoffeeBeansSeparation(){

        // 1. Load Image 
        MarvinImage image = MarvinImageIO.loadImage("./res/coffee.png");
        MarvinImage result = image.clone();

        // 2. Threshold
        thresholding(image, 30);

        MarvinImageIO.saveImage(image, "./res/coffee_threshold.png");

        // 3. Segment using erosion and floodfill (kernel size == 8)
        List<MarvinSegment> listSegments = new ArrayList<MarvinSegment>();
        List<MarvinSegment> listSegmentsTmp = new ArrayList<MarvinSegment>();
        MarvinImage binImage = MarvinColorModelConverter.rgbToBinary(image, 127);

        erosion.setAttribute("matrix", MarvinMath.getTrueMatrix(8, 8));
        erosion.process(binImage.clone(), binImage);

        MarvinImageIO.saveImage(binImage, "./res/coffee_bin_8.png");
        MarvinImage binImageRGB = MarvinColorModelConverter.binaryToRgb(binImage);
        MarvinSegment[] segments =  floodfillSegmentation(binImageRGB);

        // 4. Just consider the smaller segments
        for(MarvinSegment s:segments){
            if(s.mass < 300){   
                listSegments.add(s);
            }
        }

        showSegments(listSegments, binImageRGB);
        MarvinImageIO.saveImage(binImageRGB, "./res/coffee_center_8.png");

        // 5. Segment using erosion and floodfill (kernel size == 18)
        listSegments = new ArrayList<MarvinSegment>();
        binImage = MarvinColorModelConverter.rgbToBinary(image, 127);

        erosion.setAttribute("matrix", MarvinMath.getTrueMatrix(18, 18));
        erosion.process(binImage.clone(), binImage);

        MarvinImageIO.saveImage(binImage, "./res/coffee_bin_8.png");
        binImageRGB = MarvinColorModelConverter.binaryToRgb(binImage);
        segments =  floodfillSegmentation(binImageRGB);

        for(MarvinSegment s:segments){
            listSegments.add(s);
            listSegmentsTmp.add(s);
        }

        showSegments(listSegmentsTmp, binImageRGB);
        MarvinImageIO.saveImage(binImageRGB, "./res/coffee_center_18.png");

        // 6. Remove segments that are too near.
        MarvinSegment.segmentMinDistance(listSegments, 10);

        // 7. Show Result
        showSegments(listSegments, result);
        MarvinImageIO.saveImage(result, "./res/coffee_result.png");
    }

    private void showSegments(List<MarvinSegment> segments, MarvinImage image){
        for(MarvinSegment s:segments){
            image.fillRect((s.x1+s.x2)/2, (s.y1+s.y2)/2, 5, 5, Color.red);
        }
    }

    public static void main(String[] args) {
        new CoffeeBeansSeparation();
    }
}

Ответ 3

Есть несколько изящных ответов, но я подумал о том, чтобы поделиться тем, что я пробовал, потому что он немного отличается от других подходов.

После порогового значения и нахождения преобразования расстояния я распространяю локальные максимумы изображения, преобразуемого по расстоянию. Регулируя степень распространения максимумов, я отделяю изображение, преобразованное по расстоянию, затем отфильтровываю эти сегменты по их площади, отклоняя меньшие сегменты.

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

Результаты:

enter image description here

enter image description here

Здесь код Matlab:

clear all;
close all;

im = imread('ex2a.gif');
% threshold: coffee beans are black
bw = im2bw(im, graythresh(im));
% distance transform
di = bwdist(bw);
% mask for coffee beans
mask = double(1-bw);

% propagate the local maxima. depending on the extent of propagation, this
% will transform finer distance image to coarser segments 
se = ones(3);   % 8-neighbors
% this controls the extent of propagation. it some fraction of the max
% distance of the distance transformed image (50% here)
mx = ceil(max(di(:))*.5);
peaks = di;
for r = 1:mx
    peaks = imdilate(peaks, se);
    peaks = peaks.*mask;
end

% how many different segments/levels we have in the final image
lvls = unique(peaks(:));
lvls(1) = []; % remove first, which is 0 that corresponds to background
% impose a min area constraint for segments. we can adjust this threshold
areaTh = pi*mx*mx*.7;
% number of segments after thresholding by area
nseg = 0;

% construct the final segmented image after thresholding segments by area
z = ones(size(bw));
lblid = 10;  % label id of a segment
for r = 1:length(lvls)
    lvl = peaks == lvls(r); % pixels having a certain value(level)
    props = regionprops(lvl, 'Area', 'PixelIdxList'); % get the area and the pixels
    % threshold area
    area = [props.Area];
    abw = area > areaTh;
    % take the count that passes the imposed area threshold
    nseg = nseg + sum(abw);
    % mark the segments that pass the imposed area threshold with a unique
    % id
    for i = 1:length(abw)
        if (1 == abw(i))
            idx = props(i).PixelIdxList;
            z(idx) = lblid; % assign id to the pixels
            lblid = lblid + 1; % increment id
        end
    end
end

figure,
subplot(1, 2, 1), imshow(di, []), title('distance transformed')
subplot(1, 2, 2), imshow(peaks, []), title('after propagating maxima'), colormap(jet)
figure,
subplot(1, 2, 1), imshow(label2rgb(z)), title('segmented')
subplot(1, 2, 2), imshow(im), title('original')

Ответ 4

Вот некоторый код (в Python), который даст вам базовый уровень. Подсчитайте количество черных пикселей и разделите их на область, в которой количество кругов среднего размера может быть упаковано в квадрат вашего размера. В силу того, что вы можете сделать простейшую возможную вещь.

Если данный метод не является в среднем более точным, чем этот, то вам нужен лучший метод. BTW Я получаю около 85% точности, поэтому ваш 95% не может быть и речи.

import Image

im = Image.open('ex2a.gif').convert('RGB')
(h,w) = im.size
print h,w
num_pixels = h*w
print num_pixels
black_pixels = 0
for i in range(h):
    for j in range(w):
        q = im.getpixel((i,j)) 
        if q[0]<10 and q[1]<10 and q[2]<10:
            black_pixels = black_pixels + 1
            im.putpixel((i,j),(255,0,0))
r = 15
unpackable = (h/(2*r))*(w/(2*r))*((2*r)**2 - 3.14*r**2)
print 'unpackable:',unpackable
print 'num beans:',round((num_pixels-2*unpackable)/750.0)
im.save('qq.jpg')

Ответ 5

Erosion может помочь. Одна из этих работ - этот, но, к сожалению, я не нашел общедоступной копии.