195 lines
6.4 KiB
Python
195 lines
6.4 KiB
Python
# face_keypoints_filter.py
|
|
#
|
|
# DWPose / OpenPose の POSE_KEYPOINT から
|
|
# 顔パーツ(あご / 眉 / 目 / 鼻 / 口外周 / 口内周)を部分的に無効化するノード
|
|
#
|
|
# 前提:
|
|
# pose_kps は comfyui_controlnet_aux DWPose(DWPreprocessor) が出す
|
|
# OpenPose 互換 JSON 形式:
|
|
# [
|
|
# {
|
|
# "version": 1.3,
|
|
# "people": [
|
|
# {
|
|
# "pose_keypoints_2d": [...],
|
|
# "face_keypoints_2d": [...],
|
|
# "hand_left_keypoints_2d": [...],
|
|
# "hand_right_keypoints_2d": [...]
|
|
# }
|
|
# ],
|
|
# "canvas_width": 512,
|
|
# "canvas_height": 512
|
|
# },
|
|
# ...
|
|
# ]
|
|
|
|
|
|
class FaceKeypointsFilter:
|
|
"""
|
|
POSE_KEYPOINT 内の face_keypoints_2d から
|
|
特定の顔パーツの信頼度(score)を 0 にして無効化するノード。
|
|
DWPose / OpenPose の JSON 構造 (frame["people"][0]["face_keypoints_2d"]) に対応。
|
|
"""
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"pose_kps": ("POSE_KEYPOINT",),
|
|
# True のパーツは「無効化」される(score=0)
|
|
"remove_jaw": ("BOOLEAN", {"default": False}),
|
|
"remove_eyebrows": ("BOOLEAN", {"default": False}),
|
|
"remove_eyes": ("BOOLEAN", {"default": False}),
|
|
"remove_nose": ("BOOLEAN", {"default": False}),
|
|
# ★ 口を外周・内周で分ける
|
|
"remove_mouth_outer": ("BOOLEAN", {"default": True}),
|
|
"remove_mouth_inner": ("BOOLEAN", {"default": False}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("POSE_KEYPOINT",)
|
|
RETURN_NAMES = ("pose_kps",)
|
|
FUNCTION = "filter_keypoints"
|
|
CATEGORY = "Pose/Face Utils"
|
|
|
|
# ------------------ internal helpers ------------------ #
|
|
|
|
def _zero_indices(self, flat_list, indices):
|
|
"""
|
|
flat_list: [x0,y0,c0, x1,y1,c1, ...] の配列
|
|
indices: 無効化したい「点 index」のリスト (0〜67)
|
|
該当 index の (x,y,score) を 0 にする
|
|
"""
|
|
if flat_list is None:
|
|
return flat_list
|
|
|
|
if len(flat_list) % 3 != 0:
|
|
# 想定外の形式はそのまま返す
|
|
return flat_list
|
|
|
|
num_points = len(flat_list) // 3
|
|
if num_points < 68:
|
|
# 顔68点が入っていない場合は何もしない
|
|
return flat_list
|
|
|
|
out = list(flat_list)
|
|
for idx in indices:
|
|
if idx < 0 or idx >= num_points:
|
|
continue
|
|
base = idx * 3
|
|
out[base] = 0.0
|
|
out[base + 1] = 0.0
|
|
out[base + 2] = 0.0
|
|
|
|
return out
|
|
|
|
def _get_face_list_from_frame(self, frame):
|
|
"""
|
|
frame から face_keypoints_2d のフラット配列を取得する。
|
|
DWPose 形式 (frame["people"][0]["face_keypoints_2d"]) と
|
|
旧形式 (frame["face_keypoints_2d"]) の両方に対応。
|
|
"""
|
|
if not isinstance(frame, dict):
|
|
return None, None, None
|
|
|
|
# DWPose / OpenPose 標準 JSON:
|
|
# frame["people"][0]["face_keypoints_2d"]
|
|
if "people" in frame and isinstance(frame["people"], list) and frame["people"]:
|
|
person = frame["people"][0]
|
|
if isinstance(person, dict) and "face_keypoints_2d" in person:
|
|
return frame, person, person["face_keypoints_2d"]
|
|
|
|
# 互換用:トップレベルに face_keypoints_2d がいる場合
|
|
if "face_keypoints_2d" in frame:
|
|
return frame, None, frame["face_keypoints_2d"]
|
|
|
|
return None, None, None
|
|
|
|
# ------------------ main function ------------------ #
|
|
|
|
def filter_keypoints(
|
|
self,
|
|
pose_kps,
|
|
remove_jaw,
|
|
remove_eyebrows,
|
|
remove_eyes,
|
|
remove_nose,
|
|
remove_mouth_outer,
|
|
remove_mouth_inner,
|
|
):
|
|
"""
|
|
pose_kps: DWPose / OpenPose 由来の POSE_KEYPOINT
|
|
"""
|
|
|
|
# OpenPose 68point face のインデックス
|
|
jaw_idx = list(range(0, 17)) # あご輪郭
|
|
eyebrow_idx = list(range(17, 27)) # 眉
|
|
nose_idx = list(range(27, 36)) # 鼻
|
|
eye_idx = list(range(36, 48)) # 両目
|
|
|
|
# ★ 口を外周・内周に分割
|
|
mouth_outer_idx = list(range(48, 60)) # 48〜59: 唇の輪郭
|
|
mouth_inner_idx = list(range(60, 68)) # 60〜67: 口の開閉ライン
|
|
|
|
if not isinstance(pose_kps, list):
|
|
# 想定:pose_kps は frame のリスト
|
|
return (pose_kps,)
|
|
|
|
new_pose_kps = []
|
|
|
|
for frame in pose_kps:
|
|
# フレームを浅いコピー
|
|
if isinstance(frame, dict):
|
|
frame_copy = dict(frame)
|
|
else:
|
|
new_pose_kps.append(frame)
|
|
continue
|
|
|
|
frame_ref, person_ref, face_flat = self._get_face_list_from_frame(frame_copy)
|
|
|
|
if face_flat is None:
|
|
# 顔情報が無ければそのまま
|
|
new_pose_kps.append(frame_copy)
|
|
continue
|
|
|
|
indices_to_zero = []
|
|
if remove_jaw:
|
|
indices_to_zero += jaw_idx
|
|
if remove_eyebrows:
|
|
indices_to_zero += eyebrow_idx
|
|
if remove_eyes:
|
|
indices_to_zero += eye_idx
|
|
if remove_nose:
|
|
indices_to_zero += nose_idx
|
|
if remove_mouth_outer:
|
|
indices_to_zero += mouth_outer_idx
|
|
if remove_mouth_inner:
|
|
indices_to_zero += mouth_inner_idx
|
|
|
|
if indices_to_zero:
|
|
face_flat_new = self._zero_indices(list(face_flat), indices_to_zero)
|
|
|
|
# person 内か frame 直下かで代入先を変える
|
|
if person_ref is not None:
|
|
person_copy = dict(person_ref)
|
|
person_copy["face_keypoints_2d"] = face_flat_new
|
|
people_list = list(frame_copy.get("people", []))
|
|
if people_list:
|
|
people_list[0] = person_copy
|
|
frame_copy["people"] = people_list
|
|
else:
|
|
frame_copy["face_keypoints_2d"] = face_flat_new
|
|
|
|
new_pose_kps.append(frame_copy)
|
|
|
|
return (new_pose_kps,)
|
|
|
|
|
|
NODE_CLASS_MAPPINGS = {
|
|
"FaceKeypointsFilter": FaceKeypointsFilter,
|
|
}
|
|
|
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
|
"FaceKeypointsFilter": "Face Keypoints Filter (OpenPose)",
|
|
}
|