【问题标题】:Coffee beans separation algorithm咖啡豆分离算法
【发布时间】:2014-11-05 12:15:26
【问题描述】:

在二进制图像上分离(计数)咖啡豆的正确算法是什么?豆子可以接触并部分重叠。


(来源:beucher at cmm.ensmp.fr

我实际上不是用咖啡豆工作,而是用咖啡豆更容易描述。这是我的任务中的子问题,即计算所有在场人数以及计算超市监控视频中越过某条假想线的人数。我已经将移动对象提取到二进制掩码中,现在我需要以某种方式将它们分开。

有人在 cmets 中提到的两个有前途的算法:

  • Wathershed+距离转换+标签。正如我所说的(豆分离),这可能是这个问题的答案。
  • 跟踪视频序列中的移动对象(该算法的名称是什么?)。它可以跟踪重叠的对象。这是更有前途的算法,可能正是我解决我所拥有的任务(移动人员分离)所需要的。

【问题讨论】:

  • 投反对票的部分原因是您没有尝试过任何事情。你只是在问怎么做。
  • 这是一个非常相似的问题(甚至没有提供图像)并且得到了广泛的支持。 stackoverflow.com/questions/5560507/…我认为这是一个棘手的问题,也许你可以通过假设凸性并寻找轮廓连续性来做一些事情。
  • 分离豆类或人是非常不同的,所以你的例子不是很好(虽然我没有反对)。您应该尝试跟踪人员,然后在计数时不会出现重叠问题。
  • 我相信您在问一个研究问题,而且您似乎还没有为自己做足够的研究。如果您尝试任何图像分析书籍,您就会知道这是分割问题。您应该尝试该类别中几乎所有可能的方法。而根据我的成像经验,咖啡豆和人类之间的类比是行不通的。

标签: opencv image-processing computer-vision


【解决方案1】:

这种方法是spin-off from mmgp's answer,它详细解释了分水岭算法的工作原理。因此,如果您需要对代码的作用进行一些解释,请查看他的答案。

代码可以用来提高检测率。这里是:

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)    

边框(左),dt(右):

    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

    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(左)前后(右)形态学运算:

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)
分水岭分割的

结果(左),然后是输出(右):

【讨论】:

  • 如果您认为 C++ 代码更有帮助,我可以分享。
  • 是的,请这样做。这真的很有帮助。
  • Here it is。如果它对你有帮助,请不要忘记给这个答案投票。
  • 当你发布它时,我可能是第一个支持它的人:)。谢谢!
【解决方案2】:

下面介绍了一种查找每个 bean 中心的方法。在不同但连续的时间分析帧中分割对象的中心位置,可以跟踪它们。保持视觉轮廓或分析其路径可以提高跟踪算法在一个对象与另一个对象交叉或有一些重叠的情况下的准确性。

我使用了Marvin Image Processing Framework 和 Java。

寻找中心方法

我使用了三种基本算法:阈值、形态侵蚀和填充分割。第一步是去除背景的阈值,如下图。

下一步是应用形态侵蚀来分离豆子。在小内核矩阵的情况下,我可以将小豆子分开,但将较大的豆子放在一起,如下所示。使用每个独立片段的质量(像素数)进行过滤,可以只选择较小的片段,如下所示。

使用大核矩阵,我可以将较大的核矩阵分开,小核矩阵消失,如下所示。

结合这两个结果 - 删除距离太近且可能来自同一个 bean 的中心点 - 我得到了下面的结果。

即使没有每个豆子的实际部分,使用中心位置也可以计算和跟踪它们。这些中心还可用于找出每个豆段。

源代码

源代码是Java,但解决方案中采用的图像处理算法由大多数框架提供。


编辑:我编辑了源代码以保存每个步骤的图像。可以优化源代码,删除这些调试步骤并创建重用代码的方法。创建一些对象和列表只是为了演示这些步骤,也可以删除。
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();
    }
}

【讨论】:

  • Uau,看起来很酷。可惜我不认识马文。考虑用 JavaCV 重写你的答案。
  • 谢谢。 Marvin 最初是在巴西作为一个简单的大学项目,但如今它具有一组重要的功能,并得到了全世界人们的开发和支持。不认识马文不是什么大问题。 Marvin 旨在为 Java 开发人员提供尽可能简单的图像处理。源代码非常直观,因此您可以使用它来学习或开发新的解决方案。在短短几天内,您就会将它用作任何其他 Java 图像处理框架。
  • +1 法律。但由于它不是一个广为人知的框架,因此共享算法每一步生成的图像将使人们更容易直观地了解正在发生的事情。
【解决方案3】:

有一些优雅的答案,但我想分享我尝试过的方法,因为它与其他方法有点不同。

在阈值化并找到距离变换后,我传播距离变换图像的局部最大值。通过调整最大值传播的范围,我对距离变换后的图像进行分割,然后按面积过滤这些片段,拒绝较小的片段。

这样我可以对给定图像进行相当好的分割,尽管它没有明确定义边界。对于给定的图像,我使用在 Matlab 代码中用于控制最大值传播范围和面积阈值的参数值得到 42 段计数。

结果:

这是 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's 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 编写),可以为您提供基线。计算黑色像素的数量,并根据可以将多少个平均大小的圆圈填充到您大小的正方形中来划分面积。的优点是您可以做的最简单的事情。

    如果一个给定的方法平均而言没有比这更准确,那么您需要一个更好的方法。顺便说一句,我得到了大约 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 可能会有所帮助。这样做的一篇论文是 this one,但遗憾的是我没有找到它的公开副本。

      【讨论】:

        猜你喜欢
        • 2012-02-08
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2023-03-03
        • 1970-01-01
        • 1970-01-01
        • 2018-11-07
        • 2013-03-11
        相关资源
        最近更新 更多