C++でOpenCVをはじめる|撮影したレシートを自動でトリミング
はじめに
opencvを使って画像を処理するプログラミングを練習していきます。撮影したレシートを自動でトリミングできることをゴールとします。
- OpenCV4(C++版)を使います
- c++ 11を使います
- macOSでコンパイルします
- Jetbrains社のIDE CLionを使います
OpenCVのインストール
項目 | インストールコマンド |
---|---|
最新版 | brew install opencv |
バージョン4 | brew install opencv@4 |
ダウンロードとビルドに、それなりの時間がかかるので気長に待ちましょう。
プロジェクトの準備
プロジェクト構造
CLion
でC++11
のプロジェクトを作成します。C++11
をあえて選んでいるのは、iOSアプリへの移植も考慮に入れたいからです。
tree -I cmake-build-debug
.
├── CMakeLists.txt
├── main.cpp
└── sample.jpg
CMakeLists.txt
CMakeLists.txt の設定は以下です:
cmake_minimum_required(VERSION 3.28)
project(resitori_core)
set(CMAKE_CXX_STANDARD 11)
find_package(OpenCV REQUIRED) # 追加
include_directories(${OpenCV_INCLUDE_DIRS}) # 追加
add_executable(resitori_core main.cpp)
target_link_libraries(resitori_core ${OpenCV_LIBS}) # 追加
OpenCVのパスをCLionに設定しておく
プログラミング中に #include <opencv2/opencv.hpp>
を読み込めるようにするため、CLionでは
Preferences > Build, Execution, Deployment > CMake > CMake options
に以下を追加しておきます:
-DCMAKE_PREFIX_PATH=/opt/homebrew/opt/opencv
インストールしたOpenCVの場所が分からない場合は、次のコマンドで確認:
brew --prefix opencv
LESSON 1. 画像を読み込む
手始めに、opencvを使って画像を読み込んでみます。
main.cpp
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
cv::Mat image = cv::imread("/somewhere/sample.jpg");
if (image.empty()) {
std::cerr << "画像が読み込めませんでした。" << std::endl;
return -1;
}
cv::imshow("表示テスト", image);
cv::waitKey(0);
return 0;
}
CLion
でビルド(⌘F9)&実行(^R)すると画像ウィンドウが表示され、画像が表示されます。
LESSON 2. カラー画像 → グレースケール → 保存
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
// 入力画像ファイルの読み込み
cv::Mat colorImage = cv::imread("/somewhere/sample.jpg");
if (colorImage.empty()) {
std::cerr << "画像が読み込めません。" << std::endl;
return -1;
}
// グレースケール変換
cv::Mat grayImage;
cv::cvtColor(colorImage, grayImage, cv::COLOR_BGR2GRAY);
// グレースケール画像を保存
bool result = cv::imwrite("gray_sample.jpg", grayImage);
if (!result) {
std::cerr << "保存に失敗しました。" << std::endl;
return -1;
}
std::cout << "グレースケール画像を保存しました。" << std::endl;
cv::imshow("表示テスト", grayImage);
cv::waitKey(0);
return 0;
}
CLion
でビルド(⌘F9)&実行(^R)すると、グレースケール化された画像が、cmake-build-debug
ディレクトリ内へ保存されます。
LESSON 3. Canny法でエッジ検出
OpenCV でエッジ検出を行うには、一般的に Canny法(Canny Edge Detection) を使います。以下に、カラー画像 → グレースケール → エッジ検出 → 保存 という流れのコードを示します。
#include <opencv2/opencv.hpp>
#include <iostream>
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
// 入力画像を読み込み
cv::Mat colorImage = cv::imread("/somewhere/sample.jpg");
if (colorImage.empty()) {
std::cerr << "画像が読み込めません。" << std::endl;
return -1;
}
// グレースケールに変換
cv::Mat grayImage;
cv::cvtColor(colorImage, grayImage, cv::COLOR_BGR2GRAY);
// ノイズ除去(Canny前にぼかすと良い結果が得られる)
cv::Mat blurred;
cv::GaussianBlur(grayImage, blurred, cv::Size(5, 5), 1.4);
// エッジ検出(Canny)
cv::Mat edgeImage;
cv::Canny(blurred, edgeImage, 50, 150); // しきい値は適宜調整
// エッジ画像を保存
bool result = cv::imwrite("edges.jpg", edgeImage);
if (!result) {
std::cerr << "エッジ画像の保存に失敗しました。" << std::endl;
return -1;
}
std::cout << "エッジ画像を保存しました。" << std::endl;
// ウィンドウで表示(必要なら)
cv::imshow("Edges", edgeImage);
cv::waitKey(0);
return 0;
}
LESSON 4. レシートの四隅を検出してマスクする
- 輪郭をすべて抽出
- 面積の大きい輪郭を対象に
- cv::approxPolyDP で四角形(四隅)に近似
- cv::fillConvexPoly を使ってマスク作成
- 元画像にマスクを適用して透明部分を生成
#include <opencv2/opencv.hpp>
#include <iostream>
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
// 入力画像を読み込み
cv::Mat colorImage = cv::imread("/somewhere/sample.jpg");
if (colorImage.empty()) {
std::cerr << "画像が読み込めません。" << std::endl;
return -1;
}
// グレースケール → ぼかし → 二値化(エッジ不要)
cv::Mat gray, blurred, binary;
cv::cvtColor(colorImage, gray, cv::COLOR_BGR2GRAY);
cv::imwrite("1_gray.jpg", gray);
cv::GaussianBlur(gray, blurred, cv::Size(5, 5), 0);
cv::imwrite("2_blurred.jpg", blurred);
cv::threshold(blurred, binary, 0, 255, cv::THRESH_BINARY + cv::THRESH_OTSU);
cv::imwrite("3_binary.jpg", binary);
// エッジ検出
std::vector<std::vector<cv::Point>> contours;
cv::findContours(binary, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
std::vector<cv::Point> receiptContour;
for (const auto& contour : contours) {
double area = cv::contourArea(contour);
if (area < 10000) continue; // 小さな輪郭を無視
std::vector<cv::Point> approx;
cv::approxPolyDP(contour, approx, cv::arcLength(contour, true) * 0.02, true);
if (approx.size() == 4 && cv::isContourConvex(approx)) {
receiptContour = approx;
break;
}
}
if (receiptContour.empty()) {
std::cerr << "四角形の輪郭が見つかりませんでした。" << std::endl;
return -1;
}
// 四隅マスク作成(白=不透明、黒=透明)
cv::Mat mask = cv::Mat::zeros(colorImage.size(), CV_8UC1);
cv::fillConvexPoly(mask, receiptContour, 255);
cv::imwrite("5_mask.png", mask);
// RGBA画像を生成(透明化)
cv::Mat output(colorImage.size(), CV_8UC4);
for (int y = 0; y < colorImage.rows; ++y) {
for (int x = 0; x < colorImage.cols; ++x) {
cv::Vec3b color = colorImage.at<cv::Vec3b>(y, x);
uchar alpha = mask.at<uchar>(y, x);
output.at<cv::Vec4b>(y, x) = cv::Vec4b(color[0], color[1], color[2], alpha);
}
}
// PNG形式で保存(透過可能)
bool saved = cv::imwrite("receipt_cropped.png", output);
if (!saved) {
std::cerr << "画像の保存に失敗しました。" << std::endl;
return -1;
}
std::cout << "透過画像として保存しました。" << std::endl;
return 0;
}
画像処理から見える「ちいさな進化」
OpenCVを使って、レシートの四角い形をちゃんと見つけて、背景をマスクで切り取り、最後はきれいにトリミングするところまでできた。ChatGPTの貢献は大きいがが、こういうのが思ったとおりに動くと、やっぱり嬉しい。
この作業をしていて、ふと思ったことがある。「人類って、確実に進んでるよな」と。もちろん、それが良い方向なのか悪い方向なのかは簡単には言えない。でも、少なくともコンピュータの世界では、ちゃんと前に進んでいる。その証拠のひとつが、まさにOpenCVみたいなツールなんだと思う。
こういう地味な進化って、あんまり話題にならないけど、実はすごいことなんじゃないかと思う。OpenCVが登場し始めた頃、自分はまだ全然プログラミングもわかってなくて、「なんかすごそうだけど、難しそうだなあ」としか思えなかった。でも今は、C++にも少し慣れて、OpenCVの処理も分かるようになってきた。
今回の、レシートの画像から四隅を見つけて、まっすぐトリミングするっていう処理。人間がやるなら、まずレシートを見つけて、その輪郭に合わせて四角い枠を作って、それを画像に当てはめて……って、けっこう面倒。でも今では、それをプログラムが自動でやってくれる。しかも一瞬で。
こういう強力なツールが、普通の開発者の手にある時代ってのがすごい。昔は一部の専門家だけが扱えた技術が、今では当たり前のように使える。ツールが増えるたびに、自分の手札が増えていく感じがして、ちょっと楽しい。