【问题标题】:Detect Door Shape in Floor plan using C#使用 C# 检测平面图中的门形状
【发布时间】:2020-09-26 16:08:36
【问题描述】:

我正在绘制光栅图像,所以我的目标是只检测门形状 我正在使用 Emgu C# 并应用 Haris Corner 算法,阈值 = 50 然后检测一个角矩阵然后计算两点之间的距离以近似这两个点是门形状的开始和结束 问题:
我无法过滤图像以获得最佳检测效果,例如如何删除所有文本和噪音只保留粗体墙 [![在此处输入图像描述][1]][1] [![在此处输入图片描述][2]][2]

var img = imgList["Input"].Clone();            
                var gray = img.Convert<Gray, byte>().ThresholdBinaryInv(new Gray(100), new Gray(100)); ;
                imageBoxEx2.Image = gray.ToBitmap();
                var corners = new Mat();
                CvInvoke.CornerHarris(gray, corners,2);
                CvInvoke.Normalize(corners, corners, 255, 0, Emgu.CV.CvEnum.NormType.MinMax);
                Matrix<float> matrix = new Matrix<float>(corners.Rows, corners.Cols);
                corners.CopyTo(matrix);
                dt.Rows.Clear();
                List<Point> LstXpoints = new List<Point>();
                List<Point> LstYpoints = new List<Point>();
                List<PointF> LstF = new List<PointF>();
                for (int i = 0; i < matrix.Rows; i++)
                {
                    for (int j = 0; j < matrix.Cols; j++)
                    {
                        if (matrix[i, j] > threshold)
                        {

                            LstXpoints.Add(new Point ( j, i));
                            LstYpoints.Add(new Point(i, j));
                           // CvInvoke.Circle(img, new Point(j, i), 5, new MCvScalar(0, 0, 255), 3);
                        }
                    }
                }

【问题讨论】:

  • 您好,能否请您包含未标记的输入图像?我会做一个实验。
  • @GeorgeKerwood 是的,我用原始图片更新了帖子
  • 问题,抱歉:墙壁总是正交的吗?只意味着水平或垂直?
  • @GeorgeKerwood 我正在处理许多图像,因此墙壁将是水平的或垂直的或混合的,我希望能帮助我消除噪音和除粗线(墙壁)之外的其他绘图,不使用墙壁这将帮助检测角落,我将预测门的形状位置
  • “混合”?我问你是否可以期待一堵 45 度的墙?

标签: c# image-processing emgucv


【解决方案1】:

抱歉,python 代码。但也许这将有助于解决您的问题。 见 cmets。

import cv2 

img = cv2.imread('NHoXn.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# convert to binary image
thresh=cv2.threshold(gray, 220, 255, cv2.THRESH_BINARY )[1]

#  Morphological reconstruction (delete labels)
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (7,7))
kernel2 = cv2.getStructuringElement(cv2.MORPH_RECT, (3,3))
marker = cv2.dilate(thresh,kernel,iterations = 1)
while True:
    tmp=marker.copy()
    marker=cv2.erode(marker, kernel2)
    marker=cv2.max(thresh, marker)
    difference = cv2.subtract(tmp, marker)
    if cv2.countNonZero(difference) == 0:
        break


# only walls
se=cv2.getStructuringElement(cv2.MORPH_RECT, (4,4))
walls=cv2.morphologyEx(marker, cv2.MORPH_CLOSE, se)
walls=cv2.erode(walls, kernel2,iterations=2)

# other objects
other=cv2.compare(marker,walls, cv2.CMP_GE)
other=cv2.bitwise_not(other)

# find connected components and select by size and area
output = cv2.connectedComponentsWithStats(other, 4, cv2.CV_32S)
num_labels = output[0]
labels = output[1]
stats=output[2]
centroids = output[3]
for i in range(num_labels):
    left,top,width,height,area=stats[i]
    if abs(width-40)<12 and abs(height-40)<12 and area>85:
         cv2.rectangle(img,(left, top), (left+width, top+height), (0,255,0))

cv2.imwrite('doors.png', img)

结果:

  1. 图中显示的内容:墙壁、门、窗、家具、文字标签。
  2. 找到的门总是与墙壁接触。
  3. 墙与其他物体有何不同?粗,这些线条是粗体的。因此,用所需的结构元件进行膨胀可以只留下部分墙壁。然后,通过形态重建,将墙壁与与之相关的元素一起修复:首先是门、窗。绘图将清除所有不接触墙壁的东西。
  4. 如果进一步进行膨胀然后侵蚀,那么将只保留墙壁,窗户和门等薄元素将消失。
  5. 从第三阶段减去第四阶段(或逻辑运算),我们得到一张只包含门、窗和接触墙壁的家具的图片。
  6. 门和窗户的画法有什么区别?事实上,它们的 BB 几乎是正方形的,该图中所有门的尺寸大致相同,它们的长度大约等于 r*(1+pi/4)。 此外,在代码中,还有此类标志的选择。在这个阶段,您可以添加更多标志,以便更准确地将门与其他元素分开。

【讨论】:

  • 嗨,Alex 我用当前图像尝试了代码,它可以工作,但不能与其他人一起工作,请试试这个图像:imgur.com/a/09xHZwC
  • upload.ee/files/11862011/select_doors.py.html 要处理这种类型的图纸,您需要配置参数。不幸的是,获得了 2 个假门选择。需要调整和补充选择条件,除了大小和面积,比如凸包的面积什么的。
  • @AlexAlex 你好。我对你的方法很感兴趣,它看起来很优雅(好样的),但形态学操作对我来说是新的。您能否扩展评论/解释?或者也许引用/参考相关文档。谢谢。
  • @george-kerwood 你好。谢谢你。我会尽力解释。英语不是我的母语,所以请善待我的错误。我认为,数学形态学在 Luc Vincent 的著作和书籍中得到了很好的解释。数学形态学及其在图像和信号处理中的应用 John Goutsias、Luc M. Vincent、Dan S. Bloomberg
  • @AlexAlex 完美的英语解释!我看到你的算法和我的一样,也容易在任何其他“门大小”或具有“门状”BB 的结构上被错误检测。对于您在 6 中的最后一点,我认为如果您要添加最终验证,即您检测到的 BB 重合/触摸墙壁顶点/角落,这将是一个完美的解决方案。 IsaacBe 如果您仍在关注此线程,我会推荐此解决方案而不是我自己的解决方案。它更加优雅。
【解决方案2】:

[ 编辑 - 提供完整解决方案的完全扩展答案]

前言

我通常不会努力提供“解决方案”,因为我觉得它远远超出了有用的、可重复使用的问答格式……但这是一个有趣的问题。

回答

下面详细介绍了检测平面图中潜在门洞的基本算法。除了提供的单一案例之外,它没有经过性能优化或测试。由于 OP 仅将门定义为“指定宽度的开口”,因此也容易出现错误指示。该算法只能检测原理,正交门。

示例结果:

方法

方法如下:

  1. 在输入图像中反转和阈值,以便将最暗的元素转换为白色(全字节值)。
  2. 计算轮廓检测,以识别现在白色区域的边界。
  3. 过滤以仅选择大于所选阈值的区域上的轮廓(从而消除文本元素和噪音)。
  4. “走”选定的轮廓以确定出现“拐角”的节点。角点定义为高于阈值的角度变化。
  5. 分析检测到的角落,寻找符合“门”条件的配对。
  6. [多余的渲染] 最后,在矩形边界内对过滤后的轮廓进行光栅化,以便将它们填充到最终图像中。 (注意:这不是计算效率或优雅,但是用于轮廓填充的 EmguCV 方法仅支持凸轮廓)。 “门”也呈现为红色。

算法

// Open the image
Image<Gray, byte> baseImage = new Image<Gray, byte>(@"TestLayout.jpg");
// Invert the image
Image<Gray, byte> invBaseImage = baseImage.Not();
// Threshold the image so as "close to white" is maintained, all else is black
Image<Gray, byte> blackOnlyImage = invBaseImage.ThresholdBinary(new Gray(200), new Gray(255));
// An output image of the same size to contain the walls
Image<Gray, byte> wallsOnlyImage = new Image<Gray, byte>(blackOnlyImage.Size);

// A set of dected contours
VectorOfVectorOfPoint inputContours = new VectorOfVectorOfPoint();
// A set of validated contours
List<VectorOfPoint> validContours = new List<VectorOfPoint>();
// Perform contour detection
Mat hierarchy = new Mat();
CvInvoke.FindContours(blackOnlyImage, inputContours, hierarchy, RetrType.External, ChainApproxMethod.ChainApproxSimple);

// Filter out to select only contours bounding more that 500 pixels
int areaThreshold = 500;
for (int c = 0; c < inputContours.Size; c++)
{ 
    if (CvInvoke.ContourArea(inputContours[c]) >= areaThreshold)
    {
        validContours.Add(inputContours[c]);
    }
}

// Find all the corner points in the valid contours
List<Point> contourCorners = new List<Point>();
foreach(VectorOfPoint contour in validContours)
{
    contourCorners.AddRange(CornerWalk(contour, 80));
}

// Sort the contour corners by proximity to origin in order to optimise following loops
contourCorners.OrderBy(p => Math.Sqrt(Math.Pow(p.X, 2) + Math.Pow(p.Y, 2)));

// Extract all door candidate point pairs from all detected corners
List<Tuple<Point, Point>> doorCandidates = FindDoors(contourCorners, 2, 30, 45);

// Pixels contained within the filtered contours are walls, fill them white
RasterFill(wallsOnlyImage, validContours);

// Output Image
Image<Rgb, byte> outputImage = new Image<Rgb, byte>(wallsOnlyImage.Size);
CvInvoke.CvtColor(wallsOnlyImage, outputImage, ColorConversion.Gray2Rgb);
// Draw the doors
foreach (Tuple<Point,Point> door in doorCandidates)
{
    outputImage.Draw(new LineSegment2D(door.Item1, door.Item2), new Rgb(255,0,0), 1);
}

// Display generated output and save it to file
CvInvoke.NamedWindow("TestOutput");
CvInvoke.Imshow("TestOutput", outputImage);           
CvInvoke.WaitKey();
outputImage.Save(@"OutputImage.bmp");

角点提取

static List<Point> CornerWalk(VectorOfPoint contour, int threshold)
{
    // Create a resultant list of points
    List<Point> result = new List<Point>();

    // Points are used to store 2D vectors as dx,dy (i,j)
    Point reverseVector, forwardVector;
    double theta;
    // For each point on the contour
    for(int p = 1; p < contour.Size; p++)
    {
        // Determine the vector to the prior point
        reverseVector = new Point()
        {
            X = contour[p].X - contour[p - 1].X,
            Y = contour[p].Y - contour[p - 1].Y,
        };

        // Determine the vector to the next point
        forwardVector = p == contour.Size - 1 ?
        new Point()
        {
            X = contour[0].X - contour[p].X,
            Y = contour[0].Y - contour[p].Y,
        } :
        new Point()
        {
            X = contour[p + 1].X - contour[p].X,
            Y = contour[p + 1].Y - contour[p].Y,
        };

        // Compute the angular delta between the two vectors (Radians)
        theta = Math.Acos(((reverseVector.X * forwardVector.X) + (reverseVector.Y * forwardVector.Y)) /
            (Math.Sqrt(Math.Pow(reverseVector.X, 2) + Math.Pow(reverseVector.Y, 2)) *
            Math.Sqrt(Math.Pow(forwardVector.X, 2) + Math.Pow(forwardVector.Y, 2))));

        // Convert the angle to degrees
        theta *= 180 / Math.PI;

        // If the angle is above or equal the threshold, the point is a corner
        if (theta >= threshold) result.Add(contour[p]);
    }

    // Return the result
    return result;
}

门检测

static List<Tuple<Point, Point>> FindDoors(
    List<Point> cornerPoints,
    int inLineTolerance,
    int minDoorWidth,
    int maxDoorWidth)
{
    // Create a resultant list of pairs of points
    List<Tuple<Point, Point>> results = new List<Tuple<Point, Point>>();
    Point p1, p2;
    // For every point in the list
    for (int a = 0; a < cornerPoints.Count; a++)
    {
        p1 = cornerPoints[a];
        // Against every other point in the list
        for (int b = 0; b < cornerPoints.Count; b++)
        {
            // Don't compare a point to it's self...
            if (a == b) continue;
            p2 = cornerPoints[b];

            // If p1 to p2 qualifies as a door:
                // Vertical Doors -     A vertical door will have to points of the same X value, within tolerance, and a Y value delta within the
                //                      min-max limits of a door width.
            if (((Math.Abs(p1.X - p2.X) < inLineTolerance) && (Math.Abs(p1.Y - p2.Y) > minDoorWidth) && (Math.Abs(p1.Y - p2.Y) < maxDoorWidth)) ||
                // Horizontal Doors -   A horizontal door will have to points of the same Y value, within tolerance, and a X value delta within the
                //                      min-max limits of a door width.
                ((Math.Abs(p1.Y - p2.Y) < inLineTolerance) && (Math.Abs(p1.X - p2.X) > minDoorWidth) && (Math.Abs(p1.X - p2.X) < maxDoorWidth)))
            {
                // Add the point pair to the result
                results.Add(new Tuple<Point, Point>(p1, p2));
                // Remove them from further consideration
                cornerPoints.Remove(p1);
                cornerPoints.Remove(p2);
                // Decrement the looping indexes and start over with a new p1
                b--; a--;
                break;
            }
        }
    }
    // Finally return the result
    return results;
}

轮廓填充(渲染实用程序 - 无功能)

static void RasterFill(Image<Gray,byte> dstImg, List<VectorOfPoint> contours)
{
    Rectangle contourBounds;
    PointF testPoint;
    // For each contour detected
    foreach(VectorOfPoint contour in contours)
    {
        // Within the bounds of this contour
        contourBounds = CvInvoke.BoundingRectangle(contour);
        for (int u = contourBounds.X; u < contourBounds.X + contourBounds.Width; u++)
        {
            for (int v = contourBounds.Y; v < contourBounds.Y + contourBounds.Height; v++)
            {
                // Test to determine whether the point is within the contour
                testPoint = new PointF(u, v);
                // If it is inside the contour, OR on the contour
                if (CvInvoke.PointPolygonTest(contour, testPoint, false) >= 0)
                {
                    // Set it white
                    dstImg.Data[v, u, 0] = 255;
                }
            }
        }
    }
}

【讨论】:

  • 不客气。它对你有用吗?如果是,请标记已回答的问题。
  • 仍在尝试预测门的位置,所以我尝试获取每面墙的终点,是否有想法只获取每面墙的终点?谢谢
  • 如何在数学上定义一扇门?例如,您希望如何区分门和窗?
  • 现在只有通过获取两点之间的距离来检测门,如果相等,可以说在 35 和 55 之间,这应该是 door 的一个洞。就是这样,所以我正在努力检测每面墙的端点。
  • 我可以很容易地做到这一点:imgur.com/mUb4C9p。但这并不完美。你会得到“双门”和一些错误的结果。如果我可以问,这是为了什么?学习?工业应用?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-12-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-03-24
相关资源
最近更新 更多