Python实现不规则图形面积计算:基于Pillow的双向扫描算法

Owen
Owen
发布时间:2026-10-13 | 3人浏览过
原创

本文介绍了一种基于Python Pillow库的不规则图形面积计算方法,适用于医学影像等场景中红圈标记区域的面积占比分析。通过提取特定RGB颜色坐标,利用列扫描记录最小最大行坐标进行填充,并结合图像旋转90度后的二次扫描结果进行逻辑与操作,有效解决了多圈标记及面积计算偏差问题,相比传统OpenCV fillPoly方法具有更好的处理效果。

引言与背景

本文详细解析了利用Python计算不规则图形面积的算法实现,通过具体示例代码展示了其应用价值,为相关领域的学习与工作提供参考。

背景介绍:在大三上学期进行的医学影像识别项目中,医生在原图上使用红笔标记病灶点。通过记录红色标记的坐标位置,可以获取病灶点的外接矩形。然而,后续需要计算红圈内面积在外接矩形中的占比。由于外接矩形内可能包含多个红色标记,使用网上常见的OpenCV fillPoly填充效果不佳,且其他计算任意多边形面积的方法效果也不理想。因此,本文探索并提出了一种针对多圈及不规则图形面积计算的优化算法。

该算法能够较为准确地计算出不规则图形的面积。

算法原理与流程

算法核心思想:遍历图像的每一列,通过色差判断是否遇到标记圈,并记录所有相关坐标。对每一列的坐标进行最小行和最大行的记录,从而确定每一列的上下边界。随后进行上色处理(类似于OpenCV的fillPoly实现,但细节有所不同)。单独使用此方法效果有限,因此将图片旋转90度后再次执行相同操作,最后将两次处理的结果进行逻辑与操作。这种双向扫描策略能有效处理多圈标记问题及避免面积多算,例如在处理08-LM案例时表现良好。

算法实现细节

算法实现部分

整个实现过程仅依赖Pillow库。

首先,使用屏幕拾色器获取目标颜色的RGB值,在本例中为(237, 28, 36)。前期截取外接矩形时也需要使用相同的颜色值。


 def pixel_wanted(pix):
   return pix==(237,28, 36)

在遍历每一列时,设定一个翻转位初始为False。如果上一个像素点不是目标色而当前像素点是目标色,则开始记录坐标;一旦当前像素点不再是目标色,则停止检测。

将top_Pixel初始化为黑色(0, 0, 0)。这是因为图片最上方可能直接就是目标色,若不初始化会导致判定出错,将最上面的像素初始化为黑色可避免此问题。

coordinate_List用于记录所有符合目标颜色的点坐标。


coordinate_List = []
top_Pixel = (0,0,0)
for x in range(im.size[0]):
  flag = False #初始化每一列翻转位为False
  for y in range(im.size[1]):
    current_pixel = im.getpixel((x,y))
    last_pixel = im.getpixel((x,y-1)) if y>0 else top_Pixel
    #翻转判定
    if pixel_wanted(current_pixel) and \
        not pixel_wanted(last_pixel):
      flag = True
    if flag and not pixel_wanted(current_pixel):
      flag = False
    if(flag):
      coordinate_List.append((x,y))

coordinate_List中记录的点坐标分布如下图所示。

接下来对获取的coordinate列表进行进一步处理。

从coordinate列表中提取每一列的最小坐标和最大坐标并进行记录。

由于每一列记录的点数不确定(理论上可在上一步改进),因此需要多次遍历。

首先找到第一列出现的坐标,记录其行信息(确定最小行)。

然后遍历该列所有坐标,比较行坐标,若遇到更大的值则更新最大值(确定最大行),并将结果存入新列表。


coordinate_Min_Max_List = []
#找最小最大
for i in range(im.size[0]):
  min=-1
  max=-1
  for coordinate in coordinate_List:
    if coordinate[0] == i:
      min = coordinate[1]
      max = coordinate[1]
      break
  for coordinate in coordinate_List:
    if coordinate[0] == i:
      if coordinate[1]>max:
        max = coordinate[1]
  coordinate_Min_Max_List.append(min)
  coordinate_Min_Max_List.append(max)

在初始化min和max时,需设置为一个不存在的坐标值(如-1),以应对多圈且有空隙的情况,避免产生残影现象,具体效果如下图所示。

上一步处理后得到的列表中,第n列的最小行和最大行分别对应列表中的第2n和2n+1个元素。结果中的-1值用于在下一步绘制时排除无效区域。

随后进行图片绘制,对每一列,将列表中对应的最小行到最大行之间的区域涂满颜色。


#上色
for x in range(im.size[0]):
  for y in range(im.size[1]):
    min = coordinate_Min_Max_List[x*2]
    max = coordinate_Min_Max_List[x*2+1]
    if min<y<max:
      im.putpixel((x,y),(0,255,0))
    else:
      #可以把非红圈的上掩膜遮住
      pass

至此,实现了类似OpenCV的算法逻辑。虽然尚未进行翻转和与操作,但效果已优于OpenCV生成的结果。将其封装为函数以便后续调用。

接下来进行简单的90度旋转,再次调用上述函数执行一次扫描。


def Cal_S(im):
  im_0 = im.rotate(0)
  im_90 = im.rotate(90, expand=True)
  
  im_0 = fillPoly(im_0)
  im_90 = fillPoly(im_90)
  im_90 = im_90.rotate(-90, expand=True)

  i=0
  for x in range(im.size[0]):
    for y in range(im.size[1]):
      if(im_0.getpixel((x,y))==(0,255,0) and
      im_90.getpixel((x,y))==(0,255,0)):
        im.putpixel((x,y),(0,255,0))
        i+=1
  return i/(im.size[0]*im.size[1])

两次扫描后的效果图对比如下。

可以看到整体效果非常优秀,但个别图像仍存在误差,例如十字分布的情况。

目前的误差已大幅降低。对于极少数十字现象,可通过手动切割原图处理,或直接忽略不计。

完整代码展示如下。为了直观查看效果,函数中可顺便保存图像。总体效果如下所示。

完整代码与效果验证


from PIL import Image

def pixel_wanted(pix):
  return pix==(237,28, 36)

def fillPoly(im):
  coordinate_List = []

  top_Pixel = (0,0,0)
  for x in range(im.size[0]):
    flag = False #初始化每一列翻转位为False
    for y in range(im.size[1]):
      current_pixel = im.getpixel((x,y))
      last_pixel = im.getpixel((x,y-1)) if y>0 else top_Pixel
      #翻转判定
      if pixel_wanted(current_pixel) and \
          not pixel_wanted(last_pixel):
        flag = True
      if flag and not pixel_wanted(current_pixel):
        flag = False
      if(flag):
        coordinate_List.append((x,y))
  coordinate_Min_Max_List = []
  #找最小最大
  for i in range(im.size[0]):
    min=-1
    max=-1
    for coordinate in coordinate_List:
      if coordinate[0] == i:
        min = coordinate[1]
        max = coordinate[1]
        break
    for coordinate in coordinate_List:
      if coordinate[0] == i:
        if coordinate[1]>max:
          max = coordinate[1]
    coordinate_Min_Max_List.append(min)
    coordinate_Min_Max_List.append(max)
  #上色
  for x in range(im.size[0]):
    for y in range(im.size[1]):
      min = coordinate_Min_Max_List[x*2]
      max = coordinate_Min_Max_List[x*2+1]
      if min<y<max:
        im.putpixel((x,y),(0,255,0))
      else:
        #可以把非红圈的上掩膜遮住
        pass
  return im

def Cal_S(im):
  im_0 = im.rotate(0)
  im_90 = im.rotate(90, expand=True)

  im_0 = fillPoly(im_0)
  im_90 = fillPoly(im_90)
  im_90 = im_90.rotate(-90, expand=True)

  i=0
  for x in range(im.size[0]):
    for y in range(im.size[1]):
      if(im_0.getpixel((x,y))==(0,255,0) and
      im_90.getpixel((x,y))==(0,255,0)):
        im.putpixel((x,y),(0,255,0))
        i+=1
  return i/(im.size[0]*im.size[1])

以上就是本文的全部内容,希望对读者有所帮助,欢迎支持本站。

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表