Laplacian of Gaussian
LoG (Laplacian of Gaussian) filterは、
ガウシアンを微分して得られる計算式により、フィルターとしたもので、平滑化と、エッジ検出を同時に行うものです。
平滑化(Gaussian)フィルターの合計値は1になりますが、LoGの合計はゼロになります。
フィルターイメージ図
値が0~255の画像にフィルターを施すと、値が小さすぎて画像として表示が出来ないので、係数を乗じて画像として見やすい値にします。
左図は、計算されたLoGフィルター値で処理後53倍した後、127を加算して見やすくしたものです。
σ値は2として計算しています。
細線化処理として、水平垂直斜め方向の値が一定の値以上かをチックしていすます。
微分した値なので、画像濃度の変化が少ない部分は0に近い値になり、黒から白になる部分はプラスの大な値となり、白から黒になる部分は、マイナスの大きな値になりますので、この変化する部分を捉えれば、画像のエッジ部分を捉えることが出来ます。
左図は、指定位置に対して、上下左右、斜め方向位置の値の大きさを調べ指定した値を超えていたら白としています。
白の線の幅は、2ドットになります。
チェックの範囲は3x3になりますが、指定位置中央の値は使用されません。
白の線の幅を1ドットにする場合はチェックの範囲を2x2にして隣同士、斜め方向の値をチェックします。
上図は、フィルターの差を比べたものですが、やはりCanyの方がはるかに良い画像となっています。
プログラム
unit Main; interface uses Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.ExtDlgs, Vcl.StdCtrls, Vcl.ExtCtrls, Vcl.Grids, Math; type TForm1 = class(TForm) OpenBtn: TButton; SigumaGrid: TStringGrid; REdit: TLabeledEdit; SEdit: TLabeledEdit; FilterBtn: TButton; OpenPictureDialog1: TOpenPictureDialog; Label1: TLabel; KEdit: TLabeledEdit; Label2: TLabel; ReDrawBtn: TButton; crossBtn: TButton; OffEdit: TLabeledEdit; CheckBox1: TCheckBox; ScrollBox1: TScrollBox; Image1: TImage; procedure FilterBtnClick(Sender: TObject); procedure OpenBtnClick(Sender: TObject); procedure FormCreate(Sender: TObject); procedure FormDestroy(Sender: TObject); procedure ReDrawBtnClick(Sender: TObject); procedure crossBtnClick(Sender: TObject); procedure SEditChange(Sender: TObject); private { Private 宣言 } procedure LogMask; procedure Masking; public { Public 宣言 } end; var Form1: TForm1; implementation {$R *.dfm} const FileOpenFilter = 'Image File|*.jpg;*.jpeg;*.tif;*.tiff;*.png;*.gif;*.bmp;*.wdp' + '|*.Jpg|*.jpg' + '|*.Tif|*.tif' + '|*.Png|*.png' + '|*.Gif|*.gif' + '|*.Bmp|*.bmp' + '|*.Wdp|*.wdp'; RK = 0.298912; // グレースケール輝度係数 赤 GK = 0.586611; // グレースケール輝度係数 緑 BK = 0.114478; // グレースケール輝度係数 青 var SXY : array of array of double; // フィルター配列 MatSize : integer; // フィルターサイズ MDIV2 : integer; Total : double; Inputbitmap : Tbitmap; // ファイル入力用ビットマップ OutPutbitmap : Tbitmap; // ファイル入力用ビットマップ GrayMat : array of array of double; MaskedMat : array of array of Byte; FMaskedMat : array of array of double; GHeight, GWidth : Integer; // 入力画像サイズ RepF : Boolean; K : double; //------------------------------- // LoGフィルターの実行ルーチン //------------------------------- procedure TForm1.Masking; var X, Y, WX, WY : integer; Xm, Ym, Xd, Yd : integer; Mx, My : integer; Siguma : double; Isiguma : integer; PD : PByteArray; WD : integer; begin setlength(MaskedMat, GWidth, GHeight); setlength(FMaskedMat, GWidth, GHeight); OutPutbitmap.SetSize(GWidth, GHeight); WX := GWidth - 1; WY := GHeight - 1; // LoG フィルターの実施 for X := 0 to WX do for Y := 0 to WY do begin siguma := 0; for Xm := - MDIV2 to MDIV2 do begin Mx := Xm + MDIV2; Xd := X + Xm; if Xd < 0 then Xd := -Xd; if Xd > WX then Xd := WX + WX - Xd; // Xd := WX - (Xd - WX); for Ym := - MDIV2 to MDIV2 do begin My := Ym + MDIV2; Yd := Y + Ym; if Yd < 0 then Yd := -Yd; if Yd > WY then Yd := WY + WY - Yd; // Yd := WY - (Yd - WY); siguma := siguma + GrayMat[Xd, Yd] * SXY[Mx, My]; end; end; // 浮動小数点値 FMaskedMat[X, Y] := siguma; // 交差点判定用浮動小数点のまゝ使用します // 整数化 Isiguma := round(siguma + 127); // ゼロ点の値を中間点127に設定 if Isiguma < 0 then Isiguma := 0; if Isiguma > 255 then Isiguma := 255; // Byteデーター MaskedMat[X, Y] := Isiguma; end; // グレースケール画像の生成 for Y := 0 to WY do begin PD := OutPutbitmap.ScanLine[Y]; for X := 0 to WX do begin WD := X * 3; PD[WD ] := MaskedMat[X, Y]; PD[WD + 1] := MaskedMat[X, Y]; PD[WD + 2] := MaskedMat[X, Y]; end; end; Image1.Canvas.Draw(0, 0, Outputbitmap); // 出力枠に出力 end; //---------------------------------------------------------------- // 交差点の検出 // 中間点 X 3x3 マトリックス 2x2 マトリックス // a b c ab // d X f cd // g h i // 判定 3x3 d>X>f d<X<f b>X>h b<X<h a>X>i a<X<i c>X>g c<X<g // 判定 2x3 a>X>b a<X<b a>X>c a<X<c a>X>d a<X<d b>X>c b<X<c //---------------------------------------------------------------- procedure TForm1.crossBtnClick(Sender: TObject); var crossmat : array[0..2] of array[0..2] of double; // 交差判定用配列 X, Y : integer; ThP, Thm : double; // +- Threshold Xm, Ym : integer; PD : PByteArray; WD : integer; Offset : double; // Threshold用入力値 function crosscheck(a, b: double): boolean; begin result := false; if (a > ThP) and (b < Thm) then result := true; if (a < Thm) and (b > ThP) then result := true; end; begin val(OffEdit.Text, Offset, WD); if WD <> 0 then begin application.MessageBox('Threshold 入力に間違いがあります。', 'Threshold', 0); exit; end; // 判定値の上下設定 ThP := (Total * K + Offset); ThM := (Total * K - Offset); if CheckBox1.Checked = True then begin // 2x2 交差点検出 // 交差点判別 for X := 1 to GWidth - 1 do for Y := 1 to GHeight - 1 do begin MaskedMat[X, Y] := 0; if crosscheck(FMaskedMat[X-1, Y-1], FMaskedMat[X , Y-1]) then MaskedMat[X, Y] := 255; // 左右 if crosscheck(FMaskedMat[X-1, Y-1], FMaskedMat[X-1, Y ]) then MaskedMat[X, Y] := 255; // 上下 if crosscheck(FMaskedMat[X-1, Y-1], FMaskedMat[X , Y ]) then MaskedMat[X, Y] := 255; // 左上右下 if crosscheck(FMaskedMat[X , Y-1], FMaskedMat[X-1, Y ]) then MaskedMat[X, Y] := 255; // 右上左下 end; end else begin // 3x3 交差点検出 // 交差点判別 for X := 1 to GWidth - 2 do for Y := 1 to GHeight - 2 do begin MaskedMat[X, Y] := 0; if crosscheck(FMaskedMat[X-1, Y ], FMaskedMat[X+1, Y ]) then MaskedMat[X, Y] := 255; // 左右 if crosscheck(FMaskedMat[X , Y-1], FMaskedMat[X , Y+1]) then MaskedMat[X, Y] := 255; // 上下 if crosscheck(FMaskedMat[X-1, Y-1], FMaskedMat[X+1, Y+1]) then MaskedMat[X, Y] := 255; // 左上右下 if crosscheck(FMaskedMat[X+1, Y-1], FMaskedMat[X-1, Y+1]) then MaskedMat[X, Y] := 255; // 右上左下 end; end; // モノクロ2値画像生成 for Y := 0 to GHeight - 1 do begin PD := OutPutbitmap.ScanLine[Y]; for X := 0 to Gwidth - 1 do begin WD := X * 3; PD[WD ] := MaskedMat[X, Y]; PD[WD + 1] := MaskedMat[X, Y]; PD[WD + 2] := MaskedMat[X, Y]; end; end; Image1.Canvas.Draw(0, 0 , Outputbitmap); // 出力枠に出力 end; //--------------------- // Logフィルターの実施 //--------------------- procedure TForm1.FilterBtnClick(Sender: TObject); begin LogMask; // LoGフィルター生成 Masking; // LoGフィルターの実施 ReDrawBtn.Enabled := True; RepF := True; crossbtn.Enabled := True; end; //------------------------ // LoGフィルターの作成 //------------------------ procedure TForm1.LogMask; var sigma, LogXY : double; RS : integer; Ch : integer; X, Y : integer; begin val(REdit.Text, RS, Ch); if Ch <> 0 then begin application.MessageBox('R に間違いがあります。', '半径R', 0); exit; end; val(SEdit.Text, sigma, Ch); if Ch <> 0 then begin application.MessageBox('σ に間違いがあります。', 'シグマ σ', 0); exit; end; val(KEdit.Text, K, Ch); if Ch <> 0 then begin application.MessageBox('ゲイン G に間違いがあります。', 'ゲイン G', 0); exit; end; //フィルター配列の確保 MatSize := RS * 2 + 1; SetLength(SXY, MatSize, MatSize); // ストリンググリッドの設定 SigumaGrid.ColCount := MatSize div 2 + 2; SigumaGrid.RowCount := MatSize div 2 + 2; SigumaGrid.ClientWidth := (SigumaGrid.DefaultColWidth + 1) * (MatSize div 2 + 2); SigumaGrid.ClientHeight := (SigumaGrid.DefaultRowHeight + 1) * (MatSize div 2 + 2); // 固定セルにナンバーリング MDIV2 := MatSize div 2; for Y := MDIV2 downto 0 do SigumaGrid.Cells[0 , MDIV2 - Y + 1] := intTostr(Y); for X := MDIV2 downto 0 do SigumaGrid.Cells[X + 1, 0] := intTostr(X); // フィルター値計算と表示 Total := 0; for Y := - MDIV2 to MDIV2 do for X := - MDIV2 to MDIV2 do begin LogXY := (X * X + Y * Y - 2 * sigma * sigma) / 2 / pi / power(sigma, 6) * exp(- (X * X + Y * Y) / 2 / sigma / sigma); LogXY := LogXY * K; SXY[X + MDIV2, Y + MDIV2] := LogXY; Total := Total + LogXY; // グリッドに表示 if (X <= 0) and (Y <= 0) then // 第一象限のみ表示 SigumaGrid.Cells[1 - X , Y + MDIV2 + 1] := FloatTostrF(LogXY, ffFixed, 10, 6); end; Label1.Caption := 'Total= ' + FloatTostr(Total); end; //------------------------------------------ // ファイルのオープンとGray配列作成 //------------------------------------------ procedure TForm1.OpenBtnClick(Sender: TObject); var XX, YY : Integer; // for loop GG ポインター計算用 WIC : TWICImage; PD : PByteArray; WD : Integer; begin OpenPictureDialog1.Filter := FileOpenFilter; if OpenPictureDialog1.Execute then // ファイルが指定されたら begin WIC := TWICImage.Create; try WIC.LoadFromFile(OpenPictureDialog1.FileName); GHeight := WIC.Height; GWidth := WIC.Width; Inputbitmap.SetSize(GWidth, GHeight); Inputbitmap.Canvas.Draw(0, 0, WIC); // フォーマット変換 24ビットフォーマットにします finally WIC.Free; end; end else exit; // キャンセルだったら終了 // Gray配列に入力画像セット SetLength(GrayMat, GWidth, GHeight); for YY := 0 to GHeight - 1 do begin PD := Inputbitmap.ScanLine[YY]; for XX := 0 to GWidth - 1 do begin WD := XX * 3; GrayMat[XX, YY] := PD[WD ] * BK + PD[WD + 1] * GK + PD[WD + 2] * RK; end; end; // 入力画像表示 Image1.Height := GHeight; Image1.Width := GWidth; Image1.Picture.Bitmap.SetSize(GWidth, GHeight); Image1.Canvas.Draw(0, 0, Inputbitmap); // 出力枠に出力 FilterBtn.Enabled := true; ReDrawBtn.Enabled := False; crossBtn.Enabled := False; end; //---------------------------------- // 表示画像の切り替え //---------------------------------- procedure TForm1.ReDrawBtnClick(Sender: TObject); begin if RepF then begin Image1.Canvas.Draw(0, 0, Inputbitmap); // 出力枠に出力 RepF := False; end else begin Image1.Canvas.Draw(0, 0, Outputbitmap); // 出力枠に出力 RepF := True; end; end; //----------------------------- // σ値により半径とゲイン調整 //----------------------------- procedure TForm1.SEditChange(Sender: TObject); var Inr : double; ch : integer; begin val(SEdit.Text, Inr, Ch); if ch <> 0 then exit; REdit.Text := floatTostr(round((Inr + 0.1) * 4)); KEdit.Text := floatTostr(round(power(Inr + 0.7, 4))); end; //---------------------------- // 初期設定 //---------------------------- procedure TForm1.FormCreate(Sender: TObject); begin Inputbitmap := Tbitmap.Create; Inputbitmap.PixelFormat := pf24bit; // 24ビットフォーマット OutPutbitmap := Tbitmap.Create; OutPutbitmap.PixelFormat := pf24bit; // 24ビットフォーマット FilterBtn.Enabled := False; ReDrawBtn.Enabled := False; crossbtn.Enabled := False; end; //---------------------- // 終了処理 //---------------------- procedure TForm1.FormDestroy(Sender: TObject); begin OutPutbitmap.Free; Inputbitmap.Free; end; end.