サイトロゴ

C++でOpenCVをはじめる|撮影したレシートを自動でトリミング

著者画像
Toshihiko Arai

はじめに

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. レシートの四隅を検出してマスクする

  1. 輪郭をすべて抽出
  2. 面積の大きい輪郭を対象に
  3. cv::approxPolyDP で四角形(四隅)に近似
  4. cv::fillConvexPoly を使ってマスク作成
  5. 元画像にマスクを適用して透明部分を生成

#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の処理も分かるようになってきた。

今回の、レシートの画像から四隅を見つけて、まっすぐトリミングするっていう処理。人間がやるなら、まずレシートを見つけて、その輪郭に合わせて四角い枠を作って、それを画像に当てはめて……って、けっこう面倒。でも今では、それをプログラムが自動でやってくれる。しかも一瞬で。

こういう強力なツールが、普通の開発者の手にある時代ってのがすごい。昔は一部の専門家だけが扱えた技術が、今では当たり前のように使える。ツールが増えるたびに、自分の手札が増えていく感じがして、ちょっと楽しい。

関連記事