Python实现不规则图形面积计算:基于Pillow的双向扫描算法
本文介绍了一种基于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])
以上就是本文的全部内容,希望对读者有所帮助,欢迎支持本站。
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









