白黒画像の輪郭追跡を行いポリゴン座標を抽出 ~Delphiソースコード集
輪郭追跡のアルゴリズムは
画像処理ソリューション(https://imagingsolution.blog.fc2.com/blog-entry-198.html)を参考にさせていただきました。
また、ある座標が多角形の内側か外側かを調べる方法は、多角形の内と外の判定(https://delfusa.main.jp/delfusafloor/archive/www.nifty.ne.jp_forum_fdelphi/samples/00786.html)を使用させていただきました。
輪郭追跡ユニット
以下ソースコードを「UContourExtraction.pas」ファイルとしてプロジェクフォルダ内にに保存します。
unit UContourExtraction; interface uses System.Types, Vcl.Graphics; type TRGB=record B,G,R:Byte; end; TRGBArr=Array[0..65535] of TRGB; PRGBArr=^TRGBArr; TAAByte=Array of Array of byte; TAPoint=Array of TPoint; TAAPoint=Array of TAPoint; //輪郭抽出 procedure ExtractContours(var Lines:TAAPoint;bmp:TBitmap;ThreshHold:Byte=128); implementation //多角形の内外判定 使用させていただいたソースのURL //https://delfusa.main.jp/delfusafloor/archive/www.nifty.ne.jp_forum_fdelphi/samples/00786.html function PolygonExterior(x,y:Single;p:TAPoint):boolean; var i,ct:Integer; nx,dx,dy,rx,ry:Single; n:Integer; pt:Array of TPoint; begin if Length(p)<5 then begin result:=False; exit; end; if (p[0].X<>p[High(p)].X) or (p[0].Y<>p[High(p)].Y) then begin result:=False; exit; end; n:=Length(p); setLength(pt,n); move(p[0],pt[0],n*SizeOf(p[0])); ct :=0; for i:=Low(pt) to High(pt)-1 do begin rx:=x-pt[i].x; nx:=x-pt[i+1].x; if ((rx<0)and(nx>=0)) or ((rx>=0) and (nx<0)) then begin ry:=y-pt[i].y; dx:= pt[i+1].x- pt[i].x; dy:= pt[i+1].y- pt[i].y; if (rx*dy)<(ry*dx) then Inc(ct) else Dec(ct); end; end; //True:多角形内側 False:多角形の外側 Result:=not(ct=0); end; //取り合えず線座標を集約 Function LineAggregation(Line:TAPoint):TAPoint; var i:Integer; p1,p2:TPoint; begin //点だった場合は返さない if Length(line)<=1 then exit; //線の場合はそのまま返す if Length(line)<4 then begin SetLength(Result,Length(Line)); Move(Line[0],Result[0],Length(Line)*Sizeof(Line[0])); exit; end; SetLength(Result,1); Result[0]:=line[0]; for i := 1 to High(line)-1 do begin p1.X:=Line[i-1].X-Line[i].X; p1.Y:=Line[i-1].Y-Line[i].Y; p2.X:=Line[i].X-Line[i+1].X; p2.Y:=Line[i].Y-Line[i+1].Y; if (p2.X<>p1.X) or (p2.Y<>p1.Y) then begin SetLength(Result,Length(Result)+1); Result[High(Result)]:=Line[i]; end; end; SetLength(Result,Length(Result)+1); Result[High(Result)]:=Line[High(Line)]; end; //x,yを起点とした輪郭抽出 Function ExtractContour(x,y,h:Integer;var img,b:TAAByte):TAPoint; const move:array[0..7] of TPoint=( //探索方向 (x:-1;y:1),(x:0;y:1),(x:1;y:1), (x:1;y:0),(x:1;y:-1),(x:0;y:-1), (x:-1;y:-1),(x:-1;y:0) ); var movei,movec:Integer; MoveFlag,FinishFlag:Boolean; line:TAPoint; i: Integer; begin //URLを参考 https://imagingsolution.blog.fc2.com/blog-entry-198.html // 方向 654 // 7 3 // 012 SetLength(line,1); line[0].X:=x; line[0].Y:=y; b[y][x]:=1; FinishFlag:=False; movei:=0;//探索方向 while not FinishFlag do begin moveflag:=true; movec:=0;//探索カウント while (movec<8) and moveflag do begin if ((x+move[movei].X)>=0) and ((x+move[movei].X)<=high(img[0])) and ((y+move[movei].Y)>=0) and ((y+move[movei].Y)<=high(img)) then begin if (line[0].Y=(y+move[movei].Y)) and (line[0].X=(x+move[movei].X)) then begin //最初の点に戻った場合 x:=x+move[movei].X; y:=y+move[movei].Y; SetLength(line,Length(line)+1); line[High(line)].X:=x; line[High(line)].Y:=y; MoveFlag:=false; FinishFlag:=True; end else if (b[y+move[movei].Y][x+move[movei].X]<=1) and (img[y+move[movei].Y][x+move[movei].X]=1) then begin //有効な点が見つかった場合 inc(b[y+move[movei].Y][x+move[movei].X]); moveflag:=false; x:=x+move[movei].X; y:=y+move[movei].Y; SetLength(line,Length(line)+1); line[High(line)].X:=x; line[High(line)].Y:=y; //次の探索開始方向を設定 movei:= (movei+6)mod 8; end; end; if moveflag then begin inc(movei); if movei>High(move) then movei:=0; inc(movec); end; end; if MoveFlag then FinishFlag:=True; end; //各ピクセルに使用したフラグを立てる for i := Low(line) to High(line) do begin b[line[i].Y][line[i].X]:=2; end; //線座標を集約 line:=LineAggregation(line); result:=line; end; //輪郭抽出 procedure ExtractContours(var Lines:TAAPoint;bmp:TBitmap;ThreshHold:Byte=128); var b:TAAByte; x,y,i:Integer; sc:PRGBArr; img:TAAByte; line:TAPoint; extFlag:Boolean; begin SetLength(b, bmp.Height); SetLength(img, bmp.Height); for y := Low(b) to High(b) do begin sc:=bmp.ScanLine[y]; SetLength(b[y], bmp.Width); SetLength(img[y], bmp.Width); for x := Low(b[y]) to High(b[y]) do begin b[y][x]:=0; img[y][x]:=0; //★★「緑」成分が128以上か127以下かで閾値を決めている ★★ if sc[x].G>=ThreshHold then img[y][x]:=1 else img[y][x]:=0; end; end; for y := Low(b) to High(b) do begin for x := Low(b[y]) to High(b[y]) do begin if (b[y][x]<=1) and (img[y][x]=1) then begin //ポリゴン内判定 extFlag:=False; if Length(Lines)>0 then begin for i := Low(Lines) to High(Lines) do begin extFlag:=PolygonExterior(x,y,Lines[i]); if extFlag then begin b[y][x]:=2; break; end; end; end; //ポリゴン外の場合 if not ExtFlag then begin //x,yを起点とした輪郭抽出 line:=ExtractContour(x,y,bmp.Height,img,b); //抽出できた場合 if Length(line)>0 then begin SetLength(Lines,Length(Lines)+1); SetLength(Lines[High(Lines)],Length(Line)); Move(line[0],Lines[High(Lines)][0],Length(Line)*SizeOf(Line[0])); end; end; end; end; end; end; end.
画面設計
Delphiを起動したら[ファイル]⇒[新規作成]⇒[Windows VCL アプリケーション]をクリックして新規プロジェクトを作成します。
TButton、TMemoをフォームにドラッグドロップします。
TImageを2個、フォームにドラッグドロップします。
ソースコード
Button1をダブルクリックしてソースコードを入力します。
unit Unit1;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants,
System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ExtCtrls,
System.Types;
type
TForm1 = class(TForm)
Image1: TImage;
Button1: TButton;
Image2: TImage;
Memo1: TMemo;
procedure Button1Click(Sender: TObject);
private
{ Private 宣言 }
public
{ Public 宣言 }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
uses UContourExtraction;
procedure TForm1.Button1Click(Sender: TObject);
var ImgCanvas:TCanvas;
Lines:TAAPoint;
i,j:Integer;
pa:array[0..2] of TPoint;
begin
//Form1上のコンポーネントの初期設定
Memo1.Clear;
Image1.Picture.Bitmap.Width:=200;
Image1.Picture.Bitmap.Height:=100;
Image1.Stretch:=True;
Image1.Proportional:=True;
Image1.Picture.Bitmap.PixelFormat:=pf24bit;
Image2.Picture.Bitmap.Width:=Image1.Picture.Bitmap.Width;
Image2.Picture.Bitmap.Height:=Image1.Picture.Bitmap.Height;
Image2.Stretch:=True;
Image2.Proportional:=True;
Image2.Picture.Bitmap.PixelFormat:=pf24bit;
//Image1のビットマップのキャンバスを変数に入れる
ImgCanvas:=Image1.Picture.Bitmap.Canvas;
//Image1を黒色で塗りつぶす
ImgCanvas.Brush.Color:=clBlack;
ImgCanvas.FillRect(ImgCanvas.ClipRect);
//白色で適当に描画する
ImgCanvas.Pen.Color:=clWhite;
ImgCanvas.Pen.Width:=0;
ImgCanvas.Brush.Color:=clWhite;
ImgCanvas.FillRect( Rect(100,40,160,80) );
ImgCanvas.FillRect( Rect(160,50,171,71) );
pa[0].X:=170;
pa[0].Y:=50;
pa[1].X:=180;
pa[1].Y:=60;
pa[2].X:=170;
pa[2].Y:=70;
ImgCanvas.Polygon(pa);
ImgCanvas.Pen.Width:=2;
ImgCanvas.MoveTo(50,30);
ImgCanvas.LineTo(50,10);
ImgCanvas.MoveTo(0,0);
ImgCanvas.LineTo(3,3);
ImgCanvas.LineTo(8,3);
ImgCanvas.LineTo(8,0);
//Image1に適当に描画した画像から輪郭ポリラインの取得
ExtractContours(Lines,Image1.Picture.Bitmap);
//ポリラインの座標を出力
for i := Low(Lines) to High(Lines) do
begin
for j := Low(Lines[i]) to High(Lines[i]) do
Memo1.Lines.Add( Format('%d,%d',[Lines[i][j].X,Lines[i][j].Y]) );
Memo1.Lines.Add('');
end;
//Image2を黒色で塗りつぶす
Image2.Picture.Bitmap.Canvas.Brush.Color:=clBlack;
Image2.Picture.Bitmap.Canvas.FillRect(
Image2.Picture.Bitmap.Canvas.ClipRect
);
//輪郭ポリラインをImage2に描画する
Image2.Picture.Bitmap.Canvas.Pen.Color:=clWhite;
Image2.Picture.Bitmap.Canvas.Pen.Width:=0;
Image2.Picture.Bitmap.Canvas.Brush.Color:=clWhite;
for i := Low(Lines) to High(Lines) do
begin
Image2.Picture.Bitmap.Canvas.Polygon(Lines[i]);
end;
end;
end.
実行する
アプリケーションをコンパイルして実行します。
Button1をクリックします。
Image1を真っ黒に塗り、白色で四角形を塗ったり線を引いたりなど描画後、輪郭抽出を行いポリゴン座標を取得します。
取得したポリゴン座標をImage2に描画します。
Memo1には取得した輪郭のポリゴン座標(x,y座標の配列)が出力されます。