0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Python版の輪郭検出アルゴリズムをGdscriptへ移植

0
Last updated at Posted at 2026-01-14

はじめに

Gdscriptを使った「Suzuki85メソッド」画像輪郭抽出コードを(移植して)作った!ので紹介します、という記事であり、Suzuki85メソッドそのものを解説する記事ではありません。

Godotで「Suzuki85メソッド」を使いたいとお考えの方々(そんな人いるのか?)に、ほんの少しでも「役にたった」と思っていただけたら、すごくうれしいです。

本記事概要

画像の境界を抽出する手法として OpenCVのcontour detectionが有名です。
Godot4にて 衝突判定のためにOpenCVのような輪郭検出をさせてみたかったのですが、OpenCVをインストールを前提にしたくはなく、自前で(Gdscriptだけで)実現させたいと思っていました。

OpenCVではSuzuki85メソッドと呼ばれるアルゴリズムを採用しているとのことで、このメソッドについて調べるうちに、PythonにてSuzuki85を実装しているコード(Github)を見つけてしまいました。

Pythonコードで書かれた輪郭抽出コードをGdscriptに移植してみたところ、うまく動作しているようなので、紹介します。

移植版( Godotプロジェクト )

Github

contour_detection.gd

Godotバージョン

  • Godot4.2

利用方法

image.png

image.png

クラス「ContourDetection」を使って輪郭抽出をします。

Viewerノードのスクリプトの一部(輪郭抽出した結果を描く画像)
func _ready() -> void:
	_test01()

func _test01()->void:
    var sprite:Sprite2D = $"../Sprite2D" # sprite original image
	var org_image:Image = sprite.texture.get_image()
	
    # create new ImageTexture
	var _texture:ImageTexture = ImageTexture.new()
	var _image:Image = Image.create(
		org_image.get_size().x+10,
		org_image.get_size().y+10, 
		false, Image.FORMAT_RGBA8)
	_image.fill(Color(1,1,1,1))
	_texture.set_image(_image)
	self.texture = _texture	

    # get contours
	var _contour_detection:ContourDetection = ContourDetection.new()
	var obj_detection:ContourDetection.RasterScan = _contour_detection.raster_scan(org_image)

    # draw contours
    for _contour:ContourDetection.Contour in obj.contours:
		for cell:ContourDetection.Cell in _contour.list():
			var _pos = cell.to_vector2()
			_image.set_pixel(_pos.x, _pos.y, Color(0,0,0,1))
			_texture.set_image(_image)
			self.texture = _texture	
			#await get_tree().create_timer(0.01).timeout
Vewer2ノードのスクリプトの一部(2値化した画像)
func _ready() -> void:
	_test01()

func _test01()->void:
	var image:Image = sprite.texture.get_image()
	var _texture:ImageTexture = ImageTexture.new()
	var _image:Image = Image.create(image.get_size().x+2, image.get_size().y+10, false, Image.FORMAT_RGBA8)
	_image.fill(Color(0,0,0,0))
	_texture.set_image(_image)
	self.texture = _texture
	var contour_detection = viewer.contour_detection
	var img:ContourDetection.ScanImage = contour_detection.scan_img
	print("_image size=",_image.get_size())
	print("img size=", img._img.size(),",", img._img.get(0).size())
	print("rows,cols=", img.rows,",", img.cols)
	for i in img.cols:
		for j in img.rows:
			if img.get_init_value_i(j, i) > 0:
				_image.set_pixel(i, j, Color(0,0,0,1))
			
	_texture.set_image(_image)
	self.texture = _texture	

実行結果

image.png

輪郭を描画するの様子

10ms ごとに点を描いた様子です。

    # draw contours
    for _contour:ContourDetection.Contour in obj.contours:
		for cell:ContourDetection.Cell in _contour.list():
			var _pos = cell.to_vector2()
			_image.set_pixel(_pos.x, _pos.y, Color(0,0,0,1))
			_texture.set_image(_image)
			self.texture = _texture	
			await get_tree().create_timer(0.01).timeout # 10ms 待つ

img_3027.gif

輪郭抽出の戻り値 obj.contours は輪郭の配列です。

image.png

輪郭は、点を配列にしたもので、この点を描画すれば輪郭を描くことができます。

Pythonで書かれた輪郭抽出コードをGdscriptへ移植

Gdscriptへの移植をするときの注意点

タプルは使えない

Gdscriptでは複数の戻り値を一度に「タプル」として受け取ることができません。

Pythonコード
def test( a, b ) :
    return a+1, b+2

def main():
    x, y = test( 1, 2 )  # タプルを使用
    print(x)  # <--- 2
    print(y)  # <--- 4

戻り値はオブジェクトとして返すことにします。

Gdscriptコード
func test( a:int, b:int ) :
    var obj:TestObj = TestObj.new()
    obj.x = a+1
    obj.y = b+2
    return obj

func main():
    var obj:TestObj = test( 1, 2 )  # タプルを使用
    print(obj.x)  # <--- 2
    print(obj.y)  # <--- 4

class TestObj:
    x:int
    y:int

整数(int型)の引数の扱いは同じ!

Pythonでは整数(int型)の引数を参照型としては扱えません。
つまり、関数の中の操作でint型の引数を変更しても 呼び出し元の変数値は変化しません。

Pythonコード
def test( a ) :
    a += 5

def main():
    i = 0
    test( i )
    print( i ) # <--- 0  変化しない

Gdscript版でも整数(int型)の引数を参照型としては扱えません。

Gdscriptコード
func test( a: int )->void:
    a += 5

func main():
    var a:int = 0
    test( a )
    print( a )  # <--- 0  変化しない

関数内で変更されたint型の変数の値を使いたい場合は、次のどちらかが手っ取り早いですね。

  • 戻り値で取得する
  • オブジェクト型の引数にして渡す

配列型の引数の扱い

配列型の引数は参照型として扱われます。
つまり、関数の中の操作で配列内の要素の値を変更すると、呼び出し元の配列も変化します。

Pythonコード
def test( arr ) :
    arr[1][1] = 9

def main():
    arr = [[0,0,0],[0,0,0],[0,0,0]]
    test( arr )
    print( arr ) # <--- [[0,0,0],[0,9,0],[0,0,0]]

Gdscript版でも、配列型の変数をそのまま引数にしても Python版と同じです。

Gdscriptコード
func test( arr: Array )->void:
    arr.get(1).get(1) = 9

func main():
    var arr:Array = [[0,0,0],[0,0,0],[0,0,0]]
    test( arr )
    print( arr )  # <--- [[0,0,0],[0,9,0],[0,0,0]]

配列型の複製

PythonとGdscriptでは 配列の複製の方法が異なります。

Pythonコード
def test( arr ) :
    _arr = list( arr )  # 配列を引数として 配列を作り変える
    _arr[1][1] = 9

def main():
    arr = [[0,0,0],[0,0,0],[0,0,0]]
    test( arr )
    print( arr ) # <--- [[0,0,0],[0,0,0],[0,0,0]]  変化しない
Gdscriptコード
func test( arr: Array )->void:
    var _arr:Array = arr.duplicate(true) # deep copy
    _arr.get(1).get(1) = 9
    
func main():
    var arr:Array = [[0,0,0],[0,0,0],[0,0,0]]
    test( arr )
    print( arr )  # <--- [[0,0,0],[0,0,0],[0,0,0]] 変化しない

Suzuki85輪郭抽出アルゴリズムでの座標

Gdscriptの画像Pixelの座標(x,y)

  • x: 横方向 ( 左⇒右へ )
  • y: 縦方向 ( 上⇒下へ )

Suzuki85輪郭抽出アルゴリズムでの座標( i, j )

  • i: 縦方向 ( 上⇒下へ )
  • j: 横方向 ( 左⇒右へ )

image.png

  • img[1][2] = 1

Gdscript画像の座標 x は j に該当します。
輪郭抽出した結果をGodotで描画するときは、(x,y) = (j, i )とする必要があります。

2値化

ContourDetection.ScanImageクラスでコンストラクター内で2値化をしています。
透明な点は「0」, 不透明な点は「1」 の値をもつ、配列にしています。

ContourDetection.ScanImage
class ScanImage:
    const ON = 1
	const OFF = 0
    var image:Image
    var _img:Array[Array]
	var rows: int
	var cols: int        
    func _init(_image: Image):
		image = _image
		# 2値化
		_img = []
		_img_init = []
		var size:Vector2i = image.get_size()
		rows = size.y
		cols = size.x

		for i in range(rows): # 縦方向
			if i == 0 or i == rows-1:
				var _img_rows: Array[int]=[]
				for j in range(cols):  # 横方向
					_img_rows.append(OFF)
				_img.append(_img_rows)
			else:
				var _img_rows: Array[int]=[]
				for j in range(cols): # 横方向
					if j == 0 or j == cols - 1:
						_img_rows.append(OFF)
					else:
						var pixel = get_pixcel(image,i,j)
						if pixel.a > 0:
							_img_rows.append(ON)
						else:
							_img_rows.append(OFF)
				_img.append(_img_rows)

画像の走査

j 方向( 横向き ) に点を走査することで 輪郭検出を行います。

詳しくはContourDetection.raster_scan の関数のコードを参照してください。

Suzuki85メソッドの概要はこちらを参照してください。→suzuki contour algorithm opencv

終わりに

Suzuki85メソッドを理解したいと「私も」努力はしてみましたが、アルゴリズムが複雑で理解しきれておらず、現状は、オリジナルのPythonコードを愚直にGdscriptへ移植した!というだけになりました。残念。

でも、せっかく動いているのだから「同じことを考えている方の助けになればいいな」と思ってネタの提供をいたしました。

【終わり】

0
0
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?