PIL与OpenCV双线性插值实现差异导致模型精度不够踩坑
起因
训练分类模型使用的是PIL库的resize,指定biliner算法采样,部署使用的是c++ opencv,发现opencv resize后的和PIL resize的不一样,cv的resize图片噪点多,而PIL的很平滑反而和cv的INTER_AREA插值效果差不多,PIL的库很可能做了均值采样。
在图像处理中,双线性插值(Bilinear Interpolation)是最常用的图像缩放算法之一。然而,不同图像处理库的具体实现可能存在差异。本文通过实验对比Python中PIL(Pillow)库和OpenCV库的双线性插值实现差异。
双线性插值原理回顾
双线性插值是一种在二维空间进行线性插值的方法,它通过四个最近的像素点计算新像素值。基本步骤包括:
- 计算目标像素在原图像中的对应位置(通常是非整数坐标)
- 确定该位置周围的四个最近邻像素
- 分别在水平和垂直方向进行线性插值
- 组合两次插值结果得到最终像素值
实验设计与实现
我设计了一个对比实验来验证PIL和OpenCV在双线性插值实现上的差异:
import cv2
import numpy as np
from skimage.metrics import structural_similarity as ssim
from PIL import Image
from PIL.Image import Resamplingdef compare_images(img1, img2):"""比较两幅图像的相似度"""gray1 = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY)gray2 = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY)# 计算结构相似性指数ssim_score, _ = ssim(gray1, gray2, full=True)# 计算平均像素差异diff = cv2.absdiff(img1, img2)diff_pixels = np.sum(diff) / (img1.shape[0] * img1.shape[1] * 3)# 计算欧氏距离vec_dist = np.linalg.norm(img1.astype(float) - img2.astype(float))return ssim_score, diff_pixels, vec_dist
实验方法
对比两种缩放路径:
- 直接缩放:从原图直接缩放到目标尺寸(50×50)
- 二次缩放:先放大到中间尺寸(57×57),再缩小到目标尺寸(50×50)
PIL库实现
img = Image.open('input.jpg')
resized_direct_pil = img.resize((50, 50), Resampling.BILINEAR)
resized_up_down_pil = img.resize((100, 100), Resampling.BILINEAR).resize((50, 50), Resampling.BILINEAR)
OpenCV实现
original_img = cv2.imread('input.jpg')
resized_direct = cv2.resize(original_img, (50, 50), interpolation=cv2.INTER_LINEAR)
resized_up_down = cv2.resize(original_img, (57, 57), interpolation=cv2.INTER_LINEAR)
resized_up_down = cv2.resize(resized_up_down, (50, 50), interpolation=cv2.INTER_LINEAR)
实验结果对比
PIL库结果
结构相似性指数(SSIM): 0.9986
平均像素差异: 0.70
向量欧氏距离: 101.26
OpenCV结果
结构相似性指数(SSIM): 0.9051
平均像素差异: 4.65
向量欧氏距离: 558.88
差异分析与讨论
-
PIL的实现特性:
- 不同缩放路径下结果高度一致(SSIM 0.9986)
- 对缩放路径不敏感,表现出更好的稳定性
- 可能采用了更精确的坐标映射和权重计算
-
OpenCV的实现特性:
- 不同缩放路径下差异较明显(SSIM 0.9051)
- 对中间缩放尺寸敏感
- 可能在边界处理和插值计算上有不同策略
-
可能的原因:
- 坐标映射方式不同(PIL可能使用中心对齐,OpenCV可能使用角点对齐)
- 边界像素处理策略不同
- 浮点数计算精度差异
- 颜色通道处理顺序差异(PIL使用RGB,OpenCV使用BGR)
实际应用建议
-
一致性要求高的场景:
- 建议在整个流程中使用同一图像处理库
- 避免混合使用PIL和OpenCV的缩放操作
-
性能考虑:
- OpenCV通常在大图像处理上性能更优
- PIL在小图像处理上可能更精确
-
版本控制:
- 不同版本的库可能有不同的实现细节
- 建议记录使用的库版本号
-
预处理建议:
- 对于关键应用,建议进行充分的预处理测试
- 可以考虑添加对齐处理或后处理来减小差异
完整代码
import cv2
import numpy as np
from skimage.metrics import structural_similarity as ssim
from PIL import Image
from PIL.Image import Resampling # 新版本推荐方式
def compare_images(img1, img2):# 转换为灰度图像以计算SSIMgray1 = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY)gray2 = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY)# 计算结构相似性指数(SSIM)ssim_score, _ = ssim(gray1, gray2, full=True)# 计算像素差异diff = cv2.absdiff(img1, img2)diff_pixels = np.sum(diff) / (img1.shape[0] * img1.shape[1] * 3) # 平均每个通道的像素差异# 计算欧氏距离(向量相似度)vec_dist = np.linalg.norm(img1.astype(float) - img2.astype(float))return ssim_score, diff_pixels, vec_distdef compare_images_pil(pil_img1, pil_img2):# 将PIL图像转换为numpy数组img1_np = np.array(pil_img1)img2_np = np.array(pil_img2)# 检查图像维度if len(img1_np.shape) == 2: # 灰度图像# 直接使用灰度图像gray1 = img1_np# 不需要颜色空间转换img1_np_bgr = cv2.cvtColor(img1_np, cv2.COLOR_GRAY2BGR) if len(img1_np.shape) == 2 else img1_npelse: # 彩色图像# PIL图像是RGB格式,OpenCV需要BGR格式if img1_np.shape[2] == 4: # RGBA图像img1_np = cv2.cvtColor(img1_np, cv2.COLOR_RGBA2BGR)else: # RGB图像img1_np = cv2.cvtColor(img1_np, cv2.COLOR_RGB2BGR)gray1 = cv2.cvtColor(img1_np, cv2.COLOR_BGR2GRAY)if len(img2_np.shape) == 2: # 灰度图像gray2 = img2_npimg2_np_bgr = cv2.cvtColor(img2_np, cv2.COLOR_GRAY2BGR) if len(img2_np.shape) == 2 else img2_npelse: # 彩色图像if img2_np.shape[2] == 4: # RGBA图像img2_np = cv2.cvtColor(img2_np, cv2.COLOR_RGBA2BGR)else: # RGB图像img2_np = cv2.cvtColor(img2_np, cv2.COLOR_RGB2BGR)gray2 = cv2.cvtColor(img2_np, cv2.COLOR_BGR2GRAY)# 计算结构相似性指数(SSIM)ssim_score, _ = ssim(gray1, gray2, full=True)# 确保两个图像都是3通道(BGR)用于后续计算if len(img1_np.shape) == 2:img1_np = cv2.cvtColor(img1_np, cv2.COLOR_GRAY2BGR)if len(img2_np.shape) == 2:img2_np = cv2.cvtColor(img2_np, cv2.COLOR_GRAY2BGR)# 计算像素差异diff = cv2.absdiff(img1_np, img2_np)diff_pixels = np.sum(diff) / (img1_np.shape[0] * img1_np.shape[1] * 3) # 平均每个通道的像素差异# 计算欧氏距离(向量相似度)vec_dist = np.linalg.norm(img1_np.astype(float) - img2_np.astype(float))return ssim_score, diff_pixels, vec_dist
# 加载原始图像
original_img = cv2.imread(r'F:\sms\traindata\trainwx\empty\825_r90_1.jpg') # 替换为你的图片路径
if original_img is None:print("无法加载图像,请检查路径")exit()# 1. 调整图片到500x500(如果原图不是500x500)# 2. 方法1
resized_direct = cv2.resize(original_img, (50, 50), interpolation=cv2.INTER_LINEAR)# 3. 方法2
resized_up_down = cv2.resize(original_img, (57, 57), interpolation=cv2.INTER_LINEAR)
resized_up_down = cv2.resize(resized_up_down, (50, 50), interpolation=cv2.INTER_LINEAR)#使用python的库再验证
img = Image.open(r'F:\sms\traindata\trainwx\empty\825_r90_1.jpg')
resized_direct_pil = img.resize((50, 50), Resampling.BILINEAR)
resized_up_down_pil = img.resize((100, 100), Resampling.BILINEAR).resize((50, 50), Resampling.BILINEAR)#对比py的结果
ssim_score, diff_pixels, vec_dist = compare_images_pil(resized_direct_pil, resized_up_down_pil)print(f"py结构相似性指数(SSIM): {ssim_score:.4f}")
print(f"py平均像素差异: {diff_pixels:.2f}")
print(f"py向量欧氏距离: {vec_dist:.2f}")# 保存图片
resized_direct_pil.save('resized_direct_pil_250x250.jpg')
resized_up_down_pil.save('resized_up_down_pil_250x250.jpg')# 4. 比较两种方法的结果(理论上应该完全相同)
ssim_score, diff_pixels, vec_dist = compare_images(resized_direct, resized_up_down)# 5. 显示结果
print(f"结构相似性指数(SSIM): {ssim_score:.4f}")
print(f"平均像素差异: {diff_pixels:.2f}")
print(f"向量欧氏距离: {vec_dist:.2f}")# 保存结果图像
cv2.imwrite('resized_direct_250x250.jpg', resized_direct)
cv2.imwrite('resized_up_down_250x250.jpg', resized_up_down)