commit d37c259f67c6e5ef4c71dd05e12eaf355897b113 Author: YULE-WINDOWS\zlcbl Date: Mon Jan 12 10:03:33 2026 +0900 Initial commit: Face Keypoints Filter node for ComfyUI diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2a036f9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +.env +.venv +*.egg-info/ +.installed.cfg +*.egg diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c13f991 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e17b9cf --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +# ComfyUI-FaceKeypointsFilter + +A ComfyUI custom node for selectively filtering face keypoints from DWPose/OpenPose output. + +## Description + +This node allows you to disable specific facial parts (jaw, eyebrows, eyes, nose, outer mouth, inner mouth) from OpenPose 68-point face keypoints. This is useful when you want to preserve certain facial features while ignoring others in your ControlNet workflow. + +## Installation + +### Using ComfyUI Manager (Recommended) + +Search for "FaceKeypointsFilter" in ComfyUI Manager and install. + +### Manual Installation + +1. Navigate to your ComfyUI's `custom_nodes` folder +2. Clone this repository: + ```bash + git clone https://github.com/YOUR_USERNAME/ComfyUI-FaceKeypointsFilter.git + ``` +3. Restart ComfyUI + +## Usage + +1. Connect the output of DWPose Preprocessor (or any node that outputs `POSE_KEYPOINT`) to this node +2. Enable the facial parts you want to **remove** (disable): + - `remove_jaw`: Jaw contour (points 0-16) + - `remove_eyebrows`: Eyebrows (points 17-26) + - `remove_eyes`: Eyes (points 36-47) + - `remove_nose`: Nose (points 27-35) + - `remove_mouth_outer`: Outer lip contour (points 48-59) + - `remove_mouth_inner`: Inner mouth (points 60-67) +3. Connect the output to your ControlNet Apply node + +## Node Location + +`Pose/Face Utils` > `Face Keypoints Filter (OpenPose)` + +## Compatibility + +- Works with DWPose Preprocessor from `comfyui_controlnet_aux` +- Compatible with OpenPose JSON format (version 1.3) + +## License + +MIT License diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..b6824c7 --- /dev/null +++ b/__init__.py @@ -0,0 +1,3 @@ +from .face_keypoints_filter import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS + +__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"] diff --git a/face_keypoints_filter.py b/face_keypoints_filter.py new file mode 100644 index 0000000..1fe8f39 --- /dev/null +++ b/face_keypoints_filter.py @@ -0,0 +1,194 @@ +# 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)", +}