ガウス差分による線画抽出

 ガウスの半径を変えて平均化(ボカシ)した画像の差分を取ることにより、画像を線画化します。
半径の大きい方から、小さい方を引いたほうが良い結果が得られます。
特に、漫画の画像から、ペンの線を取り出すのに向いています。
半径とシグマ値の組み合わせとなるので、適当な値に調整します、シグマ値を小さくし過ぎると、差分が小さくなり、線画の抽出が出来なくなります。
半径を大きくしたら、シグマ値も大きくして調整しますが半径を大きくすると、抽出した線が太くなります。

サンプル0

Pro0


Pro1 プログラムは、ガウスマスクの半径ではなくサイズを指定するようになっています。
ガウスのシグマ値は、ガウシアンマスクの合計値を指定して行います。
(実際の計算には、合計が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.

    download Gauss-diff.zip

画像処理一覧へ戻る

      最初に戻る