Initial commit: Face Keypoints Filter node for ComfyUI

This commit is contained in:
YULE-WINDOWS\zlcbl
2026-01-12 10:03:33 +09:00
commit d37c259f67
5 changed files with 275 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
.env
.venv
*.egg-info/
.installed.cfg
*.egg
+21
View File
@@ -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.
+47
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
from .face_keypoints_filter import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
+194
View File
@@ -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)",
}