如何检测圣诞树?

394
哪些图像处理技术可以用于实现检测以下图片中显示的圣诞树的应用程序?
我正在寻找适用于所有这些图像的解决方案。因此,需要训练haar级联分类器或模板匹配的方法不是非常有趣。
我正在寻找一些可以使用任何编程语言编写的东西,只要它使用的是开源技术。解决方案必须在分享本问题的图像上进行测试。有 6个输入图像,答案应显示每个图像的处理结果。最后,对于每个输出图像,都必须绘制红色线条以围绕检测到的树木。
您会如何通过编程方式检测这些图像中的树木?

3
我们是否可以使用部分图片进行训练,还是所有提供的图像都应用于验证?无论哪种方式,比赛很酷 :D - Hannes Ovrén
7
@karlphillip,你希望我们将这些图像用于测试,而其他图像用于训练吗?只是目前不清楚训练集是什么。 - GilLevi
16
我的建议是放弃“开源”的要求。你使用什么语言/框架并不重要。图像处理/计算机视觉算法与语言无关,因此如果你可以在MATLAB中编写它,那么你肯定可以在OpenCV或其他任何你喜欢的框架中实现...... 另外,我仍然不清楚你认为什么是训练/测试图像! - Amro
2
@karlphillip 谢谢你动员我们所有人为你的“探索”做出贡献!这是一个很好的机会,可以有成效地度过几个小时,更重要的是,可以看到在解决单一问题时可以找到多种不同的方法... 希望你能在明年1月1日再次举办(也许是“圣诞老人雪橇挑战”?;-)) - sepdek
2
好的,我重新措辞了问题,删除了竞争要素。我认为这样就可以独立存在了。 - Brad Larson
显示剩余18条评论
10个回答

191

我有一种方法,我认为它很有趣,与其他方法有些不同。我的方法与其他方法的主要区别在于图像分割步骤的执行方式--我使用了Python scikit-learn中的DBSCAN聚类算法;它被优化用于查找可能没有单个清晰质心的略微无定形的形状。

在最高级别上,我的方法相当简单,可以分解为大约3个步骤。首先我应用一个阈值(或者实际上是两个单独且不同阈值的逻辑“或”)。与许多其他答案一样,我假设圣诞树将是场景中较亮的物体之一,因此第一个阈值只是一个简单的单色亮度测试;任何像素值在0-255范围内高于220(其中黑色为0,白色为255)的像素保存到二进制黑白图像中。第二个阈值尝试查找红色和黄色的灯光,这些灯光在六幅图像的左上和右下的树木中特别突出,并且在多数照片中普遍存在的蓝绿色背景中非常显眼。我将RGB图像转换为HSV空间,并要求色调在0.0-1.0范围内小于0.2(大致对应于黄绿色之间的边界)或大于0.95(对应于紫色和红色之间的边界),并且还需要明亮、饱和的颜色:饱和度和值必须都高于0.7。两个阈值过程的结果逻辑上“或”在一起,得到的黑白二进制图像矩阵如下所示:

Christmas trees, after thresholding on HSV as well as monochrome brightness

您可以清楚地看到每张图片都有一个大的像素聚类,大致对应于每棵树的位置,另外一些图片也有一些其他小的聚类,对应于一些建筑物窗户中的灯光或者天际线上的背景场景。下一步是让计算机识别这些是单独的聚类,并正确地标记每个像素的聚类成员身份编号。

对于这个任务,我选择了DBSCAN。相对于其他聚类算法,这里有一个相当不错的视觉比较可用,可以在这里找到。正如我之前说的,它在形状上做得很好。每个聚类以不同的颜色绘制的DBSCAN输出如下所示:

DBSCAN clustering output

当查看这个结果时,有些事情需要注意。首先,DBSCAN算法要求用户设置“接近度”参数以控制其行为,该参数有效地控制一对点之间必须分开多远才能使算法声明一个新的单独聚类,而不是将测试点聚合到已经存在的聚类中。我将这个值设置为每个图像沿对角线的大小的0.04倍。由于图像的大小从大约VGA到HD 1080不等,因此这种相对比例定义至关重要。

另一个值得注意的问题是,scikit-learn中实现的DBSCAN算法具有相当具有挑战性的内存限制,对于此样本中的一些较大图像来说,这是非常具有挑战性的。因此,对于其中一些较大的图像,我实际上不得不“抽取”(即保留每第3或4个像素并删除其他像素)每个聚类,以保持在此限制范围内。由于这个筛选过程,剩下的单个稀疏像素在一些更大的图像上很难看到。因此,仅用于显示目的,上述图像中的彩色像素已被轻微“膨胀”,以使它们更加突出。这只是为了叙述的外观操作。虽然我的代码中有提及此扩张,但请放心,它与任何实际重要的计算无关。

一旦聚类被识别和标记,第三个也是最后一个步骤变得容易:我只需取每个图像中最大的聚类(在这种情况下,我选择用成员像素的总数来衡量“大小”,尽管可以同样轻松地使用某种衡量物理范围的度量标准),并为该聚类计算凸包。然后,凸包就成为了树木的边缘。通过这种方法计算的六个凸包如下所示:

Christmas trees with their calculated borders

源代码编写为Python 2.7.6版本,并依赖于numpyscipymatplotlibscikit-learn。我将其分为两部分。第一部分负责实际的图像处理:

from PIL import Image
import numpy as np
import scipy as sp
import matplotlib.colors as colors
from sklearn.cluster import DBSCAN
from math import ceil, sqrt

"""
Inputs:

    rgbimg:         [M,N,3] numpy array containing (uint, 0-255) color image

    hueleftthr:     Scalar constant to select maximum allowed hue in the
                    yellow-green region

    huerightthr:    Scalar constant to select minimum allowed hue in the
                    blue-purple region

    satthr:         Scalar constant to select minimum allowed saturation

    valthr:         Scalar constant to select minimum allowed value

    monothr:        Scalar constant to select minimum allowed monochrome
                    brightness

    maxpoints:      Scalar constant maximum number of pixels to forward to
                    the DBSCAN clustering algorithm

    proxthresh:     Proximity threshold to use for DBSCAN, as a fraction of
                    the diagonal size of the image

Outputs:

    borderseg:      [K,2,2] Nested list containing K pairs of x- and y- pixel
                    values for drawing the tree border

    X:              [P,2] List of pixels that passed the threshold step

    labels:         [Q,2] List of cluster labels for points in Xslice (see
                    below)

    Xslice:         [Q,2] Reduced list of pixels to be passed to DBSCAN

"""

def findtree(rgbimg, hueleftthr=0.2, huerightthr=0.95, satthr=0.7, 
             valthr=0.7, monothr=220, maxpoints=5000, proxthresh=0.04):

    # Convert rgb image to monochrome for
    gryimg = np.asarray(Image.fromarray(rgbimg).convert('L'))
    # Convert rgb image (uint, 0-255) to hsv (float, 0.0-1.0)
    hsvimg = colors.rgb_to_hsv(rgbimg.astype(float)/255)

    # Initialize binary thresholded image
    binimg = np.zeros((rgbimg.shape[0], rgbimg.shape[1]))
    # Find pixels with hue<0.2 or hue>0.95 (red or yellow) and saturation/value
    # both greater than 0.7 (saturated and bright)--tends to coincide with
    # ornamental lights on trees in some of the images
    boolidx = np.logical_and(
                np.logical_and(
                  np.logical_or((hsvimg[:,:,0] < hueleftthr),
                                (hsvimg[:,:,0] > huerightthr)),
                                (hsvimg[:,:,1] > satthr)),
                                (hsvimg[:,:,2] > valthr))
    # Find pixels that meet hsv criterion
    binimg[np.where(boolidx)] = 255
    # Add pixels that meet grayscale brightness criterion
    binimg[np.where(gryimg > monothr)] = 255

    # Prepare thresholded points for DBSCAN clustering algorithm
    X = np.transpose(np.where(binimg == 255))
    Xslice = X
    nsample = len(Xslice)
    if nsample > maxpoints:
        # Make sure number of points does not exceed DBSCAN maximum capacity
        Xslice = X[range(0,nsample,int(ceil(float(nsample)/maxpoints)))]

    # Translate DBSCAN proximity threshold to units of pixels and run DBSCAN
    pixproxthr = proxthresh * sqrt(binimg.shape[0]**2 + binimg.shape[1]**2)
    db = DBSCAN(eps=pixproxthr, min_samples=10).fit(Xslice)
    labels = db.labels_.astype(int)

    # Find the largest cluster (i.e., with most points) and obtain convex hull   
    unique_labels = set(labels)
    maxclustpt = 0
    for k in unique_labels:
        class_members = [index[0] for index in np.argwhere(labels == k)]
        if len(class_members) > maxclustpt:
            points = Xslice[class_members]
            hull = sp.spatial.ConvexHull(points)
            maxclustpt = len(class_members)
            borderseg = [[points[simplex,0], points[simplex,1]] for simplex
                          in hull.simplices]

    return borderseg, X, labels, Xslice

第二部分是一个用户级别的脚本,调用第一个文件并生成所有上述的图表:

#!/usr/bin/env python

from PIL import Image
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.cm as cm
from findtree import findtree

# Image files to process
fname = ['nmzwj.png', 'aVZhC.png', '2K9EF.png',
         'YowlH.png', '2y4o5.png', 'FWhSP.png']

# Initialize figures
fgsz = (16,7)        
figthresh = plt.figure(figsize=fgsz, facecolor='w')
figclust  = plt.figure(figsize=fgsz, facecolor='w')
figcltwo  = plt.figure(figsize=fgsz, facecolor='w')
figborder = plt.figure(figsize=fgsz, facecolor='w')
figthresh.canvas.set_window_title('Thresholded HSV and Monochrome Brightness')
figclust.canvas.set_window_title('DBSCAN Clusters (Raw Pixel Output)')
figcltwo.canvas.set_window_title('DBSCAN Clusters (Slightly Dilated for Display)')
figborder.canvas.set_window_title('Trees with Borders')

for ii, name in zip(range(len(fname)), fname):
    # Open the file and convert to rgb image
    rgbimg = np.asarray(Image.open(name))

    # Get the tree borders as well as a bunch of other intermediate values
    # that will be used to illustrate how the algorithm works
    borderseg, X, labels, Xslice = findtree(rgbimg)

    # Display thresholded images
    axthresh = figthresh.add_subplot(2,3,ii+1)
    axthresh.set_xticks([])
    axthresh.set_yticks([])
    binimg = np.zeros((rgbimg.shape[0], rgbimg.shape[1]))
    for v, h in X:
        binimg[v,h] = 255
    axthresh.imshow(binimg, interpolation='nearest', cmap='Greys')

    # Display color-coded clusters
    axclust = figclust.add_subplot(2,3,ii+1) # Raw version
    axclust.set_xticks([])
    axclust.set_yticks([])
    axcltwo = figcltwo.add_subplot(2,3,ii+1) # Dilated slightly for display only
    axcltwo.set_xticks([])
    axcltwo.set_yticks([])
    axcltwo.imshow(binimg, interpolation='nearest', cmap='Greys')
    clustimg = np.ones(rgbimg.shape)    
    unique_labels = set(labels)
    # Generate a unique color for each cluster 
    plcol = cm.rainbow_r(np.linspace(0, 1, len(unique_labels)))
    for lbl, pix in zip(labels, Xslice):
        for col, unqlbl in zip(plcol, unique_labels):
            if lbl == unqlbl:
                # Cluster label of -1 indicates no cluster membership;
                # override default color with black
                if lbl == -1:
                    col = [0.0, 0.0, 0.0, 1.0]
                # Raw version
                for ij in range(3):
                    clustimg[pix[0],pix[1],ij] = col[ij]
                # Dilated just for display
                axcltwo.plot(pix[1], pix[0], 'o', markerfacecolor=col, 
                    markersize=1, markeredgecolor=col)
    axclust.imshow(clustimg)
    axcltwo.set_xlim(0, binimg.shape[1]-1)
    axcltwo.set_ylim(binimg.shape[0], -1)

    # Plot original images with read borders around the trees
    axborder = figborder.add_subplot(2,3,ii+1)
    axborder.set_axis_off()
    axborder.imshow(rgbimg, interpolation='nearest')
    for vseg, hseg in borderseg:
        axborder.plot(hseg, vseg, 'r-', lw=3)
    axborder.set_xlim(0, binimg.shape[1]-1)
    axborder.set_ylim(binimg.shape[0], -1)

plt.show()

@lennon310的解决方案是聚类。(k-means) - user3054997
1
@stachyra 在提出我的简单方法之前,我也考虑过这种方法。我认为这种方法有很大的潜力可以扩展和推广到其他情况下产生良好的结果。您可以尝试使用神经网络进行聚类。像SOM或神经气体之类的东西会做出出色的工作。无论如何,非常棒的建议,我点赞! - sepdek
4
@Faust 和 Ryan Carlson:谢谢你们!我同意赞同投票系统可以很好地处理在短时间内提交2或3个简短回答的仲裁,但在长时间内播放的长答案比赛方面存在严重的偏见。首先,早期提交的答案在晚些时候才可供公众审查之前就开始累积赞成票。如果所有答案都很长,那么一旦某个答案建立了适度领先地位,通常会出现“跟风效应”,人们只会点赞第一个而不费心去阅读其他答案。 - stachyra
2
@stachyra,好消息啊朋友!最热烈的祝贺,愿这标志着你新年的开始! - sepdek
1
@lennon310:我还没有在这个问题上尝试过局部最大值检测滤波器,但如果你想自己探索一下,scipy包含这个。我的Python源代码非常简短,以至于我实际上能够发布其中100%的内容;你只需要将我的两个代码片段复制并粘贴到不同的.py文件中,然后在我使用阈值的同一位置替换一个对scipy.ndimage.filters.maximum_filter()的调用即可。 - stachyra
显示剩余10条评论

149
EDIT NOTE: 本帖子已进行编辑,按照要求对每个树形图像进行了单独处理,并考虑了物体的亮度和形状,以提高结果的质量。


下面介绍一种考虑对象亮度和形状的方法。换句话说,它寻找三角形状且明显亮度的对象。该方法使用Java实现,使用Marvin图像处理框架。

第一步是颜色阈值处理。其目的是将分析聚焦于亮度显著的对象上。

输出图像:

源代码:

public class ChristmasTree {

private MarvinImagePlugin fill = MarvinPluginLoader.loadImagePlugin("org.marvinproject.image.fill.boundaryFill");
private MarvinImagePlugin threshold = MarvinPluginLoader.loadImagePlugin("org.marvinproject.image.color.thresholding");
private MarvinImagePlugin invert = MarvinPluginLoader.loadImagePlugin("org.marvinproject.image.color.invert");
private MarvinImagePlugin dilation = MarvinPluginLoader.loadImagePlugin("org.marvinproject.image.morphological.dilation");

public ChristmasTree(){
    MarvinImage tree;

    // Iterate each image
    for(int i=1; i<=6; i++){
        tree = MarvinImageIO.loadImage("./res/trees/tree"+i+".png");
        
        // 1. Threshold
        threshold.setAttribute("threshold", 200);
        threshold.process(tree.clone(), tree);
    }
}
public static void main(String[] args) {
    new ChristmasTree();
}
}

第二步,图像中最亮的点被扩张以形成形状。这个过程的结果是具有明显亮度的物体的可能形状。应用泛洪填充分割算法,检测到不连通的形状。

输出图像:

源代码:

public class ChristmasTree {

private MarvinImagePlugin fill = MarvinPluginLoader.loadImagePlugin("org.marvinproject.image.fill.boundaryFill");
private MarvinImagePlugin threshold = MarvinPluginLoader.loadImagePlugin("org.marvinproject.image.color.thresholding");
private MarvinImagePlugin invert = MarvinPluginLoader.loadImagePlugin("org.marvinproject.image.color.invert");
private MarvinImagePlugin dilation = MarvinPluginLoader.loadImagePlugin("org.marvinproject.image.morphological.dilation");

public ChristmasTree(){
    MarvinImage tree;
    
    // Iterate each image
    for(int i=1; i<=6; i++){
        tree = MarvinImageIO.loadImage("./res/trees/tree"+i+".png");
        
        // 1. Threshold
        threshold.setAttribute("threshold", 200);
        threshold.process(tree.clone(), tree);
        
        // 2. Dilate
        invert.process(tree.clone(), tree);
        tree = MarvinColorModelConverter.rgbToBinary(tree, 127);
        MarvinImageIO.saveImage(tree, "./res/trees/new/tree_"+i+"threshold.png");
        dilation.setAttribute("matrix", MarvinMath.getTrueMatrix(50, 50));
        dilation.process(tree.clone(), tree);
        MarvinImageIO.saveImage(tree, "./res/trees/new/tree_"+1+"_dilation.png");
        tree = MarvinColorModelConverter.binaryToRgb(tree);
        
        // 3. Segment shapes
        MarvinImage trees2 = tree.clone();
        fill(tree, trees2);
        MarvinImageIO.saveImage(trees2, "./res/trees/new/tree_"+i+"_fill.png");
}

private void fill(MarvinImage imageIn, MarvinImage imageOut){
    boolean found;
    int color= 0xFFFF0000;
    
    while(true){
        found=false;
        
        Outerloop:
        for(int y=0; y<imageIn.getHeight(); y++){
            for(int x=0; x<imageIn.getWidth(); x++){
                if(imageOut.getIntComponent0(x, y) == 0){
                    fill.setAttribute("x", x);
                    fill.setAttribute("y", y);
                    fill.setAttribute("color", color);
                    fill.setAttribute("threshold", 120);
                    fill.process(imageIn, imageOut);
                    color = newColor(color);
                    
                    found = true;
                    break Outerloop;
                }
            }
        }
        
        if(!found){
            break;
        }
    }
    
}

private int newColor(int color){
    int red = (color & 0x00FF0000) >> 16;
    int green = (color & 0x0000FF00) >> 8;
    int blue = (color & 0x000000FF);
    
    if(red <= green && red <= blue){
        red+=5;
    }
    else if(green <= red && green <= blue){
        green+=5;
    }
    else{
        blue+=5;
    }
    
    return 0xFF000000 + (red << 16) + (green << 8) + blue;
}

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

如图所示,检测到了多个形状。在这个问题中,图片中只有很少的亮点。然而,这种方法被用来处理更复杂的情况。

接下来,每个形状都会被分析。一个简单的算法检测具有类似三角形的模式的形状。该算法逐行分析对象形状。如果每个形状线的质心几乎相同(给定阈值),且随着y的增加质量增加,则该对象具有类似三角形的形状。形状线的质量是属于该形状的那条线上的像素数。想象一下你把对象水平切片,并分析每个水平部分。如果它们彼此集中,并且长度从第一个部分到最后一个部分以线性方式增加,那么你可能有一个类似三角形的对象。

源代码:

private int[] detectTrees(MarvinImage image){
    HashSet<Integer> analysed = new HashSet<Integer>();
    boolean found;
    while(true){
        found = false;
        for(int y=0; y<image.getHeight(); y++){
            for(int x=0; x<image.getWidth(); x++){
                int color = image.getIntColor(x, y);
                
                if(!analysed.contains(color)){
                    if(isTree(image, color)){
                        return getObjectRect(image, color);
                    }
                    
                    analysed.add(color);
                    found=true;
                }
            }
        }
        
        if(!found){
            break;
        }
    }
    return null;
}

private boolean isTree(MarvinImage image, int color){
    
    int mass[][] = new int[image.getHeight()][2];
    int yStart=-1;
    int xStart=-1;
    for(int y=0; y<image.getHeight(); y++){
        int mc = 0;
        int xs=-1;
        int xe=-1;
        for(int x=0; x<image.getWidth(); x++){
            if(image.getIntColor(x, y) == color){
                mc++;
                
                if(yStart == -1){
                    yStart=y;
                    xStart=x;
                }
                
                if(xs == -1){
                    xs = x;
                }
                if(x > xe){
                    xe = x;
                }
            }
        }
        mass[y][0] = xs;
        mass[y][3] = xe;
        mass[y][4] = mc;    
    }
    
    int validLines=0;
    for(int y=0; y<image.getHeight(); y++){
        if
        ( 
            mass[y][5] > 0 &&
            Math.abs(((mass[y][0]+mass[y][6])/2)-xStart) <= 50 &&
            mass[y][7] >= (mass[yStart][8] + (y-yStart)*0.3) &&
            mass[y][9] <= (mass[yStart][10] + (y-yStart)*1.5)
        )
        {
            validLines++;
        }
    }
    
    if(validLines > 100){
        return true;
    }
    return false;
}

最终,在原始图像中突出显示了每个形状类似于三角形且亮度显著的位置,例如圣诞树,如下所示。

最终输出图像:

最终源代码:

public class ChristmasTree {

private MarvinImagePlugin fill = MarvinPluginLoader.loadImagePlugin("org.marvinproject.image.fill.boundaryFill");
private MarvinImagePlugin threshold = MarvinPluginLoader.loadImagePlugin("org.marvinproject.image.color.thresholding");
private MarvinImagePlugin invert = MarvinPluginLoader.loadImagePlugin("org.marvinproject.image.color.invert");
private MarvinImagePlugin dilation = MarvinPluginLoader.loadImagePlugin("org.marvinproject.image.morphological.dilation");

public ChristmasTree(){
    MarvinImage tree;
    
    // Iterate each image
    for(int i=1; i<=6; i++){
        tree = MarvinImageIO.loadImage("./res/trees/tree"+i+".png");
        
        // 1. Threshold
        threshold.setAttribute("threshold", 200);
        threshold.process(tree.clone(), tree);
        
        // 2. Dilate
        invert.process(tree.clone(), tree);
        tree = MarvinColorModelConverter.rgbToBinary(tree, 127);
        MarvinImageIO.saveImage(tree, "./res/trees/new/tree_"+i+"threshold.png");
        dilation.setAttribute("matrix", MarvinMath.getTrueMatrix(50, 50));
        dilation.process(tree.clone(), tree);
        MarvinImageIO.saveImage(tree, "./res/trees/new/tree_"+1+"_dilation.png");
        tree = MarvinColorModelConverter.binaryToRgb(tree);
        
        // 3. Segment shapes
        MarvinImage trees2 = tree.clone();
        fill(tree, trees2);
        MarvinImageIO.saveImage(trees2, "./res/trees/new/tree_"+i+"_fill.png");
        
        // 4. Detect tree-like shapes
        int[] rect = detectTrees(trees2);
        
        // 5. Draw the result
        MarvinImage original = MarvinImageIO.loadImage("./res/trees/tree"+i+".png");
        drawBoundary(trees2, original, rect);
        MarvinImageIO.saveImage(original, "./res/trees/new/tree_"+i+"_out_2.jpg");
    }
}

private void drawBoundary(MarvinImage shape, MarvinImage original, int[] rect){
    int yLines[] = new int[6];
    yLines[0] = rect[1];
    yLines[1] = rect[1]+(int)((rect[3]/5));
    yLines[2] = rect[1]+((rect[3]/5)*2);
    yLines[3] = rect[1]+((rect[3]/5)*3);
    yLines[4] = rect[1]+(int)((rect[3]/5)*4);
    yLines[5] = rect[1]+rect[3];
    
    List<Point> points = new ArrayList<Point>();
    for(int i=0; i<yLines.length; i++){
        boolean in=false;
        Point startPoint=null;
        Point endPoint=null;
        for(int x=rect[0]; x<rect[0]+rect[2]; x++){
            
            if(shape.getIntColor(x, yLines[i]) != 0xFFFFFFFF){
                if(!in){
                    if(startPoint == null){
                        startPoint = new Point(x, yLines[i]);
                    }
                }
                in = true;
            }
            else{
                if(in){
                    endPoint = new Point(x, yLines[i]);
                }
                in = false;
            }
        }
        
        if(endPoint == null){
            endPoint = new Point((rect[0]+rect[2])-1, yLines[i]);
        }
        
        points.add(startPoint);
        points.add(endPoint);
    }
    
    drawLine(points.get(0).x, points.get(0).y, points.get(1).x, points.get(1).y, 15, original);
    drawLine(points.get(1).x, points.get(1).y, points.get(3).x, points.get(3).y, 15, original);
    drawLine(points.get(3).x, points.get(3).y, points.get(5).x, points.get(5).y, 15, original);
    drawLine(points.get(5).x, points.get(5).y, points.get(7).x, points.get(7).y, 15, original);
    drawLine(points.get(7).x, points.get(7).y, points.get(9).x, points.get(9).y, 15, original);
    drawLine(points.get(9).x, points.get(9).y, points.get(11).x, points.get(11).y, 15, original);
    drawLine(points.get(11).x, points.get(11).y, points.get(10).x, points.get(10).y, 15, original);
    drawLine(points.get(10).x, points.get(10).y, points.get(8).x, points.get(8).y, 15, original);
    drawLine(points.get(8).x, points.get(8).y, points.get(6).x, points.get(6).y, 15, original);
    drawLine(points.get(6).x, points.get(6).y, points.get(4).x, points.get(4).y, 15, original);
    drawLine(points.get(4).x, points.get(4).y, points.get(2).x, points.get(2).y, 15, original);
    drawLine(points.get(2).x, points.get(2).y, points.get(0).x, points.get(0).y, 15, original);
}

private void drawLine(int x1, int y1, int x2, int y2, int length, MarvinImage image){
    int lx1, lx2, ly1, ly2;
    for(int i=0; i<length; i++){
        lx1 = (x1+i >= image.getWidth() ? (image.getWidth()-1)-i: x1);
        lx2 = (x2+i >= image.getWidth() ? (image.getWidth()-1)-i: x2);
        ly1 = (y1+i >= image.getHeight() ? (image.getHeight()-1)-i: y1);
        ly2 = (y2+i >= image.getHeight() ? (image.getHeight()-1)-i: y2);
        
        image.drawLine(lx1+i, ly1, lx2+i, ly2, Color.red);
        image.drawLine(lx1, ly1+i, lx2, ly2+i, Color.red);
    }
}

private void fillRect(MarvinImage image, int[] rect, int length){
    for(int i=0; i<length; i++){
        image.drawRect(rect[0]+i, rect[1]+i, rect[2]-(i*2), rect[3]-(i*2), Color.red);
    }
}

private void fill(MarvinImage imageIn, MarvinImage imageOut){
    boolean found;
    int color= 0xFFFF0000;
    
    while(true){
        found=false;
        
        Outerloop:
        for(int y=0; y<imageIn.getHeight(); y++){
            for(int x=0; x<imageIn.getWidth(); x++){
                if(imageOut.getIntComponent0(x, y) == 0){
                    fill.setAttribute("x", x);
                    fill.setAttribute("y", y);
                    fill.setAttribute("color", color);
                    fill.setAttribute("threshold", 120);
                    fill.process(imageIn, imageOut);
                    color = newColor(color);
                    
                    found = true;
                    break Outerloop;
                }
            }
        }
        
        if(!found){
            break;
        }
    }
    
}

private int[] detectTrees(MarvinImage image){
    HashSet<Integer> analysed = new HashSet<Integer>();
    boolean found;
    while(true){
        found = false;
        for(int y=0; y<image.getHeight(); y++){
            for(int x=0; x<image.getWidth(); x++){
                int color = image.getIntColor(x, y);
                
                if(!analysed.contains(color)){
                    if(isTree(image, color)){
                        return getObjectRect(image, color);
                    }
                    
                    analysed.add(color);
                    found=true;
                }
            }
        }
        
        if(!found){
            break;
        }
    }
    return null;
}

private boolean isTree(MarvinImage image, int color){
    
    int mass[][] = new int[image.getHeight()][11];
    int yStart=-1;
    int xStart=-1;
    for(int y=0; y<image.getHeight(); y++){
        int mc = 0;
        int xs=-1;
        int xe=-1;
        for(int x=0; x<image.getWidth(); x++){
            if(image.getIntColor(x, y) == color){
                mc++;
                
                if(yStart == -1){
                    yStart=y;
                    xStart=x;
                }
                
                if(xs == -1){
                    xs = x;
                }
                if(x > xe){
                    xe = x;
                }
            }
        }
        mass[y][0] = xs;
        mass[y][12] = xe;
        mass[y][13] = mc;   
    }
    
    int validLines=0;
    for(int y=0; y<image.getHeight(); y++){
        if
        ( 
            mass[y][14] > 0 &&
            Math.abs(((mass[y][0]+mass[y][15])/2)-xStart) <= 50 &&
            mass[y][16] >= (mass[yStart][17] + (y-yStart)*0.3) &&
            mass[y][18] <= (mass[yStart][19] + (y-yStart)*1.5)
        )
        {
            validLines++;
        }
    }
    
    if(validLines > 100){
        return true;
    }
    return false;
}

private int[] getObjectRect(MarvinImage image, int color){
    int x1=-1;
    int x2=-1;
    int y1=-1;
    int y2=-1;
    
    for(int y=0; y<image.getHeight(); y++){
        for(int x=0; x<image.getWidth(); x++){
            if(image.getIntColor(x, y) == color){
                
                if(x1 == -1 || x < x1){
                    x1 = x;
                }
                if(x2 == -1 || x > x2){
                    x2 = x;
                }
                if(y1 == -1 || y < y1){
                    y1 = y;
                }
                if(y2 == -1 || y > y2){
                    y2 = y;
                }
            }
        }
    }
    
    return new int[]{x1, y1, (x2-x1), (y2-y1)};
}

private int newColor(int color){
    int red = (color & 0x00FF0000) >> 16;
    int green = (color & 0x0000FF00) >> 8;
    int blue = (color & 0x000000FF);
    
    if(red <= green && red <= blue){
        red+=5;
    }
    else if(green <= red && green <= blue){
        green+=30;
    }
    else{
        blue+=30;
    }
    
    return 0xFF000000 + (red << 16) + (green << 8) + blue;
}

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

这种方法的优点在于它能够分析物体的形状,因此可以处理包含其他发光物体的图像。

圣诞快乐!


编辑说明2

现在有一些争论关于该解决方案的输出图像与其他某些解决方案的相似性问题。实际上,它们非常相似。但是,这种方法不仅仅是对物体进行分割,还从某种意义上分析了物体的形状。它可以处理同一场景中的多个发光物体。实际上,圣诞树不需要是最亮的。我只是将它作为丰富讨论的一个例子。只是通过寻找最亮的物体来选择样本存在偏差,你会找到树。但是,我们真的想在这个地方停止讨论吗?在这一点上,计算机真正意义上识别出类似于圣诞树的物体有多远呢?让我们试着弥合这个差距吧。

下面是一些结果,以阐明这一点:

输入图像

enter image description here

输出

enter image description here


2
很有趣。我希望在处理每个图像时可以获得相同的结果。我在你回答之前4小时编辑了问题,特别说明了这一点。如果您能使用这些结果更新您的答案,那将是太棒了。 - karlphillip
@Marvin 在你的三角形检测中,你是如何处理质量波动的?它不是一个严格的三角形,质量随着 y 的变化而不单一。 - user3054997
2
@user3054997:这是另一个要点。正如我所发布的,该算法并不寻求严格的三角形状。它分析每个对象,并考虑一棵“类似”三角形的树,其简单标准为:物体的质量随着y的增加而增加,并且每个水平物体段的重心几乎集中在彼此之间。 - Gabriel Archanjo
@Marvin,我的解决方案非常简单,我在答案中也提到了。顺便说一句,它比你的第一个解决方案效果更好。如果我没记错的话,在你的第一个答案中,你谈到了特征描述符来检测小光纹理,这与你现在所做的不同。我只是说你目前的方法和结果更类似于我的,而不是你的第一个解决方案。当然,我不指望你承认,我只是为了记录而说。 - smeso
1
@sepdek 这里有几个解决方案比我的好得多,但它们仍然获得了我一半的赞数。从其他解决方案中“获得灵感”没有任何问题。我也看到了你的解决方案,我对你没有任何意见,你是在我之后发布的,而我的“想法”并不那么原创,以至于你只是复制了我。但是Marvin是唯一一个在我之前发布并在看到我的解决方案后使用相同算法编辑他的解决方案的人... 至少他可以说“是的,我喜欢你的解决方案,我重用了它”,这没有任何问题,这只是一个游戏。 - smeso
显示剩余4条评论

75

这是我的简单而愚蠢的解决方案。 它基于这样一个假设,即树是图片中最明亮和最大的物体。

//g++ -Wall -pedantic -ansi -O2 -pipe -s -o christmas_tree christmas_tree.cpp `pkg-config --cflags --libs opencv`
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <iostream>

using namespace cv;
using namespace std;

int main(int argc,char *argv[])
{
    Mat original,tmp,tmp1;
    vector <vector<Point> > contours;
    Moments m;
    Rect boundrect;
    Point2f center;
    double radius, max_area=0,tmp_area=0;
    unsigned int j, k;
    int i;

    for(i = 1; i < argc; ++i)
    {
        original = imread(argv[i]);
        if(original.empty())
        {
            cerr << "Error"<<endl;
            return -1;
        }

        GaussianBlur(original, tmp, Size(3, 3), 0, 0, BORDER_DEFAULT);
        erode(tmp, tmp, Mat(), Point(-1, -1), 10);
        cvtColor(tmp, tmp, CV_BGR2HSV);
        inRange(tmp, Scalar(0, 0, 0), Scalar(180, 255, 200), tmp);

        dilate(original, tmp1, Mat(), Point(-1, -1), 15);
        cvtColor(tmp1, tmp1, CV_BGR2HLS);
        inRange(tmp1, Scalar(0, 185, 0), Scalar(180, 255, 255), tmp1);
        dilate(tmp1, tmp1, Mat(), Point(-1, -1), 10);

        bitwise_and(tmp, tmp1, tmp1);

        findContours(tmp1, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE);
        max_area = 0;
        j = 0;
        for(k = 0; k < contours.size(); k++)
        {
            tmp_area = contourArea(contours[k]);
            if(tmp_area > max_area)
            {
                max_area = tmp_area;
                j = k;
            }
        }
        tmp1 = Mat::zeros(original.size(),CV_8U);
        approxPolyDP(contours[j], contours[j], 30, true);
        drawContours(tmp1, contours, j, Scalar(255,255,255), CV_FILLED);

        m = moments(contours[j]);
        boundrect = boundingRect(contours[j]);
        center = Point2f(m.m10/m.m00, m.m01/m.m00);
        radius = (center.y - (boundrect.tl().y))/4.0*3.0;
        Rect heightrect(center.x-original.cols/5, boundrect.tl().y, original.cols/5*2, boundrect.size().height);

        tmp = Mat::zeros(original.size(), CV_8U);
        rectangle(tmp, heightrect, Scalar(255, 255, 255), -1);
        circle(tmp, center, radius, Scalar(255, 255, 255), -1);

        bitwise_and(tmp, tmp1, tmp1);

        findContours(tmp1, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE);
        max_area = 0;
        j = 0;
        for(k = 0; k < contours.size(); k++)
        {
            tmp_area = contourArea(contours[k]);
            if(tmp_area > max_area)
            {
                max_area = tmp_area;
                j = k;
            }
        }

        approxPolyDP(contours[j], contours[j], 30, true);
        convexHull(contours[j], contours[j]);

        drawContours(original, contours, j, Scalar(0, 0, 255), 3);

        namedWindow(argv[i], CV_WINDOW_NORMAL|CV_WINDOW_KEEPRATIO|CV_GUI_EXPANDED);
        imshow(argv[i], original);

        waitKey(0);
        destroyWindow(argv[i]);
    }

    return 0;
}

第一步是检测图片中最明亮的像素,但是我们必须区分树本身和反射其光线的雪。在这里,我们尝试通过对颜色代码施加非常简单的过滤器来排除雪:

GaussianBlur(original, tmp, Size(3, 3), 0, 0, BORDER_DEFAULT);
erode(tmp, tmp, Mat(), Point(-1, -1), 10);
cvtColor(tmp, tmp, CV_BGR2HSV);
inRange(tmp, Scalar(0, 0, 0), Scalar(180, 255, 200), tmp);

然后我们找到每个“明亮”的像素:

dilate(original, tmp1, Mat(), Point(-1, -1), 15);
cvtColor(tmp1, tmp1, CV_BGR2HLS);
inRange(tmp1, Scalar(0, 185, 0), Scalar(180, 255, 255), tmp1);
dilate(tmp1, tmp1, Mat(), Point(-1, -1), 10);

最后,我们将这两个结果合并:

bitwise_and(tmp, tmp1, tmp1);

现在我们寻找最亮的物体:

findContours(tmp1, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE);
max_area = 0;
j = 0;
for(k = 0; k < contours.size(); k++)
{
    tmp_area = contourArea(contours[k]);
    if(tmp_area > max_area)
    {
        max_area = tmp_area;
        j = k;
    }
}
tmp1 = Mat::zeros(original.size(),CV_8U);
approxPolyDP(contours[j], contours[j], 30, true);
drawContours(tmp1, contours, j, Scalar(255,255,255), CV_FILLED);

现在,我们几乎完成了,但由于雪的原因仍有一些不完美之处。为了去除它们,我们将使用一个圆和一个矩形来构建一个掩码来近似树的形状以删除不需要的部分:

m = moments(contours[j]);
boundrect = boundingRect(contours[j]);
center = Point2f(m.m10/m.m00, m.m01/m.m00);
radius = (center.y - (boundrect.tl().y))/4.0*3.0;
Rect heightrect(center.x-original.cols/5, boundrect.tl().y, original.cols/5*2, boundrect.size().height);

tmp = Mat::zeros(original.size(), CV_8U);
rectangle(tmp, heightrect, Scalar(255, 255, 255), -1);
circle(tmp, center, radius, Scalar(255, 255, 255), -1);

bitwise_and(tmp, tmp1, tmp1);

最后一步是找到我们树的轮廓并将其绘制在原始图片上。

findContours(tmp1, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE);
max_area = 0;
j = 0;
for(k = 0; k < contours.size(); k++)
{
    tmp_area = contourArea(contours[k]);
    if(tmp_area > max_area)
    {
        max_area = tmp_area;
        j = k;
    }
}

approxPolyDP(contours[j], contours[j], 30, true);
convexHull(contours[j], contours[j]);

drawContours(original, contours, j, Scalar(0, 0, 255), 3);

很抱歉,目前我的网络连接不太好,无法上传图片。我会尝试稍后再上传。

圣诞快乐。

编辑:

以下是最终输出的一些图片:


1
你好!请确保你的回答满足以下要求:有6张输入图片,回答应该显示每张图片处理后的结果; - karlphillip
你好!你可以将文件名作为CLI参数传递给我的程序:./christmas_tree ./*.png。你可以传递任意数量的文件名,结果将会依次显示,按下任意键即可。这有问题吗? - smeso
没问题,但你仍然需要上传图片并在问题中分享它们,这样线程的观众才能真正“看到”你的结果。让人们看到你所做的事情将提高你获得赞成票的机会 ;) - karlphillip
我已经附上了一些图片。 - smeso
2
太好了!现在你可以使用以下代码在答案中重新调整它们的大小:<img src="http://i.stack.imgur.com/nmzwj.png" width="210" height="150">只需更改图片链接即可 ;) - karlphillip
显示剩余2条评论

62

我在Matlab R2007a中编写了这段代码,使用k-means算法大致提取了圣诞树。我只会展示一张中间结果图片以及全部六张的最终结果。

首先,我将RGB空间映射到Lab空间,这可以增强其b通道中红色的对比度:

colorTransform = makecform('srgb2lab');
I = applycform(I, colorTransform);
L = double(I(:,:,1));
a = double(I(:,:,2));
b = double(I(:,:,3));

除了颜色空间特征外,我还使用了与邻域相关而不是每个像素本身的纹理特征。在这里,我线性组合了来自3个原始通道(R、G、B)的强度。我这样格式化的原因是图片中的圣诞树都有红色灯,有时还有绿色/蓝色的照明。

R=double(Irgb(:,:,1));
G=double(Irgb(:,:,2));
B=double(Irgb(:,:,3));
I0 = (3*R + max(G,B)-min(G,B))/2;

我在I0上应用了3x3的本地二进制模式,使用中心像素作为阈值,并通过计算超过阈值的平均像素强度值和低于它的平均值之间的差异来获得对比度。

I0_copy = zeros(size(I0));
for i = 2 : size(I0,1) - 1
    for j = 2 : size(I0,2) - 1
        tmp = I0(i-1:i+1,j-1:j+1) >= I0(i,j);
        I0_copy(i,j) = mean(mean(tmp.*I0(i-1:i+1,j-1:j+1))) - ...
            mean(mean(~tmp.*I0(i-1:i+1,j-1:j+1))); % Contrast
    end
end

由于我总共有4个特征,我会在聚类方法中选择K=5。下面是k-means的代码(这段代码来自于吴恩达博士的机器学习课程。我之前上过这门课程,并在他的编程作业中亲自编写了这段代码)。

[centroids, idx] = runkMeans(X, initial_centroids, max_iters);
mask=reshape(idx,img_size(1),img_size(2));

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function [centroids, idx] = runkMeans(X, initial_centroids, ...
                                  max_iters, plot_progress)
   [m n] = size(X);
   K = size(initial_centroids, 1);
   centroids = initial_centroids;
   previous_centroids = centroids;
   idx = zeros(m, 1);

   for i=1:max_iters    
      % For each example in X, assign it to the closest centroid
      idx = findClosestCentroids(X, centroids);

      % Given the memberships, compute new centroids
      centroids = computeCentroids(X, idx, K);

   end
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function idx = findClosestCentroids(X, centroids)
   K = size(centroids, 1);
   idx = zeros(size(X,1), 1);
   for xi = 1:size(X,1)
      x = X(xi, :);
      % Find closest centroid for x.
      best = Inf;
      for mui = 1:K
        mu = centroids(mui, :);
        d = dot(x - mu, x - mu);
        if d < best
           best = d;
           idx(xi) = mui;
        end
      end
   end 
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function centroids = computeCentroids(X, idx, K)
   [m n] = size(X);
   centroids = zeros(K, n);
   for mui = 1:K
      centroids(mui, :) = sum(X(idx == mui, :)) / sum(idx == mui);
   end

由于程序在我的电脑上运行非常缓慢,我只运行了3次迭代。通常情况下停止条件是:(i)至少进行10次迭代,或(ii)质心不再改变。在我的测试中,增加迭代次数可以更精确地区分背景(天空和树木、天空和建筑物等),但是并没有在圣诞树提取方面显示出明显的变化。此外请注意,k-means对随机质心初始化也不免疫,因此建议多次运行程序以进行比较。

在k-means之后,选择具有最大强度I0的标记区域。然后使用边界跟踪提取边界。对我来说,最后一棵圣诞树是最难提取的,因为该图片中的对比度不够高,而前五张图片中的对比度相对较高。我方法中的另一个问题是,我使用了Matlab中的bwboundaries函数来跟踪边界,但有时内部边界也包括在内,正如您可以在第3、5和6张结果中观察到的那样。圣诞树内的暗面不仅未能与照明面聚类,而且还导致许多小的内部边界跟踪(imfill并没有很大改善)。总的来说,我的算法仍有很大的改进空间。

一些出版物表明,均值漂移可能比k-means更健壮,并且许多基于图割的算法在复杂边界分割方面也非常有竞争力。我自己编写了一个均值漂移算法,似乎能够更好地提取没有足够光的区域。但是均值漂移有些过度分割,需要一些合并策略。它甚至在我的电脑上运行得比k-means还要慢,我不得不放弃它。我热切期待看到其他人使用上述现代算法提交优秀的结果。

然而,我始终相信特征选择是图像分


2
谢谢您的回答!我只想指出Matlab不是开源的,但Scilab是。我很希望看到这个答案与其他答案竞争。 ;) - karlphillip
6
谢谢,Octave是另一个开源软件,其编程语法与Matlab几乎相同:http://www.mathworks.fr/matlabcentral/answers/14399-gnu-octave-vs-matlab。 - lennon310
有趣,我不知道那个,谢谢!你的代码在Octave上能用吗? - karlphillip
我还没有测试,但我认为这应该没问题 :) - lennon310
@lennon310 我认为如果你放弃边界并获取凸包,你将摆脱孔洞问题。请记住,凸包是包含集合中所有点的最小区域。 - sepdek
@sepdek 非常感谢您的建议。是的,我刚刚注意到regionprops包括凸壳。 这是一种很好的方法来定位目标而不会产生内部干扰。 尽管凸包可能会影响外边界表面的精度,但这仍然是在引入不必要的信号和非常粗略的直线近似之间做出的很好的权衡。 再次感谢您指出! - lennon310

58

这是我使用传统图像处理方法的最后一篇文章...

在这里,我以某种方式结合了我的另外两个提案,取得了更好的结果。事实上,当你看到该方法产生的遮罩图像时,我无法想象这些结果如何能够更好。

该方法的核心是将三个关键假设相结合:

  1. 图像应在树区域内具有高频变化
  2. 图像在树区域内应具有更高的强度
  3. 背景区域应具有低强度并且大部分呈蓝调

基于这些假设,该方法的工作流程如下:

  1. 将图像转换为HSV色彩空间
  2. 使用LoG滤波器过滤V通道
  3. 对LoG滤波后的图像进行硬阈值分割,得到'活动'掩模A
  4. 对V通道进行硬阈值分割,得到强度掩模B
  5. 使用H通道阈值分割将低强度蓝调区域捕捉到背景掩模C中
  6. 使用AND运算组合掩模,得到最终掩模
  7. 膨胀掩模以扩大区域并连接分散的像素
  8. 消除小区域,获取最终掩模,最终仅表示树木

这是MATLAB代码(同样,该脚本加载当前文件夹中的所有jpg图像,而且这远非一段经过优化的代码):

% clear everything
clear;
pack;
close all;
close all hidden;
drawnow;
clc;

% initialization
ims=dir('./*.jpg');
imgs={};
images={}; 
blur_images={}; 
log_image={}; 
dilated_image={};
int_image={};
back_image={};
bin_image={};
measurements={};
box={};
num=length(ims);
thres_div = 3;

for i=1:num, 
    % load original image
    imgs{end+1}=imread(ims(i).name);

    % convert to HSV colorspace
    images{end+1}=rgb2hsv(imgs{i});

    % apply laplacian filtering and heuristic hard thresholding
    val_thres = (max(max(images{i}(:,:,3)))/thres_div);
    log_image{end+1} = imfilter( images{i}(:,:,3),fspecial('log')) > val_thres;

    % get the most bright regions of the image
    int_thres = 0.26*max(max( images{i}(:,:,3)));
    int_image{end+1} = images{i}(:,:,3) > int_thres;

    % get the most probable background regions of the image
    back_image{end+1} = images{i}(:,:,1)>(150/360) & images{i}(:,:,1)<(320/360) & images{i}(:,:,3)<0.5;

    % compute the final binary image by combining 
    % high 'activity' with high intensity
    bin_image{end+1} = logical( log_image{i}) & logical( int_image{i}) & ~logical( back_image{i});

    % apply morphological dilation to connect distonnected components
    strel_size = round(0.01*max(size(imgs{i})));        % structuring element for morphological dilation
    dilated_image{end+1} = imdilate( bin_image{i}, strel('disk',strel_size));

    % do some measurements to eliminate small objects
    measurements{i} = regionprops( logical( dilated_image{i}),'Area','BoundingBox');

    % iterative enlargement of the structuring element for better connectivity
    while length(measurements{i})>14 && strel_size<(min(size(imgs{i}(:,:,1)))/2),
        strel_size = round( 1.5 * strel_size);
        dilated_image{i} = imdilate( bin_image{i}, strel('disk',strel_size));
        measurements{i} = regionprops( logical( dilated_image{i}),'Area','BoundingBox');
    end

    for m=1:length(measurements{i})
        if measurements{i}(m).Area < 0.05*numel( dilated_image{i})
            dilated_image{i}( round(measurements{i}(m).BoundingBox(2):measurements{i}(m).BoundingBox(4)+measurements{i}(m).BoundingBox(2)),...
                round(measurements{i}(m).BoundingBox(1):measurements{i}(m).BoundingBox(3)+measurements{i}(m).BoundingBox(1))) = 0;
        end
    end
    % make sure the dilated image is the same size with the original
    dilated_image{i} = dilated_image{i}(1:size(imgs{i},1),1:size(imgs{i},2));
    % compute the bounding box
    [y,x] = find( dilated_image{i});
    if isempty( y)
        box{end+1}=[];
    else
        box{end+1} = [ min(x) min(y) max(x)-min(x)+1 max(y)-min(y)+1];
    end
end 

%%% additional code to display things
for i=1:num,
    figure;
    subplot(121);
    colormap gray;
    imshow( imgs{i});
    if ~isempty(box{i})
        hold on;
        rr = rectangle( 'position', box{i});
        set( rr, 'EdgeColor', 'r');
        hold off;
    end
    subplot(122);
    imshow( imgs{i}.*uint8(repmat(dilated_image{i},[1 1 3])));
end

结果

results

可以在这里找到高分辨率的结果!
此处可找到更多的实验结果和附加图像。


1
太棒了!请确保您的其他答案也遵循此格式。为了竞争赏金,您必须使用开源技术,不幸的是Matlab不在其中。然而,SciLab和Octave是开源技术,并且提供类似的语法和函数。 ;) - karlphillip
@karlphillip 不知何故,这个问题最终被打上了Matlab标签。如果开源确实是必须的话,我建议将其删除。 - Dennis Jaheruddin
@sepdek 很好,或许还可以做一些工作来包括最终图片中的“空洞”。(添加所有被批准像素完全包围的像素?!) - Dennis Jaheruddin
感谢@DennisJaheruddin的建议。我在第一张图中只有几个洞,这是最难应对的。但解决方案实际上很简单。可以微调控制算法的几个阈值,就不会有洞了。 - sepdek
@sepdek,我也想奖励你的回答,但目前这个问题的最低悬赏是500声望。作为回报,我给你现有的所有回答点了赞。很高兴看到你得到了很多赞。这个帖子将会存在很多年,这些回答将会给你带来更多的赞。 - karlphillip
1
@karlphillip 谢谢你!我很高兴你觉得我的方法有趣。此外,我想祝贺你选择了最优雅的解决方案,而不是得票最多的那个! - sepdek

37

我的解决方案步骤:

  1. 获取R通道(从RGB中) - 所有的操作都在该通道上进行:

  2. 创建感兴趣区域(ROI)

    • 将R通道阈值设为最小值149(右上图像)

    • 膨胀结果区域(左中图像)

  3. 检测计算出的ROI中的边缘。树有很多边缘(中右图像)

    • 膨胀结果

    • 用更大的半径腐蚀(左下图像)

  4. 选择最大(按面积)的对象 - 它是结果区域

  5. 凸包(树是凸多边形)(右下图像)

  6. 边框(右下图像 - 绿色框)

逐步操作: enter image description here

第一个结果 - 最简单的但不是开源软件 - “Adaptive Vision Studio + Adaptive Vision Library”: 这不是开源的,但非常快速的原型:

检测圣诞树的整个算法(11个块): AVL solution

下一步。我们想要一个开源解决方案。将AVL过滤器更改为OpenCV过滤器: 我做了一些小改动,例如边缘检测使用cvCanny滤波器,为了尊重ROI,我将多区域图像与边缘图像相乘,为了选择最大的元素,我使用findContours + contourArea,但思路是相同的。

https://www.youtube.com/watch?v=sfjB3MigLH0&index=1&list=UUpSRrkMHNHiLDXgylwhWNQQ

OpenCV solution

我现在无法显示中间步骤的图像,因为我只能放置2个链接。

好的,现在我们使用开源过滤器,但仍然不是全部开源。 最后一步 - 移植到C ++代码。我使用了OpenCV 2.4.4版本

最终C++代码的结果是: enter image description here

C ++代码也相当简短:

#include "opencv2/highgui/highgui.hpp"
#include "opencv2/opencv.hpp"
#include <algorithm>
using namespace cv;

int main()
{

    string images[6] = {"..\\1.png","..\\2.png","..\\3.png","..\\4.png","..\\5.png","..\\6.png"};

    for(int i = 0; i < 6; ++i)
    {
        Mat img, thresholded, tdilated, tmp, tmp1;
        vector<Mat> channels(3);

        img = imread(images[i]);
        split(img, channels);
        threshold( channels[2], thresholded, 149, 255, THRESH_BINARY);                      //prepare ROI - threshold
        dilate( thresholded, tdilated,  getStructuringElement( MORPH_RECT, Size(22,22) ) ); //prepare ROI - dilate
        Canny( channels[2], tmp, 75, 125, 3, true );    //Canny edge detection
        multiply( tmp, tdilated, tmp1 );    // set ROI

        dilate( tmp1, tmp, getStructuringElement( MORPH_RECT, Size(20,16) ) ); // dilate
        erode( tmp, tmp1, getStructuringElement( MORPH_RECT, Size(36,36) ) ); // erode

        vector<vector<Point> > contours, contours1(1);
        vector<Point> convex;
        vector<Vec4i> hierarchy;
        findContours( tmp1, contours, hierarchy, CV_RETR_TREE, CV_CHAIN_APPROX_SIMPLE, Point(0, 0) );

        //get element of maximum area
        //int bestID = std::max_element( contours.begin(), contours.end(), 
        //  []( const vector<Point>& A, const vector<Point>& B ) { return contourArea(A) < contourArea(B); } ) - contours.begin();

            int bestID = 0;
        int bestArea = contourArea( contours[0] );
        for( int i = 1; i < contours.size(); ++i )
        {
            int area = contourArea( contours[i] );
            if( area > bestArea )
            {
                bestArea  = area;
                bestID = i;
            }
        }

        convexHull( contours[bestID], contours1[0] ); 
        drawContours( img, contours1, 0, Scalar( 100, 100, 255 ), img.rows / 100, 8, hierarchy, 0, Point() );

        imshow("image", img );
        waitKey(0);
    }


    return 0;
}

哪个编译器可以在没有错误的情况下构建这个程序? - karlphillip
我使用Visual Studio 2012进行构建。你应该使用支持c++11的c++编译器。 - AdamF
好的,这是C++11的特性 ;) 我已经修改了上面的源代码。请现在尝试。 - AdamF
好的,谢谢。我测试了一下,它很漂亮。只要这个问题重新开放(其他用户必须帮助我),我就可以设置另一个赏金来奖励你。恭喜! - karlphillip
如果你喜欢的话,谢谢。对我来说已经足够好了。虽然我是初学者,但如果能得到一些积分那就更好了,不过我相信还会有很多机会的 :) - AdamF
显示剩余2条评论

31

另一个老派的解决方案-完全基于HSV处理:

  1. 将图像转换为HSV颜色空间
  2. 根据HSV中的启发式方法创建掩码(见下文)
  3. 对掩码应用形态学膨胀以连接不连续的区域
  4. 丢弃小区域和水平块(记住树是垂直块)
  5. 计算边界框

关于HSV处理中的启发式方法:

  1. 所有色调(H)在210-320度之间的内容都被丢弃,因为它们是蓝紫色的背景或非相关区域
  2. 所有亮度(V)低于40%的内容也被丢弃,因为它们过于暗淡而不相关

当然,您可以尝试许多其他可能性来微调此方法...

这是进行此操作的MATLAB代码(警告:该代码远非优化!我使用了不推荐在MATLAB编程中使用的技术,只是为了能够跟踪过程中的任何数据-可以进行大量优化):

% clear everything
clear;
pack;
close all;
close all hidden;
drawnow;
clc;

% initialization
ims=dir('./*.jpg');
num=length(ims);

imgs={};
hsvs={}; 
masks={};
dilated_images={};
measurements={};
boxs={};

for i=1:num, 
    % load original image
    imgs{end+1} = imread(ims(i).name);
    flt_x_size = round(size(imgs{i},2)*0.005);
    flt_y_size = round(size(imgs{i},1)*0.005);
    flt = fspecial( 'average', max( flt_y_size, flt_x_size));
    imgs{i} = imfilter( imgs{i}, flt, 'same');
    % convert to HSV colorspace
    hsvs{end+1} = rgb2hsv(imgs{i});
    % apply a hard thresholding and binary operation to construct the mask
    masks{end+1} = medfilt2( ~(hsvs{i}(:,:,1)>(210/360) & hsvs{i}(:,:,1)<(320/360))&hsvs{i}(:,:,3)>0.4);
    % apply morphological dilation to connect distonnected components
    strel_size = round(0.03*max(size(imgs{i})));        % structuring element for morphological dilation
    dilated_images{end+1} = imdilate( masks{i}, strel('disk',strel_size));
    % do some measurements to eliminate small objects
    measurements{i} = regionprops( dilated_images{i},'Perimeter','Area','BoundingBox'); 
    for m=1:length(measurements{i})
        if (measurements{i}(m).Area < 0.02*numel( dilated_images{i})) || (measurements{i}(m).BoundingBox(3)>1.2*measurements{i}(m).BoundingBox(4))
            dilated_images{i}( round(measurements{i}(m).BoundingBox(2):measurements{i}(m).BoundingBox(4)+measurements{i}(m).BoundingBox(2)),...
                round(measurements{i}(m).BoundingBox(1):measurements{i}(m).BoundingBox(3)+measurements{i}(m).BoundingBox(1))) = 0;
        end
    end
    dilated_images{i} = dilated_images{i}(1:size(imgs{i},1),1:size(imgs{i},2));
    % compute the bounding box
    [y,x] = find( dilated_images{i});
    if isempty( y)
        boxs{end+1}=[];
    else
        boxs{end+1} = [ min(x) min(y) max(x)-min(x)+1 max(y)-min(y)+1];
    end

end 

%%% additional code to display things
for i=1:num,
    figure;
    subplot(121);
    colormap gray;
    imshow( imgs{i});
    if ~isempty(boxs{i})
        hold on;
        rr = rectangle( 'position', boxs{i});
        set( rr, 'EdgeColor', 'r');
        hold off;
    end
    subplot(122);
    imshow( imgs{i}.*uint8(repmat(dilated_images{i},[1 1 3])));
end

结果:

在结果中,我展示了被遮盖的图像和边界框。 输入图像描述


你好,感谢你的回答。请花点时间阅读“要求”部分,确保你的答案遵循所有的说明。你忘记分享了结果图像。 ;) - karlphillip
2
@karlphillip sepdek没有足够的声望来分享图片,我根据他的链接和说明将图片移动到了答案正文中。不过我不确定那些是否正确,请随意评论此部分。 - alko
@alko 我知道,谢谢。但是你分享的一些图片不在输入集中。答案必须展示处理所有在问题中分享的6张图片的结果。 - karlphillip
@karlphillip那是他的图片,不是我的。这正是我所说的“评论这一部分”的意思 ;) - alko
2
很抱歉造成了问题...我并非有意为之。我已经将所有图像包含在初始数据集中,并增加了更多的图像来证明我的概念是健壮的... - sepdek
显示剩余4条评论

23

一些老派的图像处理方法......
这个想法基于假设图像描绘了光照下的树木,通常是在较暗和较平滑的背景(或某些情况下是前景)上。光照下的树木区域更具“活力”和更高的强度。
该过程如下:

  1. 转换为灰度级别
  2. 应用LoG滤波器获取最活跃的区域
  3. 应用强度阈值来获取最亮的区域
  4. 结合前两者以获得初步掩模
  5. 应用形态膨胀以扩大区域并连接相邻组件
  6. 根据其面积大小消除小候选区域

您得到的是每个图像的二进制掩模和边界框。

以下是使用此朴素技术的结果: enter image description here

在MATLAB中的代码如下: 该代码在一个包含JPG图像的文件夹上运行。加载所有图像并返回检测到的结果。

% clear everything
clear;
pack;
close all;
close all hidden;
drawnow;
clc;

% initialization
ims=dir('./*.jpg');
imgs={};
images={}; 
blur_images={}; 
log_image={}; 
dilated_image={};
int_image={};
bin_image={};
measurements={};
box={};
num=length(ims);
thres_div = 3;

for i=1:num, 
    % load original image
    imgs{end+1}=imread(ims(i).name);

    % convert to grayscale
    images{end+1}=rgb2gray(imgs{i});

    % apply laplacian filtering and heuristic hard thresholding
    val_thres = (max(max(images{i}))/thres_div);
    log_image{end+1} = imfilter( images{i},fspecial('log')) > val_thres;

    % get the most bright regions of the image
    int_thres = 0.26*max(max( images{i}));
    int_image{end+1} = images{i} > int_thres;

    % compute the final binary image by combining 
    % high 'activity' with high intensity
    bin_image{end+1} = log_image{i} .* int_image{i};

    % apply morphological dilation to connect distonnected components
    strel_size = round(0.01*max(size(imgs{i})));        % structuring element for morphological dilation
    dilated_image{end+1} = imdilate( bin_image{i}, strel('disk',strel_size));

    % do some measurements to eliminate small objects
    measurements{i} = regionprops( logical( dilated_image{i}),'Area','BoundingBox');
    for m=1:length(measurements{i})
        if measurements{i}(m).Area < 0.05*numel( dilated_image{i})
            dilated_image{i}( round(measurements{i}(m).BoundingBox(2):measurements{i}(m).BoundingBox(4)+measurements{i}(m).BoundingBox(2)),...
                round(measurements{i}(m).BoundingBox(1):measurements{i}(m).BoundingBox(3)+measurements{i}(m).BoundingBox(1))) = 0;
        end
    end
    % make sure the dilated image is the same size with the original
    dilated_image{i} = dilated_image{i}(1:size(imgs{i},1),1:size(imgs{i},2));
    % compute the bounding box
    [y,x] = find( dilated_image{i});
    if isempty( y)
        box{end+1}=[];
    else
        box{end+1} = [ min(x) min(y) max(x)-min(x)+1 max(y)-min(y)+1];
    end
end 

%%% additional code to display things
for i=1:num,
    figure;
    subplot(121);
    colormap gray;
    imshow( imgs{i});
    if ~isempty(box{i})
        hold on;
        rr = rectangle( 'position', box{i});
        set( rr, 'EdgeColor', 'r');
        hold off;
    end
    subplot(122);
    imshow( imgs{i}.*uint8(repmat(dilated_image{i},[1 1 3])));
end

不要忘记上传生成的图片,就像Faust所做的那样。 - karlphillip
我是一个新手,无法上传图片。请在我的描述中提供的链接中查看结果。 - sepdek
好的,但你仍然需要像其他人一样使用问题中分享的图像。一旦处理完毕,请将其上传到某个地方并编辑您的答案以添加链接。稍后我会编辑您的答案并将图像放置在其中。 - karlphillip
链接现在似乎包含了正确的图片。 - Dennis Jaheruddin

23
使用与我以前看到的完全不同的方法,我创建了一个脚本,通过它们的灯光检测圣诞树。结果总是对称三角形,如果需要还可以得到数值,例如树的角度(“肥度”)。
这个算法最大的威胁显然是在树旁边(数量众多)或树前面的灯光(在进一步优化之前是更大的问题)。 编辑(添加):它不能做到的事情:找出是否有圣诞树,找出一个图像中的多个圣诞树,在拉斯维加斯市中心正确检测圣诞树,检测弯曲、倒置或被砍倒的圣诞树... ;)
不同的阶段包括:
  • 计算每个像素的增加亮度(R+G+B)
  • 将每个像素顶部的8个相邻像素的值相加
  • 按此值对所有像素进行排名(最亮的在前)- 我知道,不是很微妙...
  • 从前N个中选择,从顶部开始,跳过距离太近的像素
  • 计算这些前N个数的(给我们树的大致中心)
  • 从中位数位置向上开始一个扩大搜索光束,寻找所选亮度最高的灯中最顶部的灯(人们倾向于将至少一盏灯放在最上面)
  • 从那里开始,想象向左右下方60度的线(圣诞树不应该太胖)
  • 将这60度减小到最亮的灯的20%在三角形外
  • 找到三角形底部的灯,给出树的下边界
  • 完成

标记说明:

  • 树中心的大红十字:前N个最亮灯光的中位数
  • 从那里向上的虚线:树顶的“搜索光束”
  • 较小的红十字:树顶
  • 非常小的红十字:所有前N个最亮灯光
  • 红色三角形:显然!

源代码:

<?php

ini_set('memory_limit', '1024M');

header("Content-type: image/png");

$chosenImage = 6;

switch($chosenImage){
    case 1:
        $inputImage     = imagecreatefromjpeg("nmzwj.jpg");
        break;
    case 2:
        $inputImage     = imagecreatefromjpeg("2y4o5.jpg");
        break;
    case 3:
        $inputImage     = imagecreatefromjpeg("YowlH.jpg");
        break;
    case 4:
        $inputImage     = imagecreatefromjpeg("2K9Ef.jpg");
        break;
    case 5:
        $inputImage     = imagecreatefromjpeg("aVZhC.jpg");
        break;
    case 6:
        $inputImage     = imagecreatefromjpeg("FWhSP.jpg");
        break;
    case 7:
        $inputImage     = imagecreatefromjpeg("roemerberg.jpg");
        break;
    default:
        exit();
}

// Process the loaded image

$topNspots = processImage($inputImage);

imagejpeg($inputImage);
imagedestroy($inputImage);

// Here be functions

function processImage($image) {
    $orange = imagecolorallocate($image, 220, 210, 60);
    $black = imagecolorallocate($image, 0, 0, 0);
    $red = imagecolorallocate($image, 255, 0, 0);

    $maxX = imagesx($image)-1;
    $maxY = imagesy($image)-1;

    // Parameters
    $spread = 1; // Number of pixels to each direction that will be added up
    $topPositions = 80; // Number of (brightest) lights taken into account
    $minLightDistance = round(min(array($maxX, $maxY)) / 30); // Minimum number of pixels between the brigtests lights
    $searchYperX = 5; // spread of the "search beam" from the median point to the top

    $renderStage = 3; // 1 to 3; exits the process early


    // STAGE 1
    // Calculate the brightness of each pixel (R+G+B)

    $maxBrightness = 0;
    $stage1array = array();

    for($row = 0; $row <= $maxY; $row++) {

        $stage1array[$row] = array();

        for($col = 0; $col <= $maxX; $col++) {

            $rgb = imagecolorat($image, $col, $row);
            $brightness = getBrightnessFromRgb($rgb);
            $stage1array[$row][$col] = $brightness;

            if($renderStage == 1){
                $brightnessToGrey = round($brightness / 765 * 256);
                $greyRgb = imagecolorallocate($image, $brightnessToGrey, $brightnessToGrey, $brightnessToGrey);
                imagesetpixel($image, $col, $row, $greyRgb);
            }

            if($brightness > $maxBrightness) {
                $maxBrightness = $brightness;
                if($renderStage == 1){
                    imagesetpixel($image, $col, $row, $red);
                }
            }
        }
    }
    if($renderStage == 1) {
        return;
    }


    // STAGE 2
    // Add up brightness of neighbouring pixels

    $stage2array = array();
    $maxStage2 = 0;

    for($row = 0; $row <= $maxY; $row++) {
        $stage2array[$row] = array();

        for($col = 0; $col <= $maxX; $col++) {
            if(!isset($stage2array[$row][$col])) $stage2array[$row][$col] = 0;

            // Look around the current pixel, add brightness
            for($y = $row-$spread; $y <= $row+$spread; $y++) {
                for($x = $col-$spread; $x <= $col+$spread; $x++) {

                    // Don't read values from outside the image
                    if($x >= 0 && $x <= $maxX && $y >= 0 && $y <= $maxY){
                        $stage2array[$row][$col] += $stage1array[$y][$x]+10;
                    }
                }
            }

            $stage2value = $stage2array[$row][$col];
            if($stage2value > $maxStage2) {
                $maxStage2 = $stage2value;
            }
        }
    }

    if($renderStage >= 2){
        // Paint the accumulated light, dimmed by the maximum value from stage 2
        for($row = 0; $row <= $maxY; $row++) {
            for($col = 0; $col <= $maxX; $col++) {
                $brightness = round($stage2array[$row][$col] / $maxStage2 * 255);
                $greyRgb = imagecolorallocate($image, $brightness, $brightness, $brightness);
                imagesetpixel($image, $col, $row, $greyRgb);
            }
        }
    }

    if($renderStage == 2) {
        return;
    }


    // STAGE 3

    // Create a ranking of bright spots (like "Top 20")
    $topN = array();

    for($row = 0; $row <= $maxY; $row++) {
        for($col = 0; $col <= $maxX; $col++) {

            $stage2Brightness = $stage2array[$row][$col];
            $topN[$col.":".$row] = $stage2Brightness;
        }
    }
    arsort($topN);

    $topNused = array();
    $topPositionCountdown = $topPositions;

    if($renderStage == 3){
        foreach ($topN as $key => $val) {
            if($topPositionCountdown <= 0){
                break;
            }

            $position = explode(":", $key);

            foreach($topNused as $usedPosition => $usedValue) {
                $usedPosition = explode(":", $usedPosition);
                $distance = abs($usedPosition[0] - $position[0]) + abs($usedPosition[1] - $position[1]);
                if($distance < $minLightDistance) {
                    continue 2;
                }
            }

            $topNused[$key] = $val;

            paintCrosshair($image, $position[0], $position[1], $red, 2);

            $topPositionCountdown--;

        }
    }


    // STAGE 4
    // Median of all Top N lights
    $topNxValues = array();
    $topNyValues = array();

    foreach ($topNused as $key => $val) {
        $position = explode(":", $key);
        array_push($topNxValues, $position[0]);
        array_push($topNyValues, $position[1]);
    }

    $medianXvalue = round(calculate_median($topNxValues));
    $medianYvalue = round(calculate_median($topNyValues));
    paintCrosshair($image, $medianXvalue, $medianYvalue, $red, 15);


    // STAGE 5
    // Find treetop

    $filename = 'debug.log';
    $handle = fopen($filename, "w");
    fwrite($handle, "\n\n STAGE 5");

    $treetopX = $medianXvalue;
    $treetopY = $medianYvalue;

    $searchXmin = $medianXvalue;
    $searchXmax = $medianXvalue;

    $width = 0;
    for($y = $medianYvalue; $y >= 0; $y--) {
        fwrite($handle, "\nAt y = ".$y);

        if(($y % $searchYperX) == 0) { // Modulo
            $width++;
            $searchXmin = $medianXvalue - $width;
            $searchXmax = $medianXvalue + $width;
            imagesetpixel($image, $searchXmin, $y, $red);
            imagesetpixel($image, $searchXmax, $y, $red);
        }

        foreach ($topNused as $key => $val) {
            $position = explode(":", $key); // "x:y"

            if($position[1] != $y){
                continue;
            }

            if($position[0] >= $searchXmin && $position[0] <= $searchXmax){
                $treetopX = $position[0];
                $treetopY = $y;
            }
        }

    }

    paintCrosshair($image, $treetopX, $treetopY, $red, 5);


    // STAGE 6
    // Find tree sides
    fwrite($handle, "\n\n STAGE 6");

    $treesideAngle = 60; // The extremely "fat" end of a christmas tree
    $treeBottomY = $treetopY;

    $topPositionsExcluded = 0;
    $xymultiplier = 0;
    while(($topPositionsExcluded < ($topPositions / 5)) && $treesideAngle >= 1){
        fwrite($handle, "\n\nWe're at angle ".$treesideAngle);
        $xymultiplier = sin(deg2rad($treesideAngle));
        fwrite($handle, "\nMultiplier: ".$xymultiplier);

        $topPositionsExcluded = 0;
        foreach ($topNused as $key => $val) {
            $position = explode(":", $key);
            fwrite($handle, "\nAt position ".$key);

            if($position[1] > $treeBottomY) {
                $treeBottomY = $position[1];
            }

            // Lights above the tree are outside of it, but don't matter
            if($position[1] < $treetopY){
                $topPositionsExcluded++;
                fwrite($handle, "\nTOO HIGH");
                continue;
            }

            // Top light will generate division by zero
            if($treetopY-$position[1] == 0) {
                fwrite($handle, "\nDIVISION BY ZERO");
                continue;
            }

            // Lights left end right of it are also not inside
            fwrite($handle, "\nLight position factor: ".(abs($treetopX-$position[0]) / abs($treetopY-$position[1])));
            if((abs($treetopX-$position[0]) / abs($treetopY-$position[1])) > $xymultiplier){
                $topPositionsExcluded++;
                fwrite($handle, "\n --- Outside tree ---");
            }
        }

        $treesideAngle--;
    }
    fclose($handle);

    // Paint tree's outline
    $treeHeight = abs($treetopY-$treeBottomY);
    $treeBottomLeft = 0;
    $treeBottomRight = 0;
    $previousState = false; // line has not started; assumes the tree does not "leave"^^

    for($x = 0; $x <= $maxX; $x++){
        if(abs($treetopX-$x) != 0 && abs($treetopX-$x) / $treeHeight > $xymultiplier){
            if($previousState == true){
                $treeBottomRight = $x;
                $previousState = false;
            }
            continue;
        }
        imagesetpixel($image, $x, $treeBottomY, $red);
        if($previousState == false){
            $treeBottomLeft = $x;
            $previousState = true;
        }
    }
    imageline($image, $treeBottomLeft, $treeBottomY, $treetopX, $treetopY, $red);
    imageline($image, $treeBottomRight, $treeBottomY, $treetopX, $treetopY, $red);


    // Print out some parameters

    $string = "Min dist: ".$minLightDistance." | Tree angle: ".$treesideAngle." deg | Tree bottom: ".$treeBottomY;

    $px     = (imagesx($image) - 6.5 * strlen($string)) / 2;
    imagestring($image, 2, $px, 5, $string, $orange);

    return $topN;
}

/**
 * Returns values from 0 to 765
 */
function getBrightnessFromRgb($rgb) {
    $r = ($rgb >> 16) & 0xFF;
    $g = ($rgb >> 8) & 0xFF;
    $b = $rgb & 0xFF;

    return $r+$r+$b;
}

function paintCrosshair($image, $posX, $posY, $color, $size=5) {
    for($x = $posX-$size; $x <= $posX+$size; $x++) {
        if($x>=0 && $x < imagesx($image)){
            imagesetpixel($image, $x, $posY, $color);
        }
    }
    for($y = $posY-$size; $y <= $posY+$size; $y++) {
        if($y>=0 && $y < imagesy($image)){
            imagesetpixel($image, $posX, $y, $color);
        }
    }
}

// From http://www.mdj.us/web-development/php-programming/calculating-the-median-average-values-of-an-array-with-php/
function calculate_median($arr) {
    sort($arr);
    $count = count($arr); //total numbers in array
    $middleval = floor(($count-1)/2); // find the middle value, or the lowest middle value
    if($count % 2) { // odd number, middle is the median
        $median = $arr[$middleval];
    } else { // even number, calculate avg of 2 medians
        $low = $arr[$middleval];
        $high = $arr[$middleval+1];
        $median = (($low+$high)/2);
    }
    return $median;
}


?>

图片: 左上方 中下方 左下方 右上方 中上方 右下方

额外奖励:来自维基百科的德国圣诞树 Römerberg http://commons.wikimedia.org/wiki/File:Weihnachtsbaum_R%C3%B6merberg.jpg


20

我使用Python和OpenCV。

我的算法如下:

  1. 首先从图像中提取红色通道
  2. 对红色通道应用阈值(最小值为200)
  3. 然后应用形态梯度,再进行"闭合"(膨胀后跟腐蚀)
  4. 接着在平面上找到轮廓,并选择最长的轮廓。

结果:

代码:

import numpy as np
import cv2
import copy


def findTree(image,num):
    im = cv2.imread(image)
    im = cv2.resize(im, (400,250))
    gray = cv2.cvtColor(im, cv2.COLOR_RGB2GRAY)
    imf = copy.deepcopy(im)

    b,g,r = cv2.split(im)
    minR = 200
    _,thresh = cv2.threshold(r,minR,255,0)
    kernel = np.ones((25,5))
    dst = cv2.morphologyEx(thresh, cv2.MORPH_GRADIENT, kernel)
    dst = cv2.morphologyEx(dst, cv2.MORPH_CLOSE, kernel)

    contours = cv2.findContours(dst,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)[0]
    cv2.drawContours(im, contours,-1, (0,255,0), 1)

    maxI = 0
    for i in range(len(contours)):
        if len(contours[maxI]) < len(contours[i]):
            maxI = i

    img = copy.deepcopy(r)
    cv2.polylines(img,[contours[maxI]],True,(255,255,255),3)
    imf[:,:,2] = img

    cv2.imshow(str(num), imf)

def main():
    findTree('tree.jpg',1)
    findTree('tree2.jpg',2)
    findTree('tree3.jpg',3)
    findTree('tree4.jpg',4)
    findTree('tree5.jpg',5)
    findTree('tree6.jpg',6)

    cv2.waitKey(0)
    cv2.destroyAllWindows()

if __name__ == "__main__":
    main()
如果我将内核从(25,5)更改为(10,5),则在所有树上除了左下角的树之外,我会得到更好的结果。 enter image description here 我的算法假设树上有灯,并且在左下角的树中,顶部的光线比其他树上的光线少。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接