图像预处理是计算机视觉任务中至关重要的一步。原始图像往往受到噪声、光照不均、尺寸不一等多种因素的影响,直接用于后续分析(如特征提取、目标检测、机器学习模型训练等)可能会导致性能下降或结果不准确。预处理旨在通过一系列操作来增强图像质量、去除噪声、标准化图像数据,使其更适合特定应用。
本文将详细介绍几种 OpenCV 中常用的图像预处理技术,包括其 C/C++ 实现、关键参数解析以及使用时的注意事项。
将彩色图像转换为灰度图像是最常见的预处理步骤之一。
cv::cvtColor()
#include
#include // 包含 cvtColor
int main() {
cv::Mat src = cv::imread("input.jpg", cv::IMREAD_COLOR);
if (src.empty()) {
std::cerr << "Error: Image cannot be loaded!" << std::endl;
return -1;
}
cv::Mat gray;
cv::cvtColor(src, gray, cv::COLOR_BGR2GRAY); // 注意OpenCV默认加载为BGR
cv::imshow("Original", src);
cv::imshow("Grayscale", gray);
cv::waitKey(0);
cv::destroyAllWindows();
return 0;
}
参数解析 (cv::cvtColor
):
src
: 输入图像 (彩色图像)。dst
: 输出图像 (灰度图像)。code
: 颜色空间转换代码。
cv::COLOR_BGR2GRAY
: 从 BGR (OpenCV 默认的彩色图像通道顺序) 转换为灰度。cv::COLOR_RGB2GRAY
: 从 RGB 转换为灰度 (如果你的图像源是 RGB)。cv::COLOR_BGR2HSV
: 转换为 HSV 颜色空间等。注意事项:
cv::imread()
加载图像时,默认通道顺序是 BGR。如果你的图像数据来自其他源(例如某些库或传感器可能输出 RGB),请注意选择正确的转换代码。调整图像的尺寸以满足特定算法的输入要求或减少计算量。
cv::resize()
#include
#include // 包含 resize
int main() {
cv::Mat src = cv::imread("input.jpg");
if (src.empty()) {
std::cerr << "Error: Image cannot be loaded!" << std::endl;
return -1;
}
cv::Mat resized_abs, resized_rel;
cv::Size new_size(300, 200); // 目标绝对尺寸:宽300, 高200
// 方法1: 指定目标尺寸
cv::resize(src, resized_abs, new_size, 0, 0, cv::INTER_LINEAR);
// 方法2: 指定缩放因子
double scale_x = 0.5;
double scale_y = 0.5;
cv::resize(src, resized_rel, cv::Size(), scale_x, scale_y, cv::INTER_LINEAR);
cv::imshow("Original", src);
cv::imshow("Resized (Absolute)", resized_abs);
cv::imshow("Resized (Relative)", resized_rel);
cv::waitKey(0);
cv::destroyAllWindows();
return 0;
}
参数解析 (cv::resize
):
src
: 输入图像。dst
: 输出图像。dsize
: 输出图像的目标尺寸 (cv::Size(width, height)
)。如果设置为 cv::Size()
(即 cv::Size(0,0)
),则尺寸将通过 fx
和 fy
计算。fx
: 沿水平轴的缩放因子。如果 dsize
非零,则此参数无效。fy
: 沿垂直轴的缩放因子。如果 dsize
非零,则此参数无效。interpolation
: 插值方法。
cv::INTER_NEAREST
: 最近邻插值。速度最快,但效果可能较差,有马赛克感。cv::INTER_LINEAR
: 双线性插值 (默认)。速度和效果的良好折中,常用于放大。cv::INTER_CUBIC
: 双三次插值。效果较好,尤其在放大时,但计算量更大。cv::INTER_AREA
: 基于区域的重采样。在缩小图像时效果较好,可以避免波纹。放大时类似最近邻。cv::INTER_LANCZOS4
: Lanczos 插值 (8x8邻域)。效果最好,但计算量最大。注意事项:
cv::INTER_AREA
通常是最佳选择,以避免信息丢失和摩尔纹。cv::INTER_LINEAR
是一个不错的起点,cv::INTER_CUBIC
或 cv::INTER_LANCZOS4
可以提供更好的质量,但更慢。dsize
而不考虑原始长宽比,图像可能会变形。如果需要保持长宽比,应先计算一个轴的缩放因子或新尺寸,然后据此计算另一个。dsize
(非零) 和 fx
/fy
(非零),dsize
优先。用于减少图像噪声,平滑图像细节。
使用高斯核对图像进行卷积,有效去除高斯噪声。
cv::GaussianBlur()
#include
#include
int main() {
cv::Mat src = cv::imread("input_noisy.jpg"); // 假设有一张带噪声的图
if (src.empty()) { /* ... error handling ... */ return -1; }
cv::Mat blurred_gaussian;
cv::Size kernel_size(5, 5); // 高斯核尺寸,必须是正奇数
double sigmaX = 0; // X方向标准差,若为0,则由ksize.width计算
// double sigmaY = 0; // Y方向标准差,若为0,则由ksize.height计算 (或等于sigmaX)
cv::GaussianBlur(src, blurred_gaussian, kernel_size, sigmaX);
cv::imshow("Original", src);
cv::imshow("Gaussian Blurred", blurred_gaussian);
cv::waitKey(0);
cv::destroyAllWindows();
return 0;
}
参数解析 (cv::GaussianBlur
):
ksize
: 高斯核的尺寸 (cv::Size(width, height)
)。宽度和高度必须是正奇数,也可以不同。sigmaX
: X 方向的高斯核标准差。sigmaY
: Y 方向的高斯核标准差。如果为0,则设置成与 sigmaX
相同;如果两者都为0,则它们从 ksize.width
和 ksize.height
计算得出。建议将 sigmaY
设为0,让其自动等于 sigmaX
或根据 ksize
计算。borderType
: 边界处理方式,默认 cv::BORDER_DEFAULT
。注意事项 (GaussianBlur
):
ksize
越大,图像越模糊。sigmaX
(和 sigmaY
) 越大,模糊程度也越大。sigmaX
(和 sigmaY
) 设置得非常小,而 ksize
较大,效果可能不佳。通常让 sigma
根据 ksize
计算 (通过将 sigmaX
设为0) 是个好主意,或者根据经验值设定。用像素邻域内的中值替换该像素的值,对椒盐噪声特别有效。
cv::medianBlur()
#include
#include
int main() {
cv::Mat src = cv::imread("salt_pepper_noise.jpg"); // 假设有椒盐噪声图
if (src.empty()) { /* ... error handling ... */ return -1; }
cv::Mat blurred_median;
int ksize_median = 5; // 孔径线性尺寸,必须是大于1的奇数
cv::medianBlur(src, blurred_median, ksize_median);
cv::imshow("Original", src);
cv::imshow("Median Blurred", blurred_median);
cv::waitKey(0);
cv::destroyAllWindows();
return 0;
}
参数解析 (cv::medianBlur
):
ksize
: 孔径的线性尺寸 (例如,5 表示 5x5 的邻域)。必须是大于1的奇数。注意事项 (medianBlur
):
ksize
越大,平滑效果越强,但也可能丢失更多图像细节。ksize
增加而显著增加。在平滑图像的同时保持边缘清晰。
cv::bilateralFilter()
#include
#include
int main() {
cv::Mat src = cv::imread("input.jpg");
if (src.empty()) { /* ... error handling ... */ return -1; }
cv::Mat blurred_bilateral;
int d = 9; // 滤波期间每个像素邻域的直径。如果非正,则从 sigmaSpace 计算。
double sigmaColor = 75; // 颜色空间滤波器的 Sigma 值。较大值意味着邻域内更远的颜色也会混合在一起,从而产生更大区域的半相等颜色。
double sigmaSpace = 75; // 坐标空间滤波器的 Sigma 值。较大值意味着更远的像素将相互影响,只要它们的颜色足够接近。当 d>0 时,它指定邻域大小而不考虑 sigmaSpace。否则,d 与 sigmaSpace 成正比。
cv::bilateralFilter(src, blurred_bilateral, d, sigmaColor, sigmaSpace);
cv::imshow("Original", src);
cv::imshow("Bilateral Filtered", blurred_bilateral);
cv::waitKey(0);
cv::destroyAllWindows();
return 0;
}
参数解析 (cv::bilateralFilter
):
d
: 在过滤期间使用的每个像素邻域的直径。如果为非正数(例如 -1),则会从 sigmaSpace
计算出来。通常取 5 到 9 之间的值。sigmaColor
: 颜色空间的标准差。值越大,意味着越多差异较大的颜色会被认为是相似的,从而被平均。sigmaSpace
: 坐标空间的标准差。值越大,意味着越远的像素会相互影响(只要颜色相似)。如果 d > 0
,则 d
指定邻域大小,sigmaSpace
不起作用;否则 d
与 sigmaSpace
成正比。注意事项 (bilateralFilter
):
sigmaColor
和 sigmaSpace
的调整对结果影响很大,需要根据具体图像和需求进行调试。sigmaColor
值过大,效果接近高斯模糊;如果过小,则平滑效果不明显。sigmaSpace
值过大,计算量会增加。将图像转换为二值图像(通常是黑白)。
将像素值与固定阈值进行比较。
cv::threshold()
#include
#include
int main() {
cv::Mat src = cv::imread("input.jpg", cv::IMREAD_GRAYSCALE); // 阈值处理通常在灰度图上进行
if (src.empty()) { /* ... error handling ... */ return -1; }
cv::Mat thresh_binary, thresh_otsu;
double thresh_val = 127;
double max_val = 255;
// 基本二值阈值
cv::threshold(src, thresh_binary, thresh_val, max_val, cv::THRESH_BINARY);
// Otsu's 二值化 (自动计算阈值)
// 此时,输入的 thresh_val (这里是0) 被忽略,函数会自动计算最佳阈值
double otsu_thresh = cv::threshold(src, thresh_otsu, 0, max_val, cv::THRESH_BINARY | cv::THRESH_OTSU);
std::cout << "Otsu's calculated threshold: " << otsu_thresh << std::endl;
cv::imshow("Original Grayscale", src);
cv::imshow("Binary Threshold", thresh_binary);
cv::imshow("Otsu's Threshold", thresh_otsu);
cv::waitKey(0);
cv::destroyAllWindows();
return 0;
}
参数解析 (cv::threshold
):
src
: 输入图像,通常是单通道灰度图。dst
: 输出的二值图像。thresh
: 阈值。maxval
: 当像素值超过(或满足某些条件,取决于 type
)阈值时赋予的新值。type
: 阈值类型:
cv::THRESH_BINARY
: 如果 src(x,y) > thresh
,则 dst(x,y) = maxval
,否则 dst(x,y) = 0
。cv::THRESH_BINARY_INV
: cv::THRESH_BINARY
的反转。cv::THRESH_TRUNC
: 如果 src(x,y) > thresh
,则 dst(x,y) = thresh
,否则 dst(x,y) = src(x,y)
。cv::THRESH_TOZERO
: 如果 src(x,y) > thresh
,则 dst(x,y) = src(x,y)
,否则 dst(x,y) = 0
。cv::THRESH_TOZERO_INV
: cv::THRESH_TOZERO
的反转。cv::THRESH_OTSU
: Otsu’s 二值化。与上述类型之一(通常是 cv::THRESH_BINARY
)通过 |
(位或) 组合使用。函数会自动计算一个全局最优阈值。此时 thresh
参数被忽略。cv::THRESH_TRIANGLE
: Triangle 算法,也用于自动阈值确定。注意事项 (threshold
):
cv::THRESH_OTSU
和 cv::THRESH_TRIANGLE
对于双峰直方图的图像效果较好,可以自动找到合适的阈值,但对光照不均的图像可能效果不佳。thresh
值对于简单阈值至关重要,通常需要实验或基于图像直方图分析。对于光照不均的图像,使用局部阈值而非全局阈值。
cv::adaptiveThreshold()
#include
#include
int main() {
cv::Mat src = cv::imread("uneven_lighting.jpg", cv::IMREAD_GRAYSCALE); // 假设光照不均
if (src.empty()) { /* ... error handling ... */ return -1; }
cv::Mat adaptive_thresh_mean, adaptive_thresh_gaussian;
double max_val = 255;
int block_size = 11; // 邻域大小,必须是奇数
double C = 2; // 从均值或加权均值中减去的常数
cv::adaptiveThreshold(src, adaptive_thresh_mean, max_val,
cv::ADAPTIVE_THRESH_MEAN_C, cv::THRESH_BINARY,
block_size, C);
cv::adaptiveThreshold(src, adaptive_thresh_gaussian, max_val,
cv::ADAPTIVE_THRESH_GAUSSIAN_C, cv::THRESH_BINARY,
block_size, C);
cv::imshow("Original Grayscale", src);
cv::imshow("Adaptive Mean Threshold", adaptive_thresh_mean);
cv::imshow("Adaptive Gaussian Threshold", adaptive_thresh_gaussian);
cv::waitKey(0);
cv::destroyAllWindows();
return 0;
}
参数解析 (cv::adaptiveThreshold
):
src
: 输入灰度图像。dst
: 输出二值图像。maxValue
: 赋予超过阈值的像素的新值。adaptiveMethod
: 自适应阈值算法:
cv::ADAPTIVE_THRESH_MEAN_C
: 阈值是邻域的均值减去 C
。cv::ADAPTIVE_THRESH_GAUSSIAN_C
: 阈值是邻域的高斯加权和减去 C
。thresholdType
: 阈值类型,通常是 cv::THRESH_BINARY
或 cv::THRESH_BINARY_INV
。blockSize
: 用于计算阈值的像素邻域大小,必须是奇数。C
: 从均值或加权均值中减去的常数。可以是正数、零或负数。注意事项 (adaptiveThreshold
):
blockSize
和 C
的选择对结果影响很大,需要仔细调整。blockSize
越大,参与计算局部阈值的区域就越大,细节可能会丢失。太小则可能对噪声敏感。cv::ADAPTIVE_THRESH_GAUSSIAN_C
通常比 cv::ADAPTIVE_THRESH_MEAN_C
效果更好,因为它对中心点附近的像素赋予更大权重。将像素值缩放到特定范围,例如 [0, 1] 或 [0, 255]。
cv::normalize()
#include
#include // 包含 normalize
int main() {
// 假设我们有一个CV_32F类型的图像(例如,某些算法的输出)
cv::Mat src = cv::Mat::zeros(100, 100, CV_32FC1);
cv::randu(src, cv::Scalar::all(50), cv::Scalar::all(200)); // 填充50-200之间的随机值
cv::Mat normalized_img;
double alpha = 0; // 归一化后的最小值
double beta = 255; // 归一化后的最大值 (对于显示,通常是255)
// 对于机器学习特征,可能是1.0
// 归一化到 [alpha, beta] 范围
cv::normalize(src, normalized_img, alpha, beta, cv::NORM_MINMAX, CV_8UC1); // 输出为CV_8UC1用于显示
// 如果只是想归一化到 [0,1] 并且保持浮点类型
cv::Mat normalized_float;
cv::normalize(src, normalized_float, 0.0, 1.0, cv::NORM_MINMAX, CV_32FC1);
// 显示需要转换到8位
cv::Mat src_display, norm_display;
if (src.type() != CV_8U) src.convertTo(src_display, CV_8U, 255.0/(200-50), -50.0 * 255.0/(200-50)); // 手动调整范围以便显示
else src.copyTo(src_display);
normalized_img.copyTo(norm_display); // normalized_img 已经是 CV_8UC1
cv::imshow("Original (scaled for display)", src_display);
cv::imshow("Normalized to [0, 255]", norm_display);
cv::waitKey(0);
cv::destroyAllWindows();
return 0;
}
参数解析 (cv::normalize
):
src
: 输入数组。dst
: 输出数组,与 src
大小相同。alpha
: 范围归一化的下界。beta
: 范围归一化的上界 (对于 cv::NORM_MINMAX
) 或范数值 (对于其他类型)。norm_type
: 归一化类型:
cv::NORM_MINMAX
: 将值线性缩放到 [alpha, beta]
范围。cv::NORM_INF
, cv::NORM_L1
, cv::NORM_L2
: 按指定范数进行归一化 (通常 beta
用作目标范数值)。dtype
: 当为负数时,输出数组 dst
的类型与 src
相同;否则,它与 src
的通道数相同,但深度为 dtype
。例如,可以将 CV_32F 归一化为 CV_8U。mask
: 可选的操作掩码。注意事项 (normalize
):
cv::NORM_MINMAX
。dtype
设置为输出特定类型(如 CV_8U
),则会进行类型转换和可能的截断/缩放。基于形状改变图像结构的非线性操作,常用于噪声去除、物体分离/连接、寻找区域等。
cv::getStructuringElement(shape, ksize, anchor)
创建。
shape
: cv::MORPH_RECT
(矩形), cv::MORPH_ELLIPSE
(椭圆), cv::MORPH_CROSS
(十字形)。ksize
: 结构元素的尺寸。anchor
: 锚点位置,默认在中心。cv::erode
): “收缩” 或 “细化” 图像中的亮区(前景)。去除小的噪点。cv::dilate
): “扩张” 或 “加粗” 图像中的亮区。连接断开的部分,填充孔洞。#include
#include
int main() {
cv::Mat src = cv::imread("binary_image.png", cv::IMREAD_GRAYSCALE); // 最好是二值图或灰度图
if (src.empty()) { /* ... error handling ... */ return -1; }
// 确保是二值图像 (例如,经过阈值化处理)
cv::threshold(src, src, 127, 255, cv::THRESH_BINARY);
cv::Mat eroded, dilated;
cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(5, 5));
// int iterations = 1; // 操作迭代次数
cv::erode(src, eroded, kernel);
cv::dilate(src, dilated, kernel);
cv::imshow("Original Binary", src);
cv::imshow("Eroded", eroded);
cv::imshow("Dilated", dilated);
cv::waitKey(0);
cv::destroyAllWindows();
return 0;
}
参数解析 (cv::erode
, cv::dilate
):
src
: 输入图像。dst
: 输出图像。kernel
: 结构元素。如果为 cv::Mat()
,则使用 3x3 矩形核。anchor
: 锚点位置,默认 cv::Point(-1,-1)
表示在核中心。iterations
: 操作迭代次数。次数越多,效果越明显。borderType
, borderValue
: 边界处理。通过组合腐蚀和膨胀实现更复杂的操作。使用 cv::morphologyEx()
。
cv::MORPH_OPEN
): 先腐蚀后膨胀。用于去除小的噪点(暗背景上的亮噪点),平滑物体轮廓,断开细小连接。cv::MORPH_CLOSE
): 先膨胀后腐蚀。用于填充物体内的小孔洞,连接邻近物体,平滑轮廓。#include
#include
int main() {
cv::Mat src = cv::imread("noisy_binary.png", cv::IMREAD_GRAYSCALE);
if (src.empty()) { /* ... error handling ... */ return -1; }
cv::threshold(src, src, 127, 255, cv::THRESH_BINARY);
cv::Mat opened, closed;
cv::Mat kernel = cv::getStructuringElement(cv::MORPH_ELLIPSE, cv::Size(7, 7));
cv::morphologyEx(src, opened, cv::MORPH_OPEN, kernel);
cv::morphologyEx(src, closed, cv::MORPH_CLOSE, kernel);
cv::imshow("Original Binary", src);
cv::imshow("Opened", opened);
cv::imshow("Closed", closed);
cv::waitKey(0);
cv::destroyAllWindows();
return 0;
}
参数解析 (cv::morphologyEx
):
op
: 形态学操作类型:
cv::MORPH_OPEN
, cv::MORPH_CLOSE
cv::MORPH_GRADIENT
: 膨胀图与腐蚀图之差,可得到物体轮廓。cv::MORPH_TOPHAT
: 原图与开运算结果之差。cv::MORPH_BLACKHAT
: 闭运算结果与原图之差。erode
/dilate
类似。注意事项 (形态学操作):
kernel
的形状和大小对结果影响极大。矩形核适用于处理具有水平和垂直边缘的结构;椭圆或圆形核更通用。iterations
参数可以增强效果,但多次小核操作不完全等同于一次大核操作。增强图像对比度,尤其对那些像素值集中在某个狭窄范围内的图像有效。
cv::equalizeHist()
: 全局直方图均衡化。cv::CLAHE
: 对比度受限的自适应直方图均衡化 (Contrast Limited Adaptive Histogram Equalization)。#include
#include
int main() {
cv::Mat src = cv::imread("low_contrast.jpg", cv::IMREAD_GRAYSCALE); // 假设低对比度图
if (src.empty()) { /* ... error handling ... */ return -1; }
cv::Mat equalized_global;
cv::equalizeHist(src, equalized_global);
cv::Mat equalized_clahe;
// 创建 CLAHE 对象
// double clipLimit = 2.0; // 对比度限制阈值
// cv::Size tileGridSize(8, 8); // 将图像划分为的小块数量
cv::Ptr<cv::CLAHE> clahe = cv::createCLAHE();
clahe->setClipLimit(2.0);
clahe->setTileGridSize(cv::Size(8, 8));
clahe->apply(src, equalized_clahe);
cv::imshow("Original Grayscale", src);
cv::imshow("Global Histogram Equalization", equalized_global);
cv::imshow("CLAHE", equalized_clahe);
cv::waitKey(0);
cv::destroyAllWindows();
return 0;
}
参数解析:
cv::equalizeHist(src, dst)
:
src
: 输入8位单通道图像 (灰度图)。dst
: 输出均衡化后的图像。cv::createCLAHE(clipLimit, tileGridSize)
:
clipLimit
: 对比度限制。较高的值允许更大的对比度。如果噪声是问题,可以减小它。默认40.0。tileGridSize
: 将图像分割成的小块(tile)的网格尺寸。例如 cv::Size(8,8)
表示 8x8 的小块。在每个小块内进行直方图均衡化。注意事项:
cv::equalizeHist()
是全局操作,可能会导致某些区域的对比度过度增强,并放大噪声。CLAHE
是局部操作,将图像分成小块,在每个小块内进行直方图均衡化,然后通过双线性插值拼接,通常效果更好,能有效限制对比度放大,减少噪声。CLAHE
的 clipLimit
和 tileGridSize
参数需要根据图像内容调整。图像预处理是一个实验性很强的过程,没有万能的方法。
从这些基础操作开始,你可以构建复杂的预处理流水线来应对各种计算机视觉挑战。祝你在 OpenCV 的学习和应用中取得成功!