C++でマークシート読み取れるフリーソフトを自作してみた!


初めて投稿させていただきます。個人の作成記録のメモですが、記事中に出てくる疑問点に関してコメントいただけると幸いです。

マークシート等を読み取れるCell-Sheetというソフトを作成してみました。こんな感じのシートを読み取れます。端っこの「○」は全てマーカーです。https://cell-sheet.com からご利用ください。

動機

アンケート集計をする機会があり、無料で使えるマークシート処理ソフトを探しました。その結果、こちらのブログを見つけ、神奈川県立総合教育センターで配布されているフリーソフトを入手しました。こちらはかなり使いやすかったのですが、他のソフトはExcelのマクロを使ったり、有料だったりといまいち初心者には使い勝手が悪かったので、「誰にも簡単に使えるようなマークシート処理システム」を開発することを目標にしました。

現在までの経緯

「誰にも簡単に使えるようなマークシート処理システム」を開発する上で、最初はインストールなしで実行できるWebサービスを目指し、RubyとPHPを学習して挑戦しました。しかし、OpenCVのインストールやフレームワー関係のエラーを解決することに挫折し(再挑戦したいと思いますので、参考になるサイト等をご教授氏くくださると助かります)、結果、C#とC++によるWindowsアプリケーションとなりました。

1、Ruby on railsで作ろう!
→フレームワーク関係のエラーで挫折。

2、PHPで作ろう!(きっと、Rubyよりプログラミングっぽいことをできるはず)
→OpenCVの環境構築が分からない…→挫折。

3、まずは、使い慣れたC++で作ってみよう!
→とりあえず、考えたアルゴリズムがきちんと動作するかを確かめるため、使い慣れたC++で作成。
→とりあえず、完成。

4、このC++のプログラム、Linux(CentOS)で動かないかな?
→Linuxに関して全くの初心者であり、Conohaの512MBをレンタルし、CentOSにコンパイラを入れてHello,world!!と表示するだけのプログラムは動かせました。しかし、CentOS7にOpenCVをインストールするを参考にして頑張りましたが、OpenCVを使ったプログラムの実行ができませんでした。(これに関しては、なぜうまくいかないのか、未だにわかりません…涙)

5、とりあえず、ダウンロード版で作ろう!
→コンソールだと使いづらいので、C#でWindowsフォームアプリケーションを作りました。

6、中途半端だけど、公開

当初思い描いていたのとはかなり異なる形となってしまい、ダウンロードしないと使えませんが、なんとか形になりました。まだまだ作成途中なので、動作しないボタンなどもあります。

Cell-Sheetの特徴

・マーカーを検知し、シート全体をセルとして升目分けする。
・シートごとにCSVファイルで形式を作ってあげれば、いろいろな種類のシートを作成できる。

マークシート処理の大まかな流れ

1、OpenCVで画素アクセスし、配列に読み込む。(OpenCVはここにしか使っていないので、OpenCVより簡単なライブラリや標準搭載のもの、ありませんかね?)

2、マークシートの位置マーカーを取得する。

3、マーカーを並び替え、Cellごとの座標とそのCellにアクセスするためのベクトルを取得する。

4、シートの書式ファイル(CSV形式)を読み取り、読み取るべき箇所をList化する。

5、Listに基づき、シートの指定箇所がマークされているかを判定する。

6、結果を書き出す

実際のプログラム

1、OpenCVを利用し、vector>型のbitmapに画像を読み込む。この際、画像サイズが長辺が3506になるようにする。(この処理のため、現在はA4サイズのシートしか読み取り不可能。マーカーのサイズを固定し、高速化するための処置です。)

cell-sheet.cpp
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <vector>

//・・・中略・・・

    vector<vector<char>> bitmap;    //セルシートのドットマップ

    //OpenCVへ読み込み
    cv_img = cv::imread(fname.c_str(), 1);
    if (cv_img.data == NULL) return 0;

    //画像サイズを取得
    img_height = cv_img.rows;
    img_width = cv_img.cols;

    //画像が大きすぎる
    if (img_height > MaxSheetH) return 0;
    if (img_height > MaxSheetH) return 0;

    //今回、画像のサイズをこの時点で変更。大きいほうを3506に。

    int L = 3506;
    int S = 0;
    float bl;

    int H;
    int W;

    if (img_height > img_width){
        bl = (float)((float)img_height / (float)L);
        S = (int)((float)img_width * bl);

        H = L;
        W = S;
    }
    else {
        bl = (float)((float)img_width / (float)L);
        S = (int)((float)img_height * bl);

        W = L;
        H = S;
    }

    //白黒画像化の処理
    for (int iy = 0; iy < H; iy++){
        vector<char> row;
        for (int ix = 0; ix < W; ix++){
            int iix = (int)((float)(ix * bl) + 0.5f);
            int iiy = (int)((float)(iy * bl) + 0.5f);

            if (iix >= img_width || iiy >= img_height) continue;
            if (iix < 0 || iiy < 0) continue;

            //画素アクセス
            int b = cv_img.at<cv::Vec3b>(iiy, iix)[0];
            int g = cv_img.at<cv::Vec3b>(iiy, iix)[1];
            int r = cv_img.at<cv::Vec3b>(iiy, iix)[2];

            //現在は 0 or 255 です。

            if (b < JudgmentBlack_B && g < JudgmentBlack_G && r < JudgmentBlack_R){
                row.push_back(255);
            }
            else {
                row.push_back(0);
            }

            //ここまで、今後改良予定。
        }
        bitmap.push_back(row);
    }

//・・・後略・・・

2、マーカー画像(A4のスキャンデータよりも、もちろん小さい)をすべての座標にて一致率を求める。また、近くでで現在調べている座標よりもマーカー一致率の高いところがあれば、ここはマーカーではないと判断。

以下、一致率を求める関数。この関数はかなり呼び出されます。全ての画素を見なくすると早くなるのかな?

cell-sheet.cpp

//・・・前略・・・

int cell_sheet::MarkerMatchRate(int x, int y, MarkerModel *markermodel){

    //初期設定
    int match = 0;
    int counter = 0;
    int r = markermodel->r;

    //エラー判定
    if (MaxMarkerSize % r != 0) return -1;

    //検索範囲を設定
    int sx = x - r / 2;
    int sy = y - r / 2;
    int ex = x + r / 2;
    int ey = y + r / 2;

    if (sx < 0) return -1;
    if (sy < 0) return -1;
    if (ex >= img_width) return -1;
    if (ey >= img_height) return -1;

    //一致箇所をカウント
    for (int iy = sy; iy < ey; iy++){
        for (int ix = sx; ix < ex; ix++){

            counter++;

            //一致しているか?
            if (bitmap[iy][ix] == markermodel->bitmap[iy - sy][ix - sx]) match++;

        }
    }

    //四捨五入して、一致率を算出。
    int rate = (int)((float)(match) * 1000.0f / (float)(counter));

    return rate;
}

//・・・後略・・・

3、マーカーを並び替え、Cellごとの座標とそのCellにアクセスするためのベクトルを取得する。

縦の○の列と横の○の列が交わる○を原点として、座標化しました。原点からそれぞれのマーカーまでを位置ベクトルとして取得し、縦に2y個、横に2x個分進んだ際のセルの座標を取得します。

後は、指定した個所を読み取るのみですので、省略します。

最後に

マーカーの位置ベクトル化によるマークシート読み取りがここまで簡単にできるとは思っていませんでした。エラー処理等を大幅に端折っているので、それらは今後の課題とします。https://cell-sheet.com からダウンロードできますので、ぜひ動かしてみてください。アンケート集計などに使っていただけると幸いです(インストール時に警告が出てしまうのが何とも…)。

Webサーバー上で動かせるソフトが作れるよう、頑張って勉強していきたいと思います。

最後までお読みいただきありがとうございました。本文中に記載した疑問点や課題に対するコメントを頂けると助かります。