ガウス差分による線画抽出
ガウスの半径を変えて平均化(ボカシ)した画像の差分を取ることにより、画像を線画化します。
半径の大きい方から、小さい方を引いたほうが良い結果が得られます。
特に、漫画の画像から、ペンの線を取り出すのに向いています。
半径とシグマ値の組み合わせとなるので、適当な値に調整します、シグマ値を小さくし過ぎると、差分が小さくなり、線画の抽出が出来なくなります。
半径を大きくしたら、シグマ値も大きくして調整しますが半径を大きくすると、抽出した線が太くなります。
プログラムは、ガウスマスクの半径ではなくサイズを指定するようになっています。
ガウスのシグマ値は、ガウシアンマスクの合計値を指定して行います。
(実際の計算には、合計が1になるように修正された値が使用されます)
修正前の値が指定値になるように、自動的にシグマ値が決定されます。
差分の計算は、Mask 2 から Mask 1
を引いて計算され、差分を Gain倍します。
差分の値は小さいので、Gain 分大きくしていますが、閾値(Threshold)
の値、差分の値、ともに浮動小数点なので、Gain分 差分を大きくしなくても良いと思います。
単に分かりやすい値にしているだけです。
マスクのサイズを大きくすると、抽出される線が太くなります。
プログラム
unit GaussDiffMain; 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, system.Math, system.UITypes, Vcl.Grids; type D2array = array of array of Double; B2array = array of array of Byte; TForm1 = class(TForm) FileOpenBtn: TButton; OpenPictureDialog1: TOpenPictureDialog; MaskingBtn: TButton; FileSaveBtn: TButton; SavePictureDialog1: TSavePictureDialog; Timer1: TTimer; ScrollBox1: TScrollBox; Image1: TImage; magnificationEdit: TLabeledEdit; ScrollBox2: TScrollBox; Image2: TImage; GroupBox1: TGroupBox; Size1: TLabeledEdit; Sigma1: TLabeledEdit; GroupBox2: TGroupBox; Sigma2: TLabeledEdit; Size2: TLabeledEdit; GroupBox3: TGroupBox; threshold: TLabeledEdit; Gain: TLabeledEdit; GroupBox4: TGroupBox; No1: TLabeledEdit; No2: TLabeledEdit; procedure FileOpenBtnClick(Sender: TObject); procedure FormCreate(Sender: TObject); procedure FormDestroy(Sender: TObject); procedure MaskingBtnClick(Sender: TObject); procedure FileSaveBtnClick(Sender: TObject); procedure Timer1Timer(Sender: TObject); private { Private 宣言 } procedure SigumaMatSet(var Gaussian: D2array; var Total: double; MatSize :Integer; TotalGauss :Double); procedure Imageout(Image: TBitmap; ImageNo: integer; magnification: double); procedure GaussianRoutine; function datasump(posY, posX: Integer): Byte; public { Public 宣言 } end; var Form1: TForm1; implementation {$R *.dfm} const OpenFileFilter = '画像ファイル|*.png;*.jpg;*.gif;*.bmp;*.tif;*.ico;*.wdp'+ '|*.png|*.png' + '|*.jpg|*.jpg' + '|*.gif|*.gif' + '|*.bmp|*.bmp' + '|*.tif|*.tif' + '|*.ico|*.ico' + '|*.wdp|*.wdp'; SaveFileFilter = '画像ファイル|*.png;*.jpg;*.gif;*.bmp;*.tif;*.wdp' + '|*.png|*.png' + '|*.jpg|*.jpg' + '|*.gif|*.gif' + '|*.bmp|*.bmp' + '|*.tif|*.tif' + '|*.wdp|*.wdp'; ImageHWC = 440; // 表示枠サイズ Laplacian = 0; Gaussians = 1; Deconvolution = 2; Unsharp = 3; var InputDBitmap : TBitmap; // 入力データー表示用ビットマップ OutputBitmap : TBitmap; // 回転画像表示用ビットマップ GHeight : Integer; // 入力データー画像高さ GWidth : Integer; // 入力データー画像幅 VRect : Trect; // 表示サイズ設定用 InFilename : string; // 入力ファイル名 GryMat : B2array; Gaussian1 : D2array; Gaussian2 : D2array; DiffArray : B2array; const kr = 0.299; // R 輝度変換係数 kg = 0.587; // G 輝度変換係数 kb = 0.114; // B 輝度変換係数 //------------------------ // 変倍出力 //------------------------ procedure TForm1.Imageout(Image: TBitmap; ImageNo: integer; magnification: double); var Rect0 : Trect; MW, MH : Integer; begin MW := Round(GWidth * magnification); MH := Round(GHeight * magnification); Rect0 := Rect(0, 0, MW, MH); if ImageNo = 1 then begin Image1.Width := MW; Image1.Height := MH; Image1.Picture.Bitmap.SetSize(MW, MH); Image1.Canvas.StretchDraw(Rect0, Image); // 出力枠に変倍出力 end; if ImageNo = 2 then begin Image2.Width := MW; Image2.Height := MH; Image2.Picture.Bitmap.SetSize(MW, MH); Image2.Canvas.StretchDraw(Rect0, Image); // 出力枠に変倍出力 end; end; //---------------- ガウシアン用データーの計算 ------------------------------------- // シグマの値でガウス分布の計算をします // gausの値により、配列の大きさを変えられますが、あまり大きくしても効果は変りません //--------------------------------------------------------------------------------- procedure TForm1.SigumaMatSet(var Gaussian: D2array; var Total: double; MatSize :Integer; TotalGauss :Double); var X, Y : integer; KS : Double; MDIV2 : integer; Siguma : double; Dsiguma : double; // シグマ値からガウス分布値の計算 function Maskdatacalc(X, Y : Integer; Z: Double): double; begin Result := 1 / 2 / pi / Z / Z * Exp(-(X * X + Y * Y) / 2 / Z / Z); // マスクデーターの計算 end; begin setlength(Gaussian, MatSize, MatSize); // σ初期値設定 Siguma := 0.5; Dsiguma := Siguma / 2; MDIV2 := MatSize div 2; // σ値によるアンシャープフィルターデーター計算と表示 // σ値による合計の値が指定値になるように調整します repeat Total := 0; for Y := - MDIV2 to MDIV2 do for X := - MDIV2 to MDIV2 do begin // σ値からガウス値計算 KS := Maskdatacalc( X, Y, Siguma); // マスク配列に保存 Gaussian[X + MDIV2, Y + MDIV2] := KS; // 合計計算 Total := Total + KS; end; if Total > TotalGauss then Siguma := Siguma + Dsiguma else begin Siguma := Siguma - Dsiguma; Dsiguma := Dsiguma / 2; Siguma := Siguma + Dsiguma; end; // 合計指定値と合計値の差分が差分指定値より小さくなったら終了 until abs(TotalGauss - Total) < 0.0001; // Gaussianフィルターの値変換 合計値が1になるように調整します // 目的にもよりますが必ずしも必要ではありません for Y := - MDIV2 to MDIV2 do for X := - MDIV2 to MDIV2 do begin KS := Gaussian[X + MDIV2, Y + MDIV2] / Total; Gaussian[X + MDIV2, Y + MDIV2] := KS; // グリッドに表示 end; end; //-------------------------------------- // フィルター計算タイマー遅延 // ボタンの表示切り替えVCL実行の時間待ち //-------------------------------------- procedure TForm1.MaskingBtnClick(Sender: TObject); begin FileOpenBtn.Enabled := False; FileSaveBtn.Enabled := False; Timer1.Enabled := True; end; procedure TForm1.Timer1Timer(Sender: TObject); begin Timer1.Enabled := False; GaussianRoutine; FileSaveBtn.Enabled := True; FileOpenBtn.Enabled := True; end; //------------------------------------------------------ // 配列データーの取り出し // 配列の外側にX,Y が出た場合出た分を内側に折り返します //------------------------------------------------------ function TForm1.datasump(posY, posX: Integer): Byte; var MW, MH : integer; begin if posX < 0 then posX := -posX; MW := GWidth - 1; if posX > MW then begin posX := MW - (posX - MW); end; if posY < 0 then posY := -posY; MH := GHeight - 1; if posY > MH then begin posY := MH - (posY - MH); end; Result := GryMat[posY, posX]; end; //------------------------------------------------- // ガウスフィルターによるボケ画像差分計算 Gaussian //------------------------------------------------- procedure TForm1.GaussianRoutine; var sized1, sized2 : Integer; sigmad1, sigmad2 : Double; threshold0 : Double; gain0 : Double; data1 : Double; data2 : Double; DataDiff : Double; magnification : Double; x, y : Integer; xg, yg : Integer; pos : Integer; Mdiv2 : Integer; poi : Pbytearray; Bdata : Byte; Total : Double; begin // ガウシアンフィルター1 val(Size1.Text, sized1, pos); if pos <> 0 then begin application.MessageBox('入力に間違いがあります。','フィルターサイズ1', 0); exit; end; pos := sized1 mod 2; if pos = 0 then begin application.MessageBox('サイズは奇数でなければいけません。','フィルターサイズ1', 0); exit; end; // ガウシアンフィルター2 val(Size2.Text, sized2, pos); if pos <> 0 then begin application.MessageBox('入力に間違いがあります。','フィルターサイズ2', 0); exit; end; pos := sized2 mod 2; if pos = 0 then begin application.MessageBox('サイズは奇数でなければいけません。','フィルターサイズ2', 0); exit; end; // シグマ値1 val(sigma1.Text, sigmad1, pos); if pos <> 0 then begin application.MessageBox('入力に間違いがあります。','Gauss Total1', 0); exit; end; if sigmad1 >= 1 then begin application.MessageBox('1及び1以上ではいけません。','Gauss Total1', 0); exit; end; // シグマ値2 val(sigma2.Text, sigmad2, pos); if pos <> 0 then begin application.MessageBox('入力に間違いがあります。','Gauss Total 2', 0); exit; end; if sigmad2 >= 1 then begin application.MessageBox('1及び1以上ではいけません。','Gauss Total2', 0); exit; end; // 閾値 val(threshold.Text, threshold0, pos); if pos <> 0 then begin application.MessageBox('入力に間違いがあります。','閾値', 0); exit; end; // ゲイン val(gain.Text, gain0, pos); if pos <> 0 then begin application.MessageBox('入力に間違いがあります。','ゲイン', 0); exit; end; // 表示倍率 Val(magnificationEdit.Text,magnification, pos); if pos <> 0 then begin application.MessageBox('表示倍率入力に間違いがあります。','注意',0); exit; end; // ガウシアン1フィルターデーターの生成 SigumaMatSet(Gaussian1, Total, sized1, sigmad1); No1.Text := FloatTostrF(Total, ffFixed, 4, 3); // ガウシアン2フィルターデーターの生成 SigumaMatSet(Gaussian2, Total, sized2, sigmad2); No2.Text := FloatTostrF(Total, ffFixed, 4, 3); // フィルター値差分計算 for y := 0 to GHeight - 1 do begin for x := 0 to GWidth - 1 do begin // ガウスフィルター1計算 data1 := 0; MDiv2 := sized1 div 2; for yg := -Mdiv2 to Mdiv2 do for xg := -Mdiv2 to Mdiv2 do begin data1 := data1 + datasump(y + yg, x + xg) * Gaussian1[yg + Mdiv2, xg + Mdiv2]; end; // ガウスフィルター2計算 data2 := 0; MDiv2 := sized2 div 2; for yg := -Mdiv2 to Mdiv2 do for xg := -Mdiv2 to Mdiv2 do begin data2 := data2 + datasump(y + yg, x + xg) * Gaussian2[xg + Mdiv2, yg + Mdiv2]; end; // 差分計算 半径の大きい方から小さい方を引く DataDiff := (data2 - data1) * gain0; // 差分値判定 データーは反転されます if DataDiff > threshold0 then Bdata := 0 else Bdata := 255; DiffArray[y, x] := Bdata; end; end; // 結果画像表示処理 for y := 0 to GHeight - 1 do begin poi := OutputBitmap.ScanLine[y]; for x := 0 to GWidth - 1 do begin pos := x * 3; poi[pos ] := DiffArray[y, x]; poi[pos + 1] := DiffArray[y, x]; poi[pos + 2] := DiffArray[y, x]; end; end; Imageout(InputDBitmap, 1, magnification); // 出力枠元画像に変倍出力 Imageout(OutputBitmap, 2, magnification); // 出力枠処理画像に変倍出力 end; //--------------------------------------------------- // ファイルのオープンとグレイデーターとカラーの生成 //--------------------------------------------------- procedure TForm1.FileOpenBtnClick(Sender: TObject); var WIC : TWICImage; X, Y : Integer; PBA : PBytearray; WP : Integer; begin VRect := Rect(0, 0, Image1.Width, Image1.Height); Image1.Canvas.Brush.Style := bsSolid; Image1.Canvas.Brush.Color := clBtnface; Image1.Canvas.FillRect(VRect); // Canvas 画像消去 OpenPictureDialog1.Filter := OpenFileFilter; // ファイルオープンフィルターの設定 if OpenPictureDialog1.Execute = true then // ファイルが指定されたら begin WIC := TWICImage.Create; // TWICImageの生成 try InFilename := OpenPictureDialog1.FileName; // ファイル名の取得 WIC.LoadFromFile(InFilename); // 画像の読み込み GHeight := WIC.Height; // 画像高さ取得 GWidth := WIC.Width; // 画像幅 Image1.Width := GWidth; Image1.Height:= GHeight; Image1.Picture.Bitmap.SetSize(GWidth, GHeight); InputDBitmap.Width := GWidth; InputDBitmap.Height := GHeight; InputDBitmap.Canvas.Draw(0, 0, WIC); // ビットマップに描画フォーマット変換 finally WIC.Free; // TWICImage 解放 end; end else exit; OutputBitmap.Width := GWidth; OutputBitmap.Height := GHeight; setLength(GryMat, GHeight, GWidth); setLength(DiffArray, GHeight, GWidth); for Y := 0 to Gheight - 1 do begin PBA := InputDBitmap.ScanLine[Y]; for X := 0 to GWidth - 1 do begin WP := X * 3; // グレイ画像生成 GryMat[Y][X] := Round(PBA[WP] * kb + PBA[WP + 1] * kg + PBA[WP + 2] * kr); // 輝度変換 end; end; Imageout(InputDBitmap, 1, 1); // 出力枠に変倍出力 MaskingBtn.Enabled := True; end; //------------------------------ // 画像のファイルへの保存 //------------------------------ procedure TForm1.FileSaveBtnClick(Sender: TObject); var WIC : TWicImage; WICF : TWicImageFormat; Fname : String; ExeStr : String; FnameTop: String; Findex : integer; function WFormatSet: Boolean; // 拡張子によるファイルフォーマット設定 begin Result := false; ExeStr := LowerCase(ExeStr); if ExeStr = '.jpg' then begin WICF := Wifjpeg; Result := True; end; if ExeStr = '.jpeg' then begin WICF := Wifjpeg; Result := True; end; if ExeStr = '.tif' then begin WICF := Wiftiff; Result := True; end; if ExeStr = '.tiff' then begin WICF := Wiftiff; Result := True; end; if ExeStr = '.png' then begin WICF := Wifpng; Result := True; end; if ExeStr = '.gif' then begin WICF := Wifgif; Result := True; end; if ExeStr = '.bmp' then begin WICF := Wifbmp; Result := True; end; if ExeStr = '.wdp' then begin WICF := WifWMPhoto; Result := True; end; if ExeStr = '.hdp' then begin WICF := WifWMPhoto; Result := True; end; end; begin SavePictureDialog1.Filter := SaveFileFilter; // SavePictureDialog1.DefaultExt := GraphicExtension(TWicImage); if not SavePictureDialog1.Execute then exit; ExeStr := ExtractFileExt(SavePictureDialog1.FileName); if ExeStr = '' then begin // 拡張子がなかったら Findex := SavePictureDialog1.FilterIndex; // FilterIndexによる拡張子の設定 case Findex of 1, 3 : Fname := ChangeFileExt(SavePictureDialog1.FileName,'.jpg'); // 拡張子の設定 2 : Fname := ChangeFileExt(SavePictureDialog1.FileName,'.png'); // 拡張子の設定 4 : Fname := ChangeFileExt(SavePictureDialog1.FileName,'.gif'); // 拡張子の設定 5 : Fname := ChangeFileExt(SavePictureDialog1.FileName,'.bmp'); // 拡張子の設定 6 : Fname := ChangeFileExt(SavePictureDialog1.FileName,'.tif'); // 拡張子の設定 7 : Fname := ChangeFileExt(SavePictureDialog1.FileName,'.wdp'); // 拡張子の設定 end; end else Fname := SavePictureDialog1.FileName; ExeStr := ExtractFileExt(Fname); // 拡張子だけ取り出し if not WFormatSet then begin // 拡張子によるファイルフォーマット設定と確認 application.MessageBox('ファイルの拡張子が間違っています。','注意', 0); exit; end; FnameTop := ExtractFileName(Fname); // ファイル名だけ取り出し if Length(FnameTop) = Length(ExeStr) then begin // ファイル名の長さ確認 application.MessageBox('ファイル名がありません。','注意', 0); exit; end; if FileExists(Fname) then // ファイル名によるファイル検索 if MessageDlg('既に同じ名前のファイルがあります上書きしますか ' + ExtractFileName(Fname) + '?', mtConfirmation, [mbYes, mbNo], 0, mbNo) = IDNo then exit; WIC := TWicImage.Create; // TWicImage生成 try WIC.Assign(OutputBitmap); // TWicImageにビットマップデーター割り付け WIC.ImageFormat := WICF; // 保存フォーマットセット WIC.SaveTofile(Fname); // ファイルの書き出し finally WIC.Free; // TWicImage解放 end; end; //---------------- // 初期設定 //---------------- procedure TForm1.FormCreate(Sender: TObject); begin Timer1.Enabled := False; Image1.Width := ImageHWC div 2; Image1.Height := ImageHWC div 2; ScrollBox1.Height := ImageHWC; ScrollBox1.Width := ImageHWC; Image1.Top := 0; Image1.Left := 0; Image2.Width := ImageHWC div 2; Image2.Height := ImageHWC div 2; ScrollBox2.Height := ImageHWC; ScrollBox2.Width := ImageHWC; Image2.Top := 0; Image2.Left := 0; InputDBitmap := TBitmap.Create; OutputBitmap := TBitmap.Create; InputDBitmap.PixelFormat := pf24bit; OutputBitmap.PixelFormat := pf24bit; MaskingBtn.Enabled := False; FileSaveBtn.Enabled := False; end; //---------------- // 終了処理 //---------------- procedure TForm1.FormDestroy(Sender: TObject); begin InputDBitmap.Free; OutputBitmap.Free; end; end.