OpenCV3 C++ を用いて基本的な画像変換を行います。
#include <opencv2/opencv.hpp>
int main() {
cv::Mat img = cv::imread("aaa.png", -1);
if(img.empty()) {
return -1;
}
cv::Mat img2, img3;
cv::resize(img, img2, img.size() / 2);
cv::resize(img, img3, img.size() * 2);
cv::imshow("original", img);
cv::imshow("original / 2", img2);
cv::imshow("original x 2", img3);
cv::waitKey(0);
return 0;
}
関連事項にダウンサンプリング cv::pyrDown があります。
平面内の線形変換は 2x2 行列で表現できます。これに平行移動を加えたアフィン変換は 2x3 行列で表現できます。これら 6 変数からなるアフィン変換の行列 $T$ は、3 つの点について変換前後の座標が分かれば一意に定まるということになります。
$$T = \begin{bmatrix} A & t \\ \end{bmatrix} \ = \begin{bmatrix} a & b & t_x \\ c & d & t_y \\ \end{bmatrix} $$
画像ではなく点を変換したい場合は cv::transform
を利用します。関連ページとして、同次変換があります。
#include <opencv2/opencv.hpp>
#include <iostream>
int main(){
cv::Mat src = cv::imread("aaa.png", -1);
if(src.empty()) {
return -1;
}
// アフィン変換を、三点の移動を指定して決めます。
cv::Point2f srcTri[3];
cv::Point2f dstTri[3];
srcTri[0] = cv::Point2f(0, 0);
srcTri[1] = cv::Point2f(1, 0);
srcTri[2] = cv::Point2f(0, 1);
dstTri[0] = cv::Point2f(0, 0);
dstTri[1] = cv::Point2f(2, 0);
dstTri[2] = cv::Point2f(0, 1);
cv::Mat T1 = cv::getAffineTransform(srcTri, dstTri);
// アフィン変換をより直感的に指定することもできます。
cv::Point center = cv::Point(src.cols/2, src.rows/2);
double angle = 45;
double scale = 1.0;
cv::Mat T2 = cv::getRotationMatrix2D(center, angle, scale);
// 画像の各点をアフィン変換してみます。
cv::Mat dst1 = cv::Mat::zeros(src.rows, src.cols, src.type());
cv::Mat dst2 = cv::Mat::zeros(src.rows, src.cols, src.type());
warpAffine(src, dst1, T1, dst1.size());
warpAffine(src, dst2, T2, dst2.size());
// 単体の点をアフィン変換してみます。
std::vector<cv::Point2f> v1 = {cv::Point2f(0, 0), cv::Point2f(100, 100)}, v2;
cv::transform(v1, v2, T2);
std::cout << T2 << std::endl;
std::cout << v1 << std::endl; // [0, 0; 100, 100]
std::cout << v2 << std::endl; // [-41.421356, 100; 100, 100]
cv::imshow("src", src);
cv::imshow("dst1", dst1);
cv::imshow("dst2", dst2);
cv::waitKey(0);
return 0;
}
アフィン変換よりも自由度が 2 だけ高い変換として透視変換 (ホモグラフィ) があります。アフィン変換は透視変換の特殊な場合です。平面内の透視変換は 3x3 行列 $H$ で表現できます。線形変換ではなく、変換の最後に除算が必要になります。この除算のために 3x3 行列ですが、変数の個数は 8 になります。4 点について変換前後の座標が分かれば変換行列が一意に定まるということになります。
$$s \begin{bmatrix} x' \\ y' \\ 1 \end{bmatrix} \ = H \begin{bmatrix} x \\ y \\ 1 \end{bmatrix} $$
$$dst(x, y) = src\Bigl(\ \frac{H_{00} x + H_{01} y + H_{02}}{H_{20} x + H_{21} y + H_{22}}, \ \frac{H_{10} x + H_{11} y + H_{12}}{H_{20} x + H_{21} y + H_{22}} \ \Bigr) $$
cv::transform
では除算が表現できないため、画像ではなく個別の点を変換したい場合は cv::perspectiveTransform
を利用します。
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
cv::Mat src = cv::imread("aaa.png", -1);
// 透視変換を、4点の移動を指定して決めます。
std::vector<cv::Point2f> pts_src;
std::vector<cv::Point2f> pts_dst;
pts_src.push_back(cv::Point2f(0, 0));
pts_src.push_back(cv::Point2f(200, 0));
pts_src.push_back(cv::Point2f(0, 200));
pts_src.push_back(cv::Point2f(200, 200));
pts_dst.push_back(cv::Point2f(50, 0));
pts_dst.push_back(cv::Point2f(150, 0));
pts_dst.push_back(cv::Point2f(0, 200));
pts_dst.push_back(cv::Point2f(200, 200));
cv::Mat h = cv::getPerspectiveTransform(pts_src, pts_dst);
// cv::Mat h = cv::findHomography(pts_src, pts_dst);
// 画像の各点を透視変換してみます。
cv::Mat dst = cv::Mat::zeros(src.rows, src.cols, src.type());
cv::warpPerspective(src, dst, h, dst.size());
// 単体の点を透視変換してみます。
std::vector<cv::Point2f> v1 = {cv::Point2f(0, 0), cv::Point2f(100, 100)}, v2;
cv::perspectiveTransform(v1, v2, h);
std::cout << h << std::endl;
std::cout << v1 << std::endl; // [0, 0; 100, 100]
std::cout << v2 << std::endl; // [50, 3.5527137e-15; 100, 66.666664]
cv::imshow("src", src);
cv::imshow("dst", dst);
cv::waitKey(0);
return 0;
}
4点よりも多い変換前後の座標が分かっている場合は cv::getPerspectiveTransform ではなく cv::findHomography を利用すると、変換前後の座標についてノイズが含まれている場合にも対応できます。
修復対象が太すぎず、周囲に修復のために十分な情報が残っている場合に機能します。
import numpy as np
import cv2 as cv
mask = np.zeros((200, 200, 3), dtype=np.uint8)
cv.putText(mask, 'Hello World!', (10, 100), cv.FONT_HERSHEY_SIMPLEX, 1.5, (255,255,255), 1)
cv.imwrite('mask.png', mask)
img = cv.imread('aaa.png', cv.IMREAD_COLOR)
cv.putText(img, 'Hello World!', (10, 100), cv.FONT_HERSHEY_SIMPLEX, 1.5, (255,255,255), 1)
cv.imwrite('bbb.png', img)
以下のように修復できます。
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
cv::Mat src = cv::imread("bbb.png", cv::IMREAD_COLOR);
cv::Mat mask = cv::imread("mask.png", cv::IMREAD_GRAYSCALE);
cv::Mat dst = cv::Mat::zeros(src.rows, src.cols, src.type());
cv::inpaint(src, mask, dst, 3, cv::INPAINT_TELEA);
// cv::inpaint(src, mask, dst, 3, cv::INPAINT_NS);
cv::imshow("src", src);
cv::imshow("mask", mask);
cv::imshow("dst", dst);
cv::waitKey(0);
return 0;
}
グレースケール画像の全ピクセルについて画素値 (例えば 0-255) をヒストグラムにすると、ある一定の領域に集中している場合があります。cv::equalizeHist
を用いると、これを平坦化してコントラストを調整することができます。
画像によっては、cv::createCLAHE
を用いて、例えば 8x8 の領域毎に分けて同様の処理を行った方が良い場合もあります。
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
cv::Mat src = cv::imread("ccc.png", cv::IMREAD_COLOR);
cv::Mat dst, dst2;
cv::cvtColor(src, src, cv::COLOR_BGR2GRAY);
cv::equalizeHist(src, dst);
cv::Ptr<cv::CLAHE> clahe = cv::createCLAHE(2.0, cv::Size(8,8));
clahe->apply(src, dst2);
cv::imshow("src", src);
cv::imshow("dst", dst);
cv::imshow("dst2", dst2);
cv::waitKey(0);
return 0;
}
手書き数字の分類などで、事前処理として画像の傾きを補正する必要がある場合があります。
#include <opencv2/opencv.hpp>
#include <iostream>
int SZ = 43;
cv::Mat deskew(cv::Mat& img) {
cv::Moments m = cv::moments(img);
if(abs(m.mu02) < 1e-2) {
return img.clone();
}
float skew = m.mu11 / m.mu02;
cv::Mat warpMat = (cv::Mat_<float>(2,3) << 1, skew, -0.5 * SZ * skew, 0, 1, 0);
cv::Mat imgOut = cv::Mat::zeros(img.rows, img.cols, img.type());
cv::warpAffine(img, imgOut, warpMat, imgOut.size(), cv::WARP_INVERSE_MAP | cv::INTER_LINEAR);
return imgOut;
}
int main() {
cv::Mat src = cv::imread("bbb.png", cv::IMREAD_GRAYSCALE);
std::cout << src.size() << std::endl; //=> [43 x 43]
cv::Mat dst = deskew(src);
cv::imshow("src", src);
cv::imshow("dst", dst);
cv::waitKey(0);
return 0;
}
画像のモーメント $m_{p,q}$ を cv::moments
で計算して傾き具合い skew を計算します。
$$m_{p,q} = \sum^{N}_{i=1} I(x_i, y_i) x^p y^q $$
skew に応じて、画像内の各点が x 軸方向に平行移動するようにアフィン変換行列を指定します。
cv::Canny と同様に、最初に平滑化を行ってから微分してエッジ検出を行います。
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
cv::Mat img = cv::imread("aaa.png", cv::IMREAD_COLOR);
// 微分を行った際にノイズが発生しないようにするために、平滑化を行います。
cv::GaussianBlur(img, img, cv::Size(3, 3), 0, 0, cv::BORDER_DEFAULT);
// グレースケールに変換します。
cv::Mat gray;
cv::cvtColor(img, gray, cv::COLOR_BGR2GRAY);
// x軸方向、y軸方向に一次微分を行います。
// 結果は CV_16S 符号付きの16ビット整数です。
// -32768 〜 32767
cv::Mat grad_x, grad_y;
cv::Sobel(gray, grad_x, CV_16S, 1, 0, -1);
cv::Sobel(gray, grad_y, CV_16S, 0, 1, -1);
// CV_8U 符号なしの8ビット整数 0 〜 256 に変換します。
cv::Mat abs_grad_x, abs_grad_y;
cv::convertScaleAbs(grad_x, abs_grad_x);
cv::convertScaleAbs(grad_y, abs_grad_y);
// x軸方向、y軸方向の微分の絶対値について、同じ重みを付けて足し合わせます。
cv::Mat grad;
cv::addWeighted(abs_grad_x, 0.5, abs_grad_y, 0.5, 0, grad);
imshow("Sobel Simple Edge Detector", grad);
cv::waitKey(0);
return 0;
}
モルフォロジー変換は畳み込み処理の一つです。周囲のピクセルの画素値の Min を取るように畳み込む収縮 Erosion と、Max を取るように畳み込む膨張 Dilation の二つが基本となります。それら二つを組み合わせる処理として、例えばモルフォロジー勾配があります。膨張した画像と収縮した画像の差分を取ることで、物体の境界線が得られます。オープニングやクロージングによって斑点ノイズを除去することもできます。
#include <opencv2/opencv.hpp>
int main() {
cv::Mat src = cv::imread("aaa.png", cv::IMREAD_COLOR);
cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3, 3));
cv::Mat erosion_dst;
cv::erode(src, erosion_dst, kernel);
cv::Mat dilation_dst;
cv::dilate(src, dilation_dst, kernel);
cv::Mat gradient_dst;
cv::morphologyEx(src, gradient_dst, cv::MORPH_GRADIENT, kernel);
cv::imshow("src", src);
cv::imshow("erosion", erosion_dst);
cv::imshow("dilation", dilation_dst);
cv::imshow("gradient", gradient_dst);
cv::waitKey(0);
return 0;
}
カーネルのサイズを変更すると、膨張および収縮の度合いを調整できます。