Mugichoko's blog

Mugichoko’s blog

プログラミングを中心としたメモ書き.

OpenCV小技集

個人的に便利だと感じているOpenCVの小技集です.私が学生の頃にいた研究室で後輩に逐一教えるのが面倒だったのでまとめたものです.OpenCV 2.X以上を想定しています.

画像の一部切り出し

f:id:Mugichoko:20180405045147j:plain:w300

画像全体から一部だけ切り出したい場合がある.主に画像の中である大きさの窓を設けたい(テンプレートマッチング等に利用)といった場合です.このときに,いちいち画像の一部分だけど別のcv::Matにディープコピーしていてはメモリの無駄だし,処理速度も落ちるため,単に参照するだけにしたい.

下記の例では,lena.pngを読み込み,その画像の左上にある原点から (240, 235) の位置を基準に,幅55画素,高さ60画素を切り出して表示する.

#include <opencv2/opencv.hpp>
 
int main()
{
    // 画像の読み込み
    cv::Mat imgSrc = cv::imread("./inputData/lena.png");
    // 切り抜く位置と大きさの指定はcv::Rectを用いる
    cv::Rect rect(240, 235, 55, 60);
    // 下記のようにcv::Matを作成すると,一部切り出せる
    cv::Mat imgSub(imgSrc, rect);
    /*
       ※ココで注意!!
       上記の記法はシャローコピーになるため,ディープコピーを行いたい場合は下記を実行すること
       cv::Mat imgSub = cv::Mat(imgSrc, rect).clone();
       この意味が分からない諸君は下記のURLにある「浅いコピーと深いコピー」を参照のこと
       参考: http://opencv.jp/cookbook/opencv_mat.html
       また,imgSubはディープコピーしない限り連続でないため,注意が必要だ
       この意味が分からない諸君は上記のURLにある「連続データと不連続データ」を参照のこと
    */
    // 元画像の表示
    cv::imshow("imgSrc", imgSrc);
    // 切り出した画像の表示
    cv::imshow("imgSub", imgSub);
    cv::waitKey();
 
    return 0;
}

画素の参照

低速でも読みやすい

cv::Matの型が未知であればcv::Mat::atでもいいですが,最初からcv::Matの型が分かっている場合,以下の実装のようにすればもっと簡単かつ可読性を落とすことなく各画素にアクセスする方法があります.

例えば3チャンネルのucharの場合,cv::Mat_<cv::Vec3b> imgのように型を指定してcv::Matをインスタンス化できます.こうした場合,imgにはimg(r, c)のようにアクセスできます.ただし,スピードはcv::Mat::at関数と大差ないと思います.

以下の実装では,画像を1枚読み込み,上記の方法を使って別のcv::Matに移し替えています.

#include <opencv2/opencv.hpp>
 
int main()
{
    // 画像の読み込み
    cv::Mat_<cv::Vec3b> imgSrc = cv::imread("./inputData/lena.png");
    // 出力画像(入力画像と同じ大きさと型のものを用意)
    cv::Mat_<cv::Vec3b> imgDst(imgSrc.rows, imgSrc.cols, imgSrc.type());
 
    // 入力画像を出力画像にコピー
    for (int r = 0; r < imgSrc.rows; ++r)
    {
        for (int c = 0; c < imgSrc.cols; ++c)
        {
            imgDst(r, c) = imgSrc(r, c);
        }
    }
 
    // 入力画像の表示
    cv::imshow("imgSrc", imgSrc);
    // 出力画像の表示
    cv::imshow("imgDst", imgDst);
    cv::waitKey();
 
    return 0;
}

高速だけど読みにくい

cv::Mat::at関数を利用する人を結構見かけますが,結構遅いですよね.高速かつ可読性を極力落とさないという観点から,ここにある「2c」の方法を用いるのが個人的には気に入っています.

以下の実装では,画像を1枚読み込み,上記の方法を使って別のcv::Matに移し替えています.

#include <opencv2/opencv.hpp>
 
int main()
{
    // 画像の読み込み
    cv::Mat imgSrc = cv::imread("./inputData/lena.png");
    // 出力画像(入力画像と同じ大きさと型のものを用意)
    cv::Mat imgDst(imgSrc.rows, imgSrc.cols, imgSrc.type());
 
    // 入力画像を出力画像にコピー
    int ch = imgSrc.channels();
    for (int r = 0; r < imgSrc.rows; ++r) {
        uchar *pSrc = imgSrc.ptr(r);
        uchar *pDst = imgDst.ptr(r);
        // ちなみに,別の型でアクセスするには.ptr<型名>()を使えばよいです
        for (int c = 0; c < imgSrc.cols; ++c) {
            pDst[c * ch + 0] = pSrc[c * ch + 0];
            pDst[c * ch + 1] = pSrc[c * ch + 1];
            pDst[c * ch + 2] = pSrc[c * ch + 2];
        }
    }
 
    // 入力画像の表示
    cv::imshow("imgSrc", imgSrc);
    // 出力画像の表示
    cv::imshow("imgDst", imgDst);
    cv::waitKey();
 
    return 0;
}

m×n行列をN行1列にする

画像処理をしていると,m×n行列の2次元画像画像をN行1列の行列に直したい時がある.ここでは,小技1で説明した「行列が連続でない場合」を想定して2つの方法を実装して,その実行速度を比較します.

#include <iostream>
#include <opencv2/opencv.hpp>
#include "CTimer.h"     // 時間を計測して表示するクラス(下記参照)
 
const int NUM = 50000;  // 5万回繰り返した時の速度を計測する
 
int main()
{
    // m×nの画像(行列)の読み込み
    // ※グレースケール画像を想定
    cv::Mat imgSrc = cv::imread("./inputData/img.png", cv::IMREAD_GRAYSCALE);
 
    // この位置にある画像の一部をN行1列に並べる
    // つまり,この場合N = 64×64 = 4,096である
    cv::Rect rct(10, 10, 64, 64);
 
    // ----- 方法1:clone関数を使って1列に連続にする
    // 利点:実装が容易
    // 欠点:実行速度が遅い(ディープコピーを行うため,メモリ確保の時間がかかる)
    CTimer *timer = new CTimer();
    for (int i = 0; i < NUM; ++i) {
        cv::Mat imgRect = cv::Mat(imgSrc, rct).clone();
        cv::Mat imgDst(imgRect.rows * imgRect.cols, 1, CV_8U);
     
        memcpy(imgDst.data, imgRect.data, sizeof(uchar)*imgDst.rows);
    }
    delete timer;
 
    // ----- 方法2:不連続のまま各行をコピーする
    // 利点:実行速度が速い(シャローコピーを行うため,メモリ確保の時間がかからない)
    // 欠点:方法1よりも実装が難しい(というほどでもないが...)
    timer = new CTimer();
    for (int i = 0; i < NUM; ++i) {
        cv::Mat imgRect = cv::Mat(imgSrc, rct);                 // imgRect自体は連続
        cv::Mat imgDst(imgRect.rows * imgRect.cols, 1, CV_8U);  // こちらが不連続
 
        // 各行をコピーしていく
        // ※memcpyの2つ目の引数を「imgRect.data + r * imgRect.cols」にしないのは,
        //  imgRectの持つimgRect.data自体はimgSrcのimgSrc.dataの一部であるため,
        //  imgRect.colsが実際のステップとは限らないからである
        for (int r = 0; r < imgRect.rows; ++r)
            memcpy(imgDst.data + r * imgRect.cols, imgRect.data + r * imgRect.step, sizeof(uchar) * imgRect.cols);
    }
    delete timer;
 
    return 0;
}

私の実装環境の場合,結果は以下の通りでした.

  • 方法1:56 ms
  • 方法2:33 ms

ちなみに,CTimerクラスは下記の通りです.実験などでもよく使うので,覚えておくとよいかもしれない.

#pragma once
 
#include <iostream>
#include <ctime>
 
class CTimer
{
private:
    clock_t start;
public:
    CTimer()
    {
        this->start = clock();
    }
    ~CTimer()
    {
        std::cout << (clock() - start) / (double)CLOCKS_PER_SEC << std::endl;
    }
};

非圧縮AVIで保存する

連番画像を保存する際に1枚1枚を画像で保存すると,そのデータを移行する際に時間がかかるし,人為的なミスで抜けが出るなどの問題がよく発生する.それを防ぐためにも,非圧縮AVIで保存することをお勧めする.各フレームを画像として保存したいならば,非圧縮AVIから対象となるフレームを取り出して,保存すればよいのです.

#include <iostream>
#include <opencv2/opencv.hpp>
 
int main()
{
    // 15fpsで2枚の画像を表示する非圧縮AVIを生成する
    const string outputPath = "./outputData/Video.avi";
    const double frameRate = 15.0;  // フレームレート
 
    // 画像を2枚読み込む
    cv::Mat frame1 = cv::imread("./inputData/frame1.png", 0);
    cv::Mat frame2 = cv::imread("./inputData/frame2.png", 0);
    // 画像のサイズは同じものを用意する
    assert(frame1.size() == frame2.size());
     
    // cv::VideoWriterクラスを用いて非圧縮AVIを生成する
    // 第2引数に「CV_FOURCC_MACRO('D', 'I', 'B', ' ')」を指定すると非圧縮AVIとなる
    // 他の形式に保存したい場合は,下記を参照のこと
    // 参考:http://docs.opencv.org/modules/highgui/doc/reading_and_writing_images_and_video.html#videowriter
    cv::VideoWriter vidWriter(
        outputPath.c_str(), CV_FOURCC_MACRO('D', 'I', 'B', ' '),
        frameRate, frame1.size(), true);
    // オープンできていなければプログラム終了
    if (!vidWriter.isOpened()) {
        std::cerr << "[Err] Failed to open video writer!!" << std::endl;
        std::cerr << "      " << outputPath << std::endl;
        exit(EXIT_FAILURE);
    }
 
    // 画像をcv::VideoWriterクラスのインスタンスに書き込む
    vidWriter << frame1;
    vidWriter << frame2;
 
    return 0;
}