5.18. 直方图与统计¶
除了改变图像像素的操作之外,Image 类还提供了一组用于测量像素的方法——汇总像素值的分布、返回平均亮度与中位亮度、找出暗像素与亮像素之间的最佳分界点、报告各颜色通道的离散程度。这些测量结果以两种方式服务于应用:一是作为决策代码的输入,用于决定使用什么阈值、设置多大增益、场景的色调轮廓是什么样子;二是作为诊断信号——“场景是否足够亮?”——应用可以据此做出反应,而无需针对任何具体像素做决定。
几乎所有测量的起点都是直方图。
5.18.1. 直方图¶
图像的直方图统计每个可能的亮度值各有多少个像素。对于灰度图像,它是一个以 0 到 255 各值为索引的计数列表。对于彩色图像,它则是三个这样的列表——每个通道一个。
get_histogram() 用于计算直方图:
h = img.get_histogram()
返回的对象是一个 histogram 结果,它提供各通道的箱(bin)计数列表以及一些高层查询。箱计数经过归一化,使其总和为 1.0——直方图描述的是分布的轮廓而非绝对像素数量,这使得不同尺寸图像之间的测量结果可以相互比较。
对于灰度图像,直方图只有一个通道的箱,可通过 h.bins()(或等价的 h[0])获取。对于 RGB565 图像,直方图在二值阈值化页面介绍过的 LAB 颜色空间中计算,三个箱通道分别可通过 h.l_bins()、h.a_bins()、h.b_bins()(或 h[0]、h[1]、h[2])获取。LAB 与阈值化和跟踪方法所使用的颜色空间一致;直方图与阈值在“颜色是在哪个空间中测量”这一点上保持一致。
5.18.2. 箱与箱数量¶
默认直方图为每个可能的像素值分配一个箱——对于 8 位通道即 256 个箱。有时这比应用所需的分辨率更精细。一个只关心分布大致轮廓的分类器,使用较少的箱数量——32 个甚至 8 个箱——可能效果更好,既运行得更快,又能在面对噪声时产生更干净的结果。bins 关键字参数(以及用于彩色的各通道 l_bins、a_bins、b_bins)用于设置箱数量:
h = img.get_histogram(bins=32)
ROI 和阈值范围限定的工作方式与其他所有测量方法相同。传入 roi 可将直方图限定在一个像素矩形区域内;传入 thresholds 列表则只包含匹配这些范围的像素。阈值形式使得“仅计算匹配像素的直方图”成为一次调用即可完成的操作——这是一种常见模式,当应用想要刻画某个已检测区域的纹理特征,而又无需自行遍历像素时尤其有用。
一幅灰度直方图,叠加了三种汇总测量:Otsu 阈值(最能将暗像素簇与亮像素簇分开的分界点)、平均值和中位数。每种测量对同一分布揭示了不同的信息。¶
5.18.3. 统计¶
直方图描述了每个值出现的频繁程度;统计量则是由直方图推导出的数值汇总。get_statistics() 返回的 statistics 对象提供了一组标准统计量:
mean——像素值的算术平均值。median——半数像素低于其值的那个值。mode——出现次数最多的单个值。stdev——标准差,衡量数据围绕平均值的离散程度。min和max——图像中存在的最暗和最亮的像素值。lq和uq——下四分位和上四分位的分界值。
对于 RGB565 图像,各通道形式(l_mean、a_median、b_mode 等)按通道逐一提供相同的测量结果。
这些数值大多在特定场景中派上用场。mean 与 stdev 结合可给出噪声估计:本应均匀的场景其 stdev 较小,而有噪声的传感器会让同一场景的 stdev 变大。min 与 max 给出图像的对比度:二者越接近,场景越平淡;二者相距越远,算法可利用的动态范围就越大。当分布存在离群值时,median 是更稳健的中心(少数极亮像素不会像拉动平均值那样拉动中位数)。mode 是出现次数最多的单个值,对于背景占据大部分像素的图像,它有助于找出图像的背景亮度水平。
get_statistics() 在内部执行一次直方图遍历,然后对其进行汇总;传入与之前计算直方图时相同的 thresholds 和 roi 参数,即可得到针对同一组像素的统计量。
5.18.4. 百分位数与 CDF 查询¶
histogram 对象提供了一个 get_percentile() 方法,它将一个比例转换为像素值——即低于该值的像素占比恰好为所请求的比例。h.get_percentile(0.5) 即中位数;h.get_percentile(0.05) 与 h.get_percentile(0.95) 结合可给出一个稳健的最小/最大值,它会将底部和顶部各 5% 作为离群值忽略。
当应用想要刻画像素值的范围,又不希望少数零散的亮像素或暗像素扭曲结果时,就会使用这种形式。来自第 5 和第 95 百分位数的稳健最小/最大值,也是对比度拉伸处理的天然输入——即“色调校正”一节所介绍的逐像素重映射。
5.18.5. Otsu 方法¶
直方图还能回答另一个值得单独提出的问题:给定一幅像素分裂为“暗”簇和“亮”簇的图像,二者之间的分界点在哪里?阈值化页面已经按其结果命名了这一机制——一个应用可以交给 binary() 使用的全局阈值——但推迟了讲解其实现方式。其实现方式就是 Otsu 方法,它存在于直方图之上。
其直觉是:一幅具有清晰前景和背景的图像,其亮度直方图中会有两个簇,两簇之间有一个谷。最佳的阈值位置是谷底——两簇被分隔得最好的那个值。Otsu 方法会搜索每一个可能的分界点,并选出簇内方差最小(这与簇间方差最大是一回事)的那个,其结果就是针对该特定图像分布的最优二值分割。
histogram 对象通过 get_threshold 提供 Otsu 方法:
h = img.get_histogram()
t = h.get_threshold()
返回的 threshold 对象带有 value(用于灰度)或 l_value / a_value / b_value(用于彩色)属性,承载所选定的分界值。将该结果直接回传给 binary(),即可获得一个自调节的全局阈值,其分界点由图像本身选定:
img.binary([(t.value, 255)])
这种模式并不能解决基于滤波器的自适应阈值所解决的光照不均问题;它解决的是当全局阈值化已经是正确方法时“我应该在哪个值处分割?”这一问题。对于前景/背景区分明确的场景,Otsu 选出的值通常与人眼凭直觉选出的值相差不过几个单位。
5.18.6. 在差值图像上进行计算¶
关于 get_histogram() 和 get_statistics() 有一个实用的细节:两者都接受一个 difference 关键字参数,它接收另一幅 Image,并计算源图像与该图像之间逐像素差值的直方图(或统计量),而无需分配一幅单独的差值图像。这是一种廉价的方式,可以询问“自参考帧以来场景变化了多少?”,而不必为产生一幅唯一用途就是被测量的图像而付出显式调用 difference() 的代价。对于持续运行的运动检测脚本而言,这样的节省会日积月累。