最近學習了OpenCV,於是想用它實現Photoshop的主要功能,用於照片處理。
對於一張照片,PS的一般處理步驟包括:
1, 旋轉圖片,校正位置。
2,剪切,調整大小,重新構圖。
3,調整色階、曲線,使圖片曝光正確、對比適中。
4,調整對比度、飽和度
5,印章去掉不想要的東西,液化調整形體線條
6,對於人像圖片,美膚、美白
7, 用色彩平衡、可選顏色等調整色調,形成照片調性
8,加一些光效
9,銳化
以後的一系列博文將采用OpenCV逐一實現Photoshop的算法和功能, 並用計算機視覺人工智能方式,嘗試超越Photoshop一點點。
本系列博文基於OpenCV, 編程語言為C++. 由於OpenCV的跨平臺性,代碼可以在用於Windows, linux, 作個接口後可用於android,IOS.
一、圖像旋轉
OpenCV中, 用 warpAffine() 仿射變換函數即可以實現旋轉。
例如,寫一個 旋轉函數 imageRotate1() 如下:
#include <opencv2/core.hpp> #include <opencv2/imgproc.hpp> //src為原圖像, dst為新圖像, angle為旋轉角度(正值為順時針旋轉,負值為逆時針旋轉) int imageRotate1(InputArray src, OutputArray dst, double angle) { Mat input = src.getMat(); if( input.empty() ) { return -1; } //得到圖像大小 int width = input.cols; int height = input.rows; //計算圖像中心點 Point2f center; center.x = width / 2.0; center.y = height / 2.0; //獲得旋轉變換矩陣 double scale = 1.0; Mat trans_mat = getRotationMatrix2D( center, -angle, scale ); //仿射變換 warpAffine( input, dst, trans_mat, Size(width, height)); return 0; }

在函數 imageRotate1()中,新圖像沿用原圖像大小。旋轉後,圖像的角部被切掉了。
這樣顯然不正確,需要調整圖像尺寸。
調整方式一: 擴大圖片,將原圖片包含進去,計算示意圖如下:
新圖片大小為: out_width = (width*cos(a)+height*sin(a); out_height = height*cos(a)+width*sin(a))
修改原函數為 imageRotate2() :
//圖像旋轉: src為原圖像, dst為新圖像, angle為旋轉角度 int imageRotate2(InputArray src, OutputArray dst, double angle) { Mat input = src.getMat(); if( input.empty() ) { return -1; } //得到圖像大小 int width = input.cols; int height = input.rows; //計算圖像中心點 Point2f center; center.x = width / 2.0; center.y = height / 2.0; //獲得旋轉變換矩陣 double scale = 1.0; Mat trans_mat = getRotationMatrix2D( center, -angle, scale ); //計算新圖像大小 double angle1 = angle * CV_PI / 180. ; double a = sin(angle1) * scale; double b = cos(angle1) * scale; double out_width = height * fabs(a) + width * fabs(b); double out_height = width * fabs(a) + height * fabs(b); //仿射變換 warpAffine( input, dst, trans_mat, Size(out_width, out_height)); return 0; }
圖像旋轉 -17度 的結果
還是不對,新圖像變大了,但圖像中心點不對,需要在旋轉矩陣中加入平移,在一次變換中同時完成旋轉和平移,將新圖像的中心點移到正確位置。
再次修改函數為: imageRotate3()
//圖像旋轉: src為原圖像, dst為新圖像, angle為旋轉角度 int imageRotate3(InputArray src, OutputArray dst, double angle) { Mat input = src.getMat(); if( input.empty() ) { return -1; } //得到圖像大小 int width = input.cols; int height = input.rows; //計算圖像中心點 Point2f center; center.x = width / 2.0; center.y = height / 2.0; //獲得旋轉變換矩陣 double scale = 1.0; Mat trans_mat = getRotationMatrix2D( center, -angle, scale ); //計算新圖像大小 double angle1 = angle * CV_PI / 180. ; double a = sin(angle1) * scale; double b = cos(angle1) * scale; double out_width = height * fabs(a) + width * fabs(b); double out_height = width * fabs(a) + height * fabs(b); //在旋轉變換矩陣中加入平移量 trans_mat.at<double>(0, 2) += cvRound( (out_width - width) / 2 ); trans_mat.at<double>(1, 2) += cvRound( (out_height - height) / 2); //仿射變換 warpAffine( input, dst, trans_mat, Size(out_width, out_height)); return 0; }
這一次正確了,新圖像變大了,同時圖像中心點移到了新的中心點,原圖像全部能顯示出來。
在實際照片旋轉中,我們經常采用另一種剪切形式的調整方式:圖像旋轉後,縮小圖片,使圖片各個邊角均不出現黑邊。 下圖紅框即為新圖象大小,如下:
這種調整方式下,新圖像大小的計算稍為有點復雜,在網上也沒有找到範例,只能自己計算了。
1,如上,旋轉後的外邊框大小為: out_width =(width*cos(a)+height*sin(a); out_height = height*cos(a)+width*sin(a))
2, 畫幾根輔助線,如下圖:(註意右邊圖中的粉紅三角形)
其最長的邊長 len = width*cos(a)
角a 即旋轉角度
由於外邊框大小已知,則角b 可計算出來。
求解 Y: Y = len / ( 1 / tan( a ) + 1 / tan( b ) )
X = Y * 1 / tan( b )
最後求得 紅框的長、寬為: new_width = out_width - 2 * X; new_height = out_height - 2 * Y
再次修改函數為: imageRotate4()
增加了一個參數: isClip , 當isClip為true時,采取縮小圖片的剪切方式,否則采取放大圖片的方式。
//圖像旋轉: src為原圖像, dst為新圖像, angle為旋轉角度, isClip表示是采取縮小圖片的方式 int imageRotate4(InputArray src, OutputArray dst, double angle, bool isClip) { Mat input = src.getMat(); if( input.empty() ) { return -1; } //得到圖像大小 int width = input.cols; int height = input.rows; //計算圖像中心點 Point2f center; center.x = width / 2.0; center.y = height / 2.0; //獲得旋轉變換矩陣 double scale = 1.0; Mat trans_mat = getRotationMatrix2D( center, -angle, scale ); //計算新圖像大小 double angle1 = angle * CV_PI / 180. ; double a = sin(angle1) * scale; double b = cos(angle1) * scale; double out_width = height * fabs(a) + width * fabs(b); //外邊框長度 double out_height = width * fabs(a) + height * fabs(b);//外邊框高度 int new_width, new_height; if ( ! isClip ) { new_width = cvRound(out_width); new_height = cvRound(out_height); } else { //calculate width and height of clip rect double angle2 = fabs(atan(height * 1.0 / width)); //即角度 b double len = width * fabs(b); double Y = len / ( 1 / fabs(tan(angle1)) + 1 / fabs(tan(angle2)) ); double X = Y * 1 / fabs(tan(angle2)); new_width = cvRound(out_width - X * 2); new_height= cvRound(out_height - Y * 2); } //在旋轉變換矩陣中加入平移量 trans_mat.at<double>(0, 2) += cvRound( (new_width - width) / 2 ); trans_mat.at<double>(1, 2) += cvRound( (new_height - height) / 2); //仿射變換 warpAffine( input, dst, trans_mat, Size(new_width, new_height)); return 0; }
以下是 isClip為true, 旋轉角度為 10 的結果,可見圖片旋轉了、縮小了,沒有黑邊
由於不註意,人們拍照時經常拍歪了,一般歪得也不多,但照片就不好看了。
因此,有這麽一個問題: 能否智能判別圖像是否拍歪了,如果歪了,則自動計算出要旋轉擺正的角度。從而使得人們一拍照,就自動拍正。
(PS:這個功能是Photoshop沒有的,如果能實現,算不算超越Photoshop一點點呢?)
解決思路是這樣的:
1, 圖像一般有一個或兩條長直線(通常這個可能是地平線、建築物等),且傾斜角度不大
2, 利用 OpenCV圖像識別能力,識別出圖中有哪些直線。
3, 分析這些直線, 如果長度足夠長、且位置相對居中,選取最長的兩條直線,測算擺正它所需的角度,做為返回值。
事實上,人工糾正圖片的Photoshop操作方式也是這樣的:我們在圖中人眼找一個基準線,用“度量工具”畫一條線,再點菜單“圖象/ 旋轉畫布/ 任意角度", 則Photoshop將計算出需要旋轉的角度。
嘗試寫了一個函數: detectRotation(), 用於自動檢測擺正圖像的所需的旋轉角度, 如下:
/** * 智能檢測圖像傾斜度 * 返回值:返回0表示無檢測結果,返回非0表示擺正圖象需要旋轉的角度(-10至10度) */ double detectRotation(InputArray src) { double max_angle = 6; //可旋轉的最大角度 Mat in = src.getMat(); if( in.empty() ) return 0; Mat input; //轉為灰度圖 if ( in.type() == CV_8UC1 ) input = in; else if ( in.type() == CV_8UC3 ) cvtColor(in, input, CV_BGR2GRAY); else if ( in.type() == CV_8UC3 ) cvtColor(in, input, CV_BGRA2GRAY); else return 0; Mat dst, cdst; //執行Canny邊緣檢測(檢測結果為dst, 為黑白圖) double threshold1 = 90; Canny(src, dst, threshold1, threshold1 * 3, 3); //將Canny邊緣檢測結果轉化為灰度圖像(cdst) cvtColor(dst, cdst, CV_GRAY2BGR); //執行霍夫線變換,檢測直線 vector<Vec4i> lines; //存放檢測結果的vector double minLineLength = std::min(dst.cols, dst.rows) * 0.25; //最短線長度 double maxLineGap = std::min(dst.cols, dst.rows) * 0.03 ; //最小線間距 int threshold = 90; HoughLinesP(dst, lines, 1, CV_PI / 180, threshold, minLineLength, maxLineGap ); //分析所需變量 int x1, y1, x2 , y2; //直線的兩個端點 int x, y; //直線的中點 double angle, rotate_angle; //直線的角度,擺正直線需要旋轉的角度 double line_length; //直線長度 double position_weighted; //直線的位置權重:靠圖像中央的線權重為1, 越靠邊的線權重越小 double main_lens[2]; //用於存放最長的二條直線長度的數組 (這兩條直線即是主線條) double main_angles[2];//用於存放最長的二條直線的擺正需要旋轉的角度 main_lens[0] = main_lens[1] = 0; main_angles[0] = main_angles[1] = 0; //逐個分析各條直線,判斷哪個是主線條 for( size_t i = 0; i < lines.size(); i++ ) { //取得直線的兩個端點座標 x1 = lines[i][0]; y1 = lines[i][1]; x2 = lines[i][2]; y2 = lines[i][3]; x = (x1 + x2 ) / 2; y = (y1 + y2) / 2; //計算直線的角度 angle = (x1 == x2) ? 90 : ( atan ( (y1 - y2) * 1.0 / (x2 - x1) ) ) / CV_PI * 180; //擺正直線需要旋轉的角度. 如果超出可旋轉的最大角度,則忽略這個線。 if ( fabs(angle - 0) <= max_angle ) { rotate_angle = angle - 0; } else if ( fabs(angle - 90) <= max_angle ) { rotate_angle = angle - 90; } else { continue; } //計算線的長度 line_length = sqrt( (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2) ); //計算直線的位置權重:靠圖像中央的線權重為1, 越靠邊的線權重越小 position_weighted = 1; if ( x < dst.cols / 4 || x > dst.cols * 3 / 4 ) position_weighted *= 0.8; if ( x < dst.cols / 6 || x > dst.cols * 5 / 6 ) position_weighted *= 0.5; if ( x < dst.cols / 8 || x > dst.cols * 7 / 8 ) position_weighted *= 0.5; if ( y < dst.rows / 4 || y > dst.rows * 3 / 4 ) position_weighted *= 0.8; if ( y < dst.rows / 6 || y > dst.rows * 5 / 6 ) position_weighted *= 0.5; if ( y < dst.rows / 8 || y > dst.rows * 7 / 8 ) position_weighted *= 0.5; //如果 直線長度 * 位置權重 < 最小長度, 則這條線無效 line_length = line_length * position_weighted; if ( line_length < minLineLength ) continue; //如果長度為前兩名,則存入數據 if ( line_length > main_lens[1] ) { if (line_length > main_lens[0]) { main_lens[1] = main_lens[0]; main_lens[0] = line_length; main_angles[1] = main_angles[0]; main_angles[0] = rotate_angle; //如果定義了 SHOW_LINE, 則將該線條畫出來 #ifdef SHOW_LINE line( cdst, Point(x1, y1), Point(x2, y2), Scalar(0,0,255), 3, CV_AA); #endif } else { main_lens[1] = line_length; main_angles[1] = rotate_angle; } } } //如果定義了 SHOW_LINE, 則在source_window中顯示cdst #ifdef SHOW_LINE imshow(source_window, cdst); #endif //最後,分析最長的二條直線,得出結果 if ( main_lens[0] > 0 ) { //如果最長的線 與 次長的線 兩者長度相近,則返回兩者需要旋轉的角度的平均值 if (main_lens[1] > 0 && (main_lens[0] - main_lens[1] / main_lens[0] < 0.2 )) { return (main_angles[0] + main_angles[1] ) / 2; } else { return main_angles[0]; //否則,返回最長的線需要旋轉的角度 } } else { return 0; } }
使用detectRotation()函數自動測試角度,並顯示出主要線條,運行結果:
恩,有那麽一點意思, 找出了幾個主線條,得出旋轉 -5 度,則可以擺正圖片。
當然,這個 detectRotation()函數還不是很智能,可用性還有待改進。
最後, 把本文所有代碼和主程序貼上來(有點長,不過方便復制)。配置好OpenCV開發環境,把代碼復制下來,就可以調試了。
代碼中需要說明的是: 由於opencv的滾動條只能顯示正值。 本例中rotation 的 滾動條,值為100時表示旋轉角度為0。 如果小於100, 表示旋轉角度為負。
#include <iostream> #include "opencv2/core.hpp" #include "opencv2/imgproc.hpp" #include "opencv2/highgui.hpp" #include <cmath> using namespace std; using namespace cv; #define SHOW_LINE #define BASE 100 static string source_window = "source"; static string window_name = "image rotate"; static Mat src; static int rotateDegree = 0 + BASE; static int clip = 0; //圖像旋轉: src為原圖像, dst為新圖像, angle為旋轉角度(正值為順時針旋轉,負值為逆時針旋轉) int imageRotate1(InputArray src, OutputArray dst, double angle) { Mat input = src.getMat(); if( input.empty() ) { return -1; } //得到圖像大小 int width = input.cols; int height = input.rows; //計算圖像中心點 Point2f center; center.x = width / 2.0; center.y = height / 2.0; //獲得旋轉變換矩陣 double scale = 1.0; Mat trans_mat = getRotationMatrix2D( center, -angle, scale ); //仿射變換 warpAffine( input, dst, trans_mat, Size(width, height)); return 0; } //圖像旋轉: src為原圖像, dst為新圖像, angle為旋轉角度 int imageRotate2(InputArray src, OutputArray dst, double angle) { Mat input = src.getMat(); if( input.empty() ) { return -1; } //得到圖像大小 int width = input.cols; int height = input.rows; //計算圖像中心點 Point2f center; center.x = width / 2.0; center.y = height / 2.0; //獲得旋轉變換矩陣 double scale = 1.0; Mat trans_mat = getRotationMatrix2D( center, -angle, scale ); //計算新圖像大小 double angle1 = angle * CV_PI / 180. ; double a = sin(angle1) * scale; double b = cos(angle1) * scale; double out_width = height * fabs(a) + width * fabs(b); double out_height = width * fabs(a) + height * fabs(b); //仿射變換 warpAffine( input, dst, trans_mat, Size(out_width, out_height)); return 0; } //圖像旋轉: src為原圖像, dst為新圖像, angle為旋轉角度 int imageRotate3(InputArray src, OutputArray dst, double angle) { Mat input = src.getMat(); if( input.empty() ) { return -1; } //得到圖像大小 int width = input.cols; int height = input.rows; //計算圖像中心點 Point2f center; center.x = width / 2.0; center.y = height / 2.0; //獲得旋轉變換矩陣 double scale = 1.0; Mat trans_mat = getRotationMatrix2D( center, -angle, scale ); //計算新圖像大小 double angle1 = angle * CV_PI / 180. ; double a = sin(angle1) * scale; double b = cos(angle1) * scale; double out_width = height * fabs(a) + width * fabs(b); double out_height = width * fabs(a) + height * fabs(b); //在旋轉變換矩陣中加入平移量 trans_mat.at<double>(0, 2) += cvRound( (out_width - width) / 2 ); trans_mat.at<double>(1, 2) += cvRound( (out_height - height) / 2); //仿射變換 warpAffine( input, dst, trans_mat, Size(out_width, out_height)); return 0; } //圖像旋轉: src為原圖像, dst為新圖像, angle為旋轉角度, isClip表示是采取縮小圖片的方式 int imageRotate4(InputArray src, OutputArray dst, double angle, bool isClip) { Mat input = src.getMat(); if( input.empty() ) { return -1; } //得到圖像大小 int width = input.cols; int height = input.rows; //計算圖像中心點 Point2f center; center.x = width / 2.0; center.y = height / 2.0; //獲得旋轉變換矩陣 double scale = 1.0; Mat trans_mat = getRotationMatrix2D( center, -angle, scale ); //計算新圖像大小 double angle1 = angle * CV_PI / 180. ; double a = sin(angle1) * scale; double b = cos(angle1) * scale; double out_width = height * fabs(a) + width * fabs(b); //外邊框長度 double out_height = width * fabs(a) + height * fabs(b);//外邊框高度 int new_width, new_height; if ( ! isClip ) { new_width = cvRound(out_width); new_height = cvRound(out_height); } else { //calculate width and height of clip rect double angle2 = fabs(atan(height * 1.0 / width)); //即角度 b double len = width * fabs(b); double Y = len / ( 1 / fabs(tan(angle1)) + 1 / fabs(tan(angle2)) ); double X = Y * 1 / fabs(tan(angle2)); new_width = cvRound(out_width - X * 2); new_height= cvRound(out_height - Y * 2); } //在旋轉變換矩陣中加入平移量 trans_mat.at<double>(0, 2) += cvRound( (new_width - width) / 2 ); trans_mat.at<double>(1, 2) += cvRound( (new_height - height) / 2); //仿射變換 warpAffine( input, dst, trans_mat, Size(new_width, new_height)); return 0; } /** * 檢測圖像傾斜度 * 返回值:返回0表示無檢測結果,返回非0表示擺正圖象需要旋轉的角度(-10至10度) */ double detectRotation(InputArray src) { double max_angle = 6; //可旋轉的最大角度 Mat in = src.getMat(); if( in.empty() ) return 0; Mat input; //轉為灰度圖 if ( in.type() == CV_8UC1 ) input = in; else if ( in.type() == CV_8UC3 ) cvtColor(in, input, CV_BGR2GRAY); else if ( in.type() == CV_8UC3 ) cvtColor(in, input, CV_BGRA2GRAY); else return 0; Mat dst, cdst; //執行Canny邊緣檢測(檢測結果為dst, 為黑白圖) double threshold1 = 90; Canny(src, dst, threshold1, threshold1 * 3, 3); //將Canny邊緣檢測結果轉化為灰度圖像(cdst) cvtColor(dst, cdst, CV_GRAY2BGR); //執行霍夫線變換,檢測直線 vector<Vec4i> lines; //存放檢測結果的vector double minLineLength = std::min(dst.cols, dst.rows) * 0.25; //最短線長度 double maxLineGap = std::min(dst.cols, dst.rows) * 0.03 ; //最小線間距 int threshold = 90; HoughLinesP(dst, lines, 1, CV_PI / 180, threshold, minLineLength, maxLineGap ); //分析所需變量 int x1, y1, x2 , y2; //直線的兩個端點 int x, y; //直線的中點 double angle, rotate_angle; //直線的角度,擺正直線需要旋轉的角度 double line_length; //直線長度 double position_weighted; //直線的位置權重:靠圖像中央的線權重為1, 越靠邊的線權重越小 double main_lens[2]; //用於存放最長的二條直線長度的數組 (這兩條直線即是主線條) double main_angles[2];//用於存放最長的二條直線的擺正需要旋轉的角度 main_lens[0] = main_lens[1] = 0; main_angles[0] = main_angles[1] = 0; //逐個分析各條直線,判斷哪個是主線條 for( size_t i = 0; i < lines.size(); i++ ) { //取得直線的兩個端點座標 x1 = lines[i][0]; y1 = lines[i][1]; x2 = lines[i][2]; y2 = lines[i][3]; x = (x1 + x2 ) / 2; y = (y1 + y2) / 2; //計算直線的角度 angle = (x1 == x2) ? 90 : ( atan ( (y1 - y2) * 1.0 / (x2 - x1) ) ) / CV_PI * 180; //擺正直線需要旋轉的角度. 如果超出可旋轉的最大角度,則忽略這個線。 if ( fabs(angle - 0) <= max_angle ) { rotate_angle = angle - 0; } else if ( fabs(angle - 90) <= max_angle ) { rotate_angle = angle - 90; } else { continue; } //計算線的長度 line_length = sqrt( (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2) ); //計算直線的位置權重:靠圖像中央的線權重為1, 越靠邊的線權重越小 position_weighted = 1; if ( x < dst.cols / 4 || x > dst.cols * 3 / 4 ) position_weighted *= 0.8; if ( x < dst.cols / 6 || x > dst.cols * 5 / 6 ) position_weighted *= 0.5; if ( x < dst.cols / 8 || x > dst.cols * 7 / 8 ) position_weighted *= 0.5; if ( y < dst.rows / 4 || y > dst.rows * 3 / 4 ) position_weighted *= 0.8; if ( y < dst.rows / 6 || y > dst.rows * 5 / 6 ) position_weighted *= 0.5; if ( y < dst.rows / 8 || y > dst.rows * 7 / 8 ) position_weighted *= 0.5; //如果 直線長度 * 位置權重 < 最小長度, 則這條線無效 line_length = line_length * position_weighted; if ( line_length < minLineLength ) continue; //如果長度為前兩名,則存入數據 if ( line_length > main_lens[1] ) { if (line_length > main_lens[0]) { main_lens[1] = main_lens[0]; main_lens[0] = line_length; main_angles[1] = main_angles[0]; main_angles[0] = rotate_angle; //如果定義了 SHOW_LINE, 則將該線條畫出來 #ifdef SHOW_LINE line( cdst, Point(x1, y1), Point(x2, y2), Scalar(0,0,255), 3, CV_AA); #endif } else { main_lens[1] = line_length; main_angles[1] = rotate_angle; } } } //如果定義了 SHOW_LINE, 則在source_window中顯示cdst #ifdef SHOW_LINE imshow(source_window, cdst); #endif //最後,分析最長的二條直線,得出結果 if ( main_lens[0] > 0 ) { //如果最長的線 與 次長的線 兩者長度相近,則返回兩者需要旋轉的角度的平均值 if (main_lens[1] > 0 && (main_lens[0] - main_lens[1] / main_lens[0] < 0.2 )) { return (main_angles[0] + main_angles[1] ) / 2; } else { return main_angles[0]; //否則,返回最長的線需要旋轉的角度 } } else { return 0; } } static void callbackAdjust(int , void *) { Mat dst; //imageRotate1(src, dst, rotateDegree - BASE); //imageRotate2(src, dst, rotateDegree - BASE); //imageRotate3(src, dst, rotateDegree - BASE); bool isClip = ( clip == 1 ); imageRotate4(src, dst, rotateDegree - BASE, isClip ); imshow(window_name, dst); } int main() { src = http://blog.csdn.net/c80486/article/details/imread("building.jpg"); if ( !src.data ) { cout << "error read image" << endl; return -1; } namedWindow(source_window); imshow(source_window, src); namedWindow(window_name); createTrackbar("rotate", window_name, &rotateDegree, BASE * 2, callbackAdjust); createTrackbar("clip", window_name, &clip, 1, callbackAdjust); //自動檢測旋轉角度 double angle = detectRotation(src); if ( angle != 0 ) { rotateDegree = angle + BASE; setTrackbarPos("rotate", window_name, rotateDegree); } callbackAdjust(0, 0); waitKey(); return 0; }
Tags: Photoshop Android 人工智能 Windows 照片處理
文章來源: