03 OpenCV--图像预处理--图像插值方法、边界填充、图像矫正、图像掩膜、图像水印添加、图像噪点消除

文章目录

  • 图像预处理
    • 图像与像素级插值方法
        • [1. 最近邻插值](#1. 最近邻插值)
        • [2. 双线性插值](#2. 双线性插值)
        • [3. 像素区域插值](#3. 像素区域插值)
        • [4. 双三次插值](#4. 双三次插值)
        • [4. Lanczos 插值](#4. Lanczos 插值)
    • 边界填充
        • [1. **复制边缘(BORDER_REPLICATE)**](#1. 复制边缘(BORDER_REPLICATE))
        • [2. **边界反射(BORDER_REFLECT)**](#2. 边界反射(BORDER_REFLECT))
        • [3. **边界反射101(BORDER_REFLECT_101 )**](#3. 边界反射101(BORDER_REFLECT_101 ))
        • [4. **边界常数(BORDER_CONSTANT)**](#4. 边界常数(BORDER_CONSTANT))
        • [5. **边界包裹(BORDER_WRAP)**](#5. 边界包裹(BORDER_WRAP))
    • 图像矫正(透视变换)
    • 图像掩膜
      • [1 制作掩膜](#1 制作掩膜)
      • [2 与运算](#2 与运算)
      • [3 颜色替换](#3 颜色替换)
    • ROI切割
    • 图像添加水印
    • 图像噪点消除
      • [1 均值滤波](#1 均值滤波)
      • [2 方框滤波](#2 方框滤波)
      • [3 高斯滤波](#3 高斯滤波)
      • [4 中值滤波](#4 中值滤波)
      • [5 双边滤波](#5 双边滤波)
      • [6 去噪方法总结](#6 去噪方法总结)

图像预处理

图像与像素级插值方法

在图像处理中,调整图像尺寸(缩放、旋转、扭曲等)时原像素的排列会发生变化,插值算法通过估算新位置的像素值来填充空缺。

1. 最近邻插值
  • 原理:对于目标图像中的每个像素点,直接使用源图像中距离其最近的像素值。

    dst = cv.warpAffine(img,M,(w,h),cv2.INTER_NEAREST)

假设源图像(3×3):

复制代码
[1, 2, 3]
[4, 5, 6]
[7, 8, 9]

放大到4×4目标图像:

  1. 计算映射关系:

    • 宽度比例:3/4 = 0.75
    • 高度比例:3/4 = 0.75
  2. 计算目标图像(0,0)位置:

    • x_src = 0 * 0.75 = 0 → round(0) = 0
    • y_src = 0 * 0.75 = 0 → round(0) = 0
    • 取源图像(0,0)值:1
  3. 计算目标图像(1,0)位置:

    • x_src = 1 * 0.75 = 0.75 → round(0.75) = 1
    • y_src = 0 * 0.75 = 0 → round(0) = 0
    • 取源图像(1,0)值:2
  4. 最终得到放大后的图像:

    复制代码
    [1, 2, 2, 3]
    [4, 5, 5, 6]
    [4, 5, 5, 6]
    [7, 8, 8, 9]
py 复制代码
import cv2 as cv
import numpy as np
#读图
img1 = cv.imread('../images/2.jpg')
w=img1.shape[1]
h=img1.shape[0]
#旋转
#获取旋转矩阵
M = cv.getRotationMatrix2D((w//2,h//2),45,0.5)
#使用仿射变换函数
#最近邻插值
dst = cv.warpAffine(img1,M,(w,h),cv.INTER_NEAREST)

cv.imshow('img1',img1)
cv.imshow('dst',dst)
cv.waitKey(0)
cv.destroyAllWindows()
2. 双线性插值
  • 原理 :在二维空间中,先对目标点((x,y))周围的4 个相邻像素(形成矩形)分别进行水平方向的线性插值,再对结果进行垂直方向的线性插值。 设周围像素为((x_0,y_0),(x_0,y_1),(x_1,y_0),(x_1,y_1)),权重由目标点与像素的距离决定(距离越近权重越大)。

首先要了解线性插值,而双线性插值本质上就是在两个方向上做线性插值。还是给出目标点与原图像中点的计算公式
s r c X = d s t X ∗ s r c W i d t h d s t W i d t h s r c X=d s t X*{\frac{s r c W i d t h}{d s t W i d t h}} srcX=dstX∗dstWidthsrcWidth

s r c Y = d s t Y ∗ s r c H e i g h t d s t H e i g h t s r c Y=d s t Y*{\frac{s r c H e i g h t}{d s t H e i g h t}} srcY=dstY∗dstHeightsrcHeight

比如我们根据上述公式计算出了新图像中的某点所对应的原图像的点P,其周围的点分别为Q12、Q22、Q11、Q21, 要插值的P点不在其周围点的连线上,这时候就需要用到双线性插值了。首先延申P点得到P和Q11、Q21的交点R1与P和Q12、Q22的交点R2,如下图所示:

然后根据Q11、Q21得到R1的插值,根据Q12、Q22得到R2的插值,然后根据R1、R2得到P的插值即可,这就是双线性插值。以下是计算过程:

首先计算R1和R2的插值:
f ( R 1 ) ≈ x 2 − x x 2 − x 1 f ( Q 11 ) + x − x 1 x 2 − x 1 f ( Q 21 ) f(R_{1})\approx\frac{x_{2}-x}{x_{2}-x_{1}}f(Q_{11})+\frac{x-x_{1}}{x_{2}-x_{1}}f(Q_{21}) f(R1)≈x2−x1x2−xf(Q11)+x2−x1x−x1f(Q21)

f ( R 2 ) ≈ x 2 − x x 2 − x 1 f ( Q 12 ) + x − x 1 x 2 − x 1 f ( Q 22 ) f(R_{2})\approx\frac{x_{2}-x}{x_{2}-x_{1}}f(Q_{12})+\frac{x-x_{1}}{x_{2}-x_{1}}f(Q_{22}) f(R2)≈x2−x1x2−xf(Q12)+x2−x1x−x1f(Q22)

然后根据R1和R2计算P的插值:

f ( P ) ≈ y 2 − y y 2 − y 1 f ( R 1 ) + y − y 1 y 2 − y 1 f ( R 2 ) f(P)\approx{\frac{y_{2}-y}{y_{2}-y_{1}}}f(R_{1})+{\frac{y-y_{1}}{y_{2}-y_{1}}}f(R_{2}) f(P)≈y2−y1y2−yf(R1)+y2−y1y−y1f(R2)

这样就得到了P点的插值。注意此处如果先在y方向插值、再在x方向插值,其结果与按照上述顺序双线性插值的结果是一样的。

复制代码
dst = cv.warpAffine(img,M,(w,h),cv2.INTER_LINEAR)
3. 像素区域插值

像素区域插值主要分两种情况,缩小图像和放大图像的工作原理并不相同。

  • 当使用像素区域插值方法进行缩小图像 时,它就会变成一个均值滤波器

  • 当使用像素区域插值方法进行放大图像

    • 如果图像放大的比例是整数倍,那么其工作原理与最近邻插值类似;

    • 如果放大的比例不是整数倍,那么就会调用双线性插值进行放大。

      dst = cv.warpAffine(img,M,(w,h),cv.INTER_AREA)

  • img:输入图像。

  • M:2×3 的仿射变换矩阵(定义旋转、平移、缩放等)。

  • (w, h):输出图像的宽度和高度。

  • flags=cv.INTER_AREA:指定使用区域插值方法。

  • 若变换后的图像尺寸 (w, h) 小于 原始尺寸,cv.INTER_AREA 会通过像素区域重采样生成平滑结果。

  • 若尺寸大于 原始尺寸,cv.INTER_AREA 会退化为最近邻插值(cv.INTER_NEAREST),导致锯齿。

py 复制代码
import cv2 as cv
import numpy as np
#读图
img1 = cv.imread('../images/2.jpg')
w=img1.shape[1]
h=img1.shape[0]
#旋转
#获取旋转矩阵
M = cv.getRotationMatrix2D((w//2,h//2),45,0.5)
#使用仿射变换函数
#像素区域插值
#缩小:均值滤波;放大: 整数:最近邻 非整数:双线性 4点 2X2
dst = cv.warpAffine(img1,M,(w,h),cv.INTER_AREA)

cv.imshow('img1',img1)
cv.imshow('dst',dst)
cv.waitKey(0)
cv.destroyAllWindows()
4. 双三次插值
  • 原理 :基于目标点周围的16 个相邻像素(4×4 网格),通过三次多项式对水平和垂直方向分别插值,最终融合结果。权重函数考虑像素距离的三次关系,对近距离像素赋予更高权重,同时抑制远距离像素的干扰。

假如下图中的P点就是目标图像B在(X,Y)处根据上述公式计算出的对应于原图像A中的位置,P的坐标位置会出现小数部分,所以我们假设P点的坐标为(x+u,y+v),其中x、y表示整数部分,u、v表示小数部分,那么我们就可以得到其周围的最近的16个像素的位置,我们用a(i,j)(i,j=0,1,2,3)来表示,如下图所示。

然后给出BiCubic函数:

  • a 是调整参数(通常取 (a = -0.5) 或 (a = -0.75))
  • d 为目标像素与邻域像素的距离。

我们要做的就是将上面的16个点相较于p点的位置距离算出来,获取16像素所对应的权重 W ( d ) W(d) W(d)。然而BiCubic函数是一维的,所以我们需要将像素点的行与列分开计算,比如a00这个点,我们需要将 d x d_x dx带入BiCubic函数中,计算a00点对于P点的x方向的权重,然后将 d y d_y dy带入BiCubic函数中,计算a00点对于P点的y方向的权重,其他像素点也是这样的计算过程,最终我们就可以得到P所对应的目标图像B在(X,Y)处的像素值为:
B ( X , Y ) = ∑ i = 0 3 ∑ j = 0 3 a i j × W ( i ) × W ( j ) B(X,Y)=\sum_{i=0}^{3}\sum_{j=0}^{3}a_{i j}\times W_{(i)}\times W_{(j)} B(X,Y)=i=0∑3j=0∑3aij×W(i)×W(j)

依此办法我们就可以得到目标图像中所有的像素点的像素值。

刚刚我们说拿到了目标点的坐标为 ( x + u , y + v ) (x+u,y+v) (x+u,y+v) ,其中 x 、 y x、y x、y 表示整数部分,u、v表示小数部分。那么我们取坐标的整数部分作为参考点,也就是 ( x , y ) (x,y) (x,y),小数部分表示目标像素相对于参考点的偏移量。

比如说目标点的坐标为 ( 3.7 , 2.4 ) (3.7,2.4) (3.7,2.4),那么我们的参考点就是 ( 3 , 2 ) (3,2) (3,2)。

目标部分相对于参考点的偏移量:

​ b h = v , b_h=v, bh=v,b_w=u

然后我们才需要来计算相邻像素和目标像素的距离:

最后利用我们的加权求和算出像素值:

公式
B ( X , Y ) = ∑ i = 0 3 ∑ j = 0 3 a i j × W ( i ) × W ( j ) B(X,Y)=\sum_{i=0}^{3}\sum_{j=0}^{3}a_{i j}\times W_{(i)}\times W_{(j)} B(X,Y)=i=0∑3j=0∑3aij×W(i)×W(j)
B ( X , Y ) = ( a 00 ∗ w i 0 ∗ w j 0 ) + ( a 01 ∗ w i 0 ∗ w j 1 ) + ( a 02 ∗ w i 0 ∗ w j 2 ) + ( a 03 ∗ w i 0 ∗ w j 3 ) + ( a 10 ∗ w i 1 ∗ w j 0 ) + ( a 11 ∗ w i 1 ∗ w j 1 ) + ( a 12 ∗ w i 1 ∗ w j 2 ) + ( a 13 ∗ w i 1 ∗ w j 3 ) + ( a 20 ∗ w i 2 ∗ w j 0 ) + ( a 21 ∗ w i 2 ∗ w j 1 ) + ( a 22 ∗ w i 2 ∗ w j 2 ) + ( a 23 ∗ w i 2 ∗ w j 3 ) + ( a 30 ∗ w i 3 ∗ w j 0 ) + ( a 31 ∗ w i 3 ∗ w j 1 ) + ( a 22 ∗ w i 3 ∗ w j 2 ) + ( a 33 ∗ w i 3 ∗ w j 3 ) B(X,Y) =(a_{00}*w_{i0}*w_{j0})+(a_{01}*w_{i0}*w_{j1})+(a_{02}*w_{i0}*w_{j2})+(a_{03}*w_{i0}*w_{j3}) +(a_{10}*w_{i1}*w_{j0})+(a_{11}*w_{i1}*w_{j1})+(a_{12}*w_{i1}*w_{j2})+(a_{13}*w_{i1}*w_{j3}) +(a_{20}*w_{i2}*w_{j0})+(a_{21}*w_{i2}*w_{j1})+(a_{22}*w_{i2}*w_{j2})+(a_{23}*w_{i2}*w_{j3}) +(a_{30}*w_{i3}*w_{j0})+(a_{31}*w_{i3}*w_{j1})+(a_{22}*w_{i3}*w_{j2})+(a_{33}*w_{i3}*w_{j3}) B(X,Y)=(a00∗wi0∗wj0)+(a01∗wi0∗wj1)+(a02∗wi0∗wj2)+(a03∗wi0∗wj3)+(a10∗wi1∗wj0)+(a11∗wi1∗wj1)+(a12∗wi1∗wj2)+(a13∗wi1∗wj3)+(a20∗wi2∗wj0)+(a21∗wi2∗wj1)+(a22∗wi2∗wj2)+(a23∗wi2∗wj3)+(a30∗wi3∗wj0)+(a31∗wi3∗wj1)+(a22∗wi3∗wj2)+(a33∗wi3∗wj3)

复制代码
dst = cv.warpAffine(img,M,(w,h),cv2.INTER_CUBIC)
py 复制代码
import cv2 as cv
import numpy as np
#读图
img1 = cv.imread('../images/2.jpg')
w=img1.shape[1]
h=img1.shape[0]
#旋转
#获取旋转矩阵
M = cv.getRotationMatrix2D((w//2,h//2),45,0.5)
#使用仿射变换函数
#双三次 16点 4X4
dst = cv.warpAffine(img1,M,(w,h),cv.INTER_CUBIC)

cv.imshow('img1',img1)
cv.imshow('dst',dst)
cv.waitKey(0)
cv.destroyAllWindows()
4. Lanczos 插值
  • 原理 :基于 Lanczos 窗函数(一种 sinc 函数的截断形式),使用目标点周围更多像素(通常 9×9 或更大网格)进行加权插值,权重随距离衰减并抑制高频噪声。核心是通过窗函数平衡插值平滑性与细节保留。

目标像素点与原图像的像素点的对应公式如下所示:
s r c X = d s t X ∗ s r c W i d t h d s t W i d t h s r c X=d s t X*{\frac{s r c W i d t h}{d s t W i d t h}} srcX=dstX∗dstWidthsrcWidth

s r c Y = d s t Y ∗ s r c H e i g h t d s t H e i g h t s r c Y=d s t Y*{\frac{s r c H e i g h t}{d s t H e i g h t}} srcY=dstY∗dstHeightsrcHeight

下面我们举例说明,假设原图像A大小为m*n,缩放后的目标图像B的大小为M*N。其中A的每一个像素点是已知的,B是未知的,我们想要求出目标图像B中每一个像素点(X,Y)的值,必须先找出像素(X,Y)在原图像A中对应的像素(x,y),再根据原图像A距离像素(x,y)最近的64个像素点作为计算目标图像B(X,Y)处像素值的参数,利用权重函数求出64个像素点的权重,图B像素(x,y)的值就等于64个像素点的加权叠加。

假如下图中的P点就是目标图像B在(X,Y)处根据上述公式计算出的对应于原图像A中的位置,P的坐标位置会出现小数部分,所以我们假设P点的坐标为(x+u,y+v),其中x、y表示整数部分,u、v表示小数部分,那么我们就可以得到其周围的最近的64个像素的位置,我们用a(i,j)(i,j=0,1,2,3,4,5,6,7)来表示,如下图所示。

然后给出权重公式:

复制代码
a通常取2或者3,当a=2时,该算法适用于图像缩小。a=3时,该算法适用于图像放大。

与双三次插值一样,这里也需要将像素点分行和列分别带入计算权重值,其他像素点也是这样的计算过程,最终我们就可以得到P所对应的目标图像B在(X,Y)处的像素值为:
S ( x , y ) = ∑ i = [ x ] − a + 1 [ x ] + a ∑ j = [ y ] − a + 1 [ y ] + a s i j L ( x − i ) L ( y − j ) S(x,y)=\sum_{i=[x]-a+1}^{[x]+a}\sum_{j=[y]-a+1}^{[y]+a}s_{i j}L(x-i)L(y-j) S(x,y)=i=[x]−a+1∑[x]+aj=[y]−a+1∑[y]+asijL(x−i)L(y−j)

其中 [ x ] [x] [x]、 [ y ] [y] [y]表示对坐标值向下取整,通过该方法就可以计算出新的图像中所有的像素点的像素值。

复制代码
dst = cv.warpAffine(img,M,(w,h),cv2.INTER_LANCZOS4)
py 复制代码
import cv2 as cv
import numpy as np
#读图
img1 = cv.imread('../images/2.jpg')
w=img1.shape[1]
h=img1.shape[0]
#旋转
#获取旋转矩阵
M = cv.getRotationMatrix2D((w//2,h//2),45,0.5)
#使用仿射变换函数
#LANCZOS4插值 64点 8X8
dst = cv.warpAffine(img1,M,(w,h),cv.INTER_LANCZOS4)

cv.imshow('img1',img1)
cv.imshow('dst',dst)
cv.waitKey(0)
cv.destroyAllWindows()

边界填充

1. 复制边缘(BORDER_REPLICATE)

复制最边缘的像素值到边界:

python 复制代码
bordered = cv2.warpAffine(img,M,(w,h),cv.INTER_LANCZOS4,borderMode=cv2.BORDER_REPLICATE)
  • 特点:边界与原图边缘连续,无明显过渡。
  • 应用:平滑过渡场景(如高斯模糊、均值滤波)。
py 复制代码
import cv2 as cv
import numpy as np
#读图
img1 = cv.imread('../images/2.jpg')
w=img1.shape[1]
h=img1.shape[0]

#获取旋转矩阵
M = cv.getRotationMatrix2D((w//2,h//2),45,0.5)
#使用仿射变换函数
#最近邻插值 + 边界复制
dst = cv.warpAffine(img1,M,(w,h),cv.INTER_NEAREST,borderMode=cv.BORDER_REPLICATE)

cv.imshow('img1',img1)
cv.imshow('REPLICATE',dst)
cv.waitKey(0)
cv.destroyAllWindows()
2. 边界反射(BORDER_REFLECT)

以边缘为轴反射填充(不包含边缘像素本身):

python 复制代码
bordered = cv2.warpAffine(img,M,(w,h),cv.INTER_LANCZOS4,borderMode=cv.BORDER_REFLECT)
  • 示例 :原图像为 abcdef → 填充后为 fedcba|abcdef|fedcba
  • 特点:保持边界附近的局部对称性,自然过渡。
py 复制代码
import cv2 as cv
import numpy as np
#读图
img1 = cv.imread('../images/2.jpg')
w=img1.shape[1]
h=img1.shape[0]
#旋转
#获取旋转矩阵
M = cv.getRotationMatrix2D((w//2,h//2),45,0.5)
#边界反射
dst = cv.warpAffine(img1,M,(w,h),cv.INTER_LANCZOS4,borderMode=cv.BORDER_REFLECT)

cv.imshow('img1',img1)
cv.imshow('dst',dst)
cv.waitKey(0)
cv.destroyAllWindows()
3. 边界反射101(BORDER_REFLECT_101 )

以边缘为轴反射填充(包含边缘像素本身):

python 复制代码
bordered = cv2.warpAffine(img,M,(w,h),cv.INTER_LANCZOS4,borderMode=cv2.BORDER_REFLECT_101)
  • 示例 :原图像为 abcdef → 填充后为 edcbab|abcdef|fedcba
  • 特点 :比 BORDER_REFLECT 更自然,避免重复边缘像素。
py 复制代码
import cv2 as cv
import numpy as np
#读图
img1 = cv.imread('../images/2.jpg')
w=img1.shape[1]
h=img1.shape[0]
#旋转
#获取旋转矩阵
M = cv.getRotationMatrix2D((w//2,h//2),45,0.5)
#边界反射101
dst = cv.warpAffine(img1,M,(w,h),cv.INTER_LANCZOS4,cv.BORDER_REPLICATE,borderMode=cv.BORDER_REFLECT_101)

cv.imshow('img1',img1)
cv.imshow('dst',dst)
cv.waitKey(0)
cv.destroyAllWindows()
4. 边界常数(BORDER_CONSTANT)

用指定的常数值填充边界:

python 复制代码
bordered = cv2.warpAffine(img,M,(w,h),cv.INTER_LANCZOS4,borderMode=cv2.BORDER_CONSTANT, value=0)  # 填充黑色
  • 特点:边界为纯色,可能导致明显的边缘痕迹。
  • 应用:简单快速,适合对边界不敏感的场景。
py 复制代码
import cv2 as cv
import numpy as np
#读图
img1 = cv.imread('../images/2.jpg')
w=img1.shape[1]
h=img1.shape[0]
#旋转
#获取旋转矩阵
M = cv.getRotationMatrix2D((w//2,h//2),45,0.5)
#边界常数
dst4 = cv.warpAffine(img1,M,(w,h),cv.INTER_CUBIC,borderMode=cv.BORDER_CONSTANT,borderValue=(255,0,0))
cv.imshow('img1',img1)
cv.imshow('dst',dst4)
cv.waitKey(0)
cv.destroyAllWindows()
5. 边界包裹(BORDER_WRAP)

将对侧的像素值直接复制到边界:

python 复制代码
bordered = cv2.warpAffine(img,M,(w,h),cv.INTER_LANCZOS4,borderMode=cv2.BORDER_WRAP)
  • 示例 :原图像为 abcdef → 填充后为 defabc|abcdef|abcdef
  • 特点:适用于周期性模式(如纹理合成)。
py 复制代码
import cv2 as cv
import numpy as np
#读图
img1 = cv.imread('../images/2.jpg')
w=img1.shape[1]
h=img1.shape[0]
#旋转
#获取旋转矩阵
M = cv.getRotationMatrix2D((w//2,h//2),45,0.5)
#边界包裹
dst = cv.warpAffine(img1,M,(w,h),cv.INTER_LANCZOS4,cv.BORDER_REPLICATE,borderMode=cv.BORDER_WRAP,borderValue=(255,255,255))


cv.imshow('img1',img1)
cv.imshow('dst',dst)
cv.waitKey(0)
cv.destroyAllWindows()

图像矫正(透视变换)

透视变换是一种将图像从 "一个透视视角" 映射到 "另一个透视视角" 的几何变换,它将三维空间中的物体投影到二维平面上的过程,这个过程会导致物体在图像中出现形变和透视畸变。。

复制代码
M=getPerspectiveTransform(pt1,pt2)

在该函数中,需要提供两个参数:

  • pt1:原图像上需要进行透视变化的四个点的坐标,这四个点用于定义一个原图中的四边形区域。

  • pt2:透视变换后,src的四个点在新目标图像的四个新坐标。

该函数会返回一个透视变换矩阵,得到透视变化矩阵之后,使用warpPerspective()函数即可进行透视变化计算,并得到新的图像。该函数需要提供如下参数:

复制代码
cv2.warpPerspective(src, M, dsize, flags, borderMode)
  • src:输入图像。

  • M:透视变换矩阵。这个矩阵可以通过getPerspectiveTransform函数计算得到。

  • dsize:输出图像的大小。它可以是一个Size对象,也可以是一个二元组。

  • flags:插值方法的标记。

  • borderMode:边界填充的模式。

py 复制代码
import cv2 as cv
import numpy as np
#读图
img = cv.imread('../images/image.png')
shape = img.shape
#获取透视变换矩阵
#四点:左上 右上 左下 右下
pt1 = np.float32([[460,298], [1142,342], [115,596], [908,703]])
pt2 = np.float32([[0,0],[shape[1],0],[0,shape[0]],[shape[1],shape[0]]])
M = cv.getPerspectiveTransform(pt1, pt2)
#透视变换
dst = cv.warpPerspective(img,M,(shape[1],shape[0]),cv.INTER_LINEAR,borderMode=cv.BORDER_REFLECT)
cv.imshow('img',img)
cv.imshow('dst',dst)
cv.waitKey(0)
cv.destroyAllWindows()


图像掩膜

图像掩膜是一个与原图像尺寸相同的单通道图像(通常为 8 位灰度图),其像素值表示对应位置的原图像像素是否参与后续处理:

  • 值为 0:表示该区域被 "遮挡",处理时会被忽略。
  • 值为非零(通常为 255):表示该区域被 "保留",处理时会被应用相应操作。

核心作用

  • 选择性处理图像区域(如只对特定区域进行滤波、颜色调整)。
  • 图像分割与提取(如从背景中分离前景物体)。
  • 图像合成与融合(如将不同图像的部分区域组合)。

1 制作掩膜

通过颜色阈值分割生成掩膜,适用于颜色差异明显的场景(如绿色背景抠图)。
原理

将图像转换到 HSV 等颜色空间,通过 cv2.inRange() 函数定义颜色范围,生成二值掩膜。

  • HSV 空间中,H(色相)表示颜色种类,S(饱和度)表示颜色纯度,V(明度)表示亮度。

  • 颜色范围可通过 lowerupper 数组指定

    python 复制代码
    lower_blue = np.array([100, 150, 0])  # HSV中的蓝色下界
    upper_blue = np.array([140, 255, 255])  # HSV中的蓝色上界
    mask = cv2.inRange(hsv_image, lower_blue, upper_blue)
py 复制代码
import cv2 as cv
import numpy as np
demo = cv.imread('../images/demo.png')
demo = cv.resize(demo, (640, 640))
#转为hsv颜色空间
hsv = cv.cvtColor(demo,cv.COLOR_BGR2HSV)

# 定义颜色空间-->蓝
low = np.array([100,43,46])
high = np.array([124,255,255])

#创建掩膜 cv.inRange(src, low, high, dst) 传的是hsv颜色空间下的图
mask = cv.inRange(hsv,low,high)

cv.imshow('demo',demo)
cv.imshow('mask',mask)
cv.waitKey(0)
cv.destroyAllWindows()

2 与运算

位与运算遵循以下真值表:

A B A AND B
0 0 0
0 1 0
1 0 0
1 1 1

在图像处理中:

  • 掩膜中的 0(黑色)表示"忽略"
  • 掩膜中的 255(白色)表示"保留"
  • 原图像像素与掩膜进行 AND 运算
复制代码
原始像素 (B,G,R): [120, 100, 80]   [200, 150, 100]
掩膜值:           [0]              [255]
AND结果:          [0, 0, 0]        [200, 150, 100]

通过掩膜与原图的与运算,我们就可以提取出图像中被掩膜覆盖的区域

复制代码
cv2.bitwise_and(src1,src2[,mask])
  • src1:第一个输入图像(通常为原始图像)
  • src2:第二个输入图像(可以是相同图像或纯色图像)
  • mask:(可选)操作掩膜,指定操作生效的区域
  • 返回值 :输出数组,应用掩膜后的图像,与输入数组大小和类型相同
python 复制代码
import cv2 as cv
import numpy as np
demo = cv.imread('../images/demo.png')
demo = cv.resize(demo, (640, 640))
#转为hsv颜色空间
hsv = cv.cvtColor(demo,cv.COLOR_BGR2HSV)

# 定义颜色空间-->蓝
low = np.array([100,43,46])
high = np.array([124,255,255])

#创建掩膜 cv.inRange(src, low, high, dst) 传的是hsv颜色空间下的图
mask = cv.inRange(hsv,low,high)

#颜色提取 cv.bitwise_and(src1, src2, dst[, mask])
#传的是原图
dst = cv.bitwise_and(demo,demo,mask=mask)
cv.imshow('dst',dst)

cv.imshow('demo',demo)
cv.imshow('mask',mask)
cv.waitKey(0)
cv.destroyAllWindows()

3 颜色替换

当执行mask == 255时,NumPy 会对掩膜中的每个像素进行比较,生成一个与掩膜尺寸相同的布尔数组

  • True:对应位置的像素值等于 255(即掩膜中的白色区域)。
  • False:对应位置的像素值不等于 255(即掩膜中的黑色区域)

通过布尔数组,可以直接索引并修改原图像中对应位置的像素值。这种操作是逐元素并行的,比使用循环效率高得多。

py 复制代码
import cv2 as cv
import numpy as np
demo = cv.imread('../images/demo.png')
demo = cv.resize(demo, (640, 640))
#转为hsv颜色空间
hsv = cv.cvtColor(demo,cv.COLOR_BGR2HSV)
# 定义颜色空间-->蓝
low = np.array([100,43,46])
high = np.array([124,255,255])

#创建掩膜 cv.inRange(src, low, high, dst) 传的是hsv颜色空间下的图
mask = cv.inRange(hsv,low,high)
arr = mask==255
#改为绿色
demo[arr]=(0,255,0)

cv.imshow('demo_replace',demo)
cv.waitKey(0)
cv.destroyAllWindows()

ROI切割

我们在使用OpenCV进行读取图像时,图像数据会被存储为Numpy数组,这也意味着我们可以使用Numpy数组的一些操作来对图像数据进行处理,比如切片。

注意:在OpenCV中,坐标的x轴的正方向是水平向右,y轴的正方向是垂直向下,与数学上的二维坐标并不相同。

因此,我们可以通过指定切片的范围来选择特定的高度和宽度区域来完成ROI切割的操作。

python 复制代码
import cv2 as cv
#读图
bg = cv.imread('../images/image.png')

roi = bg[20:400,:550]
cv.imshow('bg',bg)
cv.imshow('roi',roi)

cv.waitKey(0)
cv.destroyAllWindows()

图像添加水印

本实验中添加水印的概念其实可以理解为将一张图片中的某个物体或者图案提取出来,然后叠加到另一张图片上。

1.通过将原始水印图片转换成灰度图,并进行二值化处理,去除背景部分,得到一个类似掩膜的图像。

2.将这个二值化图像与另一张图片中要添加水印的区域进行"与"运算,使得目标物体的形状出现在要添加水印的区域。

3.将得到的目标物体图像与要添加水印的区域进行相加,就完成了添加水印的操作。

模板输入

对于读入的水印图片,先进行灰度化操作和二值化,得到掩膜

py 复制代码
import cv2 as cv
#读图
bg = cv.imread('../images/image.png')
logo = cv.imread('../images/logohq.png')
h,w = logo.shape[0:2]

#调整水印大小
logo = cv.resize(logo,(int(w*3),int(h*3)))
roi = bg[:int(h*3),:int(w*3)]
# 转灰度
gray = cv.cvtColor(logo,cv.COLOR_BGR2GRAY)

# 二值化
# 黑logo白底 与 背景  
# 阈值法:小于等于阈值的像素就被设置为0大于阈值的像素就被设置为maxval(
_,mask1 = cv.threshold(gray,200,255,cv.THRESH_BINARY)

# 白logo黑底 与 logo
_,mask2 = cv.threshold(gray,200,255,cv.THRESH_BINARY_INV)

与运算

得到水印的掩膜后,对其进行

有了模板的掩膜之后(也就是二值化图),我们就可以在要添加水印的图像中,根据掩膜的大小切割出一个ROI区域,也就是我们要添加水印的区域,之后让其与模板的掩膜进行与运算,由于模板的掩膜中目标物体的像素值为黑色,所以经过与运算后,就会在ROI区域中得到模板图的形状。

py 复制代码
_,mask1 = cv.threshold(gray,200,255,cv.THRESH_BINARY)
bg1 = cv.bitwise_and(roi,roi,mask=mask1)

# 白logo黑底 与 logo
_,mask2 = cv.threshold(gray,200,255,cv.THRESH_BINARY_INV)
logo1 = cv.bitwise_and(logo,logo,mask=mask2)

图像融合(图像位与操作)

将模板的形状添加到水印区域之后,就可以将该图像与原始的模板图进行图像融合。该组件的目的就是将图像对应的数组中的对应元素进行相加(一定要注意这里的两个数组是规格相同的,也就是说要么都是灰度图,要么都是彩图),其过程如下图所示。


因此就可以让原始的模板图与添加模板图形状的ROI区域进行图像融合,得到添加水印的ROI区域。

python 复制代码
# 融合 
roi[:] = cv.add(bg1,logo1)

完整代码如下:

python 复制代码
import cv2 as cv
#读图
bg = cv.imread('../images/image.png')
logo = cv.imread('../images/logohq.png')
h,w = logo.shape[0:2]

#调整水印大小
logo = cv.resize(logo,(int(w*3),int(h*3)))
roi = bg[:int(h*3),:int(w*3)]
# 转灰度
gray = cv.cvtColor(logo,cv.COLOR_BGR2GRAY)

# 二值化
# 黑logo白底 与 背景  
# 阈值法:小于等于阈值的像素就被设置为0大于阈值的像素就被设置为maxval(
_,mask1 = cv.threshold(gray,200,255,cv.THRESH_BINARY)
bg1 = cv.bitwise_and(roi,roi,mask=mask1)

# 白logo黑底 与 logo
_,mask2 = cv.threshold(gray,200,255,cv.THRESH_BINARY_INV)
logo1 = cv.bitwise_and(logo,logo,mask=mask2)

# 融合 
roi[:] = cv.add(bg1,logo1)
cv.imshow('bg',bg)

cv.waitKey(0)
cv.destroyAllWindows()

图像噪点消除

图像噪点(Noise)是指图像中出现的随机、不规则的像素值波动,会降低图像质量,影响后续分析(如目标检测、识别)

常见的有:

高斯噪声(Gaussian Noise)

  • 特征:像素值服从高斯分布(正态分布),均匀分布于整幅图像。
  • 成因:传感器热噪声、电路噪声。
  • 表现:图像整体蒙上一层 "雾" 状颗粒,尤其在暗部区域。

椒盐噪声(Salt-and-Pepper Noise)

  • 特征:随机出现的黑白像素点("盐"= 白色,"椒"= 黑色)。
  • 成因:图像传输错误、传感器故障。
  • 表现:图像中零星分布的黑白噪点,类似撒了盐和胡椒。

滤波器:也可以叫做卷积核,与自适应二值化中的核一样,本身是一个小的区域,有着特定的核值,并且工作原理也是在原图上进行滑动并计算中心像素点的像素值。

  1. 线性滤波器
  • 原理:输出像素值是输入像素值的加权和,权重由卷积核(Kernel)决定。
  • 特点:满足叠加原理(即多个信号的响应等于各自响应的叠加)。
  • 常见类型:均值滤波、高斯滤波

​ 2. 非线性滤波器

  • 原理:输出像素值取决于邻域像素值的排序或统计特性,而非简单加权。
  • 特点:对特定噪声(如椒盐噪声)效果更佳,可能破坏线性特性。
  • 常见类型:中值滤波、最大值 / 最小值滤波、双边滤波。

1 均值滤波

均值滤波是一种最简单的滤波处理,它取的是卷积核区域内元素的均值,如3×3的卷积核:
k e r n e l = 1 9 [ 1 1 1 1 1 1 1 1 1 ] k e r n e l={\frac{1}{9}}{\Bigg[}\begin{array}{l l l}{1}&{1}&{1}\\{1}&{1}&{1}\\{1}&{1}&{1}\end{array}{\Bigg]} kernel=91[111111111]

复制代码
cv2.blur(src, ksize)
  • ksize,代表卷积核的大小
  • 默认borderType=BORDER_REFLECT_101

比如有一张4*4的图片,现在使用一个3*3的卷积核进行均值滤波时,其过程如下所示:

对于边界的像素点,则会进行边界填充,以确保卷积核的中心能够对准边界的像素点进行滤波操作。在OpenCV中,默认的是使用BORDER_REFLECT_101的方式进行填充。通过卷积核在原图上从左上角滑动计算到右下角,从而得到新的4*4的图像的像素值。

python 复制代码
import cv2
blurred = cv2.blur(image, (3, 3))  # 3x3 均值滤波

2 方框滤波

方框滤波跟均值滤波很像,如3×3的滤波核如下:
k e r n e l = a [ 1 1 1 1 1 1 1 1 1 ] k e r n e l={a}{\Bigg[}\begin{array}{l l l}{1}&{1}&{1}\\{1}&{1}&{1}\\{1}&{1}&{1}\end{array}{\Bigg]} kernel=a[111111111]

复制代码
dst = cv2.boxFilter(src, ddepth, ksize, anchor, normalize, borderType)
  • ksize:代表卷积核的大小,eg:ksize=3,则代表使用3×3的卷积核。

  • ddepth:输出图像的深度,-1代表使用原图像的深度。

  • normalize:

    • 当normalize为True的时候,方框滤波就是均值滤波,上式中的a就等于1/9;
    • normalize为False的时候,a=1,相当于求区域内的像素和。

其滤波的过程与均值滤波一模一样,都采用卷积核从图像左上角开始,逐个计算对应位置的像素值,并从左至右、从上至下滑动卷积核,直至到达图像右下角,唯一的区别就是核值可能会不同。

python 复制代码
import cv2
normalized = cv2.boxFilter(image, -1, (3, 3), normalize=True)

3 高斯滤波

用高斯核(正态分布权重)对邻域像素加权平均。

中间像素点权重最高,越远离中心的像素权重越小。以核的中心位置为坐标原点,然后计算周围点的坐标,然后带入下面的高斯公式中。
g ( x , y ) = 1 2 π σ 2 e − ( x 2 + y 2 ) 2 σ 2 g(x,y)=\frac{1}{2\pi\sigma^{2}}e^{-\frac{(x^{2}+y^{2})}{2\sigma^{2}}} g(x,y)=2πσ21e−2σ2(x2+y2)

其中,x和 y 是相对于中心点的坐标偏移量,σ 是标准差,控制着高斯函数的宽度和高度。较大的 σ 值会导致更广泛的平滑效果。

卷积核通常是一个方形矩阵,其元素值根据高斯函数计算得出,并且这些值加起来等于1,近似于正态分布,以确保输出图像的亮度保持不变。

其中的值也是与自适应二值化里的一样,当时会取固定的系数,当kernel大于7并且没有设置时,会使用固定的公式进行计算 σ \sigma σ的值:
σ = 0.3 ∗ ( ( k s i z e − 1 ) ∗ 0.5 − 1 ) + 0.8 \sigma=0.3*\left((k s i z e-1)*0.5-1\right)+0.8 σ=0.3∗((ksize−1)∗0.5−1)+0.8

我们还是以3*3的卷积核为例,其核值如下所示:
k e r n e l = [ 0.0625 0.125 0.0625 0.125 0.25 0.125 0.0625 0.125 0.0625 ] = [ 1 16 1 8 1 16 1 8 1 4 1 8 1 16 1 8 1 16 ] \ k e r n e l=\left[\begin{array}{c}{{0.0625~~~~0.125~~~~0.0625}}\\{{0.125~~~~0.25~~~~0.125}}\\{{0.0625~~~~0.125~~~~0.0625}} \end{array}\right]=\left[\begin{array}{c c c}{\frac{1}{16}~~~\frac{1}{8}~~~\frac{1}{16}}\\{\frac{1}{8}~~~\frac{1}{4}~~~\frac{1}{8}}\\{\frac{1}{16}~~~\frac{1}{8}~~~\frac{1}{16}}\end{array}\right] kernel= 0.0625 0.125 0.06250.125 0.25 0.1250.0625 0.125 0.0625 = 161 81 16181 41 81161 81 161

得到了卷积核的核值之后,其滤波过程与上面两种滤波方式的滤波过程一样,都是用卷积核从图像左上角开始,逐个计算对应位置的像素值,并从左至右、从上至下滑动卷积核,直至到达图像右下角,唯一的区别就是核值不同。

复制代码
dst = cv2.GaussianBlur(src, ksize, sigmaX)
  • ksize:代表卷积核的大小。
  • sigmaX:就是高斯函数里的值,σx值越大,模糊效果越明显。

高斯滤波相比均值滤波效率要慢,但可以有效消除高斯噪声,能保留更多的图像细节,所以经常被称为最有用的滤波器。

均值滤波与高斯滤波的对比结果如下(均值滤波丢失的细节更多):

4 中值滤波

中值又叫中位数,是所有数排序后取中间的值。中值滤波从左上角开始,将卷积核区域内的像素值进行排序,并选取中值作为卷积核的中点的像素值,其过程如下所示:

中值滤波就是用区域内的中值来代替本像素值,所以那种孤立的斑点,如0或255很容易消除掉,适用于去除椒盐噪声斑点噪声

复制代码
cv2.medianBlur(src, ksize)
  • ksize 为奇数(如 3、5)

    dst4 = cv.medianBlur(img2,3)

比如下面这张斑点噪声图,用中值滤波显然更好:

5 双边滤波

同时考虑空间距离和像素值相似度的加权平均,且保留了更多边缘信息

双边滤波的基本思路是同时考虑将要被滤波的像素点的空域信息(周围像素点的位置的权重)和值域信息(周围像素点的像素值的权重)。

双边滤波采用了两个高斯滤波的结合,一个负责计算空间邻近度的权值(也就是空域信息),也就是上面的高斯滤波器,另一个负责计算像素值相似度的权值(也就是值域信息)。

复制代码
cv2.bilateralFilter(src, d, sigmaColor, sigmaSpace)
  • d:过滤时周围每个像素领域的直径,这里已经设置了核大小。d=9===>9x9

    • 过大的滤波器(d>5)执行效率低。

      对于实时应用,建议取d=5;

      对于需要过滤严重噪声的离线应用,可取d=9;

  • sigmaColor:在color space(色彩空间)中过滤sigma。参数越大,那些颜色足够相近的的颜色的影响越大。较大的sigmaColor值意味着更大的颜色差异将被允许参与到加权平均中,从而使得颜色相近但不完全相同的像素也能够相互影响。

  • sigmaSpace:在coordinate space(坐标空间)中过滤sigma。这个参数是坐标空间中的标准差,决定了像素位置对滤波结果的影响程度。它控制着滤波器作用的范围大小。

关于2个sigma参数:

  • 简单起见,可以令2个sigma的值相等;
  • 如果他们很小(小于10),那么滤波器几乎没有什么效果;
  • 如果他们很大(大于150),那么滤波器的效果会很强,使图像显得非常卡通化。

6 去噪方法总结

在不知道用什么滤波器好的时候,优先高斯滤波,然后均值滤波。

斑点和椒盐噪声优先使用中值滤波。

要去除噪点的同时尽可能保留更多的边缘信息,使用双边滤波。

线性滤波方式:均值滤波、方框滤波、高斯滤波(速度相对快)

非线性滤波方式:中值滤波、双边滤波(速度相对慢)。