Python から扱う方法ではなく C++ で OpenCV を扱うためのサンプルコードを記載します。ビルドには cmake を用います。
Debian の場合は以下のコマンドで必要なライブラリがインストールされます。
sudo apt install libopencv-dev
#include <opencv2/opencv.hpp>
int main() {
cv::Mat img = cv::imread("aaa.png", -1);
if(img.empty()) {
return -1;
}
cv::namedWindow("Example", cv::WINDOW_AUTOSIZE);
cv::imshow("Example", img);
cv::waitKey(0);
cv::destroyWindow("Example");
return 0;
}
CMakeLists.txt
cmake_minimum_required(VERSION 3.1)
project( DisplayImage )
find_package( OpenCV REQUIRED )
add_executable( DisplayImage main.cpp )
target_link_libraries( DisplayImage ${OpenCV_LIBS} )
ビルド例
mkdir -p build
cd build/
cmake ..
make
実行例
./DisplayImage
OpenCV は RGB ではなく BGR で画像を処理します。
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
cv::Mat img = cv::imread("aaa.png", -1);
cv::Vec3b bgr = img.at<cv::Vec3b>(10, 0);
unsigned int b = bgr[0];
unsigned int g = bgr[1];
unsigned int r = bgr[2];
std::cout << "(r,g,b) = (" << r << ", " << g << ", " << b << ")" << std::endl;
bgr[0] = 0;
img.at<cv::Vec3b>(10, 0) = bgr;
return 0;
}
出力例
(r,g,b) = (254, 166, 92)
from matplotlib import pyplot as plt
import matplotlib.image as mpimg
img = mpimg.imread('aaa.png')
for i in range(3):
print(img.item(10,0,i) * 255)
254.00000005960464
166.00000530481339
92.00000211596489
#include <opencv2/opencv.hpp>
int main() {
cv::namedWindow("Example", cv::WINDOW_AUTOSIZE);
cv::VideoCapture cap;
cap.open("sample.mov");
cv::Mat frame;
while(true) {
cap >> frame;
if(frame.empty()) {
break;
}
cv::imshow("Example", frame);
if((char)cv::waitKey(33) >= 0) { // wait 33 msec for key
break;
}
}
return 0;
}
#include <opencv2/opencv.hpp>
int main() {
cv::VideoCapture cap;
cap.open("sample.mov");
int frames = (int)cap.get(cv::CAP_PROP_FRAME_COUNT);
int w = (int)cap.get(cv::CAP_PROP_FRAME_WIDTH);
int h = (int)cap.get(cv::CAP_PROP_FRAME_HEIGHT);
std::cout << "frames: " << frames << std::endl;
std::cout << "dimensions: (" << w << ", " << h << ")" << std::endl;
cap.set(cv::CAP_PROP_POS_FRAMES, 123);
int current_pos = (int)cap.get(cv::CAP_PROP_POS_FRAMES);
std::cout << "current_pos: " << current_pos << std::endl;
return 0;
}
出力例
frames: 659
dimensions: (1080, 720)
current_pos: 123
入力画像を 5x5 の領域で走査して、ガウス関数にしたがった重みをつけて各領域内の 25 画素の値を平均した値を、出力画像における 5x5 の領域の中心の画素値とするような、平滑化の変換は以下のようになります。
#include <opencv2/opencv.hpp>
int main() {
cv::Mat in = cv::imread("aaa.png");
cv::namedWindow("Example-in", cv::WINDOW_AUTOSIZE);
cv::namedWindow("Example-out", cv::WINDOW_AUTOSIZE);
cv::imshow("Example-in", in);
cv::Mat out;
cv::GaussianBlur(in, out, cv::Size(5,5), 3, 3);
cv::imshow("Example-out", out);
cv::Mat out2;
cv::GaussianBlur(out, out2, cv::Size(5,5), 3, 3);
cv::imshow("Example-out2", out2);
cv::waitKey(0);
return 0;
}
フィルタとなる 5x5 の行列はカーネルともよばれます。OpenCV にはカーネルを用いて畳み込みを行うカーネル関数が多数実装されており GaussianBlur
はその一つです。変換の性質上、GaussianBlur
を用いる際のカーネルのサイズは奇数である必要があります。第 4,5 引数はガウス関数の x 方向と y 方向の標準偏差です。例えば標準偏差を非常に小さくしたり、カーネルのサイズを 1x1 にしたりすると、出力画像は入力画像とほぼ同じになります。
#include <opencv2/opencv.hpp>
int main() {
cv::Mat in = cv::imread("aaa.png");
cv::namedWindow("Example-in", cv::WINDOW_AUTOSIZE);
cv::namedWindow("Example-out", cv::WINDOW_AUTOSIZE);
cv::imshow("Example-in", in);
cv::Mat out;
cv::GaussianBlur(in, out, cv::Size(5,5), 0.00001, 0.00001);
cv::imshow("Example-out", out);
cv::Mat out2;
cv::GaussianBlur(out, out2, cv::Size(1,1), 3, 3);
cv::imshow("Example-out2", out2);
cv::waitKey(0);
return 0;
}
畳み込み処理におけるカーネル関数としてデルタ関数を用いると、数ピクセル毎にサンプリングを行うことができます。結果として、例えば入力画像の2分の1のサイズの画像を出力することができ、このような変換をダウンサンプリングとよびます。隣接するピクセル同士の画素値の変化について、入力画像よりもダウンサンプリングによる出力画像の方が大きくなってしまう可能性があります。出力画像(信号)に高周波数が入ってしまうことを防ぐためには、ダウンサンプリングする前に、入力画像の平滑化を行います。つまり、入力画像の隣接するピクセル同士の画素値の変化が十分小さくなるような、ローパスフィルタをかけておきます。
cv::pyrDown()
関数を用いると、Gaussian による平滑化とダウンサンプリングを行うことができます。入力画像にダウンサンプリングを繰り返し適用していくと、解像度の異なる画像の集合が得られます。これを、画像ピラミッド、あるいはスケール空間とよびます。pyrDown の pyr はピラミッドを意味します。
#include <opencv2/opencv.hpp>
int main() {
cv::Mat in, out;
cv::namedWindow("Example-in", cv::WINDOW_AUTOSIZE);
cv::namedWindow("Example-out", cv::WINDOW_AUTOSIZE);
in = cv::imread("aaa.png");
cv::imshow("Example-in", in);
cv::pyrDown(in, out);
cv::imshow("Example-out", out);
cv::waitKey(0);
return 0;
}
畳み込みは、輪郭線などのエッジを検出するためにも利用されます。隣接するピクセル同士の画素値の変化率を取得するような、微分を行うカーネル関数を用いれば、エッジにおける画素値の変化率が大きいという仮定のもと、エッジを検出できます。こちらのページに記載の cv::Canny
はエッジ検出アルゴリズムの一つです。エッジ検出はノイズの影響を受けやすいため、内部的に Gaussian フィルタを用いて平滑化してからエッジ検出します。第3引数の閾値よりも変化率が小さい画素はエッジではないとします。第4引数の閾値よりも変化率が大きい画素はエッジであるとします。更に、エッジは連続しているという仮定のもと、変化率の大きさが第3引数と第4引数の間の画素は、第4引数の閾値よりも変化率が大きい画素と連続していればエッジであるとします。cv::Canny
への入力画像のチャネル数は一つでよいため cv::cvtColor
でグレー画像に変換 (cvt; convert) します。
#include <opencv2/opencv.hpp>
int main() {
cv::Mat rgb, gry, cny;
cv::namedWindow("Example Gray", cv::WINDOW_AUTOSIZE);
cv::namedWindow("Example Canny", cv::WINDOW_AUTOSIZE);
rgb = cv::imread("aaa.png");
cv::cvtColor(rgb, gry, cv::COLOR_BGR2GRAY);
cv::imshow("Example Gray", gry);
cv::Canny(gry, cny, 10, 100);
cv::imshow("Example Canny", cny);
cv::waitKey(0);
return 0;
}
入力動画と同じサイズの動画を出力する例です。上記 cv::Canny
エッジ検出アルゴリズムで各フレームを変換しています。キーコード 27 は Esc です。動画の拡張子 avi
に対応するコーデックは複数存在します。以下では XVID を指定しています。avi
に対応するものとして、他に例えば MJPG (モーション JPG) があります。動画のコーデックは以下のように確認できます。
$ file out.avi
out.avi: RIFF (little-endian) data, AVI, 1080 x 720, >30 fps, video: XviD
$ file out.avi
out.avi: RIFF (little-endian) data, AVI, 1080 x 720, 30.00 fps, video: Motion JPEG
main.cpp
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
cv::namedWindow("Example-in", cv::WINDOW_AUTOSIZE);
cv::namedWindow("Example-out", cv::WINDOW_AUTOSIZE);
cv::VideoCapture capture("sample.mov");
double fps = capture.get(cv::CAP_PROP_FPS);
cv::Size size((int)capture.get(cv::CAP_PROP_FRAME_WIDTH),
(int)capture.get(cv::CAP_PROP_FRAME_HEIGHT));
cv::VideoWriter writer;
writer.open("out.avi", CV_FOURCC('X', 'V', 'I', 'D'), fps, size, false);
cv::Mat bgr, gry, cny;
while(true) {
capture >> bgr;
if(bgr.empty()) {
break;
}
cv::imshow("Example-in", bgr);
cv::cvtColor(bgr, gry, cv::COLOR_BGR2GRAY);
cv::Canny(gry, cny, 10, 100);
cv::imshow("Example-out", cny);
writer << cny;
char c = (char)cv::waitKey(33);
if(c == 27) {
break;
}
}
writer.release();
capture.release();
return 0;
}