7 segment digits 自動検出の試み

肝心の正規化相関計算ルーチンの作成と検証

佳境に入ってきました。現時点で作成して基本的な動作確認をしておきましょう。正規化相関計算については、vmapの記事

を参考に願います。図を再掲すると、ncc(normalized cross correlation)は、

この図でfとgが画像の小領域(RectないしROI)として、nccを計算する具体的なルーチンは、

// NCC(正規化相互相関)を計算する関数
// TBitmapは24bit(pf24bit)または32bit(pf32bit)を想定
double CalculateNCC(TBitmap* target, TBitmap* templateImg, int startX, int startY)
{
    // フォーマットを強制(ピクセルアクセスを単純化するためグレースケール化するか、輝度で計算)
    target->PixelFormat = pf32bit;
    templateImg->PixelFormat = pf32bit;

    int tWidth = templateImg->Width;
    int tHeight = templateImg->Height;

    double sumT = 0, sumI = 0;
    double sumTT = 0, sumII = 0;
    double sumTI = 0;
    double N = tWidth * tHeight;

    for (int y = 0; y < tHeight; y++) {
        // ScanLineによる高速ピクセルアクセス
        RGBQUAD* rowT = (RGBQUAD*)templateImg->ScanLine[y];
        RGBQUAD* rowI = (RGBQUAD*)target->ScanLine[startY + y];

        for (int x = 0; x < tWidth; x++) {
            // 輝度(Y)の計算 (簡易的にRGBの平均、または一般的RGB重み付け)
            double valT = (rowT[x].rgbRed + rowT[x].rgbGreen + rowT[x].rgbBlue) / 3.0;
            double valI = (rowI[startX + x].rgbRed + rowI[startX + x].rgbGreen + rowI[startX + x].rgbBlue) / 3.0;

            sumT += valT;
            sumI += valI;
            sumTT += valT * valT;
            sumII += valI * valI;
            sumTI += valT * valI;
        }
    }

    // NCC分子・分母の計算
    double num = sumTI - (sumT * sumI) / N;
    double den = std::sqrt((sumTT - (sumT * sumT) / N) * (sumII - (sumI * sumI) / N));

    if (den == 0.0) return 0.0;
    return num / den; // -1.0 から 1.0 の範囲。1.0に近いほど一致
}

になります。引数並びをご覧になれば明白ですが、rawなimage bufferではなく、TBitmap同士の計算で行います。ただし内部ではグレイスケールで計算しています。つまり引数では32bitないし24bitのTBitmapが可です。これを使って、まずここまでで作成したtemplateなBitmapをロードしておきます。ファイルは今までで作った、frame0.pngからframe9.pngです。これをTBitmap*の配列digitsに入れておきます。

void LoadDigits()
{
	for( int i = 0 ; i < 10 ; i++ ){
		String fname = "frame" + IntToStr(i) + ".png";
		TPngImage* img = new TPngImage();
		img->LoadFromFile(fname);

		Graphics::TBitmap* bmp32 = new Graphics::TBitmap();
		bmp32->Assign(img);
		digits[i] = bmp32;

		Form1->Image2->Picture->Bitmap->Assign(bmp32);

	}
}

フォームは、

Unit1.hは、

//---------------------------------------------------------------------------

#ifndef Unit1H
#define Unit1H
//---------------------------------------------------------------------------
#include <System.Classes.hpp>
#include <Vcl.Controls.hpp>
#include <Vcl.StdCtrls.hpp>
#include <Vcl.Forms.hpp>
#include <Vcl.ExtCtrls.hpp>
#include <Vcl.Dialogs.hpp>
//---------------------------------------------------------------------------
class TForm1 : public TForm
{
__published:	// IDE で管理されるコンポーネント
	TImage *Image1;
	TOpenDialog *OpenDialog1;
	TButton *AddRect;
	TImage *Image2;
	TButton *Auto;
	TMemo *Memo1;
	void __fastcall FormDestroy(TObject *Sender);
	void __fastcall Image1MouseDown(TObject *Sender, TMouseButton Button, TShiftState Shift,
          int X, int Y);
	void __fastcall Image1MouseMove(TObject *Sender, TShiftState Shift, int X, int Y);
	void __fastcall Image1MouseUp(TObject *Sender, TMouseButton Button, TShiftState Shift,
          int X, int Y);
	void __fastcall FormPaint(TObject *Sender);
	//void __fastcall OpenClick(TObject *Sender);
	void __fastcall AddRectClick(TObject *Sender);
	//void __fastcall SaveClick(TObject *Sender);
	void __fastcall AutoClick(TObject *Sender);

private:	// ユーザー宣言
    bool FDrawing;     // ドラッグ中かどうかのフラグ
    TPoint FStartPos;  // マウスを押し下げた初期座標
	TPoint FLastPos;   // 直前のマウス座標(線を消す用)
    TPoint FEndPos;
	TBitmap *FBkBmp;   // 背景画像を一時保存するビットマップ

public:		// ユーザー宣言
	__fastcall TForm1(TComponent* Owner);
};
//---------------------------------------------------------------------------
extern PACKAGE TForm1 *Form1;
//---------------------------------------------------------------------------
#endif

Unit1.cppは、

//---------------------------------------------------------------------------

#include <vcl.h>
#pragma hdrstop
#include <jpeg.hpp>
#include <Vcl.Imaging.pngimage.hpp>
#include "Unit1.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;
TRect absarea;
int width;
int height;
int vheight;

TBitmap* digits[10];
TBitmap* targetbitmap;

// NCC(正規化相互相関)を計算する関数
// TBitmapは24bit(pf24bit)または32bit(pf32bit)を想定
double CalculateNCC(TBitmap* target, TBitmap* templateImg, int startX, int startY)
{
    // フォーマットを強制(ピクセルアクセスを単純化するためグレースケール化するか、輝度で計算)
    target->PixelFormat = pf32bit;
    templateImg->PixelFormat = pf32bit;

    int tWidth = templateImg->Width;
    int tHeight = templateImg->Height;

    double sumT = 0, sumI = 0;
    double sumTT = 0, sumII = 0;
    double sumTI = 0;
    double N = tWidth * tHeight;

    for (int y = 0; y < tHeight; y++) {
        // ScanLineによる高速ピクセルアクセス
        RGBQUAD* rowT = (RGBQUAD*)templateImg->ScanLine[y];
        RGBQUAD* rowI = (RGBQUAD*)target->ScanLine[startY + y];

        for (int x = 0; x < tWidth; x++) {
            // 輝度(Y)の計算 (簡易的にRGBの平均、または一般的RGB重み付け)
            double valT = (rowT[x].rgbRed + rowT[x].rgbGreen + rowT[x].rgbBlue) / 3.0;
            double valI = (rowI[startX + x].rgbRed + rowI[startX + x].rgbGreen + rowI[startX + x].rgbBlue) / 3.0;

            sumT += valT;
            sumI += valI;
            sumTT += valT * valT;
            sumII += valI * valI;
            sumTI += valT * valI;
        }
    }

    // NCC分子・分母の計算
    double num = sumTI - (sumT * sumI) / N;
    double den = std::sqrt((sumTT - (sumT * sumT) / N) * (sumII - (sumI * sumI) / N));

    if (den == 0.0) return 0.0;
    return num / den; // -1.0 から 1.0 の範囲。1.0に近いほど一致
}

void LoadDigits()
{
	for( int i = 0 ; i < 10 ; i++ ){
		String fname = "frame" + IntToStr(i) + ".png";
		TPngImage* img = new TPngImage();
		img->LoadFromFile(fname);

		Graphics::TBitmap* bmp32 = new Graphics::TBitmap();
		bmp32->Assign(img);
		digits[i] = bmp32;

		Form1->Image2->Picture->Bitmap->Assign(bmp32);

	}
}

void LoadJpegFile(const AnsiString & Src,
							Graphics::TBitmap * Bitmap)
{
		TJPEGImage * Jpeg = new TJPEGImage();

	Jpeg->Performance = jpBestQuality;
		Jpeg->PixelFormat = jf24Bit;
		Jpeg->Grayscale = false;
		Jpeg->LoadFromFile(Src);

		Bitmap->HandleType = bmDIB;
		Bitmap->PixelFormat = pf32bit;
		Bitmap->Width = Jpeg->Width;
		width = Jpeg->Width;
		Bitmap->Height = Jpeg->Height;
		height = Jpeg->Height;
		Bitmap->Canvas->Draw(0,0,Jpeg);

		vheight = 640* height / width ;

		delete Jpeg;
}

//---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
	: TForm(Owner)
{


		FDrawing = false;
		FBkBmp = new TBitmap();

		LoadDigits();
		LoadJpegFile("frame0.jpg",FBkBmp);
		//Image1->Canvas->Draw(0,0,FBkBmp);
		TRect dest = {0,0,640,vheight};

		Image1->Height = vheight;
		Image1->Width = 640;

		//Image1->Canvas->Draw(dest,FBkBmp);
		Image1->Canvas->StretchDraw(dest,FBkBmp);
		//Canvas->StretchDraw()


}


//---------------------------------------------------------------------------
void __fastcall TForm1::FormDestroy(TObject *Sender)
{
	 delete FBkBmp;
}
//---------------------------------------------------------------------------
void __fastcall TForm1::Image1MouseDown(TObject *Sender, TMouseButton Button, TShiftState Shift,
		  int X, int Y)
{
	if (Button == mbLeft) // 左クリックのみ対象
	{
		FDrawing = true;
		FStartPos = Point(X, Y);
		FLastPos = Point(X, Y);

        // 現在の Image1 の状態(背景)を FBkBmp にコピーして保存
        FBkBmp->Assign(Image1->Picture->Bitmap);
    }

}
//---------------------------------------------------------------------------
void __fastcall TForm1::Image1MouseMove(TObject *Sender, TShiftState Shift, int X,
          int Y)
{
     if (FDrawing)
    {
        // 毎回バックアップから背景を書き戻す(古いドラッグ枠を消すため)
        Image1->Picture->Bitmap->Assign(FBkBmp);

        // 描画設定
        Image1->Canvas->Pen->Color = clRed;
        Image1->Canvas->Pen->Width = 1;
        Image1->Canvas->Brush->Style = bsClear; // 中は透明

        // 開始位置から現在のカーソル位置まで四角形を描画
        Image1->Canvas->Rectangle(FStartPos.x, FStartPos.y, X, Y);

		// 画面更新

		FEndPos.x = X;
		FEndPos.y = Y;

        Image1->Refresh();
    }

}
//---------------------------------------------------------------------------
void __fastcall TForm1::Image1MouseUp(TObject *Sender, TMouseButton Button, TShiftState Shift,
          int X, int Y)
{
    if (Button == mbLeft && FDrawing)
    {
        FDrawing = false; // ドラッグ終了

        // 最後に確定した図形を描画
        Image1->Canvas->Pen->Color = clRed;
        Image1->Canvas->Pen->Width = 1;
        Image1->Canvas->Brush->Style = bsClear;
        Image1->Canvas->Rectangle(FStartPos.x, FStartPos.y, X, Y);

        Image1->Refresh();
    }

}
//---------------------------------------------------------------------------
void __fastcall TForm1::FormPaint(TObject *Sender)
{
		//Graphics::TBitmap * Bitmap = new Graphics::TBitmap();
		//LoadJpegFile("IMG_0201.JPG",Bitmap);
		//Image1->Canvas->Draw(0,0,Bitmap);
		//delete Bitmap;
}
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------

void __fastcall TForm1::AddRectClick(TObject *Sender)
{
	TRect area = { FStartPos.X+1,FStartPos.Y+1,FEndPos.X-1,FEndPos.Y-1};

	int	width2 = area.Width();
	int	height2 = area.Height();

	Image2->Width = width2;
	Image2->Height = height2;


	TRect dst = {0,0,Image2->Width,Image2->Height};

	Image2->Canvas->CopyRect(dst, Image1->Canvas, area);

	absarea = area;

	targetbitmap = Image2->Picture->Bitmap;

	for( int i = 0 ; i < 10 ; i++ ){
		double corr = CalculateNCC(targetbitmap, digits[i], 0, 0);
		Memo1->Lines->Add(IntToStr(i) +" " + FloatToStr(corr));
	}
}
//---------------------------------------------------------------------------

/*
void __fastcall TForm1::SaveClick(TObject *Sender)
{
	Image2->Picture->SaveToFile("output.png");
}
*/

//---------------------------------------------------------------------------

void __fastcall TForm1::AutoClick(TObject *Sender)
{
// validate ncc using self-correlation

/*
	double results;


	results =  CalculateNCC(digits[0], digits[0], 0, 0);

	Memo1->Lines->Add("corr value for 0:0 " + FloatToStr(results));



	for( int i = 0 ; i < 10 ; i++ )
		for( int j = i+1 ; j < 10 ; j++ ){
			results =  CalculateNCC(digits[i], digits[j], 0, 0);

			Memo1->Lines->Add("corr value for " + IntToStr(i) + ":" +IntToStr(j) + " " + FloatToStr(results));

		}
 */

}
//---------------------------------------------------------------------------

正規化相関係数計算の検証のために、digits[n]とdigites[m]を与えて係数を計算すると、n=mの場合は、当然1.0で、以下


corr value for 0:1 0.497403843568344
corr value for 0:2 0.513521340671304
corr value for 0:3 0.544676194603459
corr value for 0:4 0.441693910906116
corr value for 0:5 0.523433517943657
corr value for 0:6 0.673184580965456
corr value for 0:7 0.562422706729977
corr value for 0:8 0.855937674234769
corr value for 0:9 0.670953225098394
corr value for 1:2 0.144512768275543
corr value for 1:3 0.618791626149772
corr value for 1:4 0.703411902318827
corr value for 1:5 0.24580687335676
corr value for 1:6 0.110811423262732
corr value for 1:7 0.821422184485028
corr value for 1:8 0.43676599126348
corr value for 1:9 0.507283094887215
corr value for 2:3 0.674584734835016
corr value for 2:4 0.138069110287927
corr value for 2:5 0.386484401439309
corr value for 2:6 0.526528194702154
corr value for 2:7 0.248703178868609
corr value for 2:8 0.701244969210292
corr value for 2:9 0.44524429777504
corr value for 3:4 0.565195747453251
corr value for 3:5 0.720428444692662
corr value for 3:6 0.544930625583829
corr value for 3:7 0.66982517784849
corr value for 3:8 0.720440773478347
corr value for 3:9 0.794073718147514
corr value for 4:5 0.540371934843182
corr value for 4:6 0.415059229461342
corr value for 4:7 0.493116005050189
corr value for 4:8 0.615000558958087
corr value for 4:9 0.694134879989582
corr value for 5:6 0.855956808475601
corr value for 5:7 0.283619341465053
corr value for 5:8 0.696968736729038
corr value for 5:9 0.769361084245236
corr value for 6:7 0.165892166815509
corr value for 6:8 0.822315523564578
corr value for 6:9 0.629434363785216
corr value for 7:8 0.486173411804205
corr value for 7:9 0.594712156491278
corr value for 8:9 0.82662032412464

相関値が0.8以上を赤で示しました。結果は辻褄があっていて、例えば、digit 0と8では、並べてみれば明白ですが、

なので類似性が高いです。segmentが一本足りないだけですから。

同様にして、8と9では

同じくsegment一本の違いです。非常に簡単な検証ですが、一応それらしい動作はしていそうです。実際に”誤認”しなければおけなのですが、1回テストしてみましょう。

数値列が小さくて恐縮ですが、digits 0との一致度(相関値)が0.94で最大ですからこれでRectで囲った領域は”0”と判定されます。何故1.0にならないのかは、微妙に切り取り座標の位置ないし大きさがテンプレートとズレているからですね。誤認されなければ良いですね。実際の自動判定画面は、

なので、数字の大きさは3種類ありますから、a prioriでわかっている範囲でtemplateの拡大縮小ができないとダメですね。そのあたりと自動判定は以後の記事で書くかもしれません。一番大事な相互相関係数計算ルーチンはよさそうなので、山場は越えたのでしょうか。誤判定が少なく実用的に使えそうかは、今後の検証によります。

コメント