从一点开始的许多行中保留一条线

Keep one line out of many that starts from one point

我正在使用 OpenCV 和 python 进行一个项目,但遇到了这个小问题。

我在列表中存储了很多行的端点坐标。有时会出现从一个点检测到多条线的情况。在这些线中,我想保留最短的线并消除所有其他线,因此我的图像将不包含绘制多于一条线的点。

我的变量存储了最初检测到的所有线的信息(两个端点的坐标)如下:

var = [[Line1_EndPoint1, Line1_EndPoint2],
       [Line2_EndPoint1, Line2_EndPoint2],
       [Line3_EndPoint1, Line3_EndPoint2],
       [Line4_EndPoint1, Line4_EndPoint2],
       [Line5_EndPoint1, Line5_EndPoint2]]

其中,LineX_EndPointY(行号“X”,该行的端点“Y”)的类型为 [x, y],其中 x 和 y 是图像中该点的坐标。

有人可以建议我如何解决这个问题。

您可以修改线路数据的存储方式。如果修改请说明你的数据结构和创建方式

此类数据的示例:

[[[551, 752], [541, 730]], 
 [[548, 738], [723, 548]],
 [[285, 682], [226, 676]],
 [[416, 679], [345, 678]],
 [[345, 678], [388, 674]],
 [[249, 679], [226, 676]],
 [[270, 678], [388, 674]],
 [[472, 650], [751, 473]],
 [[751, 473], [716, 561]],
 [[731, 529], [751, 473]]]

Python 代码将是可观的。

我决定写一个基于Pandas的解决方案更容易。 原因是:

  • 我可以使用列名(代码可读性更好),
  • Pandas API 更强大,尽管它比“纯”Numpy.

进行如下操作:

  1. var转换为DataFrame:

     lines = pd.DataFrame(var.reshape(10,4), columns=pd.MultiIndex.from_product(
         (['P1', 'P2'], ['x','y'])))
    

    的开头部分是:

         P1        P2     
          x    y    x    y
     0  551  752  541  730
     1  548  738  723  548
     2  285  682  226  676
     3  416  679  345  678
    
  2. 计算每条线的长度的平方:

     lines[('', 'lgth')] = (lines[('P1', 'x')] - lines[('P2', 'x')]) ** 2\
         + (lines[('P1', 'y')] - lines[('P2', 'y')]) ** 2
     lines.columns = lines.columns.droplevel()
    

    我故意“停”在平方的长度,因为它是 足以比较长度(计算根不会改变 比较结果)。

    另请注意,需要列上的第一级 MultiIndex 只是为了更容易表达感兴趣的列。他们将进一步 不需要,所以我放弃了。

    这次我把行的全部内容:

          x    y    x    y    lgth
     0  551  752  541  730     584
     1  548  738  723  548   66725
     2  285  682  226  676    3517
     3  416  679  345  678    5042
     4  345  678  388  674    1865
     5  249  679  226  676     538
     6  270  678  388  674   13940
     7  472  650  751  473  109170
     8  751  473  716  561    8969
     9  731  529  751  473    3536
    
  3. 下一步是计算 DataFrame,其中所有点(开始和 每行的末尾)在同一列中,以及(平方)长度 对应行:

     points = pd.concat([lines.iloc[:,[0, 1, 4]],
         lines.iloc[:,[2, 3, 4]]], keys=['P1', 'P2'])\
         .sort_values(['x', 'y', 'lgth']).reset_index(level=1)
    

    现在我用iloc指定列(第一次为起点 第二个是终点)。 为了更容易阅读这个 DataFrame,我传递了 keys,以包含“origin 指标”,然后我对行进行排序。

    内容为:

         level_1    x    y    lgth
     P2        5  226  676     538
     P2        2  226  676    3517
     P1        5  249  679     538
     P1        6  270  678   13940
     P1        2  285  682    3517
     P1        4  345  678    1865
     P2        3  345  678    5042
     P2        4  388  674    1865
     P2        6  388  674   13940
     P1        3  416  679    5042
     P1        7  472  650  109170
     P2        0  541  730     584
     P1        1  548  738   66725
     P1        0  551  752     584
     P2        8  716  561    8969
     P2        1  723  548   66725
     P1        9  731  529    3536
     P2        9  751  473    3536
     P1        8  751  473    8969
     P2        7  751  473  109170
    

    注意,例如226, 676 点出现了两次。第一次发生 在 5 行和 2 行中的第二行(var 行中的索引).

  4. 要查找要删除的行的索引,运行:

     toDrop = points[points.duplicated(subset=['x', 'y'])]\
         .level_1.reset_index(drop=True);
    

    为了更容易理解这段代码的工作原理,运行 一步一步来 检查每个步骤的结果。

    结果是:

     0    2
     1    3
     2    6
     3    8
     4    7
     Name: level_1, dtype: int64
    

    请注意,上面的左列只是索引(无关紧要)。 真实信息在右栏(值)中。

  5. 要显示应该保留的行,运行:

     result = lines.drop(toDrop)
    

    得到:

          x    y    x    y   lgth
     0  551  752  541  730    584
     1  548  738  723  548  66725
     4  345  678  388  674   1865
     5  249  679  226  676    538
     9  731  529  751  473   3536
    

    以上结果不包含例如:

    • 2,因为点226, 676出现在行5,
    • 3,因为点345, 678出现在行4,

    只有这些行(23)已被删除,因为它们是 比第二条提到的两行都长(见前面的部分结果)。

也许这就足够了,或者如果您需要从 var(原来的Numpy数组),并将结果保存在另一个 变量,运行:

var2 = np.delete(var, toDrop, axis=0)

一个Numpy解决方案

与我第一个答案相同的结果仅基于 在 Numpy.

首先定义2个函数:

  1. 计算直线长度的平方:

     def sqLgth(line):
         p1, p2 = line
         return (p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2
    
  2. 将向量(1D数组)转换为列数组(2D数组 单列:

     def toColumn(tbl):
         return tbl.reshape(-1, 1)
    

后面都会用到。

然后进行如下操作:

  1. 获取行数:

     lineNo = var.shape[0]
    
  2. 生成线索引(lineInd列在points数组中的内容 (稍后创建)):

     id = np.repeat(np.arange(lineNo), 2)
    
  3. 生成“起点指标”(1 - 开始,2 - 结束),以方便分析 任何中间打印输出:

     origin = np.tile(np.array([1, 2]), lineNo)
    
  4. 计算线长(lgth列的内容):

     lgth = np.repeat([ sqLgth(line) for line in var ], 2)
    
  5. 创建一个包含一些附加数据的点列表(连续 列包含 originlineIndxylgth):

     points = np.hstack([toColumn(origin), toColumn(id),
         var.reshape(-1, 2), toColumn(lgth)])
    
  6. 计算要排序的“标准数组”:

     r = np.core.records.fromarrays(points[:, 2:].transpose(),
         names='x, y, lgth')
    
  7. 排序(按xylgth):

     points = points[r.argsort()]
    
  8. 计算“反向唯一索引”到点:

     _, inv = np.unique(points[:,2:4], axis=0, return_inverse=True)
    
  9. inv 移动 1 个位置:

     rInv = np.roll(inv,1)
    

    将在下一步中使用,获取上一个元素。

  10. 生成要删除的线索引列表:

    toDrop = points[[ i for i in range(2 * lineNo)
        if inv[i] == rInv[i] ], 1]
    

    行索引(在数组中)是重复点(元素 in inv 等于前一个元素)。

    列索引 (1) - 指定 lineInd 列。

    整个结果 (toDrop) 是“拥有”行的索引列表 (包含重复点)。

  11. 生成结果:var从 上一步:

    var2 = np.delete(var, toDrop, axis=0)
    

要打印减少的行列表,您可以运行:

for line in var2:
    print(f'{line[0]}, {line[1]}')

结果是:

[551 752], [541 730]
[548 738], [723 548]
[345 678], [388 674]
[249 679], [226 676]
[731 529], [751 473]

要完全理解此代码的工作原理:

  • 分别执行每一步,
  • 打印结果,
  • 将其与前面步骤的打印输出进行比较。

有时单独打印一些表达式是有益的 (部分说明),例如var.reshape(-1, 2) - 转换你的 var(形状为 (10, 2, 2))到 2D 点数组(每个行是 一个点)。

整个结果当然和我第一个答案一样, 但是正如您所写,您在 Pandas 方面几乎没有经验,现在您可以 比较这两种方法并查看 Pandas 允许的情况 更简单、更直观的东西。

很好的例子是按某些列排序或查找重复的行。 在 Pandas 中,这是一个单一指令的问题,具有合适的 参数,而在 Numpy 中你必须使用更多指令 并知道如何做同样的事情的各种细节和技巧。