Initial commit: Face Keypoints Filter node for ComfyUI
This commit is contained in:
+10
@@ -0,0 +1,10 @@
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
.env
|
||||
.venv
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
from .face_keypoints_filter import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
|
||||
|
||||
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
|
||||
@@ -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)",
|
||||
}
|
||||
Reference in New Issue
Block a user