导入必要的库
python
import cv2
from PIL import Image, ImageDraw, ImageFont
import numpy as np
cv2:OpenCV 库,用于图像的读取、处理和显示。
Image、ImageDraw、ImageFont:来自PIL(Python Imaging Library),用于在图像上绘制文本。
numpy:用于数值计算,在处理图像数据和计算轮廓相关参数时会用到。
加载图像的函数
python
def load_image(image_path):
try:
# 使用cv2.imread读取指定路径的图像
img = cv2.imread(image_path)
# 如果图像读取失败,img会为None
if img is None:
print(f"无法读取图像: {image_path}")
return None
return img
except Exception as e:
# 捕获读取图像过程中可能出现的异常并打印错误信息
print(f"读取图像时出错: {e}")
return None
load_image函数接受一个图像路径作为参数。
使用cv2.imread读取图像,如果读取失败(返回None),会打印错误信息并返回None。
捕获读取过程中可能出现的异常并打印错误信息,最终返回图像数据或None。
对图像进行预处理的函数
python
def preprocess_image(img):
# 将图像从BGR颜色空间转换为灰度颜色空间
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 对灰度图像进行二值化处理,使用OTSU算法自动确定阈值
_, img_binary = cv2.threshold(img_gray, 127, 255,
cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
# 对二值化图像进行高斯模糊,以减少噪声
img_blur = cv2.GaussianBlur(img_binary, (5, 5), 7)
return img_blur
preprocess_image函数接受一个图像作为参数。
首先将图像从 BGR 颜色空间转换为灰度颜色空间,因为后续的处理在灰度图像上更方便。
然后使用 OTSU 算法对灰度图像进行二值化处理,得到黑白分明的图像。
最后对二值化图像进行高斯模糊,减少噪声。
查找图像中轮廓的函数
python
def find_contours(img_blur):
# 使用cv2.findContours查找图像中的轮廓
# mode=cv2.RETR_LIST表示查找所有轮廓,不建立轮廓之间的层次关系
# method=cv2.CHAIN_APPROX_SIMPLE表示只保留轮廓的端点
contours, _ = cv2.findContours(img_blur,
mode=cv2.RETR_LIST,
method=cv2.CHAIN_APPROX_SIMPLE)
return contours
find_contours函数接受一个经过预处理的模糊图像作为参数。
使用cv2.findContours查找图像中的轮廓,mode=cv2.RETR_LIST表示查找所有轮廓且不建立层次关系,method=cv2.CHAIN_APPROX_SIMPLE表示只保留轮廓的端点,减少数据量。
返回找到的轮廓列表。
根据逼近多边形的顶点数量判断形状的函数
python
def detect_shape(approx_pt):
shape = "无"
# 如果逼近多边形有3个顶点,则判断为三角形
if len(approx_pt) == 3:
shape = "三角形"
# 如果逼近多边形有4个顶点
elif len(approx_pt) == 4:
# 计算逼近多边形的外接矩形
x, y, w, h = cv2.boundingRect(approx_pt)
# 通过宽高比判断是否为正方形
if 0.95 <= w / h <= 1.05:
shape = "正方形"
else:
shape = "长方形"
# 如果逼近多边形有5个顶点,则判断为五边形
elif len(approx_pt) == 5:
shape = "五边形"
# 如果逼近多边形有2个顶点,则判断为线
elif len(approx_pt) == 2:
shape = "线"
else:
# 计算轮廓的面积
area = cv2.contourArea(approx_pt)
# 计算轮廓的周长
perimeter = cv2.arcLength(approx_pt, True)
if perimeter == 0:
circularity = 0
else:
# 计算圆度,圆度越接近1越接近圆形
circularity = 4 * np.pi * (area / (perimeter * perimeter))
if circularity > 0.8:
shape = "圆形"
return shape
detect_shape函数接受一个逼近多边形的顶点列表作为参数。
根据顶点数量判断形状:3 个顶点为三角形,4 个顶点根据宽高比判断是正方形还是长方形,5 个顶点为五边形,2 个顶点为线。
对于其他情况,计算轮廓的面积和周长,进而计算圆度,圆度大于 0.8 则判断为圆形。
返回判断出的形状名称。
在图像上绘制轮廓和标注形状信息的函数
python
def draw_contours_and_text(img, contours):
for cnt in contours:
# 计算轮廓的周长
perimeter = cv2.arcLength(cnt, True)
# 对轮廓进行多边形逼近
approx_pt = cv2.approxPolyDP(cnt, perimeter * 0.035, closed=True)
# 绘制逼近后的多边形轮廓,颜色为红色,线宽为2
cv2.drawContours(img, [approx_pt], -1, (0, 0, 255), 2)
# 绘制原始轮廓,颜色为黑色,线宽为1
cv2.drawContours(img, [cnt], -1, (0, 0, 0), 1)
# 计算轮廓的外接矩形
x, y, w, h = cv2.boundingRect(cnt)
# 绘制外接矩形,颜色为蓝色,线宽为2
cv2.rectangle(img, (x, y), (x + w, y + h), (255, 0, 0), 2)
# 判断形状
shape = detect_shape(approx_pt)
# 计算轮廓的面积
area = cv2.contourArea(cnt)
# 计算轮廓的重心
M = cv2.moments(cnt)
if M["m00"] != 0:
cx = int(M["m10"] / M["m00"])
cy = int(M["m01"] / M["m00"])
# 将OpenCV图像转换为PIL图像
img_pil = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
draw = ImageDraw.Draw(img_pil)
try:
# 加载字体,用于绘制文本
font = ImageFont.truetype("simhei.ttf", 18)
except Exception as e:
# 捕获字体加载过程中可能出现的异常并打印错误信息
print(f"加载字体时出错: {e}")
continue
# 要绘制的文本,包含形状和面积信息
text = f"{shape} 面积: {area:.2f}"
# 计算文本的边界框
bbox = draw.textbbox((0, 0), text, font=font)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[0]
# 计算文本的起始位置,使其中心与图形中心对齐
text_x = cx - text_width // 2
text_y = cy - text_height // 2
# 在图像上绘制文本,颜色为绿色
draw.text((text_x, text_y), text, font=font, fill=(0, 255, 0))
# 将PIL图像转换回OpenCV图像
img = cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR)
return img
draw_contours_and_text函数接受一个图像和轮廓列表作为参数。
对于每个轮廓,计算其周长并进行多边形逼近。
绘制逼近后的多边形轮廓(红色)、原始轮廓(黑色)和外接矩形(蓝色)。
判断轮廓的形状并计算其面积和重心。
将 OpenCV 图像转换为 PIL 图像,以便使用PIL的ImageDraw和ImageFont绘制中文文本。
加载字体,计算文本的边界框和起始位置,使其中心与图形中心对齐。
在图像上绘制包含形状和面积信息的文本(绿色)。
将 PIL 图像转换回 OpenCV 图像并返回。
主函数
python
def main(image_path, save_path=None):
# 加载图像
img = load_image(image_path)
if img is None:
return
# 对图像进行预处理
img_blur = preprocess_image(img)
# 查找图像中的轮廓
contours = find_contours(img_blur)
# 在图像上绘制轮廓和标注信息
img = draw_contours_and_text(img, contours)
# 显示处理后的图像
cv2.imshow("image", img)
# 等待用户按键
cv2.waitKey(0)
if save_path:
try:
# 保存处理后的图像
cv2.imwrite(save_path, img)
print(f"图像已保存至: {save_path}")
except Exception as e:
# 捕获保存图像过程中可能出现的异常并打印错误信息
print(f"保存图像时出错: {e}")
if __name__ == "__main__":
# 要处理的图像路径
image_path = "../images/xingzhuang.png"
# 保存处理后图像的路径
save_path = "output.png"
main(image_path, save_path)
main函数接受一个图像路径和一个可选的保存路径作为参数。
调用load_image加载图像,如果加载失败则返回。
调用preprocess_image对图像进行预处理。
调用find_contours查找图像中的轮廓。
调用draw_contours_and_text在图像上绘制轮廓和标注信息。
使用cv2.imshow显示处理后的图像,等待用户按键。
如果提供了保存路径,尝试使用cv2.imwrite保存处理后的图像,并打印保存信息,捕获保存过程中可能出现的异常并打印错误信息。
基础操作类
1.在这个项目里,cv2.imread函数在读取图像失败时会返回什么?在代码中是如何处理读取失败情况的?
cv2.imread函数在读取图像失败时会返回None。在代码中,通过判断返回值是否为None来处理读取失败的情况。如果返回值为None,则会打印出 “无法读取图像: {image_path}” 的提示信息,并返回None。具体代码如下:
img = cv2.imread(image_path)
if img is None:
print(f"无法读取图像: {image_path}")
return None
2.为什么要把图像从 BGR 颜色空间转换为灰度颜色空间?cv2.cvtColor函数在这里起到了什么作用?
将图像从 BGR 颜色空间转换为灰度颜色空间主要是为了简化后续的处理操作,因为在进行形状检测等任务时,灰度图像能提供足够的信息,并且处理起来相对简单,还可以减少数据量。许多图像处理算法在灰度图像上能够更有效地发挥作用。cv2.cvtColor函数的作用是实现图像在不同颜色空间之间的转换,在本项目中,使用cv2.COLOR_BGR2GRAY作为参数,将 BGR 颜色空间的图像转换为灰度颜色空间的图像,代码示例为:img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)。
图像预处理类
3.代码里运用了 OTSU 算法进行二值化处理,你能阐述一下 OTSU 算法的原理,以及它在这个项目中的作用吗?
OTSU 算法(大津法)的原理是基于图像的灰度分布,通过遍历所有可能的灰度阈值,将图像分为前景和背景两部分,计算不同阈值下前景和背景的类间方差,选择使得类间方差最大的阈值作为图像的二值化阈值。这样可以自动找到一个合适的阈值,将图像中的目标和背景尽可能清晰地区分开来。在本项目中,OTSU 算法的作用是自动确定二值化的阈值,将灰度图像转换为黑白分明的二值图像,使得图像中的形状轮廓更加明显,便于后续进行轮廓检测和形状识别等操作。具体代码为:_, img_binary = cv2.threshold(img_gray, 127, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)。
4.对二值化图像进行高斯模糊的目的是什么?cv2.GaussianBlur函数的参数(5, 5)和7分别代表什么含义?
对二值化图像进行高斯模糊的目的是减少图像中的噪声,平滑图像,避免噪声对后续的轮廓检测和形状识别等操作产生干扰。cv2.GaussianBlur函数的参数(5, 5)表示高斯核的大小,即卷积核的尺寸为 5x5,用于定义在图像上进行卷积操作的范围。参数7表示高斯核在 X 和 Y 方向上的标准差,它决定了高斯分布的形状,标准差越大,模糊效果越明显。代码为:img_blur = cv2.GaussianBlur(img_binary, (5, 5), 7)。
轮廓检测类
5.cv2.findContours函数中的mode=cv2.RETR_LIST和method=cv2.CHAIN_APPROX_SIMPLE分别代表什么意思?这样设置的原因是什么?
mode=cv2.RETR_LIST表示查找图像中所有的轮廓,并且不建立轮廓之间的层次关系,即所有的轮廓都被视为独立的个体。method=cv2.CHAIN_APPROX_SIMPLE表示只保留轮廓上的端点,对于水平、垂直和对角线方向的直线段,只保留其两个端点,这样可以大大减少轮廓数据的存储量,提高处理效率。在本项目中这样设置的原因是只需要检测出图像中的所有轮廓,不需要考虑轮廓之间的层次结构,并且为了减少数据量和提高计算速度,选择只保留轮廓的端点信息。具体函数调用代码为:contours, _ = cv2.findContours(img_blur, mode=cv2.RETR_LIST, method=cv2.CHAIN_APPROX_SIMPLE)。
6.如何通过轮廓来计算图形的面积和周长?在代码中是使用哪些函数实现的?
通过轮廓计算图形的面积可以使用cv2.contourArea函数,该函数接受轮廓作为参数,返回轮廓所围成图形的面积。计算图形的周长可以使用cv2.arcLength函数,它接受轮廓和一个布尔值作为参数,布尔值表示轮廓是否为闭合的,返回轮廓的周长。在代码中,计算轮廓面积的代码为:area = cv2.contourArea(cnt);计算轮廓周长的代码为:perimeter = cv2.arcLength(cnt, True)。
形状检测类
7.依据逼近多边形的顶点数量判断形状时,对于有 4 个顶点的情况,是怎样区分正方形和长方形的?
当逼近多边形有 4 个顶点时,通过计算其外接矩形的宽高比来区分正方形和长方形。首先使用cv2.boundingRect函数计算逼近多边形的外接矩形,得到外接矩形的左上角坐标(x, y)、宽度w和高度h。然后计算宽高比w/h,如果宽高比在 0.95 到 1.05 之间(即0.95 <= w / h <= 1.05),则判断该图形为正方形;否则,判断为长方形。具体代码为:
x, y, w, h = cv2.boundingRect(approx_pt)
if 0.95 <= w / h <= 1.05:
shape = "正方形"
else:
shape = "长方形"
8.代码里是如何判断一个轮廓是否接近圆形的?圆度的计算公式是什么,它的原理是什么?
在代码中,通过计算轮廓的圆度来判断一个轮廓是否接近圆形。圆度的计算公式为:\(circularity = 4 \times \pi \times \frac{area}{perimeter^2}\),其中area是轮廓的面积,通过cv2.contourArea函数计算得到;perimeter是轮廓的周长,通过cv2.arcLength函数计算得到。圆度的原理是基于图形的面积和周长之间的关系,当图形越接近圆形时,其面积与周长的平方的比值在乘以\(4 \times \pi\)后越接近 1。在代码中,当计算得到的圆度circularity大于 0.8 时(即if circularity > 0.8:),判断该轮廓为圆形。具体代码如下:
area = cv2.contourArea(approx_pt)
perimeter = cv2.arcLength(approx_pt, True)
if perimeter == 0:
circularity = 0
else:
circularity = 4 * np.pi * (area / (perimeter * perimeter))
if circularity > 0.8:
shape = "圆形"
图像绘制类
9.cv2.drawContours函数和cv2.rectangle函数在代码中分别用于绘制什么?它们的参数都有哪些含义?
cv2.drawContours函数在代码中用于绘制图像的轮廓。它的参数含义如下:img表示要绘制轮廓的目标图像;[approx_pt]表示要绘制的轮廓列表(这里使用[approx_pt]是因为cv2.drawContours函数要求输入的轮廓是一个列表形式);-1表示绘制列表中的所有轮廓;(0, 0, 255)表示轮廓的颜色,这里是红色(在 OpenCV 中颜色通常以 BGR 格式表示);2表示轮廓线的宽度。具体代码为:cv2.drawContours(img, [approx_pt], -1, (0, 0, 255), 2)。
cv2.rectangle函数在代码中用于绘制轮廓的外接矩形。它的参数含义如下:img同样是要绘制矩形的目标图像;(x, y)表示外接矩形的左上角坐标;(x + w, y + h)表示外接矩形的右下角坐标;(255, 0, 0)表示矩形的颜色,这里是蓝色;2表示矩形边框的宽度。具体代码为:cv2.rectangle(img, (x, y), (x + w, y + h), (255, 0, 0), 2)。
10.由于 OpenCV 在处理中文文本时存在局限性,所以使用了PIL库。你能说一下是如何在这两个库之间进行图像转换的吗?
由于 OpenCV 使用的是 BGR 颜色空间,而PIL库使用的是 RGB 颜色空间,所以在进行图像转换时需要注意颜色空间的转换。将 OpenCV 图像转换为PIL图像的步骤如下:首先使用cv2.cvtColor函数将 OpenCV 图像从 BGR 颜色空间转换为 RGB 颜色空间,代码为cv2.cvtColor(img, cv2.COLOR_BGR2RGB);然后使用Image.fromarray函数将转换后的图像数组转换为PIL图像,代码为Image.fromarray(...)。在PIL图像上绘制完中文文本后,再将PIL图像转换回 OpenCV 图像,步骤为:先使用np.array函数将PIL图像转换为 NumPy 数组,代码为np.array(...);然后再使用cv2.cvtColor函数将颜色空间从 RGB 转换回 BGR,代码为cv2.cvtColor(..., cv2.COLOR_RGB2BGR)。
综合类
11.假如输入的图像噪声较多,你会对现有的预处理步骤做出哪些调整?
如果输入的图像噪声较多,可以对现有的预处理步骤做出以下调整:
增大高斯模糊的核大小,例如将cv2.GaussianBlur函数中的核大小从(5, 5)调整为(7, 7)或更大,以增强对噪声的平滑效果,但要注意避免过度模糊导致图像的轮廓信息丢失。
在进行二值化处理之前,可以先使用中值滤波等其他滤波方法进一步去除噪声。中值滤波能够有效地去除椒盐噪声等脉冲噪声,并且对图像的边缘信息保留较好。可以使用cv2.medianBlur函数,例如img_gray = cv2.medianBlur(img_gray, 5),其中参数5表示中值滤波核的大小。
也可以尝试调整 OTSU 算法的参数,或者结合其他的二值化方法,以获得更好的二值化效果,使得图像中的目标和背景能够更清晰地区分。
12.请简要描述这个项目的整体流程,以及每个步骤的关键作用。
这个项目的整体流程及每个步骤的关键作用如下:
加载图像:使用cv2.imread函数加载指定路径的图像,关键作用是获取待处理的图像数据,并通过判断返回值来确保图像成功读取,若读取失败则进行相应的提示和处理。
图像预处理:
将图像从 BGR 颜色空间转换为灰度颜色空间,简化后续处理,减少数据量,便于后续算法处理。
使用 OTSU 算法进行二值化处理,自动确定合适的阈值,将图像转换为黑白分明的二值图像,突出形状轮廓。
对二值化图像进行高斯模糊,减少噪声,平滑图像,避免噪声对后续操作的干扰。
轮廓检测:使用cv2.findContours函数查找图像中的所有轮廓,为后续的形状检测提供基础数据。
形状检测:对每个轮廓进行多边形逼近,根据逼近多边形的顶点数量和圆度等特征判断轮廓所代表的形状,实现对图像中不同形状的识别。
图像绘制:在图像上绘制轮廓、外接矩形,并标注形状和面积信息,将检测和识别的结果直观地展示在图像上。
显示和保存图像:使用cv2.imshow函数显示处理后的图像,让用户能够直观地查看处理结果;如果指定了保存路径,则使用cv2.imwrite函数保存处理后的图像,以便后续查看和使用。