Skip to content

制作你自己的线性滤波器

在本教程中,你将学到:

  • 如何使用 OpenCV 函数 filter2D() 来创建你自己的线性滤波器。

从非常一般的意义上讲,相关是图像的每一个部分与一个算子(核,kernel)之间的运算。

filter_2d_tutorial_kernel_theory.png

核本质上是一个固定大小的数值系数数组,数组中还有一个锚点(anchor point),通常位于中心。

假设你想知道图像中某个特定位置的结果值。相关值的计算方式如下:

  1. 把核的锚点放在某个确定的像素之上,核的其余部分覆盖图像中对应的局部像素。
  2. 把核的系数与对应的图像像素值相乘,再求和。
  3. 把结果放到输入图像中锚点所在的位置。
  4. 让核在整个图像上扫描,对所有像素重复该过程。

把上面的过程用方程的形式表达出来,就是:

H(x,y)=∑i=0Mi−1∑j=0Mj−1I(x+i−ai,y+j−aj)K(i,j)H(x,y) = \sum_{i=0}^{M_{i} - 1} \sum_{j=0}^{M_{j}-1} I(x+i - a_{i}, y + j - a_{j})K(i,j)

幸运的是,OpenCV 提供了 filter2D() 函数,你不必自己把这些运算都写出来。

  • 加载一幅图像
  • 执行一个归一化盒式滤波器。例如,对于大小为 size=3size = 3 的核,该核为:
1 & 1 & 1 \\ ```cpp 1 & 1 & 1 \\ 1 & 1 & 1 ``` \end{bmatrix}$$ 程序将分别使用大小为 3、5、7、9 和 11 的核执行滤波操作。 - 每个核的滤波输出将显示 500 毫秒。 ## 代码 本教程的代码如下所示。 **C++:** 你也可以从 [这里](https://raw.githubusercontent.com/opencv/opencv/4.x/samples/cpp/tutorial_code/ImgTrans/filter2D_demo.cpp) 下载。 ```cpp /** * @file filter2D_demo.cpp * @brief Sample code that shows how to implement your own linear filters by using filter2D function * @author OpenCV team */ # include "opencv2/imgproc.hpp" # include "opencv2/imgcodecs.hpp" # include "opencv2/highgui.hpp" using namespace cv; /** * @function main */ int main ( int argc, char** argv ) { // Declare variables Mat src, dst; Mat kernel; Point anchor; double delta; int ddepth; int kernel_size; const char* window_name = "filter2D Demo"; const char* imageName = argc >=2 ? argv[1] : "lena.jpg"; // Loads an image src = imread( samples::findFile( imageName ), IMREAD_COLOR ); // Load an image if( src.empty() ) { printf(" Error opening image\n"); printf(" Program Arguments: [image_name -- default lena.jpg] \n"); return EXIT_FAILURE; } // Initialize arguments for the filter anchor = Point( -1, -1 ); delta = 0; ddepth = -1; // Loop - Will filter the image with different kernel sizes each 0.5 seconds int ind = 0; for(;;) { // Update kernel size for a normalized box filter kernel_size = 3 + 2*( ind%5 ); kernel = Mat::ones( kernel_size, kernel_size, CV_32F )/ (float)(kernel_size*kernel_size); // Apply filter filter2D(src, dst, ddepth , kernel, anchor, delta, BORDER_DEFAULT ); imshow( window_name, dst ); char c = (char)waitKey(500); // Press 'ESC' to exit the program if( c == 27 ) { break; } ind++; ``` } return EXIT_SUCCESS; } ``` **Java:** 你也可以从 [这里](https://raw.githubusercontent.com/opencv/opencv/4.x/samples/java/tutorial_code/ImgTrans/Filter2D/Filter2D_Demo.java) 下载。 /** * @file Filter2D_demo.java * @brief Sample code that shows how to implement your own linear filters by using filter2D function */ import org.opencv.core.*; import org.opencv.core.Point; import org.opencv.highgui.HighGui; import org.opencv.imgcodecs.Imgcodecs; import org.opencv.imgproc.Imgproc; class Filter2D_DemoRun { public void run(String[] args) { // Declare variables Mat src, dst = new Mat(); Mat kernel = new Mat(); Point anchor; double delta; int ddepth; int kernel_size; String window_name = "filter2D Demo"; String imageName = ((args.length > 0) ? args[0] : "../data/lena.jpg"); // Load an image src = Imgcodecs.imread(imageName, Imgcodecs.IMREAD_COLOR); // Check if image is loaded fine if( src.empty() ) { System.out.println("Error opening image!"); System.out.println("Program Arguments: [image_name -- default ../data/lena.jpg] \n"); System.exit(-1); } ``` // Initialize arguments for the filter anchor = new Point( -1, -1); delta = 0.0; ddepth = -1; // Loop - Will filter the image with different kernel sizes each 0.5 seconds int ind = 0; while( true ) { // Update kernel size for a normalized box filter kernel_size = 3 + 2*( ind%5 ); Mat ones = Mat.ones( kernel_size, kernel_size, CvType.CV_32F ); Core.multiply(ones, new Scalar(1/(double)(kernel_size*kernel_size)), kernel); ```cpp // Apply filter Imgproc.filter2D(src, dst, ddepth , kernel, anchor, delta, Core.BORDER_DEFAULT ); HighGui.imshow( window_name, dst ); int c = HighGui.waitKey(500); // Press 'ESC' to exit the program if( c == 27 ) { break; } ``` ind++; } System.exit(0); ``` } } public class Filter2D_Demo { public static void main(String[] args) { // Load the native library. System.loadLibrary(Core.NATIVE_LIBRARY_NAME); new Filter2D_DemoRun().run(args); ``` } } ``` **Python:** 你也可以从 [这里](https://raw.githubusercontent.com/opencv/opencv/4.x/samples/python/tutorial_code/ImgTrans/Filter2D/filter2D.py) 下载。 """ @file filter2D.py @brief Sample code that shows how to implement your own linear filters by using filter2D function """ import sys import cv2 as cv import numpy as np def main(argv): window_name = 'filter2D Demo' imageName = argv[0] if len(argv) > 0 else 'lena.jpg' # Loads an image src = cv.imread(cv.samples.findFile(imageName), cv.IMREAD_COLOR) # Check if image is loaded fine if src is None: print ('Error opening image!') print ('Usage: filter2D.py [image_name -- default lena.jpg] \n') return -1 # Initialize ddepth argument for the filter ddepth = -1 # Loop - Will filter the image with different kernel sizes each 0.5 seconds ind = 0 while True: # Update kernel size for a normalized box filter kernel_size = 3 + 2 * (ind % 5) kernel = np.ones((kernel_size, kernel_size), dtype=np.float32) kernel /= (kernel_size * kernel_size) # Apply filter dst = cv.filter2D(src, ddepth, kernel) cv.imshow(window_name, dst) c = cv.waitKey(500) if c == 27: break ``` ind += 1 ``` return 0 if __name__ == "__main__": main(sys.argv[1:]) ``` ## 说明 ### 加载图像 **C++:** ```cpp const char* imageName = argc >=2 ? argv[1] : "lena.jpg"; // Loads an image src = imread( samples::findFile( imageName ), IMREAD_COLOR ); // Load an image if( src.empty() ) { printf(" Error opening image\n"); printf(" Program Arguments: [image_name -- default lena.jpg] \n"); return EXIT_FAILURE; } ``` **Java:** ```java String imageName = ((args.length > 0) ? args[0] : "../data/lena.jpg"); // Load an image src = Imgcodecs.imread(imageName, Imgcodecs.IMREAD_COLOR); // Check if image is loaded fine if( src.empty() ) { System.out.println("Error opening image!"); System.out.println("Program Arguments: [image_name -- default ../data/lena.jpg] \n"); System.exit(-1); } ``` **Python:** ```python imageName = argv[0] if len(argv) > 0 else 'lena.jpg' # Loads an image src = cv.imread(cv.samples.findFile(imageName), cv.IMREAD_COLOR) # Check if image is loaded fine if src is None: print ('Error opening image!') print ('Usage: filter2D.py [image_name -- default lena.jpg] \n') return -1 ``` ### 初始化参数 **C++:** ```cpp // Initialize arguments for the filter anchor = Point( -1, -1 ); delta = 0; ddepth = -1; ``` **Java:** ```java // Initialize arguments for the filter anchor = new Point( -1, -1); delta = 0.0; ddepth = -1; ``` **Python:** ```python # Initialize ddepth argument for the filter ddepth = -1 ``` ### 循环 执行一个无限循环,不断更新核的大小,并把我们的线性滤波器应用到输入图像上。下面来详细分析一下: - 首先定义滤波器要使用的核。代码如下: **C++:** ```cpp // Update kernel size for a normalized box filter kernel_size = 3 + 2*( ind%5 ); kernel = Mat::ones( kernel_size, kernel_size, CV_32F )/ (float)(kernel_size*kernel_size); ``` **Java:** ```java // Update kernel size for a normalized box filter kernel_size = 3 + 2*( ind%5 ); Mat ones = Mat.ones( kernel_size, kernel_size, CvType.CV_32F ); Core.multiply(ones, new Scalar(1/(double)(kernel_size*kernel_size)), kernel); ``` **Python:** ```python # Update kernel size for a normalized box filter kernel_size = 3 + 2 * (ind % 5) kernel = np.ones((kernel_size, kernel_size), dtype=np.float32) kernel /= (kernel_size * kernel_size) ``` 第一行把 *kernel_size* 更新为 $[3,11]$ 范围内的奇数值。 第二行才是真正地构造核:先填充一个全为 $1$ 的矩阵,再除以元素个数进行归一化。 - 设置好核之后,就可以用 **filter2D()** 函数生成滤波结果: **C++:** ```cpp // Apply filter filter2D(src, dst, ddepth , kernel, anchor, delta, BORDER_DEFAULT ); ``` **Java:** ```java // Apply filter Imgproc.filter2D(src, dst, ddepth , kernel, anchor, delta, Core.BORDER_DEFAULT ); ``` **Python:** ```python # Apply filter dst = cv.filter2D(src, ddepth, kernel) ``` - 各参数含义: - *src*:源图像 - *dst*:目标图像 - *ddepth*:*dst* 的深度。负值(例如 $-1$)表示与源图像深度相同。 - *kernel*:要在图像上扫描的核 - *anchor*:锚点相对于其核的位置。位置 *Point(-1, -1)* 表示默认为中心。 - *delta*:相关过程中加到每个像素上的值。默认为 $0$。 - *BORDER_DEFAULT*:我们保持其默认值(更多细节见下一篇教程)。 - 我们的程序会执行一个 *while* 循环,每 500 ms 在指定范围内更新滤波器的核大小。 ## 结果 ![filter_2d_tutorial_result.jpg](/img/opencv/filter_2d_tutorial_result.jpg) 1. 编译上面的代码后,你可以传入一幅图像的路径作为参数来运行它。结果应当是一个显示被归一化滤波器模糊后的图像的窗口。每隔 0.5 秒核大小就会变化,正如下面这组截图所示: