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() {
::Mat image = cv::imread("/somewhere/sample.jpg");
cvif (image.empty()) {
std::cerr << "画像が読み込めませんでした。" << std::endl;
return -1;
}
::imshow("表示テスト", image);
cv::waitKey(0);
cvreturn 0;
}
CLion でビルド(⌘F9)&実行(^R)すると画像ウィンドウが表示され、画像が表示されます。

LESSON 2. カラー画像 → グレースケール → 保存
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
// 入力画像ファイルの読み込み
::Mat colorImage = cv::imread("/somewhere/sample.jpg");
cvif (colorImage.empty()) {
std::cerr << "画像が読み込めません。" << std::endl;
return -1;
}
// グレースケール変換
::Mat grayImage;
cv::cvtColor(colorImage, grayImage, cv::COLOR_BGR2GRAY);
cv
// グレースケール画像を保存
bool result = cv::imwrite("gray_sample.jpg", grayImage);
if (!result) {
std::cerr << "保存に失敗しました。" << std::endl;
return -1;
}
std::cout << "グレースケール画像を保存しました。" << std::endl;
::imshow("表示テスト", grayImage);
cv::waitKey(0);
cvreturn 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() {
// 入力画像を読み込み
::Mat colorImage = cv::imread("/somewhere/sample.jpg");
cvif (colorImage.empty()) {
std::cerr << "画像が読み込めません。" << std::endl;
return -1;
}
// グレースケールに変換
::Mat grayImage;
cv::cvtColor(colorImage, grayImage, cv::COLOR_BGR2GRAY);
cv
// ノイズ除去(Canny前にぼかすと良い結果が得られる)
::Mat blurred;
cv::GaussianBlur(grayImage, blurred, cv::Size(5, 5), 1.4);
cv
// エッジ検出(Canny)
::Mat edgeImage;
cv::Canny(blurred, edgeImage, 50, 150); // しきい値は適宜調整
cv
// エッジ画像を保存
bool result = cv::imwrite("edges.jpg", edgeImage);
if (!result) {
std::cerr << "エッジ画像の保存に失敗しました。" << std::endl;
return -1;
}
std::cout << "エッジ画像を保存しました。" << std::endl;
// ウィンドウで表示(必要なら)
::imshow("Edges", edgeImage);
cv::waitKey(0);
cv
return 0;
}

LESSON 4. レシートの四隅を検出してマスクする
- 輪郭をすべて抽出
- 面積の大きい輪郭を対象に
- cv::approxPolyDP で四角形(四隅)に近似
- cv::fillConvexPoly を使ってマスク作成
- 元画像にマスクを適用して透明部分を生成
#include <opencv2/opencv.hpp>
#include <iostream>
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
// 入力画像を読み込み
::Mat colorImage = cv::imread("/somewhere/sample.jpg");
cvif (colorImage.empty()) {
std::cerr << "画像が読み込めません。" << std::endl;
return -1;
}
// グレースケール → ぼかし → 二値化(エッジ不要)
::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);
cv
// エッジ検出
std::vector<std::vector<cv::Point>> contours;
::findContours(binary, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
cv
std::vector<cv::Point> receiptContour;
for (const auto& contour : contours) {
double area = cv::contourArea(contour);
if (area < 10000) continue; // 小さな輪郭を無視
std::vector<cv::Point> approx;
::approxPolyDP(contour, approx, cv::arcLength(contour, true) * 0.02, true);
cv
if (approx.size() == 4 && cv::isContourConvex(approx)) {
= approx;
receiptContour break;
}
}
if (receiptContour.empty()) {
std::cerr << "四角形の輪郭が見つかりませんでした。" << std::endl;
return -1;
}
// 四隅マスク作成(白=不透明、黒=透明)
::Mat mask = cv::Mat::zeros(colorImage.size(), CV_8UC1);
cv::fillConvexPoly(mask, receiptContour, 255);
cv::imwrite("5_mask.png", mask);
cv
// RGBA画像を生成(透明化)
::Mat output(colorImage.size(), CV_8UC4);
cvfor (int y = 0; y < colorImage.rows; ++y) {
for (int x = 0; x < colorImage.cols; ++x) {
::Vec3b color = colorImage.at<cv::Vec3b>(y, x);
cvuchar alpha = mask.at<uchar>(y, x);
.at<cv::Vec4b>(y, x) = cv::Vec4b(color[0], color[1], color[2], alpha);
output}
}
// 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の処理も分かるようになってきた。
今回の、レシートの画像から四隅を見つけて、まっすぐトリミングするっていう処理。人間がやるなら、まずレシートを見つけて、その輪郭に合わせて四角い枠を作って、それを画像に当てはめて……って、けっこう面倒。でも今では、それをプログラムが自動でやってくれる。しかも一瞬で。
こういう強力なツールが、普通の開発者の手にある時代ってのがすごい。昔は一部の専門家だけが扱えた技術が、今では当たり前のように使える。ツールが増えるたびに、自分の手札が増えていく感じがして、ちょっと楽しい。
関連記事
- 【Python】OpenCVで画像操作いろいろ(グレースケール・モノ・輪郭抽出・切り抜く・透過)
- 【Python】OpenCVで画像をアフィン変換【移動・拡大・回転・剪断】
- 【Python】OpenCVでコーナーの検出【Harris/Shi-Tomasi】
- 【Python】OpenCVで図形の描画からアニメーションまで【線・四角・丸・塗りつぶし】
- 【Python】OpenCVで特徴点の追跡【メダカの軌跡】