在本教程中,您将学习如何使用OpenCV和scikit-image进行直方图匹配。
上周我们讨论了直方图均衡化,这是一种基本的图像处理技术,可以提高输入图像的对比度。
但是,如果你想自动匹配两幅图像的对比度或颜色分布,该怎么办呢?
例如,假设我们有一个输入图像和一个参考图像。我们的目标是:
- 计算每个图像的直方图
- 以参考图像直方图为例
- 使用参考直方图更新输入图像中的像素强度值,使其匹配
当将图像处理管道应用于在不同光照条件下捕获的图像时,直方图匹配是有益的,从而创建图像的“标准化”表示,无论它们是在何种光照条件下捕获的(对光照条件的变化程度设定合理的期望)。
今天我们将详细讨论直方图匹配。下周我将展示如何使用直方图均衡化进行色彩校正和色彩稳定性。
1.使用 OpenCV、scikit-image 和 Python 进行直方图匹配
在本教程的第一部分,我们将讨论直方图匹配,并使用OpenCV和scikit-image实现直方图匹配。
一旦我们理解了项目结构,我们将实现一个Python脚本,它将:
- 加载输入图像(即源图像)
- 加载参考图像
- 计算两个图像的直方图
- 将输入图像与参考图像进行匹配,从而将参考图像的颜色/强度分布转移到源图像中
2.直方图匹配是什么?
直方图匹配可以被认为是一种“变换”。我们的目标是获取一幅输入图像(“源”)并更新其像素强度,使输入图像的直方图分布与参考图像的分布相匹配。
在输入图像的实际内容不发生变化的情况下,像素分布却发生了变化,从而根据参考图像的分布来调整输入图像的光照和对比度。
应用直方图匹配可以让我们获得有趣的美学结果(我们将在本教程的后面看到)。
此外,我们可以使用直方图匹配作为基本颜色校正/颜色恒常性的一种形式,使我们能够构建更好、更可靠的图像处理管道,而无需利用复杂、计算成本高的机器学习和深度学习算法。
3.OpenCV和scikit-image如何用于直方图匹配?
手动实现直方图匹配可能会很痛苦,但对我们来说幸运的是,scikit-image
库已经有了 match_histograms
函数。
因此,应用直方图匹配就像使用 OpenCV 的 cv2.imread 加载两个图像然后调用 scikit-image 的 match_histograms 函数一样简单:
src = cv2.imread(args["source"])
ref = cv2.imread(args["reference"])
multi = True if src.shape[-1] > 1 else False
matched = exposure.match_histograms(src, ref, multichannel=multi)
4.项目结构
在我们使用OpenCV和scikit-image实现直方图匹配之前,让我们先使用我们的项目目录结构。
我们今天只需要一个Python脚本,match_histograms.py
,它将加载empire_state_cloud.png
(源图像)和empire_state_sunset.png
(参考图像)。
然后我们的脚本将应用直方图匹配,将颜色分布从参考图像转移到源图像上。
5.利用OpenCV和scikit-image实现直方图匹配
5.1代码实现
# match_histograms.py
# 用法
# python match_histograms.py --source empire_state_cloudy.png --reference empire_state_sunset.png
# 导入库
from skimage import exposure
import matplotlib.pyplot as plt
import argparse
import cv2
# 构造参数解析器并解析参数
ap = argparse.ArgumentParser()
ap.add_argument("-s", "--source", required=True, help="Path to the input source image")
ap.add_argument("-r", "--reference", required=True, help="Path to the input reference image")
args = vars(ap.parse_args())
# 加载源和参考图像
print("[INFO] Loading source and reference images...")
src = cv2.imread(args["source"])
ref = cv2.imread(args["reference"])
# 确定我们是否执行多通道直方图匹配,然后执行直方图匹配本身
print("[INFO] Performing histogram matching...")
multi = True if src.shape[-1] > 1 else False
matched = exposure.match_histograms(src, ref, multichannel=multi)
# 显示输出图像
cv2.imshow("Source", src)
cv2.imshow("Reference", ref)
cv2.imshow("Matched", matched)
cv2.waitKey(0)
# 构造一个图形来显示应用直方图匹配前后每个通道的直方图图
(fig, axs) = plt.subplots(nrows=3, ncols=3, figsize=(8, 8))
# 循环遍历源图像、参考图像和输出匹配图像
for (i, image) in enumerate((src, ref, matched)):
# 转换图像从BGR到RGB通道顺序
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
# 按RGB顺序循环通道名称
for (j, color) in enumerate(("red", "green", "blue")):
# 计算当前通道的直方图并绘制它
(hist, bins) = exposure.histogram(image[..., j], source_range="dtype")
axs[j, i].plot(bins, hist/hist.max())
# 计算当前通道的累积分布函数并绘制它
(cdf, bins) = exposure.cumulative_distribution(image[..., j])
axs[j, i].plot(bins, cdf)
# 将当前图形的y轴标签设置为当前颜色通道的名称
axs[j, 0].set_ylabel(color)
# 设置轴标题
axs[0, 0].set_title("Source")
axs[0, 1].set_title("Reference")
axs[0, 2].set_title("Matched")
# 显示输出图
plt.tight_layout()
plt.show()
“Source”列显示了我们输入源图像中像素强度的分布。 “Reference”列显示我们从磁盘加载的参考图像的分布。最后,“Matched”列显示应用直方图匹配的输出。
请注意源像素强度是如何调整的,以匹配参考图像的分布!这个操作,本质上,就是直方图匹配。
5.2代码解析
# 导入库
from skimage import exposure
import matplotlib.pyplot as plt
import argparse
import cv2
# 构造参数解析器并解析参数
ap = argparse.ArgumentParser()
ap.add_argument("-s", "--source", required=True,
help="path to the input source image")
ap.add_argument("-r", "--reference", required=True,
help="path to the input reference image")
args = vars(ap.parse_args())
我们需要scikit-image的exposure库来计算图像直方图,累积分布函数,并应用直方图匹配。
我们将使用matplotlib来绘制我们的直方图,这样我们就可以在应用直方图匹配之前和之后可视化它们。
我们导入argparse用于命令行参数解析,同时导入cv2。
接下来是我们的命令行参数:
- source:我们输入图像的路径。
- reference:参考图像的路径。
请记住,我们在这里所做的是从参考中获取颜色分布,然后将其传输到源。源图像本质上是将更新其颜色分布的图像。
# 加载源和参考图像
print("[INFO] loading source and reference images...")
src = cv2.imread(args["source"])
ref = cv2.imread(args["reference"])
# 确定我们是否执行多通道直方图匹配,然后执行直方图匹配本身
print("[INFO] performing histogram matching...")
multi = True if src.shape[-1] > 1 else False
matched = exposure.match_histograms(src, ref, multichannel=multi)
# 显示输出图像
cv2.imshow("Source", src)
cv2.imshow("Reference", ref)
cv2.imshow("Matched", matched)
cv2.waitKey(0)
加载我们的 src 和 ref 图像。
加载这两幅图像后,我们可以进行直方图匹配。
直方图匹配既适用于单通道图像,也适用于多通道图像。设置一个布尔值multi,这取决于我们使用的是多通道图像(True)还是单通道图像(False)。
应用直方图匹配就像在 scikit-image 的exposure子模块中调用 match_histogram 函数一样简单。
显示我们的源、参考和输出直方图匹配的图像到我们的屏幕。
到这里,我们在技术上已经完成了,但是为了充分理解直方图匹配的作用,让我们检查一下 src、ref 和matched的颜色直方图:
# 构建一个图形来显示应用直方图匹配前后每个通道的直方图
(fig, axs) = plt.subplots(nrows=3, ncols=3, figsize=(8, 8))
# 循环我们的源图像、参考图像和输出匹配图像
for (i, image) in enumerate((src, ref, matched)):
# 将图像从 BGR 转换为 RGB 通道排序
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
# 按RGB顺序循环通道名称
for (j, color) in enumerate(("red", "green", "blue")):
# 计算当前通道的直方图并绘制它
(hist, bins) = exposure.histogram(image[..., j],
source_range="dtype")
axs[j, i].plot(bins, hist / hist.max())
# 计算当前通道的累积分布函数并绘制它
(cdf, bins) = exposure.cumulative_distribution(image[..., j])
axs[j, i].plot(bins, cdf)
# 将当前图形的y轴标签设置为当前颜色通道的名称
axs[j, 0].set_ylabel(color)
创建一个3 × 3的图形,分别显示src、ref和matched的红色、绿色和蓝色通道的直方图。
从这里开始,循环遍历每个src、ref和matched的图像。然后我们将当前图像从BGR转换为RGB通道排序。
接下来是实际的绘图:
- 计算当前图像的当前通道的直方图
- 然后绘制直方图
- 计算当前通道的累积分布函数并绘图
- 设置 y 轴标签
最后一步是显示绘图:
# 设置坐标轴标题
axs[0, 0].set_title("Source")
axs[0, 1].set_title("Reference")
axs[0, 2].set_title("Matched")
# 显示输出图
plt.tight_layout()
plt.show()
在这里,我们设置每个轴的标题,然后在屏幕上显示直方图。
6.总结
虽然本教程从美学的角度关注直方图匹配,但直方图匹配还有更重要的实际应用。 直方图匹配可用作图像处理管道中的归一化技术,作为颜色校正和颜色匹配的一种形式。即使光照条件发生变化,也可以获得一致的、归一化的图像表示。
作为计算机视觉从业者,实现这种标准化会让我们的生活更轻松。如果我们可以安全地假设一个特定的光照条件范围,我们可以硬编码参数,包括Canny边缘检测阈值,高斯模糊大小,等等。
参考目录
https://www.pyimagesearch.com/2021/02/08/histogram-matching-with-opencv-scikit-image-and-python/