画像の拡大

例えば、下図のように2x2画素の画像を4x4の画像に拡大する場合、アフィン変換を使えばいいんでしょ!と、安易に考えていると、思わぬ落とし穴があったりもします。

大事なポイントとして、

●画像の座標の原点は左上の画素の中心が原点(0.0、0.0)となる。(例外もあります)

●アフィン変換の拡大縮小は原点を基準として拡大縮小される。

 

となります。

これを気にせず、ただ、アフィン変換で画像を2倍に拡大すると、左上の画素の中心を基点に画像が拡大されます。

これで、一見良さそうにも感じるのですが、拡大後の画像において、画素の中心が原点であることから、4x4画素の領域は下図の四角で示した領域であり、画像全体が左上に0.5画素ズレた状態になっていまいます。

 

 

アフィン変換で画像を拡大する時の変換前と変換後の状態は、以下のようになるのが正解です。

 

この変換をアフィン変換で実現するには以下のように行います。

 

変換前の状態

 

①画像全体を右下に(+0.5,+0.5)画素移動
$$\begin{pmatrix} 1 & 0 & 0.5 \\ 0 & 1 & 0.5 \\ 0 & 0 & 1 \end{pmatrix}$$
 

②画像を2倍に拡大

$$\begin{pmatrix} 2 & 0 & 0 \\ 0 & 2 & 0 \\ 0 & 0 & 1 \end{pmatrix}$$
 

③画像全体を左上に(-0.5,-0.5)画素移動

$$\begin{pmatrix} 1 & 0 & -0.5 \\ 0 & 1 & -0.5 \\ 0 & 0 & 1 \end{pmatrix}$$

 

となります。

この一連の変換をアフィン変換行列であらわすと

$$\begin{pmatrix} { x }^{ ‘ } \\ { y }^{ ‘ } \\ 1 \end{pmatrix}=\begin{pmatrix} 1 & 0 & -0.5 \\ 0 & 1 & -0.5 \\ 0 & 0 & 1 \end{pmatrix}\begin{pmatrix} 2 & 0 & 0 \\ 0 & 2 & 0 \\ 0 & 0 & 1 \end{pmatrix}\begin{pmatrix} 1 & 0 & 0.5 \\ 0 & 1 & 0.5 \\ 0 & 0 & 1 \end{pmatrix}\begin{pmatrix} x \\ y \\ 1 \end{pmatrix}\\ \\ $$

$$\begin{pmatrix} { x }^{ ‘ } \\ { y }^{ ‘ } \\ 1 \end{pmatrix}=\begin{pmatrix} 2 & 0 & 0.5 \\ 0 & 2 & 0.5 \\ 0 & 0 & 1 \end{pmatrix}\begin{pmatrix} x \\ y \\ 1 \end{pmatrix}\\ \\ $$

 

となり、単に拡大のアフィン変換行列だけを掛ければOKでは無いことが分かります。

 

ちなみに、C#で2x2画素の画像をPictureBoxのSizeModeプロパティをZoomにして、ImageプロパティにBitmapを設定すると、このようになります。

 

なんとなく、画像が左上にズレているようで、なんか怪しい!!

 

【関連記事】

【C#】画像の座標系

アフィン変換(平行移動、拡大縮小、回転、スキュー行列)

画素の補間(Nearest neighbor,Bilinear,Bicubic)の計算方法

画像の回転

 

画像処理のためのC#へ戻る

【C#】画像の輝度値の取得

C#で画像ファイルの輝度値を取得するにはLockBits~UnlockBitsでポインタを取得して配列へコピーするなどするのが、C#では定番となっています。

例えば、下記のようなサンプルコード

var bmp = new Bitmap("01.png"); 

// Bitmapをロック 
var bmpData = bmp.LockBits( 
	new Rectangle(0, 0, bmp.Width, bmp.Height), 
	System.Drawing.Imaging.ImageLockMode.ReadWrite, 
	bmp.PixelFormat 
	); 

// メモリの幅のバイト数を取得 
var stride = Math.Abs(bmpData.Stride); 

// 画像データ格納用配列 
var data = new byte[stride * bmpData.Height]; 

// Bitmapデータを配列へコピー 
System.Runtime.InteropServices.Marshal.Copy( 
	bmpData.Scan0, 
	data, 
	0, 
	stride * bmpData.Height 
	);

// アンロック 
bmp.UnlockBits(bmpData);

このプログラムで、ほとんどの場合、問題ない事が多いのですが、モノクロ8Bitのpng画像ファイルを開くと、なぜか?32bitカラーの画像として認識してしまいます。

(評価に用いた画像) Deep Learningでは定番のMNISTの手書き文字に似せて作成した8bitモノクロ画像

(デバッグ画面)もとの画像は8bitモノクロ画像ですが、PixelFormatがFormat32bppArgbになっています。

 

8bitの画像が32bitとして認識してしまうという事は、無駄に画像データが4倍大きくなってしまうので、画像処理的には処理速度的にも不都合が起こります。

 

そこで、私は基本WindowsFormsを使っているのですが、クラスだけWPFのクラスを使う事で問題を解決します。

 

WPFのクラスを用いるにはプロジェクトの参照から

下記の2つを追加します。

PresentationCore
WindowsBase

 

これでWPFのクラスが使えるようになります。
そこで、最初に書いたBitmapクラスを用いたプログラムと同等のことをWPFのクラスを用いて作成すると

byte[] data; 

using (var fs = new System.IO.FileStream("01.png", System.IO.FileMode.Open, System.IO.FileAccess.Read)) 
{ 
	var bitmapFrame = System.Windows.Media.Imaging.BitmapFrame.Create( 
		fs, 
		System.Windows.Media.Imaging.BitmapCreateOptions.PreservePixelFormat, 
		System.Windows.Media.Imaging.BitmapCacheOption.Default 
		); 

	int stride = ((bitmapFrame.PixelWidth * bitmapFrame.Format.BitsPerPixel + 31) / 32) * 4; 

	// 画像データ格納用配列 
	data = new byte[stride * bitmapFrame.PixelHeight]; 
	
	// 輝度データを配列へコピー 
	bitmapFrame.CopyPixels(data, stride, 0); 
}

となります。

これでFormatもGray8となり、正しいサイズで画像データを取得できるようになります。

 

画像処理のためのC#テクニックへ戻る

関連記事

【C#】画像の輝度値の取得設定速度の比較

【C#】グローバル変換を使ったアフィン変換

.NETで画像や線などを描画する時はGraphicsオブジェクトに対して描画を行いますが、このGraphicsオブジェクトの座標系をアフィン変換する処理をグローバル変換と言います。

 

グローバル変換された Graphicsオブジェクトに対し描画を行うと、アフィン変換された状態で、画像や線が描画されます。

 

ただ、一般的なアフィン変換では

 

 

のように表現される場合が多いかと多いと思いますが、マイクロソフトの仕様では、上記の行列を転置(行と列を反転する)した

 

 

のように表示されます。

また、アフィン変換の座標系は下図のように、左上が原点、Y軸は下方向、回転は時計周りが正の方向となります。

 

 

そのため、平行移動や拡大縮小、回転の変換行列も一般的な変換行列を転置した表現となります。

 

X軸方向へTx、Y軸方向へTyだけ移動する平行移動の変換行列は、

 

X軸方向へSx倍、Y軸方向へSy倍だけ拡大する変換行列は、

 

原点まわりにθ°回転移動する変換行列は

 

となります。

少し紛らわしいので、一般的なアフィン変換と混同しないように注意して下さい。

このように表現するのは、私の経験的にはマイクロソフトだけ?だと思います。

(参考)

http://msdn.microsoft.com/ja-jp/library/vstudio/c499ats3.aspx

http://msdn.microsoft.com/ja-jp/library/vstudio/8667dchf.aspx

 

しかし、.NET Frameworkでは特に変換行列を知らなくても、アフィン変換用のメソッドが用意されているので、それらを使うと間違いが少ないでしょう。

 

平行移動は TranslateTransform
拡大縮小は ScaleTransform
回転移動は RotateTransform

 

(参考)Graphicsメソッド

http://msdn.microsoft.com/ja-jp/library/system.drawing.graphics_methods%28v=vs.80%29.aspx

 

逆に、行列を使ってアフィン変換する事も可能なので、スキュー変換のような変換も行う事ができます。

(参考)

http://msdn.microsoft.com/ja-jp/library/system.drawing.drawing2d.matrix_methods%28v=vs.80%29.aspx

 

また、アフィン変換を行うと、変換後の座標から、変換前の座標を計算したい場合がありますが、System.Drawing.Graphics.TransformPointsというメソッドがあるので、これを使うと簡単に座標が求められます。

 

これらのメソッドを使って、アフィン変換のサンプルプログラムを作成しました。

 

(ダウンロード)GlobalTransformations.zip(Visual Studio 2008 Exress C#)

 

このサンプルでは見た目上では一般的に用いられる行列の表現に合わせています。
また、エラー処理などは行っていないので、ご了承下さい。

アフィン変換の勉強の手助けになれば...

 

C#へ戻る

多ビット(10Bit,12Bit,30Bit)画像データの表示、フォーマット

多ビット(10Bit、12Bitなど)の画像データは10Bitや12Bitの型が存在しないため、モノクロの場合はushort型の16bit中、下位10Bit、12Bitなどを使って画像データを格納します。
30Bitとは、R,G,Bの各色が10bitのデータとなるカラー画像の場合(RGB101010)で32Bit中の下位30Bitを使って画像データを格納します。

 

この××Bit中のどのビットをR,G,Bの色に割り振るかは、ビットフィールドというものを使います。
ビットフィールドを使うにはBITMAPINFOHEADERbiCompressionBI_BITFIELDSに設定します。

 

どのビットをR,G,Bの色に割り振るかはRGBQUAD型の32Bitを使って有効ビットを指定します。
bmiColors[0]R(赤)bmiColors[1]G(緑)bmiColors[2]B(青)の色のビットを設定するのに用います。

 

実際の描画にはWin32APIのSetDIBitsToDevice関数かStretchDIBits関数を使います。

 

モノクロ画像の場合(10~16Bitのとき)

(例)モノクロ10Bitの場合、biBitCount = 16 とし、16Bit中下位10Bitを使います。
表示に有効なビットを以下のように指定します。

画像データ 00000011 11111111
bmiColors[0] 00000000 00000000 00000011 11111111
bmiColors[1] 00000000 00000000 00000011 11111111
bmiColors[2] 00000000 00000000 00000011 11111111

以上より、bmiColorsを10進数で表示すると以下のようになります。

bmiColors[0].rgbReserved = 0;
bmiColors[0].rgbRed          = 0;
bmiColors[0].rgbGreen       = 3;
bmiColors[0].rgbBlue          = 255;
bmiColors[1].rgbReserved = 0;
bmiColors[1].rgbRed          = 0;
bmiColors[1].rgbGreen       = 3;
bmiColors[1].rgbBlue          = 255;
bmiColors[2].rgbReserved = 0;
bmiColors[2].rgbRed          = 0;
bmiColors[2].rgbGreen       = 3;
bmiColors[2].rgbBlue          = 255;

カラー画像の場合(R10G10B10Bitのとき)

(例)カラー10Bitの場合、biBitCount = 32 とし、32Bit中、下位30Bitを使います。
表示に有効なビットを以下のように指定します。

画像データ 00111111 11111111 11111111 11111111
bmiColors[0] 00111111 11110000 00000000 00000000
bmiColors[1] 00000000 00001111 11111100 00000000
bmiColors[2] 00000000 00000000 00000011 11111111

以上より、bmiColorsを10進数で表示すると以下のようになります。

bmiColors[0].rgbReserved = 63;
bmiColors[0].rgbRed          = 240;
bmiColors[0].rgbGreen       = 0;
bmiColors[0].rgbBlue          = 0;
bmiColors[1].rgbReserved = 0;
bmiColors[1].rgbRed          = 15;
bmiColors[1].rgbGreen       = 252;
bmiColors[1].rgbBlue          = 0;
bmiColors[2].rgbReserved = 0;
bmiColors[2].rgbRed          = 0;
bmiColors[2].rgbGreen       = 3;
bmiColors[2].rgbBlue          = 255;

サンプル画像

画像データはこちら(16BitGray.zip

【画像データプロパティ】
幅:1024
高さ:256
ビット数:16
BitField:16ビット中、下位10ビットの上位8ビットを表示設定
輝度値:0~1023のグラデーション

上記のビットマップファイルをTSXBINというバイナリエディタでヘッダ部分を表示すると以下の通りです。(値は16進数表示)

エクプスローラのプロパティや標準的なビューアソフトでは16ビットとして認識されない場合が多いのでご注意下さい。

注意事項

この記事では10Bitなどの多ビット画像データを8Bitデータにシフト処理などする事なく、画像データを表示する方法を説明しています。実際のモニタ上にはRGB各8Bitの解像度で表示されます。
モニタ上にRGB各10Bitで表示するには、別途、

■10Bit対応のモニタ
■10Bit対応のビデオカード
■10Bit対応の表示プログラム(Direct-X、OpenGL)

の3点セットが必要となります。

(参考資料)
10ビット/8ビット表示はドコが違う!?:「FlexScan SX2462W」のDisplayPort入力で“約10億色リアル表示”を体感する (1/3) – ITmedia +D PC USER
株式会社エーキューブ – ナナオ社製10bit対応モニタ「Flexscan」「ColorEdge」による動作確認
AMD(ATI)の資料
nVIDIAの資料

最近のCCDカメラでは8Bitのみならず、10Bit、12Bitの出力を持つカメラが増えてきています。多ビットの画像データは低コントラストの画像データを処理するには有功な場合が多いのですが、ほとんどの画像処理ライブラリでは多ビットの画像データに対応していない場合が多いため、画像処理部分のプログラムを全て自作する必要が出てきます。
また、8Bitが10Bitになるだけで、カメラから画像入力ボードへのデータ転送量は倍(16Bit/画素)となるため、フレームレートやスキャンレートの最速レートを出せなくなる場合があります。

 

そのため、総合的に捉えて多ビットの画像データをお取り扱い下さい。

ビットマップファイルフォーマット

ビットマップファイル(*.bmp)のファイルフォーマットです。

 

ビットマップ全体の構造

BITMAPFILEHEADER 14Byte
BITMAPINFOHEADER 40Byte
カラーテーブル(無い場合もあり) 4Byte*Index数
画像データ

 

各構造体について

■BITMAPFILEHEADER

typedef struct tagBITMAPFILEHEADER { 
	WORD bfType; 
	DWORD bfSize; 
	WORD bfReserved1; 
	WORD bfReserved2; 
	DWORD bfOffBits; 
} BITMAPFILEHEADER, *PBITMAPFILEHEADER;

 

bfType ファイルタイプ ‘BM’
bfSize ファイル全体のサイズ(バイト数)
bfReserved1 予約領域 常に0
bfReserved2 予約領域 常に0
bfOffBits ファイル先頭から画像データまでのオフセット数(バイト単位)

 

■BITMAPINFO

typedef struct tagBITMAPINFO { 
	BITMAPINFOHEADER bmiHeader; 
	RGBQUAD bmiColors[1]; 
} BITMAPINFO, *PBITMAPINFO;

●BITMAPINFOHEADER

typedef struct tagBITMAPINFOHEADER{
	DWORD  biSize;
	LONG   biWidth;
	LONG   biHeight;
	WORD   biPlanes;
	WORD   biBitCount;
	DWORD  biCompression;
	DWORD  biSizeImage;
	LONG   biXPelsPerMeter;
	LONG   biYPelsPerMeter;
	DWORD  biClrUsed;
	DWORD  biClrImportant;
} BITMAPINFOHEADER, *PBITMAPINFOHEADER;
biSize 構造体のサイズ 40
biWidth 画像の幅(ピクセル数)
biHeight 画像の高さ(ピクセル数)
値が負の場合、画像の上下が逆になる
biPlanes プレーン数 常に1
biBitCount 1画素あたりのビット数 1,4,8,16,24,32
biCompression 圧縮形式 BI_RGB, BI_RLE8, BI_RLE4
BI_BITFIELDS, BI_JPEG, BI_PNG
biSizeImage 画像データのサイズ(バイト数) BI_RGBの場合0でも可
biXPelsPerMeter 水平方向の1Mあたりの画素数 0でも可
biYPelsPerMeter 垂直方向の1Mあたりの画素数 0でも可
biClrUsed カラーテーブルの色数 0でも可
biClrImportant 表示に必要なカラーテーブルの色数 0でも可

 

●RGBQUAD(カラーテーブル)

typedef struct tagRGBQUAD {
	BYTE    rgbBlue;
	BYTE    rgbGreen;
	BYTE    rgbRed;
	BYTE    rgbReserved;
} RGBQUAD;
rgbBlue 青の輝度値 0~255
rgbGreen 緑の輝度値 0~255
rgbRed 赤の輝度値 0~255
rgbReserved 予約領域 常に0

 

biBitCountが1,4,8のとき、RGBQUAD構造体のカラーテーブルが指定されます。
biBitCountが24,32のときは存在しません。
ただし、biCompressionがBI_BITFIELDS かつ、biBitCountが16,32の場合、
多ビット(10Bit,12Bitなど)が表示可能となり、ビットフィールドが指定されます。

 

■画像データ

モノクロ画像の場合、輝度値が格納されています。

24Bitカラーの場合、B,G,R,B,G,R・・・の順で各輝度値が格納されています。
32Bitカラーの場合、B,G,R,A,B,G,R,A・・・の順で各輝度値が格納されています。

 

画像データは、画像の左から右、下から上へ向かう順番で格納されます。

 

 

また、1行あたりのメモリのサイズは4の倍数バイトになるように調整されています。
この値は以下のようにして計算しています。

 

VBの場合

((biWidth * biBitCount + 31) \ 32) * 4

Cの場合

((biWidth * biBitCount + 31) / 32) * 4

 

4の倍数バイト(32ビットの倍数)になるように調整しています。

 

ビットマップファイルを開く場合の注意点について

ビットマップファイルを開く場合、ヘッダ情報をもとに画像データ格納用のメモリを確保し、
そのメモリにデータを格納しますが、ヘッダの値はすべて正しく記載されているとは限りません。私の場合、以下の値を信じてメモリの確保などを行っています。

biWidth, biHeight, biBitCountを用いて画像データ格納用メモリの確保
biOffBitsを用いて、画像データまでのファイルのシークを行う。

biSizeやbiSizeImageなどは信じない方が良いと思います。

 

フィルタ処理の高速化アルゴリズム(縦横に処理を分ける)

前回、フィルタ処理の高速化アルゴリズム(重複した計算を行わない)で紹介した方法ではカーネルの値が全て同じでないと使えないので、今回はフィルタ処理を縦方向と横方向に分けて行う事でフィルタ処理の高速化を行う方法をガウシアンフィルタを例にとって紹介します。

 

ガウシアンフィルタのカーネルには、

 

が良く用いられますが、この処理を注目画素の周辺の輝度値をI0~I8とした場合、
ガウシアンフィルタの処理を行列で

 

と、表すこともでき、この事は縦方向に3×1のガウシアンフィルタ処理をおこなってから、
横方向に1×3のガウシアンフィルタ処理を行うことを意味しています。
(横方向に処理をしてから縦方向に処理をしても同じです。)

 

このように処理を縦と横に分けることで、カーネルのサイズm×nの場合、通常の処理では
m×n回の掛け算を行うところ、m+n回の掛け算で済む事になります。
(ただし、縦横に処理を分ける事で全画素を2回参照することになるので、カーネルのサイズが
小さいと効果はあまりありません。)

他にも、移動平均フィルタの場合

ソーベルフィルタの場合

 

となります。
ソーベルフィルタの行列を見ると、縦方向にガウシアンフィルタ処理をしてから、横方向に微分処理している事が分かりやすくなっているかと思います。

 

また、比較的処理の重いメディアンフィルタにおいても、処理を縦と横に分けることによって、
ほぼ、同様な効果を得ることができます。
厳密には同じ結果にはならないのですが、スパイクノイズを除去するという意味では
十分な結果を得る事が出来ます。

試しに何回か、メディアンフィルタ処理を縦方向に1列分の処理を行ってから、横方向に1列分の
処理を行ってみましたが、ほぼ、良好な結果を得る事ができていると思います。
(画像にするともう少し分かりやすいかと思いますが、プログラムが無いもので...)

 

 

画像処理アルゴリズムへ戻る

 

フィルタ処理の高速化アルゴリズム(重複した計算を行わない)

画像フィルタ処理の高速化のテクニックを移動平均フィルタを例にとって紹介したいと思います。

 

カーネルのサイズが5×5の移動平均フィルタの場合、注目画素の周辺の5×5の輝度値を合計し、
その輝度値の合計を画素数(5×5=25)で割る処理をラスタスキャンしながら、全画素に対して
処理を行います。

 

 

ここで、隣の画素へ処理が移った時に、輝度値の合計の計算処理は前に行った輝度値の合計の
処理とかなりかぶっている(下図の緑色の部分)事に気が付きます。

 

 

そこで、最初の輝度値の平均値を計算をした時の輝度値の合計値を保持しておき、最初の輝度値の
合計値から、最初のカーネルの左端の1列分の輝度値(上図の赤色の部分)を引き、
次のカーネルの右端の1列分の輝度値(上図の青色の部分)を足すと、次のカーネル内の
輝度値の合計値を求める事が出来ます。

 

そうすると、カーネル内の輝度値の合計の計算に25回の足し算をしていたところ、5回の引き算と
5回の足し算の計10回の計算で済ませることが分かります。
この効果はカーネルサイズが大きくなればなる程、大きくなります。

 

さらに画像の1行分の合計値を確保するメモリを確保しておくと、縦方向に関しても同様の処理が
できるので、高速化が期待できます。

 

と、今回は画像のフィルタ処理を例にとって紹介していますが、この考え方は他にもいろいろと
応用が効くので、輝度値の合計の計算に留まらず、毎回同じような処理をしているな~と思ったら、
前回行った処理の使いまわしができないか?検討してみると良いでしょう。

 

画像処理アルゴリズムへ戻る