OpenSeeFaceとGodot4を繋ぐ

OpenSeeFace1で顔のランドマークを検出して、結果をソケット通信(UDP)で受け取ったときのメモです。

c#で受け取る例を示します。ついでに、pythonをGodotから実行する方法も載せておきます。

環境

Godot: v4.0.2.stable.official

Python: 3.9.9

OpenSeeFace: 9e07f6e1993022e7679f2e5ee3daec0ff617fac1

OpenSeeFaceとは

OpenSeeFaceは、MobileNetV3に基づいた顔のランドマーク検出モデルを実装したトラッキングライブラリです。 顔の特徴点(ランドマーク)を検出し、それを基に顔の動きを追跡することができます。

Webカメラ入力またはビデオファイルからの顔のトラッキングを行い、そのトラッキングデータをUDP経由で送信します。

この送信されたデータは3Dモデル、またはLive2Dモデルのアニメーションで利用できます。

このライブラリでは、複数のモデルを提供しており、ユーザーの環境に合わせて変更できます。

データの送信側

OpenSeeFaceではfacetracker.py2を実行して起動します。

$ python facetracker.py -P 1 --log-data output.log

facetracker.pyにてsendtoによりトラッキングデータが送信されています。

sock.sendto(packet,  (target_ip,  target_port))

デフォルトではipがlocalhost(127.0.0.1)、portは11573です。実行時の引数として与えることもできます。

packetbytearray()によって作成され、packet.extend(bytearray(struct.pack("d", now)))のようにデータを追加しています。

何のデータが出力されているかはlogファイルを確認するのが良いでしょう。少し多すぎるのでここでの記載は省略しますが、各フィードのメモをおまけとして追加しておきます。

Godotで受け取る

c#では、UDPのサーバーとしてUDPServer3を使用できます。

公式ドキュメントを参考に、以下のようにOpenSeeFaceからデータを取得することができます。

extends Node

var server := UDPServer.new()

func _ready():
	server.listen(11573)


func _process(delta):
	server.poll()
	if server.is_connection_available():
		var peer = server.take_connection()
		var packet = peer.get_packet()

取得したデータ(packet)はbyte形式ですので、こちらを変換する必要があります。

そこで一旦、変換用の関数を定義しておきます。(もしかするともう少しいい変換方法があるかもです。)

func bytes_to_double(bytes):
	var double: float = 0.0
	if bytes.size() == 8:
		var f64 = bytes.decode_double(0)
		double = float(f64)
	return double


func bytes_to_int(bytes):
	var integer: int = 0
	if bytes.size() == 4:
		var i32 = bytes.decode_u32(0)
		integer = int(i32)
	return integer


func bytes_to_float(bytes):
	var float_number: float = 0.0
	if bytes.size() == 4:
		var f32 = bytes.decode_float(0)
		float_number = float(f32)
	return float_number

あとはいい感じに受け取ったデータを区切って、辞書型に保存する関数を定義します。

これでdataを取得することで特徴データを使用できます。


func parse_packet(packet):
	var data = {}
	var index = 0

	var timestamp_bytes = packet.slice(index, index + 8)
	var timestamp = bytes_to_double(timestamp_bytes)
	data["timestamp"] = timestamp
	index += 8

	var face_id_bytes = packet.slice(index, index + 4)
	var face_id = bytes_to_int(face_id_bytes)
	data["face_id"] = face_id
	index += 4

	var width_bytes = packet.slice(index, index + 4)
	var width = bytes_to_float(width_bytes)
	data["width"] = width
	index += 4

	var height_bytes = packet.slice(index, index + 4)
	var height = bytes_to_float(height_bytes)
	data["height"] = height
	index += 4

	data["eye_blink"] = []
	data["eye_blink"].append(bytes_to_float(packet.slice(index, index + 4)))
	index += 4
	data["eye_blink"].append(bytes_to_float(packet.slice(index, index + 4)))
	index += 4

	var success_bytes = packet.slice(index, index + 1)
	var success = bytes_to_int(success_bytes)
	data["success"] = success
	index += 1

	var pnp_error_bytes = packet.slice(index, index + 4)
	var pnp_error = bytes_to_float(pnp_error_bytes)
	data["pnp_error"] = pnp_error
	index += 4

	var quaternions = []
	for i in range(4):
		quaternions.append(bytes_to_float(packet.slice(index + i * 4, index + (i + 1) * 4)))
		data["quaternion"] = quaternions

	var eulers = []
	for i in range(3):
		eulers.append(bytes_to_float(packet.slice(index + i * 4, index + (i + 1) * 4)))
	data["euler"] = eulers
	index += 12

	var translations = []
	for i in range(3):
		translations.append(bytes_to_float(packet.slice(index + i * 4, index + (i + 1) * 4)))
	data["translation"] = translations
	index += 12

	index += 66 * 3 * 4  # Landmark

	index += 66 * 3 * 4  # Point3D

	index += 18 * 4  # ???

	# Feature list
	var features = [
		"eye_l", "eye_r", "eyebrow_steepness_l", "eyebrow_quirk_l",
		"eyebrow_steepness_r", "eyebrow_quirk_r", "eyebrow_updown_l",
		"eyebrow_updown_r", "mouth_corner_updown_l", "mouth_corner_inout_l",
		"mouth_corner_updown_r", "mouth_corner_inout_r", "mouth_open", "mouth_wide"
	]

	data["features"] = {}
	for i in range(14):
		data["features"][features[i]] = bytes_to_float(packet.slice(index, index + 4))
		index += 4

	return data

pythonをGodotから呼ぶ

いちいち、facetracker.pyを起動して、Godotでパペットを起動してとするのは非常に手間がかかります。

そこでmainのスクリプトなどで最初に実行してしまうことにします。

OS.execute4を使うと、OpenSeeFaceが停止するまで、それ以降の処理が実行されません。

そのため、OS.create_process5を使用して、新しいプロセスを作成し、実行します。

const python_cmd = "<path to python>"

func run_py():
	var output = []
	var args = [
		"OpenSeeFace/facetracker.py", 
		"--visualize", "3", 
		"--pnp-points", "1", 
		"--max-threads", "4", 
		"-c", "0", 
		"-P", "1", 
		"-F", "60"]
	var pid = OS.create_process(python_cmd, args)
	
	return pid

これで実行はされます。しかし、これだとGodotが終了してもプロセスが実行され続けてしまいます。

_notification関数内でNOTIFICATION_WM_CLOSE_REQUESTを使用してウィンドウが閉じられたことを検出します。。

あとは、実行時に取得したpidを使ってOS.killによってkillします。

func _notification(what):
	if what == NOTIFICATION_WM_CLOSE_REQUEST:
		get_tree().set_auto_accept_quit(false)
		OS.kill(pid)
		get_tree().quit()

おまけ: パペット風の出力例

上記スクリプトを使って、ためしにパペットを作ってみました。

取得した特徴データのうち目の開閉と口の開閉、加えて顔の傾きを使用して出力しています。

おまけ: 特徴データのメモ

各フィードのメモです。

参考文献