Canny アルゴリズムによるエッジ抽出

 John F. Canny 氏により考案された方法で、エッジ検出の性能は一番です。
輪郭抽出の細線化(Hilditch)を参照にしてください。

1.ガウシアンフィルターで、平滑化(ボケ画像の生成)
2.エッジ強度と勾配方向の計算(4方向に量子化)
3.細線化処理(Non Maximum Suppression)
4.ヒステリシス閾処理(Hysteresis Threshold)
5.エッジ端点の勾配方向と、周囲6点からエッジ再検索

の手順でエッジの検出を行います。

ガウシアンフィルター
 
ガウスフィルター
上記計算式で計算された値で、フィルタリングします。
詳しい内容は、アンシャープマスキングを参照してください

エッジ強度と勾配方向の計算
ソーベルフィルター
ソーベルフィルター
ソーベルフィルターの実行により、エッジの方向と、勾配の強度を計算します。
勾配の強度( I ) 勾配の方向角度( θ
ソーベル計算
エッジの方向は、下図の丸印が、値の大きい部分で、無い場所は値が小さいとすると、値の大きい場所と、フィルターの値の関係で、下図矢印の様に四方向に分類します。

エッジ方向

arctan2の値によって、360度を8等分した方向にします。
     0        1         2         3 
  -22.5°~+22.5°    +22.5°~+67.5°   +67.5°~+112.5° +112.5°~+157.5°
+157.5°~+202.5° +202.5°~+247.5° +247.5°~+292.5° +292.5°~+337.5°(-22.5°)

方向に関して符号なしとし、4方向とします。 (例 左右 右左 は、同じ方向とします。) 

arctan2(y,x)の値は  -π ~ +π の結果となります。

 d :=  round((ArcTan2(coly, colx) + pi)  * 4  /  pi) mod 4;

マイナスの符号をとるため、 π を加算し、4倍して π  で除算し、四捨五入 次いで 4 で割った余りを求めます。
これで、-22.5°~ +337.5°を45°で分割した符号なしの方向を示す値 0~3 となります。

 d :=  int((ArcTan2(coly, colx) + pi + pi / 8)  * 4  /  pi) mod 4;

四捨五入を行わず切り捨てる場合は、pi / 8 を追加加算後整数化すれば、四捨五入と同じになります。


勾配の強度(   ) の最大値は、
勾配の最大値
上図のような場合が最大値となり、1442 が最大値となります。
実際には、ガウシアンフィルターでボケ画像となる為、最大値になることはありません。
ガウスフィルター半径4、theta値1で最大値は、680近辺です。
theta値を非常に小さくすれば、フィルターなしと同じになるので1442となる可能性はあります。

** 説明の都合上 ソーベルフィルターのY方向のマイナス方向を下側にしていますが、ここでのサンプルプログラムでは、画像データーをポインターでアクセスする為、画像の下側から上側に向かってアクセスします。
その為、Y方向のフィルターは上下逆に作用するので、斜め勾配方向が逆になります。**

画像方向
左図はの文字を表しています。
メモリー上では、図形の左下を原点として、図形が正立しています。
Windowsの画像の原点は左上となっているので、注意が必要です。
ソーベルフィルターのY方向は、メモリー上の図形に対して左図の方向で適用されます。
X方向に関しては、変化がありません。
ScanLine[Height - 1];は、左下の原点のポインター値を取得することができます。
P[0]が、原点です。



細線化処理


  細線化処理は、勾配の方向と、勾配の強度から、勾配の強度のピークとなる点を検出しそこをエッジとする作業です。

ヒステリシス閾処理

 閾値一つで、エッジを検出するのは難しいので、閾値を二つ設定して、条件によりエッジを判別します。
大きい値の閾値より大きい勾配強度の場所が有ったら、そこをエッジとし、それに繋がる、低いほうの閾値より大きい値の場所が有れば、エッジとします。
二重検索が発生しない様に、一度検索した場所には、チェック済みフラグセットします。
新しいエッジの場所を見つけると、更にそこにつながる、低いほうの閾値より大きい値の場所を検索します。
見つからなくなったら、次の場所にいどうします。

 閾値処理が終了したら、エッジ(255)以外の場所をゼロ(0)にします。

エッジ再検索追加処理

 閾値を一つ設定します。
エッジとした場所の端点を検索します。(エッジとして連続して繋がっている端の点です)
その点があったら、その点の周りに、エッジを除いた場所の勾配強度値を調べ指定した閾値より大きい場所が有ったら、その点をエッジとします。
勾配強度を調べるのは、勾配方向と直角方向の六ケ所の位置です、勾配方向の二ヶ所は調べません。
   
 閾値より大きい場所が一か所だったら更に検索を続け、それより多い場合又は無い場合は、次の端点の検索に移ります。
これにより、ヒステリシス閾処理で漏れた、小さい値のエッジを追加します。

サンプル画像

サンプルプログラム

 サンプルプログラムは、http://codes-sources.commentcamarche.net/source/54560-detection-de-contour-canny からダウンロードしました。
基本的なプロセスには間違いがないのですが、元々のソースは C ではないかと思われ、勾配強度方向の計算に間違いが有ったので修正しました。
勾配強度は、宣言をWordに変更し、ポインタの使用をやめました。
メモリーの節約をするならば、カラーを24ビット単位にすればよいのですが、あまりにも変更が多いので、32ビットのまゝとしました。
 浮動小数点は、Single から Double に変更です。
ガウシアンフィルターに浮動小数点演算を使用していますが、Single より Double の方が計算が速いためです。
フィルターの値を1000倍して整数にしてフィルターの計算しても良いのですが、現在では浮動小数点でもかなり高速なので、計算速度はあまり改善されません。

unit CannyMain;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ExtCtrls, ExtDlgs, ComCtrls, Jpeg;

type
  TFrmDemoMain = class(TForm)
    Panel1: TPanel;
    btnDoOpenBMP: TButton;
    GroupBox1: TGroupBox;
    Panel2: TPanel;
    Splitter1: TSplitter;
    ScrollBox1: TScrollBox;
    imgOriginal: TImage;
    ScrollBox2: TScrollBox;
    imgResized: TImage;
    OpenPictureDialog1: TOpenPictureDialog;
    ComboStep: TComboBox;
    Label5: TLabel;
    Label6: TLabel;
    ComboMap: TComboBox;
    EAdd: TEdit;
    Label7: TLabel;
    Button2: TButton;
    SavePictureDialog1: TSavePictureDialog;
    GroupBox2: TGroupBox;
    Label1: TLabel;
    Label2: TLabel;
    ERadius: TEdit;
    ETheta: TEdit;
    Button1: TButton;
    GroupBox3: TGroupBox;
    Label3: TLabel;
    Label4: TLabel;
    ELow: TEdit;
    EHigh: TEdit;
    procedure btnDoOpenBMPClick(Sender: TObject);
    procedure Button1Click(Sender: TObject);
    procedure ComboChange(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

var
  FrmDemoMain: TFrmDemoMain;

implementation

uses CannySub;

{$R *.dfm}

// ファイルのオープン jpg とビットマップ
procedure TFrmDemoMain.btnDoOpenBMPClick(Sender: TObject);
var
  name: string;
  jpg:  tjpegimage;
begin
  if not OpenPictureDialog1.Execute then exit;
  name := OpenPictureDialog1.FileName;

  if lowercase(extractfileext(name)) = '.bmp' then
      imgOriginal.Picture.Bitmap.LoadFromFile(Name);
  if lowercase(extractfileext(name)) ='.jpg' then
    begin
      jpg:=tjpegimage.Create;
      jpg.LoadFromFile(name);

      imgOriginal.Picture.Bitmap.Assign(jpg);
      jpg.Free;
    end;
  Button1.Enabled := True;
  ComboMap.Enabled := True;
  ComboStep.Enabled := True;
end;

// Canny プロセス処理
procedure TFrmDemoMain.Button1Click(Sender: TObject);
var
  theta   : double;
  radius  : integer;
  l, h    : Smallint;
  a       : Smallint;
  check   : integer;
begin
  Button1.Enabled := False;
  Button2.Enabled := False;
  ComboMap.Enabled := False;
  ComboStep.Enabled := False;
  application.ProcessMessages;
  if not trystrtoint(ERadius.Text, radius) then begin     // ガウシアンフィルター半径
    radius := 3;
    ERadius.Text := '3';
  end;
  if radius < 1 then begin
    radius := 1;
    ERadius.Text := '1';
  end;
  if radius > 20 then begin
    radius := 20;
    ERadius.Text := '20';
  end;

  if not trystrtoFloat(ETheta.Text, theta) then begin    // thetaとなってしますがsiguma(σ)が一般的です
    theta  := 1.0;
    ETheta.Text := '1.0';
  end;
  if theta <= 0 then begin
    theta  := 1.0;
    ETheta.Text := '1.0';
  end;
  if theta > 10 then begin
    theta  := 10;
    ETheta.Text := '10';
  end;

  val(ELow.Text, l, Check);                                // ヒステリシス閾処理の低い方の値
  if Check <> 0 then begin
    l := 30;
    ELow.Text := '30';
  end;
  if l < 0 then  begin
    l := 0;
    ELow.Text := '0';
  end;
  if l > 255 then  begin
    l := 255;
    ELow.Text := '255';
  end;

  val(EHigh.Text, h, Check);                              // ヒステリシス閾処理の高い方の値
  if Check <> 0 then begin
    h := 60;
    EHigh.Text := '60';
  end;
  if h < l then begin
    h := l;
    EHigh.Text := ELow.Text;
  end;
  if h > 255 then begin
    h := 255;
    EHigh.Text := '255';
  end;

  val(EAdd.Text, a, Check);                               // 追加エッジの閾値
  if Check <> 0 then begin
    a := 10;
    EAdd.Text := '10';
  end;
  if a < 0 then begin
    a := 0;
    EAdd.Text := '0';
  end;
  if a > 1442 then begin
    a := 1442;
    EAdd.Text := '1442';
  end;
  // Canny プロセス処理
  Canny(imgOriginal.Picture.Bitmap, imgResized.Picture.Bitmap, radius, theta, l, h, a,
        ComboMap.ItemIndex, ComboStep.ItemIndex);
  imgResized.Invalidate;                          // 再描画
  Button2.Enabled := True;
  Button1.Enabled := True;
  ComboMap.Enabled := True;
  ComboStep.Enabled := True;
end;

// プロセスの選択
procedure TFrmDemoMain.ComboChange(Sender: TObject);
begin
  Button1Click(self);
end;

// 初期設定
procedure TFrmDemoMain.FormCreate(Sender: TObject);
begin
  Button1.Enabled := False;
  Button2.Enabled := False;
  ComboMap.Enabled := False;
  ComboStep.Enabled := False;
  imgOriginal.Picture.Bitmap.PixelFormat := pf32bit;
  imgResized.Picture.Bitmap.PixelFormat  := pf32bit;
end;

// ファイル保存 Jpg 又は ビットマップ
procedure TFrmDemoMain.Button2Click(Sender: TObject);
var
  name: string;
  jpg: tjpegimage;
begin
  if not SavePictureDialog1.Execute then exit;
  name := SavePictureDialog1.FileName;
  case SavePictureDialog1.FilterIndex of
    1: name := ChangeFileExt(name,'.jpg');
    2: name := ChangeFileExt(name,'.bmp');
  end;
  if Length(ExtractFileName(name)) <= 4 then exit;
  if lowercase(extractfileext(name)) = '.bmp' then
    imgResized.Picture.Bitmap.SaveToFile(Name);
  if lowercase(extractfileext(name)) = '.jpg' then
  begin
    jpg:=tjpegimage.Create;
    jpg.Assign(imgResized.Picture.Bitmap);
    jpg.SaveToFile(name);
    jpg.Free;
  end;
end;

end.
unit CannySub;

interface

uses Windows, Graphics, Math,StrUtils, SysUtils;

procedure Canny(const bm_in: TBitmap; const bm_out: TBitmap;
                GaussSize: integer; GaussTheta: double;
                lowerThreshold, upperThreshold: byte; addThreshold: Word;
                ColorMap, Level: integer);

implementation

type
  TLongArray = array[0..0] of Cardinal;          // 配列のポインターのみ必要なので長さ[0..0]でOK
//  TLongArray = array[0..65535] of integer;
  PLongArray = ^TLongArray;

var
  Pbm_In, Pbm_Out: PLongArray;
  tmp_In  : array of Cardinal;
  edgeDir : array of Cardinal;
  gradient: array of Word;                        // ソーベル値の保存 最大値は1442
  Hyst    : array of Cardinal;

  WPicth          : integer;
  Width, Height   : integer;
  ColorMapMode    : integer;
  Threshold       : Word;
  Maxg            : Word;

// fonctions diverses...
//==============================================================================
// colormap mode check
function colormapcheck(i: integer): Boolean;
var
  ic : integer;
begin
  Result := False;
  ic := i mod 4;
  if ic = 3 then exit;                  // 4バイト目(フラグバイト)は、モードに関係なく処理しない
  if ic = 0 then begin                  // 1バイト目(blue)は、モードに関係なく処理
    Result := True;                     //  ColorMapMode = 1 以外は 1バイト目(blue)のみ使用します
    exit;
  end;
  if ColorMapMode = 1 then             // ColorMapmode RGB 時は全ての色 B, g, r 処理
    Result := True;
// if ((ColorMapMode <> 1) and (i mod 4 <> 0)) or (i mod 4 = 3)) then Result := False
//                                                               else Result := True;
// 理解しやすい形に書き換えました、実際には上記2行と同じです。
end;

// r g b a byte data to 32bit color data
function RGBA(r, g, b, a: Byte): COLORREF;
begin
  Result := (r or (g shl 8) or (b shl 16) or (a shl 24));
end;

// 色相変換
function ArcEnCiel(a: Word): COLORREF;
                                           // gradient の最大値 1140     1140 = 19 * 60
  function aconv(ain : Word): byte;
  begin
    result := 17 * (ain mod 60) shr 2;
  end;

begin         // B    G    R
  result := RGB(255, 255, 255);
  case (a div 60) mod 6 of
    0: result := RGB(           255,       aconv(a),              0);
    1: result := RGB(255 - aconv(a),            255,              0);
    2: result := RGB(             0,            255,       aconv(a));
    3: result := RGB(             0, 255 - aconv(a),            255);
    4: result := RGB(      aconv(a),              0,            255);
    5: result := RGB(           255,              0, 255 - aconv(a));
  end;
end;

// rgb to gray scale
function RGBtoGray(c: COLORREF): COLORREF;
var
  r, g, b: cardinal;
begin
  r := (c and $FF0000) shr 16;
  g := (c and $00FF00) shr  8;
  b := (c and $0000FF) ;

  c := (r + g + b) div 3;                   // エッジ検出なので平均値を使用します。
//  c := (r * 38 + g * 75 + b * 15) shr 7;    // NTSC係数変換
  result := $010101 * byte(c);              // RGB同じ値セット
end;

function getPixel(pt: pointer; xx, yy: integer): COLORREF;
begin
  result:= PLongArray(pt)[xx + yy * Width];
end;

procedure SetPixel(pt: pointer; xx, yy: integer; px: COLORREF);
begin
  PLongArray(pt)[xx + yy * Width] := px;
end;

function getBytePixel(pt: pointer; xx, yy: integer): Byte;
begin
  if xx < 0 then                                       // 範囲がマイナイ側に超える場合一番外側の値で代用
    if xx mod 4 = 0 then xx := 0                       // 範囲を超えるのはガウシアンフィルター時
                    else xx := 4 + xx mod 4;
  if yy < 0 then yy := 0;
  if xx > WPicth - 1 then xx := WPicth - 4 + xx mod 4; // 範囲がプラス側に超える場合一番外側の値で代用
  if yy > Height - 1 then yy := Height - 1;            // 範囲を超えるのはガウシアンフィルター時
  result:= PByteArray(pt)[xx + yy * WPicth];
end;

procedure SetBytePixel(pt: pointer; xx, yy: integer; px: byte);
begin
  PByteArray(pt)[xx + yy * WPicth] := px;               // Blue バイト書き込み
  if ColorMapMode <> 1 then begin                       // RGB モード以外は r,g,b 同じ値書き込み
    PByteArray(pt)[xx + 1 + yy * WPicth] := px;
    PByteArray(pt)[xx + 2 + yy * WPicth] := px;
    PByteArray(pt)[xx + 3 + yy * WPicth] := 0;          // 4バイト目カラーフラグは0ゼロをセット
  end;
end;

procedure copyimage(pt_In, pt_Out: pointer);
var
  i, j: integer;
begin
  for j := 0 to Height - 1 do
    for i := 0 to Width - 1 do
      SetPixel(pt_Out, i, j, GetPixel(pt_In, i, j));
end;

//==============================================================================
// ソーベル値 傾斜の大きさカラー表示
procedure ColorGradient;
var
  i, j: integer;
  Msc: double
begin
  Msc := 359 / Maxg;            // 最大値を359に設定する
  for j := 0 to Height - 1 do
    for i := 0 to Width - 1 do
      SetPixel(Pbm_Out, i, j, Arcenciel(round(gradient[i * 4 + j * WPicth] * Msc)));
end;

// ソーベルによる傾斜方向色表示
procedure ColorDirection;
var
  i, j: integer;
  dc  : integer;
const
                                    // blue     red    yellow   green
  EdgeColor: array[0..3] of Tcolor = ($00001, $010000, $010100, $000100);
begin
  for j := 0 to Height-1 do
    for i := 0 to Width - 1 do begin
      dc := gradient[i * 4 + j * WPicth];                         // gradient 最大値1140
      if dc > 255 then dc := 255;                                 // byte 255
      SetPixel(Pbm_Out, i, j, EdgeColor[GetBytePixel(PLongArray(edgeDir), i * 4, j)] * dc);
    end;
end;

// passe l'image en niveau de gris
//==============================================================================
procedure MakeGrayScale;
var
  i, j: integer;
begin
  for j := 0 to Height - 1 do
    for i := 0 to Width - 1 do
      case ColorMapMode of
        0: SetPixel(PLongArray(Tmp_In), i, j, RGBtoGray(GetPixel(Pbm_In, i, j)));           // GrayScale
        1: SetPixel(PLongArray(Tmp_In), i, j, getPixel(Pbm_In, i, j));                      // FRGB
        2: SetBytePixel(PLongArray(Tmp_In), i * 4, j, getbytePixel(Pbm_In, i * 4 + 2, j));  // Red
        3: SetBytePixel(PLongArray(Tmp_In), i * 4, j, getbytePixel(Pbm_In, i * 4 + 1, j));  // Green
        4: SetBytePixel(PLongArray(Tmp_In), i * 4, j, getbytePixel(Pbm_In, i * 4 + 0, j));  // Blue
      end;
end;

// applique un flou gaussien
//==============================================================================

procedure GaussianBlur(size: integer; theta: double);
var
  i, j, x, y: integer;
  col     : double;
  c       : Byte;
  theta2  : double;
  Gauss   : double;
  GaussSum: double;
  GaussMatrice: array of array of double;
begin
 // フィルターサイズが1だったら単純コピー
  if size = 1 then begin
    for j := 0 to Height - 1 do
      for i := 0 to Width - 1 do
        SetPixel(Pbm_Out, i, j, getPixel(Pbm_In, i, j));
    exit;
  end;

  // Gauss matrice filter calc
  theta2 := 2 * theta * theta;
  size := size - 1;
  setlength(GaussMatrice, size * 2 + 1);
  GaussSum := 0;
  for j := -size to size do begin
    setlength(GaussMatrice[size + j], size * 2 + 1);
    for i := 0 to size do begin
      Gauss := exp(-(j * j + i * i) / theta2) / (pi * theta2);
      GaussMatrice[size + j, size + i] := Gauss;
      GaussSum := GaussSum + Gauss;                             // 全体が1にならないので合計値計算
      if i = 0 then continue;                                   // 零時中心位置なので反対側なし
      GaussMatrice[size + j, size - i] := Gauss;
      GaussSum := GaussSum + Gauss;                             // 全体が1にならないので合計値計算
    end;
  end;

  // filterの適用
  for j := 0 to Height - 1 do
    for i := 0 to WPicth - 1 do
      if colormapcheck(i) then begin                            // 計算の条件確認
        col := 0;
        for y := -size to size do
          for x := -size to size do begin
            c := getBytePixel(PLongArray(Tmp_In), i + x * 4, j + y);
            col := col + GaussMatrice[size + x, size + y] * c;
          end;
        SetBytePixel(Pbm_Out, i, j, round(col / GaussSum));      // 合計値で補正  保存
      end;
end;

// applique le filtre de Sobel qui recherche les contours suivant X et Y
//==============================================================================
const
  Matrice_Sobel_x: array[-1..1, -1..1] of integer = ((-1, 0, 1),  (-2, 0, 2),  (-1, 0, 1));   // array[ y, x]
  Matrice_Sobel_y: array[-1..1, -1..1] of integer = (( 1, 2, 1),  ( 0, 0, 0),  (-1,-2,-1));

procedure Sobel;
const
  angle45   = pi / 4;         // angle45   = 45°
var
  i, j, x, y: integer;
  colx, coly: integer;
  c         : integer;
  angle     : Byte;
  grad      : Word
begin
  // on efface le tableau
  for j := 0 to Height - 1 do
    for i := 0 to WPicth - 1 do
      gradient[i + j * WPicth] := 0;
  for j := 0 to Height - 1 do
    for i := 0 to Width - 1 do
      SetPixel(PLongArray(edgeDir),  i, j, 0);

  Maxg := 0;
  for j := 1 to Height - 2 do                                         // ソーベルフィルターサイズ分小さくスキャン
    for i := 4 to WPicth - 5 do
      if colormapcheck(i) then begin
        colx := 0;
        coly := 0;
        for y := -1 to 1 do
          for x := -1 to 1 do begin
            c := getBytePixel(Pbm_Out, i + x * 4, j + y);
            colx := colx + Matrice_Sobel_x[y, x] * c;                 // X方向ソーベルフィルター
            coly := coly + Matrice_Sobel_y[y, x] * c;                 // Y方向ソーベルフィルター
          end;
        grad := Round(Sqrt(colx * colx + coly * coly));               // ソーベルの値保存  最大値1140
        gradient[i + j * WPicth] := grad;
        if grad > Maxg then Maxg := grad;
        angle := round((ArcTan2(coly, colx) + pi) / angle45) mod 4;         // 0~3  edge 角度方向係数を求める
        SetBytePixel(PLongArray(edgeDir), i, j, angle);
      end;
end;

// trace un trait sur chaque contour (Non maximum Suppression)
//==============================================================================
procedure findEdge(x_Shift, y_Shift, x, y:integer);
var
  g1, g2, g: Word;
begin
  g  := gradient[ x + y * WPicth];                                // 中央位置の値取得
  g1 := gradient[ x + x_Shift + (y + y_Shift) * WPicth];          // プラス位置値取得
  g2 := gradient[ x - x_Shift + (y - y_Shift) * WPicth];          // マイナス位置値取得
  if (g > g1) and (g > g2) then begin                             // g 中央の値が一番大きかったら出力画像にその値セット
    if g > 255 then g := 255;                                     // ソーベルgradient最大値は1140  byteは255
    SetBytePixel(Pbm_Out, x, y, g);
  end
  else SetBytePixel(Pbm_Out, x, y, 0);                            // 違ったらゼロをセット
end;

procedure TraceAllEdges;
var
 i, j:integer;
begin
  for j := 1 to Height - 2 do
    for i := 4 to WPicth - 5 do
      if colormapcheck(i) then begin
        case GetBytePixel(PLongArray(edgeDir), i, j) of       // 方向データー取得 方向によってエッジ検出
          0: findEdge( 4, 0, i, j);                           // 右  左
          1: findEdge(-4, 1, i, j);                           // 左上  右下
          2: findEdge( 0, 1, i, j);                           // 上  下
          3: findEdge( 4, 1, i, j);                           // 右上  左下
        end;
      end;
end;

// cherche les points maxi sur les contours et efface les autres
//==============================================================================

procedure suppressNonMax( i, j: integer; lowerThreshold: byte);
begin
  // pas assez blanc
  if GetBytePixel(Pbm_Out, i, j) <= lowerThreshold then exit;
  // pas checked pos
  if GetBytePixel(PLongArray(Hyst), i, j) = 255 then exit;

  SetBytePixel(PLongArray(Hyst), i, j, 255);
  SetBytePixel(Pbm_Out, i, j, 255);
  // 周囲8か所確認
  suppressNonMax(i - 4, j - 1, lowerThreshold);     // 左下
  suppressNonMax(i    , j - 1, lowerThreshold);     // 下
  suppressNonMax(i + 4, j - 1, lowerThreshold);     // 右下
  suppressNonMax(i - 4, j    , lowerThreshold);     // 左
  suppressNonMax(i + 4, j    , lowerThreshold);     // 右
  suppressNonMax(i - 4, j + 1, lowerThreshold);     // 左上
  suppressNonMax(i    , j + 1, lowerThreshold);     // 上
  suppressNonMax(i + 4, j + 1, lowerThreshold);     // 右上
end;

procedure Hysteresis(lowerThreshold, upperThreshold: byte);
var
  i, j: integer;
begin
  // on efface le tableau
  for j := 0 to Height - 1 do
    for i := 0 to Width - 1 do
      SetPixel(PLongArray(Hyst), i, j, 0);

  // 周囲をチェック済みにします。
  // 周囲1ラインはソーベルフィルタの適用をしていないので黒にセット
  for j := 0 to Height - 1 do begin
    SetPixel(Pbm_Out,          0, j, 0);         // 左
    SetPixel(Pbm_Out,  Width - 1, j, 0);         // 右
  end;
  for i := 0 to Width  - 1 do begin
    SetPixel(Pbm_Out, i,          0, 0);         // 下
    SetPixel(Pbm_Out, i, Height - 1, 0);         // 上
  end;

  for j := 0 to Height - 1 do
    for i := 0 to WPicth - 1 do
      if colormapcheck(i) then begin
       // point dej・trait・ on passe au suivant
        if GetBytePixel(PLongArray(Hyst), i, j) = 255 then continue;
        // pas assez blanc pour le traitement
        if GetBytePixel(Pbm_Out, i, j) < upperThreshold then continue;
        suppressNonMax( i, j, lowerThreshold);
      end;
end;

// 255 以下は 0に
procedure ClearNonWhitePixel;
var
  i, j: integer;
  c   : Byte;
begin
  for j := 0 to Height - 1 do
    for i := 0 to WPicth - 1 do
      if colormapcheck(i) then begin
        c := GetBytePixel(Pbm_Out, i, j);
       if c < 255 then SetBytePixel(Pbm_Out, i, j, 0);
      end;
end;

// AddEgde  エッジ検索追加
//==============================================================================

// 周囲8か所チェック 0以外カウント
function CountNeighbors(i, j: integer): Byte;
begin
  result := 0;
  if GetBytePixel(Pbm_Out, i, j) = 0 then exit;

  if GetBytePixel(Pbm_Out, i + 4, j + 1) > 0 then inc(result);      // 右上
  if GetBytePixel(Pbm_Out, i    , j + 1) > 0 then inc(result);      // 上
  if GetBytePixel(Pbm_Out, i - 4, j + 1) > 0 then inc(result);      // 左上

  if GetBytePixel(Pbm_Out, i + 4, j    ) > 0 then inc(result);      // 右
  if GetBytePixel(Pbm_Out, i - 4, j    ) > 0 then inc(result);      // 左

  if GetBytePixel(Pbm_Out, i + 4, j - 1) > 0 then inc(result);      // 右下
  if GetBytePixel(Pbm_Out, i    , j - 1) > 0 then inc(result);      // 下
  if GetBytePixel(Pbm_Out, i - 4, j - 1) > 0 then inc(result);      // 左下
end;

type
  TMatrisse = array[1..6] of TPoint;

const
  Matrisses : array[0..3] of TMatrisse = (
                                                  //                 4 5 6
  ((x: -4; y: -1),(x: +0; y: -1),(x: +4; y: -1),  //                   C
   (x: -4; y: +1),(x: +0; y: +1),(x: +4; y: +1)), // Matrisse_N_S    1 2 3

                                                  //                   1 2
  ((x: +0; y: +1),(x: +4; y: +1),(x: +4; y: +0),  //                 4 C 3
   (x: -4; y: -0),(x: -4; y: -1),(x: -0; y: -1)), // Matrisse_NE_SW  5 6

                                                  //                 3   6
  ((x: -4; y: -1),(x: -4; y: +0),(x: -4; y: +1),  //                 2 C 5
   (x: +4; y: -1),(x: +4; y: +0),(x: +4; y: +1)), // Matrisse_E_W    1   4

                                                  //                 2 3
  ((x: -4; y: +0),(x: -4; y: +1),(x: -0; y: +1),  //                 1 C 4
   (x: +4; y: -0),(x: +4; y: -1),(x: +0; y: -1))  // Matrisse_NW_SE    6 5
  );

procedure findNextEdge(x, y: integer; mat: TMatrisse);
var
  dx, dy: integer;
  g: array[1..6] of Word;
  m, i, j: integer;
begin
  m := 0;
  for i := 1 to 6 do begin
    g[i] := gradient[ x + mat[i].X + (y + mat[i].Y) * WPicth];                // ソーベル値取得
    if GetBytePixel(Pbm_Out, x + mat[i].X, y + mat[i].Y) > 0 then g[i] := 0;  // 出力値が255 だったらg[l]=0
    if g[i] > Threshold then m := 1;               // 指定値より大きい値があったらフラグセット
  end;
  // pas de voisin avec in gradient assez haut
  if m = 0 then exit;                              // 指定値より大きい値がなかったら此処まで

  m := 0;
  j := 0;
  for i := 1 to 6 do
    if g[i] > m then begin                         // 最大値検索
      m := g[i];
      j := i;
    end;
  dx := mat[j].X;
  dy := mat[j].y;
  SetBytePixel(Pbm_Out, x + dx, y + dy, 255);      // 最大値の場所を白に
  if CountNeighbors(x + dx, y + dy) = 1 then       // 周囲に0以外が1か所だったら再帰呼び出し 端点だったら
    findNextEdge(x + dx, y + dy, Matrisses[GetBytePixel(PLongArray(edgeDir), x + dx, y + dy)]); // 方向セットエッジ検索
end;

// エッジ再検索
procedure AddEgde;
var
  i, j: integer;
begin
  for j := 1 to Height - 2 do                      // ソーベルフィルターサイズ分小さくスキャン
    for i := 4 to WPicth - 5 do
      if colormapcheck(i) then begin
        if CountNeighbors(i , j) = 1 then          // i,j の位置が0でなく周囲に一か所0以外があったら 端点だったら
          findNextEdge(i, j, Matrisses[GetBytePixel(PLongArray(edgeDir), i, j)]);   // 方向セットエッジ検索
      end;
end;

//==============================================================================
//==============================================================================

procedure Canny(const bm_in: TBitmap; const bm_out: TBitmap;
                GaussSize: integer; GaussTheta: double;
                lowerThreshold, upperThreshold: byte; addThreshold: Word;
                ColorMap, Level: integer);
begin
  // initialisation des variables
  Width := bm_In.Width;
  Height:= bm_In.Height;

  bm_Out.Width  := Width;                 // ハンドルのみ継承されるので条件設定
  bm_Out.Height := Height;

  bm_In.PixelFormat  := pf32bit;          // Format変換 ファイルは24bit が多い
  bm_Out.PixelFormat := pf32bit;          // フォーマット未設定なのでここで設定

  WPicth := Width * 4;                    // 1 Pixel 4 bytes

  Pbm_In  := bm_In.ScanLine[Height - 1];  // 先頭アドレポインタのセット 
  Pbm_Out := bm_Out.ScanLine[Height - 1]; // メモリーの方向逆なので注意  [0] ではNG [0]は 最後のライン

  setlength(  Tmp_In,  Width * Height);   // 配列の大きさ確保 Cardinal
  setlength( edgeDir,  Width * Height);   //                  Cardinal
  setlength(    Hyst,  Width * Height);   //                  Cardinal
  setlength(gradient, WPicth * Height);   // ソーベル値用     Word

  ColorMapMode := ColorMap;
  Threshold := addThreshold;

  // traitement de l'image
  MakeGrayScale;                              // グレースケール化
  if level = 0 then begin
    copyimage(PLongArray(Tmp_In), Pbm_Out);
    exit;
  end;

  GaussianBlur(GaussSize, GaussTheta);        // ガウスフィルターによるぼかし
  if level = 1 then exit;

  Sobel;                                      // X Y 方向ソーベルフィルター輪郭抽出 ソーベル(微分フィルター)
  if level = 2 then begin
    ColorGradient;                            // 勾配(微分結果)色相カラー表示
    exit;
  end;

  if level = 3 then begin
    ColorDirection;                           // 角度カラー表示
    exit;
  end;

  TraceAllEdges;                              // エッジ抽出 ソーベルの値と方向値からエッジ検索
  if level = 4 then exit;

  Hysteresis(lowerThreshold, upperThreshold); // アッパースレッドより大きい場所の周囲の
                                              // ロアースレッドより大きい値検束
  if level = 5 then exit;

  ClearNonWhitePixel;                         // 255(白)より小さい値はゼロ(黒)に Non maximum Suppression
  if level = 6 then exit;

  AddEgde;                                    // エッジ追加 指定値より大きいソーベルの値と方向からエッジ再検索

end;

end.

    download cannytest.zip

画像処理一覧へ戻る

      最初に戻る