はじめに
Gdscriptを使った「Suzuki85メソッド」画像輪郭抽出コードを(移植して)作った!ので紹介します、という記事であり、Suzuki85メソッドそのものを解説する記事ではありません。
Godotで「Suzuki85メソッド」を使いたいとお考えの方々(そんな人いるのか?)に、ほんの少しでも「役にたった」と思っていただけたら、すごくうれしいです。
本記事概要
画像の境界を抽出する手法として OpenCVのcontour detectionが有名です。
Godot4にて 衝突判定のためにOpenCVのような輪郭検出をさせてみたかったのですが、OpenCVをインストールを前提にしたくはなく、自前で(Gdscriptだけで)実現させたいと思っていました。
OpenCVではSuzuki85メソッドと呼ばれるアルゴリズムを採用しているとのことで、このメソッドについて調べるうちに、PythonにてSuzuki85を実装しているコード(Github)を見つけてしまいました。
Pythonコードで書かれた輪郭抽出コードをGdscriptに移植してみたところ、うまく動作しているようなので、紹介します。
移植版( Godotプロジェクト )
Godotバージョン
- Godot4.2
利用方法
クラス「ContourDetection」を使って輪郭抽出をします。
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
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
実行結果
輪郭を描画するの様子
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 待つ
輪郭抽出の戻り値 obj.contours は輪郭の配列です。
輪郭は、点を配列にしたもので、この点を描画すれば輪郭を描くことができます。
Pythonで書かれた輪郭抽出コードをGdscriptへ移植
Gdscriptへの移植をするときの注意点
タプルは使えない
Gdscriptでは複数の戻り値を一度に「タプル」として受け取ることができません。
def test( a, b ) :
return a+1, b+2
def main():
x, y = test( 1, 2 ) # タプルを使用
print(x) # <--- 2
print(y) # <--- 4
戻り値はオブジェクトとして返すことにします。
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型の引数を変更しても 呼び出し元の変数値は変化しません。
def test( a ) :
a += 5
def main():
i = 0
test( i )
print( i ) # <--- 0 変化しない
Gdscript版でも整数(int型)の引数を参照型としては扱えません。
func test( a: int )->void:
a += 5
func main():
var a:int = 0
test( a )
print( a ) # <--- 0 変化しない
関数内で変更されたint型の変数の値を使いたい場合は、次のどちらかが手っ取り早いですね。
- 戻り値で取得する
- オブジェクト型の引数にして渡す
配列型の引数の扱い
配列型の引数は参照型として扱われます。
つまり、関数の中の操作で配列内の要素の値を変更すると、呼び出し元の配列も変化します。
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版と同じです。
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では 配列の複製の方法が異なります。
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]] 変化しない
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: 横方向 ( 左⇒右へ )
- img[1][2] = 1
Gdscript画像の座標 x は j に該当します。
輪郭抽出した結果をGodotで描画するときは、(x,y) = (j, i )とする必要があります。
2値化
ContourDetection.ScanImageクラスでコンストラクター内で2値化をしています。
透明な点は「0」, 不透明な点は「1」 の値をもつ、配列にしています。
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へ移植した!というだけになりました。残念。
でも、せっかく動いているのだから「同じことを考えている方の助けになればいいな」と思ってネタの提供をいたしました。
【終わり】





