一、core 模塊
Mat 是一個類,由兩個數據部分組成:矩陣頭(包含矩陣尺寸,存儲方法,存儲地址等信息)和一個指向存儲所有像素值的矩陣(根據所選存儲方法的不同矩陣可以是不同的維數)的指針。
創建Mat對象方法:
1 1->Mat() 構造函數: 2 Mat M(2,2, CV_8UC3, Scalar(0,0,255)); 3 int sz[3] = {2,2,2}; 4 Mat L(3,sz, CV_8UC(1), Scalar::all(0)); 5 2->Create() function: 函數 6 M.create(4,4, CV_8UC(2)); 7 3-> 初始化zeros(), ones(), :eyes()矩陣 8 Mat E = Mat::eye(4, 4, CV_64F); 9 Mat O = Mat::ones(2, 2, CV_32F); 10 Mat Z = Mat::zeros(3,3, CV_8UC1); 11 4->用逗號分隔的初始化函數: 12 Mat C = (Mat_<double>(3,3) << 0, -1, 0, -1, 5, -1, 0, -1, 0);
常用操作:
1 Mat A, C; // 只創建信息頭部分
2 A = imread(argv[1], CV_LOAD_IMAGE_COLOR); // 這里為矩陣開辟內存
3 Mat B(A); // 使用拷貝構造函數
4 C = A; // 賦值運算符
5 Mat D (A, Rect(10, 10, 100, 100) ); // using a rectangle
6 Mat E = A(Range:all(), Range(1,3)); // using row and column boundaries
7 Mat F = A.clone(); 8 Mat G; 9 A.copyTo(G); //使用函數 clone() 或者 copyTo() 來拷貝一副圖像的矩陣。
2、圖像基本操作(Mat操作)
2.1 濾波器掩碼
濾波器在圖像處理中的應用廣泛,OpenCV也有個用到了濾波器掩碼(也稱作核)的函數。使用這個函數,你必須先定義一個表示掩碼的 Mat 對象:
1 Mat kern = (Mat_<char>(3,3) << 0, -1, 0, 2 -1, 5, -1, 3 0, -1, 0); 4 filter2D(I, K, I.depth(), kern );
2.2 圖像混合(addWeighted函數)
線性混合操作 也是一種典型的二元(兩個輸入)的 像素操作 :
通過在范圍 內改變
,這個操可以用來對兩幅圖像或兩段視頻產生時間上的 畫面疊化 (cross-dissolve)效果。
1 alpha = 0.3; 2 beta = ( 1.0 - alpha ); 3 addWeighted( src1, alpha, src2, beta, 0.0, dst);
2.3 改變圖像的對比度和亮度
兩種常用的點過程(即點算子),是用常數對點進行 乘法 和 加法 運算:
兩個參數 和
一般稱作 增益 和 偏置 參數。我們往往用這兩個參數來分別控制 對比度 和 亮度 。
你可以把 看成源圖像像素,把
看成輸出圖像像素。這樣一來,上面的式子就能寫得更清楚些:
其中, 和
表示像素位於 第i行 和 第j列 。
1 double alpha; 2 int beta; 3 Mat image = imread( argv[1] ); 4 Mat new_image = Mat::zeros( image.size(), image.type() ); 5 for( int y = 0; y < image.rows; y++ ) 6 { 7 for( int x = 0; x < image.cols; x++ ) 8 { 9 for( int c = 0; c < 3; c++ ) 10 { 11 new_image.at<Vec3b>(y,x)[c] = saturate_cast<uchar>( alpha*( image.at<Vec3b>(y,x)[c] ) + beta ); 12 } 13 } 14 }
2.4 離散傅立葉變換
對一張圖像使用傅立葉變換就是將它分解成正弦和余弦兩部分。也就是將圖像從空間域(spatial domain)轉換到頻域(frequency domain)。 2維圖像的傅立葉變換可以用以下數學公式表達:
式中 f 是空間域(spatial domain)值, F 則是頻域(frequency domain)值。
1 Mat padded; //將輸入圖像延擴到最佳的尺寸
2 int m = getOptimalDFTSize( I.rows ); 3 int n = getOptimalDFTSize( I.cols ); // 在邊緣添加0
4 copyMakeBorder(I, padded, 0, m - I.rows, 0, n - I.cols, BORDER_CONSTANT, Scalar::all(0)); 5 Mat planes[] = {Mat_<float>(padded), Mat::zeros(padded.size(), CV_32F)}; 6 Mat complexI; 7 merge(planes, 2, complexI); // 為延擴后的圖像增添一個初始化為0的通道
8 dft(complexI, complexI); // 變換結果很好的保存在原始矩陣中
9 split(complexI, planes); // planes[0] = Re(DFT(I), planes[1] = Im(DFT(I))
10 magnitude(planes[0], planes[1], planes[0]);// planes[0] = magnitude
11 Mat magI = planes[0];
2.5 基本繪圖
1 Point: 2 Point pt; 3 pt.x = 10; 4 pt.y = 8; 5 或者 6 Point pt = Point(10, 8); 7
8 Scalar: 9 Scalar( B, G, R ) //定義的RGB顏色值為:Blue,Green, Red
10
11 line 繪直線: 12 line( img, //輸出圖像
13 start, //起始點
14 end, //結束點
15 Scalar( 0, 0, 0 ), //顏色
16 thickness=2, //線條粗細
17 lineType=8 ); //線條類型
18
19 ellipse 繪橢圓: 20 ellipse( img, //輸出圖像
21 Point( w/2.0, w/2.0 ), //中心為點 (w/2.0, w/2.0)
22 Size( w/4.0, w/16.0 ), //大小位於矩形 (w/4.0, w/16.0) 內
23 angle, //旋轉角度為 angle
24 0, 25 360, //擴展的弧度從 0 度到 360 度
26 Scalar( 255, 0, 0 ), //顏色
27 thickness, //線條粗細
28 lineType ); //線條類型
29
30 circle 繪圓: 31 circle( img, //輸出圖像
32 center, //圓心由點 center 定義
33 w/32.0, /圓的半徑為: w/32.0
34 Scalar( 0, 0, 255 ), //顏色
35 thickness, //線條粗細
36 lineType ); //線條類型
37
38 rectangle 繪矩形: 39 rectangle( rook_image, 40 Point( 0, 7*w/8.0 ), 41 Point( w, w), //矩形兩個對角頂點為 Point( 0, 7*w/8.0 ) 和 Point( w, w)
42 Scalar( 0, 255, 255 ), 43 thickness = -1, 44 lineType = 8 ); 45
46 fillPoly 繪填充的多邊形: 47 fillPoly( img, 48 ppt, //多邊形的頂點集為 ppt
49 npt, //要繪制的多邊形頂點數目為 npt
50 1, //要繪制的多邊形數量僅為 1
51 Scalar( 255, 255, 255 ), 52 lineType );
2.6 隨機數發生器
RNG的實現了一個隨機數發生器。 在上面的例子中, rng 是用數值 0xFFFFFFFF 來實例化的一個RNG對象。
RNG rng( 0xFFFFFFFF );
二、imgproc 模塊
1、圖像平滑處理
平滑也稱模糊, 平滑處理時需要用到一個 濾波器 。 最常用的濾波器是 線性 濾波器,線性濾波處理的輸出像素值 (i.e. ) 是輸入像素值 (i.e.
)的加權和 :
稱為 核, 它僅僅是一個加權系數。
不妨把 濾波器 想象成一個包含加權系數的窗口,當使用這個濾波器平滑處理圖像時,就把這個窗口滑過圖像。
1.1 歸一化塊濾波器 (Normalized Box Filter)
1 blur( src, //輸入圖像
2 dst, //輸出圖像
3 Size( i, i ), //定義內核大小( w 像素寬度, h 像素高度)
4 Point(-1,-1)) //指定錨點位置(被平滑點), 如果是負值,取核的中心為錨點。
1.2 高斯濾波器 (Gaussian Filter)
1 GaussianBlur( src, //輸入圖像
2 dst, //輸出圖像
3 Size( i, i ), //定義內核的大小(需要考慮的鄰域范圍)。 w 和 h 必須是正奇數,否則將使用 和 參數來計算內核大小。
4 0, //: x 方向標准方差, 如果是 0 則 使用內核大小計算得到。
5 0 ) //: y 方向標准方差, 如果是 0 則 使用內核大小計算得到。
1.3 中值濾波器 (Median Filter)
1 medianBlur ( src, //輸入圖像
2 dst, //輸出圖像
3 i ); //內核大小 (只需一個值,因為我們使用正方形窗口),必須為奇數。
1.4 雙邊濾波 (Bilateral Filter)
1 bilateralFilter ( src, //輸入圖像
2 dst, //輸出圖像
3 i, //像素的鄰域直徑
4 i*2, //: 顏色空間的標准方差
5 i/2 ); //: 坐標空間的標准方差(像素單位)
2、形態學變換
形態學操作就是基於形狀的一系列圖像處理操作。最基本的形態學操作有二:腐蝕與膨脹(Erosion 與 Dilation)。 他們的運用廣泛:
消除噪聲
分割(isolate)獨立的圖像元素,以及連接(join)相鄰的元素。
尋找圖像中的明顯的極大值區域或極小值區域。
2.1 腐蝕(Erosion)
此操作將圖像 A 與任意形狀的內核 B(通常為正方形或圓形)進行卷積,將內核 B 覆蓋區域的最小相素值提取,並代替錨點位置的相素。這一操作將會導致圖像中的亮區開始“收縮”。
1 erode( src, //原圖像
2 erosion_dst, //輸出圖像
3 element ); //腐蝕操作的內核,默認為一個簡單的 3x3 矩陣。也可以使用函數 getStructuringElement。
4 Mat element = getStructuringElement( erosion_type, 5 Size( 2*erosion_size + 1, 2*erosion_size+1 ), 6 Point( erosion_size, erosion_size ) );
2.2 膨脹 (Dilation)
此操作將圖像 A 與任意形狀的內核 B(通常為正方形或圓形)進行卷積,將內核 B 覆蓋區域的最大相素值提取,並代替錨點位置的相素。這一操作將會導致圖像中的亮區開始“擴展”。
1 ditale( src, //原圖像
2 dilate_dst, //輸出圖像
3 element ); //腐蝕操作的內核,默認為一個簡單的 3x3 矩陣。也可以使用函數 getStructuringElement。
4 Mat element = getStructuringElement( dilation_type, 5 Size( 2*dilation_size + 1, 2*dilation_size+1 ), 6 Point( dilation_size, dilation_size ) );
2.3 開運算 (Opening)
開運算是通過先對圖像腐蝕再膨脹實現的。能夠排除小團塊物體(假設物體較背景明亮)。
2.4 閉運算(Closing)
閉運算是通過先對圖像膨脹再腐蝕實現的。能夠排除小型黑洞(黑色區域)。
2.5 形態梯度(Morphological Gradient)
膨脹圖與腐蝕圖之差。能夠保留物體的邊緣輪廓。
2.6 頂帽(Top Hat)
原圖像與開運算結果圖之差。
閉運算結果圖與原圖像之差
3、圖像金字塔
一個圖像金字塔是一系列圖像的集合 - 所有圖像來源於同一張原始圖像 - 通過梯次向下采樣獲得,直到達到某個終止條件才停止采樣。有兩種類型的圖像金字塔常常出現在文獻和應用中:
高斯金字塔(Gaussian pyramid): 用來向下采樣
拉普拉斯金字塔(Laplacian pyramid): 用來從金字塔低層圖像重建上層未采樣圖像
用如下方法向上采樣:
將 與高斯內核卷積:
將所有偶數行和列去除。
pyrUp( tmp, dst, Size( tmp.cols*2, tmp.rows*2 ) );
對圖像的向下采樣:
- 將圖像在每個方向擴大為原來的兩倍,新增的行和列以0填充(
)
- 使用先前同樣的內核(乘以4)與放大后的圖像卷積,獲得 “新增像素” 的近似值。
pyrDown( tmp, dst, Size( tmp.cols/2, tmp.rows/2 ) );
4、閾值操作
最簡單的圖像分割的方法。 例如:可以將該物體的像素點的灰度值設定為:‘0’(黑色),其他的像素點的灰度值為:‘255’(白色);當然像素點的灰度值可以任意,但最好設定的兩種顏色對比度較強,方便觀察結果
1 threshold( src_gray, //輸入的灰度圖像
2 dst, //輸出圖像
3 threshold_value, //進行閾值操作時閾值的大小
4 max_BINARY_value, //設定的最大灰度值
5 threshold_type ); // 閾值的類型。
6 0: 二進制閾值 7 1: 反二進制閾值 8 2: 截斷閾值 9 3: 0閾值 10 4: 反0閾值
5、給圖像添加邊界
圖像的卷積操作中,處理卷積邊緣時需要考慮邊緣填充。
1 copyMakeBorder( src, 2 dst, 3 top,bottom, left,right, //各邊界的寬度
4 borderType, //邊界類型,可選擇常數邊界BORDER_CONSTANT或者復制邊界BORDER_REPLICATE。
5 value ); //如果 borderType 類型是 BORDER_CONSTANT, 該值用來填充邊界像素。
6、圖像卷積
6.1 函數 filter2D 就可以生成濾波器
1 filter2D(src, 2 dst, 3 ddepth, //dst 的深度。若為負值(如 -1 ),則表示其深度與源圖像相等。
4 kernel, //用來遍歷圖像的核
5 anchor, //核的錨點的相對位置,其中心點默認為 (-1, -1) 。
6 delta, //在卷積過程中,該值會加到每個像素上。默認情況下,這個值為 0 。
7 BORDER_DEFAULT ); //默認即可
6.2 Sobel 導數
Sobel 算子用來計算圖像灰度函數的近似梯度。Sobel 算子結合了高斯平滑和微分求導。
計算:
1.在兩個方向求導:
水平變化: 將 與一個奇數大小的內核
進行卷積。比如,當內核大小為3時,
的計算結果為:
垂直變化: 將:math:I 與一個奇數大小的內核 進行卷積。比如,當內核大小為3時,
的計算結果為:

2.在圖像的每一點,結合以上兩個結果求出近似 梯度:
有時也用下面更簡單公式代替:
1 /// 求 X方向梯度
2 //Scharr( src_gray, grad_x, ddepth, x_order=1, y_order=0, scale, delta, BORDER_DEFAULT );
3 Sobel( src_gray, grad_x, ddepth, x_order=1, y_order=0, 3, scale, delta, BORDER_DEFAULT ); 4 convertScaleAbs( grad_x, abs_grad_x ); //將中間結果轉換到 CV_8U
5
6 /// 求Y方向梯度
7 //Scharr( src_gray, grad_y, ddepth, x_order=0, y_order=1, scale, delta, BORDER_DEFAULT );
8 Sobel( src_gray, grad_y, ddepth, x_order=0, y_order=1, 3, scale, delta, BORDER_DEFAULT ); 9 convertScaleAbs( grad_y, abs_grad_y ); 10
11 /// 合並梯度(近似)
12 addWeighted( abs_grad_x, 0.5, abs_grad_y, 0.5, 0, grad );
6.3 Laplace 算子
- 二階導數可以用來 檢測邊緣。 因為圖像是 “2維”, 我們需要在兩個方向求導。使用Laplacian算子將會使求導過程變得簡單。
- Laplacian 算子 的定義:、
1 Laplacian( src_gray, 2 dst, 3 ddepth, //輸出圖像的深度。 因為輸入圖像的深度是 CV_8U
4 kernel_size, //內部調用的 Sobel算子的內核大小
5 scale, 6 delta, 7 BORDER_DEFAULT );
6.4 Canny 邊緣檢測
1. Canny 使用了滯后閾值,滯后閾值需要兩個閾值(高閾值和低閾值):
2. 如果某一像素位置的幅值超過 高 閾值, 該像素被保留為邊緣像素。
3.如果某一像素位置的幅值小於 低 閾值, 該像素被排除。
4.如果某一像素位置的幅值在兩個閾值之間,該像素僅僅在連接到一個高於 高 閾值的像素時被保留。
Canny 推薦的 高:低 閾值比在 2:1 到3:1之間
1 Canny( detected_edges, //原灰度圖像
2 detected_edges, //輸出圖像
3 lowThreshold, //低閾值
4 lowThreshold*ratio, //設定為低閾值的3倍 (根據Canny算法的推薦)
5 kernel_size ); //設定為 3 (Sobel內核大小,內部使用)
6.5 霍夫線變換
- 霍夫線變換是一種用來尋找直線的方法.
- 是用霍夫線變換之前, 首先要對圖像進行邊緣檢測的處理,也即霍夫線變換的直接輸入只能是邊緣二值圖像.
1 Canny(src, dst, 50, 200, 3); //用Canny算子對圖像進行邊緣檢測
2 vector<Vec2f> lines; 3 /*標准霍夫線變換*/
4 HoughLines( dst, 5 lines, //儲存着檢測到的直線的參數對 (r,\theta) 的容器
6 rho=1, //參數極徑 rho 以像素值為單位的分辨率.
7 CV_PI/180, //參數極角 \theta 以弧度為單位的分辨率. 我們使用 1度 (即CV_PI/180)
8 threshold=100, 9 0,0 ); //srn and stn: 參數默認為0. 10 //通過畫出檢測到的直線來顯示結果.
11 for( size_t i = 0; i < lines.size(); i++ ) 12 { 13 float rho = lines[i][0], theta = lines[i][1]; 14 Point pt1, pt2; 15 double a = cos(theta), b = sin(theta); 16 double x0 = a*rho, y0 = b*rho; 17 pt1.x = cvRound(x0 + 1000*(-b)); 18 pt1.y = cvRound(y0 + 1000*(a)); 19 pt2.x = cvRound(x0 - 1000*(-b)); 20 pt2.y = cvRound(y0 - 1000*(a)); 21 line( cdst, pt1, pt2, Scalar(0,0,255), 3, CV_AA); 22 } 23
24 /*統計概率霍夫線變換*/
25 vector<Vec4i> lines; 26 HoughLinesP( dst, 27 lines, //儲存着檢測到的直線的參數對 (x_{start}, y_{start}, x_{end}, y_{end}) 的容器
28 rho=1, //參數極徑 rho 以像素值為單位的分辨率.
29 CV_PI/180, 30 threshold=50, //要”檢測” 一條直線所需最少的的曲線交點
31 minLinLength=50, //能組成一條直線的最少點的數量. 點數量不足的直線將被拋棄.
32 maxLineGap=10 ); //能被認為在一條直線上的亮點的最大距離. 33 //通過畫出檢測到的直線來顯示結果.
34 for( size_t i = 0; i < lines.size(); i++ ) 35 { 36 Vec4i l = lines[i]; 37 line( cdst, Point(l[0], l[1]), Point(l[2], l[3]), Scalar(0,0,255), 3, CV_AA); 38 }
6.6 霍夫圓變換
霍夫圓變換的基本原理和上個教程中提到的霍夫線變換類似, 只是點對應的二維極徑極角空間被三維的圓心點x, y還有半徑r空間取代.
1 GaussianBlur( src_gray, src_gray, Size(9, 9), 2, 2 ); //高斯模糊以降低噪聲
2 vector<Vec3f> circles; 3 HoughCircles( src_gray, //輸入圖像 (灰度圖)
4 circles, //存儲下面三個參數: x_{c}, y_{c}, r 集合的容器來表示每個檢測到的圓.
5 CV_HOUGH_GRADIENT, //指定檢測方法. 現在OpenCV中只有霍夫梯度法
6 dp = 1, //累加器圖像的反比分辨率
7 min_dist = src_gray.rows/8, //檢測到圓心之間的最小距離
8 param_1 = 200, // //圓心檢測閾值.
9 param_2 = 100, //圓心檢測閾值.
10 0, //能檢測到的最小圓半徑,默認為0.
11 0 ); //能檢測到的最大圓半徑, 默認為0 12 //繪出檢測到的圓
13 for( size_t i = 0; i < circles.size(); i++ ) 14 { 15 Point center(cvRound(circles[i][0]), cvRound(circles[i][1])); 16 int radius = cvRound(circles[i][2]); 17 // circle center
18 circle( src, center, 3, Scalar(0,255,0), -1, 8, 0 ); 19 // circle outline
20 circle( src, center, radius, Scalar(0,0,255), 3, 8, 0 ); 21 }
7、 Remapping 重映射
把一個圖像中一個位置的像素放置到另一個圖片指定位置的過程。
我們通過重映射來表達每個像素的位置 :
1 remap( src, 2 dst, 3 map_x, // x方向的映射參數. 它相當於方法 h(i,j) 的第一個參數
4 map_y, //y方向的映射參數. 注意 map_y 和 map_x 與 src 的大小一致。
5 CV_INTER_LINEAR, //非整數像素坐標插值標志. 這里給出的是默認值(雙線性插值).
6 BORDER_CONSTANT, // 默認
7 Scalar(0,0, 0) );
8、 仿射變換
一個任意的仿射變換都能表示為 乘以一個矩陣 (線性變換) 接着再 加上一個向量 (平移).
使用 矩陣來表示仿射變換.
考慮到我們要使用矩陣 和
對二維向量
做變換, 所以也能表示為下列形式:
or
1 //映射的三個點來定義仿射變換:
2 srcTri[0] = Point2f( 0,0 ); 3 srcTri[1] = Point2f( src.cols - 1, 0 ); 4 srcTri[2] = Point2f( 0, src.rows - 1 ); 5
6 dstTri[0] = Point2f( src.cols*0.0, src.rows*0.33 ); 7 dstTri[1] = Point2f( src.cols*0.85, src.rows*0.25 ); 8 dstTri[2] = Point2f( src.cols*0.15, src.rows*0.7 ); 9
10 //getAffineTransform 來求出仿射變換
11 warp_mat = getAffineTransform( srcTri, dstTri ); 12 warpAffine( src, 13 warp_dst, 14 warp_mat, //仿射變換矩陣
15 warp_dst.size() ); //輸出圖像的尺寸
9、 直方圖均衡化
直方圖均衡化是通過拉伸像素強度分布范圍來增強圖像對比度的一種方法.
equalizeHist( src, dst );
10、模板匹配
matchTemplate( img, templ, result, match_method );
三、feature2d 模塊. 2D特征框架
圖像特征類型:
- 邊緣
- 角點 (感興趣關鍵點)
- 斑點(Blobs) (感興趣區域)
1、Harris 角點檢測子
1 cornerHarris( src_gray, 2 dst, 3 blockSize=2, 4 apertureSize=3, 5 k=0.04, 6 BORDER_DEFAULT ); 7 /// Normalizing
8 normalize( dst, dst_norm, 0, 255, NORM_MINMAX, CV_32FC1, Mat() ); 9 convertScaleAbs( dst_norm, dst_norm_scaled ); 10
11 /// Drawing a circle around corners
12 for( int j = 0; j < dst_norm.rows ; j++ ) 13 { for( int i = 0; i < dst_norm.cols; i++ ) 14 { 15 if( (int) dst_norm.at<float>(j,i) > thresh ) 16 { 17 circle( dst_norm_scaled, Point( i, j ), 5, Scalar(0), 2, 8, 0 ); 18 } 19 } 20 } 21 /// Showing the result
22 namedWindow( corners_window, CV_WINDOW_AUTOSIZE ); 23 imshow( corners_window, dst_norm_scaled ); 24 }
2、Shi-Tomasi角點檢測子
1 /// Apply corner detection
2 goodFeaturesToTrack( src_gray, 3 corners, 4 maxCorners, 5 qualityLevel, 6 minDistance, 7 Mat(), 8 blockSize, 9 useHarrisDetector, 10 k ); 11
12 /// Draw corners detected
13 cout<<"** Number of corners detected: "<<corners.size()<<endl; 14 int r = 4; 15 for( int i = 0; i < corners.size(); i++ ) 16 { circle( copy, 17 corners[i], 18 r, 19 Scalar(rng.uniform(0,255), 20 rng.uniform(0,255), 21 rng.uniform(0,255)), -1, 8, 0 ); 22 } 23 /// Show what you got
24 namedWindow( source_window, CV_WINDOW_AUTOSIZE ); 25 imshow( source_window, copy );
3、特征點檢測
1 //--Detect the keypoints using SURF Detector
2 int minHessian = 400; 3
4 SurfFeatureDetector detector( minHessian ); 5
6 std::vector<KeyPoint> keypoints_1, keypoints_2; 7
8 detector.detect( img_1, keypoints_1 ); 9 detector.detect( img_2, keypoints_2 ); 10
11 //-- Draw keypoints
12 Mat img_keypoints_1; Mat img_keypoints_2; 13
14 drawKeypoints( img_1, keypoints_1, img_keypoints_1, Scalar::all(-1), DrawMatchesFlags::DEFAULT ); 15 drawKeypoints( img_2, keypoints_2, img_keypoints_2, Scalar::all(-1), DrawMatchesFlags::DEFAULT ); 16
17 //-- Show detected (drawn) keypoints
18 imshow("Keypoints 1", img_keypoints_1 ); 19 imshow("Keypoints 2", img_keypoints_2 );