0

@ALL 这是对原始问题的编辑,以使该主题更加清晰。

问题陈述

  • 假设有一个工业 P&ID 图。
  • 旨在仅对过程重要的一些线条着色。
  • 用户只应单击(鼠标左键单击)线段以使其着色。

问题方法

我是编程新手-> 使用 Python (3.5) 来尝试一下。我看到它的算法是这样的:

  • 该图将采用 .pdf 格式。因此,我可以使用 PIL ImageGrab 或将 .pdf 转换为 .png,如本例所示
  • 该算法将搜索鼠标单击周围的像素,然后将其与相同大小的另一部分(假设是 6x3 像素的条带)进行比较,但向左/向右一步(可能是 1-5 像素)
  • 检查它们差异的平均值将告诉我们两条条带是否相同
  • 这样算法应该找到行尾、箭头、角或其他元素
  • 一旦找到,记录位置并绘制标记线,用户应该选择另一条线

总结

  • 点击想要的线路
  • 围绕鼠标单击抓取图像的一小部分
  • 检查线条是水平的还是垂直的
  • 裁剪给定大小的水平/垂直切片
  • 查找行尾并记录行尾位置
  • 在两个找到的位置之间画一条特定颜色的线(比如说绿色)
  • 等待下一行被选中并重复

其他想法

  • 附上您可以找到两张示例图像的图片以及我想要实现的目标。
  • 尝试使用此处找到的方法在切片中找到“洞”:OpenCV to find line endings
  • 坚持 ImageGrab 例程或类似的东西没有严格的规定
  • 如果您知道我可以使用的其他策略,请随时发表评论
  • 欢迎任何建议并真诚感谢

示例图片:

示例图像

期望的结果(在 Paint 中修改):

期望的结果(在 Paint 中修改)

使用我迄今为止尝试过的工作为帖子添加更新

我对原始代码做了一些修改,所以我将在下面发布。注释中的所有内容要么用于调试,要么用于解释。非常感谢您的帮助!不要害怕干预。

import win32gui as w
from PIL import ImageStat, ImageChops, Image, ImageDraw
import win32api as wa

img=Image.open("Trials.jpg")
img_width=img.size[0]
img_height=img.size[1]
#Using 1920 x 1080 resolution
#Hide the taskbar to center the Photo Viewer
#Defining a way to make sure the mouse click is inside the image
#Substract the width from total and divide by 2 to get base point of the crop
width_lim = (1920 - img_width)/2
height_lim = (1080 - img_height)/2-7
#After several tests, the math in calculating the height is off by 7 pixels, hence the correction
#Use these values when doing the crop

#Check if left mouse button was pressed and record its position
left_p = wa.GetKeyState(0x01)
#print(left_p)
while True :
    a=wa.GetKeyState(0x01)
    if a != left_p:
        left_p = a
        if a<0 :
            pos = w.GetCursorPos()
            pos_x=pos[0]-width_lim
            pos_y=pos[1]-height_lim
#            print(pos_x,pos_y)
        else:
            break


#img.show()
#print(img.size)

#Define the crop height; size is doubled
height_size = 10
#Define max length limit
#Getting a horizontal strip
im_hor = img.crop(box=[0, pos_y-height_size, img_width, pos_y+height_size])
#im_hor.show()



#failed in trying crop a small square of 3x3 size using the pos_x
#sq_size = 3
#st_sq = im_hor.crop(box=[pos_x,0,pos_x+sq_size,height_size*2])
#st_sq.show()

#going back to the code it works
#crop a standard strip and compare with a new test one
#if the mean of difference is zero, the strips are identical
#still looking for a way to find the position of the central pixel (that would be the one with maximum value - black)
strip_len = 3
step = 3
i = pos_x
st_sq = im_hor.crop(box=[i,0,i+strip_len,height_size*2])
test_sq = im_hor.crop(box=[i+step,0,i+strip_len+step,height_size*2])
diff = ImageChops.difference(st_sq,test_sq)
stat=ImageStat.Stat(diff)
mean = stat.mean
mean1 = stat.mean
#print(mean)

#iterate to the right until finding a different strip, record position
while mean==[0,0,0]:
    i = i+1
    st_sq = im_hor.crop(box=[i,0,i+strip_len,height_size*2])
    #st_sq.show()
    test_sq = im_hor.crop(box=[i+step,0,i+strip_len+step,height_size*2])
    #test_sq.show()
    diff = ImageChops.difference(st_sq,test_sq)
    #diff.show()
    stat=ImageStat.Stat(diff)
    mean = stat.mean
#    print(mean)
print(i-1)

r = i-1
#print("STOP")
#print(r)
#record the right end as r = i-1

#iterate to the left until finding a different strip. record the position
while mean1==[0,0,0]:
    i = i-1
    st_sq = im_hor.crop(box=[i,0,i+strip_len,height_size*2])
    #st_sq.show()
    test_sq = im_hor.crop(box=[i+step,0,i+strip_len+step,height_size*2])
    #test_sq.show()
    diff = ImageChops.difference(st_sq,test_sq)
    #diff.show()
    stat=ImageStat.Stat(diff)
    mean1 = stat.mean
#    print(mean)
#print("STOP")
print(i+1)

l = i+1
#record the left end as l=i+1
test_draw = ImageDraw.Draw(img)
test_draw.line([l,pos_y,r,pos_y], fill=128)
img.show()

#find another approach or die trying!!! 

下面是我得到的结果。这不是我所希望的,但我觉得自己走在了正确的轨道上。我真的可以使用一些帮助来查找条带中的像素位置并使其相对于大图片像素位置。

另一个类似的图像,质量更好,但也带来了更多的问题。

4

1 回答 1

0

因此,此解决方案不是您确切问题的完整解决方案,但我认为这可能是一种很好的方法,可以让您至少部分解决问题。我对线检测方法的一般问题是它们通常严重依赖多个超参数。更烦人的是,它们的速度很慢,因为它们搜索的角度范围很广;你的线条要么是水平的,要么是垂直的。因此,我建议使用形态学。您可以在 OpenCV 网站上找到形态学的一般概述,您可以在 OpenCV 网站上的本教程中看到它应用于移除乐谱中的音乐条。

我认为的基本思想是:

  1. 检测水平和垂直线
  2. 在检测到的线路上运行connectedComponents()以分别识别每条线路
  3. 获取用户鼠标位置并在其周围定义一个窗口
  4. 如果连接组件的标签在该窗口中,则抓住该组件
  5. 在图像上绘制该组件

现在,这是一个非常基本的想法,忽略了其中的一些挑战。但是,可以肯定的是,如果您单击任意位置并且在该单击窗口内的图像中有一条线,您得到它。这里没有遗漏的线路。另一个好消息是它不会忽略图像中较粗的边框等,您自然希望它停止(请注意,线检测方案存在此问题)。这只会检测具有定义宽度的线条,如果线条变粗(变成箭头或击中不同方向的线条),它将切断它。坏消息是这会为您的线条使用预定义的宽度。您可以通过使用命中或未命中转换来绕过这个问题,但请注意,对于早于 3.3-rc 的 OpenCV 版本,该实现目前已被破坏;看这里更多(您可以轻松绕过损坏的实现)。无论如何,这里的命中或未命中变换允许您说“我想要一条水平线,但它可以是几个像素宽或只有一个像素宽”。当然,你做得越宽,越多的东西不是线条可能会变成一个。您可以稍后根据大小将它们过滤掉(将所有小于某个大小的线都用侵蚀或膨胀折腾)。


现在在代码中看起来像什么?我决定做一个简单的例子并应用它,但请注意代码是放在一起的,所以这里没有真正的错误,你想写得更好。无论哪种方式,给出上述方法的示例都只是一个快速的技巧。

首先,我们将创建图像并绘制一些线条:

import cv2
import numpy as np 

img = 255*np.ones((500, 500), dtype=np.uint8)
cv2.line(img, (10, 350), (200, 350), color=0, thickness=1)
cv2.line(img, (100, 150), (400, 150), color=0, thickness=1)
cv2.line(img, (300, 250), (300, 500), color=0, thickness=1)
cv2.line(img, (100, 50), (100, 350), color=0, thickness=1)
bin_img = cv2.bitwise_not(img)

简单的线条示例

请注意,我还创建了相反的图像,因为我更喜欢保留我试图检测白色的东西,而黑色是背景。

现在我们将使用形态(在本例中为侵蚀)抓取那些水平和垂直线:

h_kernel = np.array([[0, 0, 0],
                     [1, 1, 1],
                     [0, 0, 0]], dtype=np.uint8)
v_kernel = np.array([[0, 1, 0],
                     [0, 1, 0],
                     [0, 1, 0]], dtype=np.uint8)

h_lines = cv2.morphologyEx(bin_img, cv2.MORPH_ERODE, h_kernel)
v_lines = cv2.morphologyEx(bin_img, cv2.MORPH_ERODE, v_kernel)

水平线

垂直线

现在我们将标记每一行:

h_n, h_labels = cv2.connectedComponents(h_lines)
v_n, v_labels = cv2.connectedComponents(v_lines)

这些图像将与每个像素的颜色/值相同h_labels,但不是每个像素的颜色/值是白色,而是图像中每个不同组件的值是一个整数。所以背景像素的值为 0,一条线用 1s 标记,另一条线用 2s 标记。对于具有更多线条的图像,依此类推。v_labelsh_linesv_lines

现在我们将围绕用户鼠标单击定义一个窗口。而不是在这里实现该管道,我只是要对鼠标单击位置进行硬编码:

mouse_click = [101, 148]  # x, y
click_radius = 3  # pixel width around mouse click
window = [[mouse_click[0] - i, mouse_click[1] - j]
          for i in range(-click_radius, click_radius+1)
          for j in range(-click_radius, click_radius+1)]

最后要做的是循环遍历内部的所有位置window并检查标签是否为正(即它不是背景)。如果是这样,那么我们已经打了一条线。所以现在我们可以查看所有具有该标签的像素,这将是整行。然后我们可以使用任意数量的方法在原件上画线img

label = 0
for pixel in window:
    if h_labels[pixel[1], pixel[0]] > 0:
        label = h_labels[pixel[1], pixel[0]]
        bin_labeled = 255*(h_labels == label).astype(np.uint8)
    elif v_labels[pixel[1], pixel[0]] > 0:
        label = v_labels[pixel[1], pixel[0]]
        bin_labeled = 255*(v_labels == label).astype(np.uint8)
    if label > 0:
        rgb_labeled = cv2.merge([img, img+bin_labeled, img])
        break

标记线

IMO 直接在上面的这段代码真的很草率,有更好的方法来绘制它,但我不想花时间在不是问题真正核心的事情上。


改进这一点的一种简单方法是连接近线——在找到组件之前,您仍然可以使用形态来完成此操作。更好的绘制方法可能是简单地在图像中找到该标签的最小/最大位置,然后将它们用作 OpenCVline()函数绘制的端点坐标,这样您就可以轻松选择颜色和线条粗细。如果可能的话,我建议做的一件事是在用户点击之前在鼠标悬停上显示这些行(这样他们就知道他们正在点击正确的区域)。这样,如果用户接近两行,他们就会知道他们选择的是哪一行。

于 2017-11-23T13:09:09.640 回答