2、图像读取与显示:使用OpenCV读取热成像原始数据、灰度图显示、直方图均衡化
好,咱们正式开始动手了。上一章聊了热成像的原理和数据类型,这一章咱们直接上代码。说白了,图像处理的第一步就是把数据从文件里拽出来,然后想办法让它能看。热成像的原始数据跟普通照片不太一样,你直接用看图软件打开,大概率是一片漆黑或者一片惨白。为什么?因为它的像素值范围跟我们肉眼习惯的0-255不一样。
我个人习惯先把原始数据读成16位灰度图,然后再做映射。这样能保留最多的细节。你想想看,热成像传感器采集到的温度数据,往往是10位、12位甚至14位的,直接转成8位会丢掉很多信息。嗯,这里要注意,我们后面做的直方图均衡化,就是为了把这些被压缩的信息重新“撑开”。
2.1 读取热成像原始数据
OpenCV读取图像最常用的函数就是cv2.imread()。但热成像数据通常以两种形式存在:一种是标准的图像文件(比如PNG、TIFF),但像素值是16位的;另一种是厂商自定义的原始二进制格式(.raw、.bin等)。咱们先讲第一种,也是最常见的情况。
核心要点:读取时一定要指定 cv2.IMREAD_UNCHANGED 标志,否则OpenCV默认会帮你转成8位,数据就废了。
import cv2
import numpy as np
import matplotlib.pyplot as plt
# 读取16位热成像原始数据
# 注意:一定要用 IMREAD_UNCHANGED 保持原始位深
img_16bit = cv2.imread('thermal_raw.png', cv2.IMREAD_UNCHANGED)
# 检查是否读取成功
if img_16bit is None:
print("文件读取失败,请检查路径")
exit()
# 查看数据类型和形状
print(f"数据类型: {img_16bit.dtype}") # 应该是 uint16
print(f"图像尺寸: {img_16bit.shape}") # (height, width) 或 (height, width, channels)
print(f"像素值范围: {img_16bit.min()} ~ {img_16bit.max()}")
我在项目中遇到过一种情况:客户给的文件后缀是.png,但实际上是12位数据打包在16位容器里。直接用cv2.imread()读出来,最大值才4095,而不是65535。这时候你就知道,它的有效数据位是12位。这个细节在做后续映射时特别重要。
小技巧:如果读取后 img_16bit.dtype 显示 uint8,说明OpenCV自动做了截断。赶紧检查 cv2.IMREAD_UNCHANGED 有没有写对。
2.2 灰度图显示
16位数据直接显示是不行的。大多数显示器只支持8位,所以我们需要把16位映射到0-255范围。最简单的办法就是线性拉伸。
说白了,就是把原始数据的最小值映射到0,最大值映射到255。但这样做有个问题:如果数据中有几个异常噪点,整个图像的对比度就会被拉得很差。我曾经在野外测试时遇到过这种情况——一个热斑把整个画面的动态范围都占满了,周围细节全成了黑色。
# 方法一:简单线性拉伸(不推荐,容易受噪点影响)
def linear_stretch(img):
min_val = img.min()
max_val = img.max()
# 防止除零
if max_val == min_val:
return np.zeros_like(img, dtype=np.uint8)
stretched = ((img.astype(np.float32) - min_val) / (max_val - min_val) * 255)
return stretched.astype(np.uint8)
# 方法二:百分比截断拉伸(推荐,抗噪能力强)
def percentile_stretch(img, low_perc=2, high_perc=98):
# 计算指定百分位的像素值
low_val = np.percentile(img, low_perc)
high_val = np.percentile(img, high_perc)
# 截断并拉伸
clipped = np.clip(img, low_val, high_val)
stretched = ((clipped.astype(np.float32) - low_val) / (high_val - low_val) * 255)
return stretched.astype(np.uint8)
# 显示结果
img_8bit = percentile_stretch(img_16bit)
cv2.imshow('Thermal Gray Image', img_8bit)
cv2.waitKey(0)
cv2.destroyAllWindows()
你可能会问:为什么用2%和98%这两个值?这是我个人习惯。在大多数热成像场景中,图像边缘或者传感器坏点会产生极端的亮暗值,去掉头尾2%的像素,基本就能把这些干扰排除掉。当然,具体数值可以根据你的数据质量调整。
注意:百分比截断拉伸会改变像素间的相对温差关系。如果你需要做精确的温度测量,请保留原始16位数据,只在显示时做映射。
2.3 直方图均衡化
好,现在图像能看了,但你可能发现一个问题:画面整体偏暗或者偏亮,对比度不够。这是因为热成像数据的直方图往往集中在某个狭窄的区间内。直方图均衡化就是用来解决这个问题的——它把像素值重新分布,让直方图尽可能平坦。
OpenCV提供了现成的函数cv2.equalizeHist(),但它只接受8位单通道图像。所以我们需要先做拉伸,再做均衡化。
# 先拉伸到8位
img_8bit = percentile_stretch(img_16bit)
# 直方图均衡化
img_eq = cv2.equalizeHist(img_8bit)
# 对比显示
plt.figure(figsize=(12, 4))
plt.subplot(1, 3, 1)
plt.imshow(img_8bit, cmap='gray')
plt.title('原始拉伸')
plt.axis('off')
plt.subplot(1, 3, 2)
plt.imshow(img_eq, cmap='gray')
plt.title('直方图均衡化')
plt.axis('off')
plt.subplot(1, 3, 3)
plt.hist(img_8bit.ravel(), 256, [0, 256], alpha=0.5, label='原始')
plt.hist(img_eq.ravel(), 256, [0, 256], alpha=0.5, label='均衡化')
plt.legend()
plt.title('直方图对比')
plt.tight_layout()
plt.show()
你想想看,均衡化后的图像是不是细节更丰富了?原本隐藏在暗区的纹理,现在能看清楚了。但凡事都有代价——均衡化会放大噪声。我在处理一些低端热成像仪的数据时,均衡化后整个画面出现了明显的颗粒感。这时候就需要考虑用自适应直方图均衡化(CLAHE)来替代。
# 自适应直方图均衡化(CLAHE)—— 推荐用于热成像
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
img_clahe = clahe.apply(img_8bit)
# 对比三种方法
plt.figure(figsize=(15, 4))
plt.subplot(1, 4, 1)
plt.imshow(img_8bit, cmap='gray')
plt.title('原始拉伸')
plt.axis('off')
plt.subplot(1, 4, 2)
plt.imshow(img_eq, cmap='gray')
plt.title('全局均衡化')
plt.axis('off')
plt.subplot(1, 4, 3)
plt.imshow(img_clahe, cmap='gray')
plt.title('CLAHE')
plt.axis('off')
plt.subplot(1, 4, 4)
# 显示局部细节放大
roi = img_clahe[100:200, 100:200]
plt.imshow(roi, cmap='gray', extent=[100, 200, 200, 100])
plt.title('CLAHE细节')
plt.axis('off')
plt.tight_layout()
plt.show()
经验总结:
- 全局均衡化:适合直方图分布均匀的场景,速度快,但容易过增强噪声
- CLAHE:适合热成像这种局部对比度差异大的场景,效果好,但参数需要调
- clipLimit:我一般从2.0开始试,值越大对比度越强,但噪声也越明显
- tileGridSize:默认(8,8)够用,如果图像分辨率高可以适当增大
2.4 本章知识体系
下面这张图总结了本章的核心流程。你可以把它当作一个操作清单,以后处理热成像数据时照着走就行。
我的建议:在实际项目中,不要一上来就用CLAHE。先试试简单的百分比拉伸,如果效果不满意再上均衡化。我曾经在一个项目中,客户觉得CLAHE处理后的图像“太假了”,反而喜欢原始拉伸那种“自然”的感觉。嗯,审美这东西,有时候比技术还难搞。
好了,这一章的内容就到这里。代码不多,但每一步都有坑。你回去练习的时候,可以找几张不同场景的热成像图试试——比如白天拍的建筑、夜晚拍的人脸、还有工业设备的热图。对比一下不同拉伸方法和均衡化策略的效果差异。只有亲手调过参数,才能真正理解这些算法的脾气。
公众号:蓝海资料掘金营,微信deep3321