diff --git a/face_shaper.py b/face_shaper.py index 78d06ca..da1f80e 100644 --- a/face_shaper.py +++ b/face_shaper.py @@ -239,31 +239,33 @@ def _normalize_svg_coordinates(polylines: List[List[Tuple[float, float]]], # relative coordinates for female face (0–1 range) -# Extracted from Face_Mask_female.svg (1024x1024 normalized coordinates) +# Extracted from Face_Mask_female.svg using viewBox (0, 0, 270.93331, 270.93331) normalization FEMALE_FACE: Dict[str, List[Tuple[float, float]]] = { - # Outer head outline + # Outer head outline - extracted from path173 with g41 transform applied "outer_head": [ - (0.571760, 0.935952), - (0.679310, 0.854875), - (0.752114, 0.752289), - (0.791825, 0.621574), - (0.791825, 0.547116), - (0.818299, 0.464385), - (0.791825, 0.381654), - (0.791825, 0.193027), - (0.626362, 0.072239), - (0.500000, 0.072239), - (0.373638, 0.072239), - (0.208175, 0.193027), - (0.208175, 0.381654), - (0.181701, 0.464385), - (0.208175, 0.547116), - (0.208175, 0.621574), - (0.247886, 0.752289), - (0.320690, 0.854875), (0.428240, 0.935952), - (0.500000, 0.935174), - (0.571760, 0.935952), # Close the polyline + (0.320690, 0.854875), + (0.247886, 0.752289), + (0.208175, 0.621574), + (0.208175, 0.547116), + (0.181701, 0.464385), + (0.208175, 0.381654), + (0.208285, 0.218750), + (0.265626, 0.125000), + (0.373638, 0.072239), + (0.500000, 0.062500), + (0.626362, 0.072239), + (0.734375, 0.125000), + (0.791715, 0.218750), + (0.791825, 0.381654), + (0.818299, 0.464385), + (0.791825, 0.547116), + (0.791825, 0.621574), + (0.752114, 0.752289), + (0.679310, 0.854875), + (0.571760, 0.935952), + (0.500000, 0.953125), + (0.428240, 0.935952), # Close the polyline (z command) ], # Cheeks "cheek_right": [ @@ -394,7 +396,11 @@ FEMALE_FACE_IRISES = { } # Number of parameters in settings_list (for import/export functionality) -# Set to 60 to allow for future expansion (currently 48 parameters are used) +# Current list: 49 parameters + padding for future expansion +# Breakdown: +# - 47 feature control parameters (eyes, eyebrows, nose, lips, etc.) +# - 1 cheeks_enabled toggle (boolean) +# - 2 canvas dimensions (width, height) - exported but not imported SETTINGS_LIST_LENGTH = 60 # Cheek connection points (hardcoded from SVG coordinates) @@ -573,6 +579,7 @@ class ComfyUIFaceShaper: "FLOAT", {"default": 0.0, "min": -45.0, "max": 45.0, "step": 0.1}, ), + "cheeks_enabled": ("BOOLEAN", {"default": True}), "cheek_left_pos_x": ( "FLOAT", {"default": 0.0, "min": -0.3, "max": 0.3, "step": 0.005}, @@ -668,6 +675,7 @@ class ComfyUIFaceShaper: eyebrow_right_pos_x: float, eyebrow_right_pos_y: float, eyebrow_right_rotation: float, + cheeks_enabled: bool, cheek_left_pos_x: float, cheek_left_pos_y: float, cheek_right_pos_x: float, @@ -741,9 +749,14 @@ class ComfyUIFaceShaper: camera_pos_y = settings_list[44] fov_mm = settings_list[45] line_thickness = settings_list[46] + # Index 48: cheeks_enabled (new parameter, fallback to True if missing) + if len(settings_list) > 48: + cheeks_enabled = settings_list[48] + else: + cheeks_enabled = True # Note: canvas_width/canvas_height (indices 47-48) are exported but not imported # as they are always provided as direct parameters to the method - # Total: 48 parameters (46 feature controls + 2 canvas dimensions) + # Total: 49 parameters (47 feature controls + 1 cheeks_enabled + 2 canvas dimensions - canvas not imported) face_points = _face_data_for_gender(gender) iris_data = _iris_data_for_gender(gender) @@ -1022,34 +1035,35 @@ class ComfyUIFaceShaper: pixel_points = [to_pixel(pt) for pt in lips_lower_scaled] draw.line(pixel_points, fill=(0, 0, 0), width=stroke_width) - # Draw cheeks with position offsets and connect them to outer head - if "cheek_left" in face_points: - cheek_left = translate_polygon( - face_points["cheek_left"], - cheek_left_pos_x, - cheek_left_pos_y, - ) - pixel_points = [to_pixel(pt) for pt in cheek_left] - draw.line(pixel_points, fill=(0, 0, 0), width=stroke_width) - # Connect cheek_left last point to outer head point - cheek_left_end = to_pixel((CHEEK_LEFT_END_POINT[0] + cheek_left_pos_x, - CHEEK_LEFT_END_POINT[1] + cheek_left_pos_y)) - outer_head_point = to_pixel(CHEEK_LEFT_OUTER_HEAD_POINT) - draw.line([cheek_left_end, outer_head_point], fill=(0, 0, 0), width=stroke_width) + # Draw cheeks with position offsets and connect them to outer head (if enabled) + if cheeks_enabled: + if "cheek_left" in face_points: + cheek_left = translate_polygon( + face_points["cheek_left"], + cheek_left_pos_x, + cheek_left_pos_y, + ) + pixel_points = [to_pixel(pt) for pt in cheek_left] + draw.line(pixel_points, fill=(0, 0, 0), width=stroke_width) + # Connect cheek_left last point to outer head point + cheek_left_end = to_pixel((CHEEK_LEFT_END_POINT[0] + cheek_left_pos_x, + CHEEK_LEFT_END_POINT[1] + cheek_left_pos_y)) + outer_head_point = to_pixel(CHEEK_LEFT_OUTER_HEAD_POINT) + draw.line([cheek_left_end, outer_head_point], fill=(0, 0, 0), width=stroke_width) - if "cheek_right" in face_points: - cheek_right = translate_polygon( - face_points["cheek_right"], - cheek_right_pos_x, - cheek_right_pos_y, - ) - pixel_points = [to_pixel(pt) for pt in cheek_right] - draw.line(pixel_points, fill=(0, 0, 0), width=stroke_width) - # Connect cheek_right last point to outer head point - cheek_right_end = to_pixel((CHEEK_RIGHT_END_POINT[0] + cheek_right_pos_x, - CHEEK_RIGHT_END_POINT[1] + cheek_right_pos_y)) - outer_head_point = to_pixel(CHEEK_RIGHT_OUTER_HEAD_POINT) - draw.line([cheek_right_end, outer_head_point], fill=(0, 0, 0), width=stroke_width) + if "cheek_right" in face_points: + cheek_right = translate_polygon( + face_points["cheek_right"], + cheek_right_pos_x, + cheek_right_pos_y, + ) + pixel_points = [to_pixel(pt) for pt in cheek_right] + draw.line(pixel_points, fill=(0, 0, 0), width=stroke_width) + # Connect cheek_right last point to outer head point + cheek_right_end = to_pixel((CHEEK_RIGHT_END_POINT[0] + cheek_right_pos_x, + CHEEK_RIGHT_END_POINT[1] + cheek_right_pos_y)) + outer_head_point = to_pixel(CHEEK_RIGHT_OUTER_HEAD_POINT) + draw.line([cheek_right_end, outer_head_point], fill=(0, 0, 0), width=stroke_width) # Transform and draw both eyes with scale → rotate → translate eye_right = transform_eye( @@ -1152,6 +1166,7 @@ class ComfyUIFaceShaper: line_thickness, canvas_width, canvas_height, + cheeks_enabled, ] return (tensor, settings_export) diff --git a/test_render.py b/test_render.py index 66b409e..cd0d010 100644 --- a/test_render.py +++ b/test_render.py @@ -47,6 +47,7 @@ def test_default_render(): iris_right_size=1.0, iris_right_pos_x=0.0, iris_right_pos_y=0.0, eyebrow_left_size_x=1.0, eyebrow_left_size_y=1.0, eyebrow_left_pos_x=0.0, eyebrow_left_pos_y=0.0, eyebrow_left_rotation=0.0, eyebrow_right_size_x=1.0, eyebrow_right_size_y=1.0, eyebrow_right_pos_x=0.0, eyebrow_right_pos_y=0.0, eyebrow_right_rotation=0.0, + cheeks_enabled=True, cheek_left_pos_x=0.0, cheek_left_pos_y=0.0, cheek_right_pos_x=0.0, cheek_right_pos_y=0.0, nose_pos_y=0.0, nose_size_x=1.0, nose_size_y=1.0, @@ -88,6 +89,7 @@ def test_nose_tip_adjustment(): iris_right_size=1.0, iris_right_pos_x=0.0, iris_right_pos_y=0.0, eyebrow_left_size_x=1.0, eyebrow_left_size_y=1.0, eyebrow_left_pos_x=0.0, eyebrow_left_pos_y=0.0, eyebrow_left_rotation=0.0, eyebrow_right_size_x=1.0, eyebrow_right_size_y=1.0, eyebrow_right_pos_x=0.0, eyebrow_right_pos_y=0.0, eyebrow_right_rotation=0.0, + cheeks_enabled=True, cheek_left_pos_x=0.0, cheek_left_pos_y=0.0, cheek_right_pos_x=0.0, cheek_right_pos_y=0.0, nose_pos_y=0.0, nose_size_x=1.0, nose_size_y=1.0, @@ -128,6 +130,7 @@ def test_debug_geometry(): iris_right_size=1.0, iris_right_pos_x=0.0, iris_right_pos_y=0.0, eyebrow_left_size_x=1.0, eyebrow_left_size_y=1.0, eyebrow_left_pos_x=0.0, eyebrow_left_pos_y=0.0, eyebrow_left_rotation=0.0, eyebrow_right_size_x=1.0, eyebrow_right_size_y=1.0, eyebrow_right_pos_x=0.0, eyebrow_right_pos_y=0.0, eyebrow_right_rotation=0.0, + cheeks_enabled=True, cheek_left_pos_x=0.0, cheek_left_pos_y=0.0, cheek_right_pos_x=0.0, cheek_right_pos_y=0.0, nose_pos_y=0.0, nose_size_x=1.0, nose_size_y=1.0, @@ -169,6 +172,7 @@ def test_eye_rotation(): iris_right_size=1.0, iris_right_pos_x=0.0, iris_right_pos_y=0.0, eyebrow_left_size_x=1.0, eyebrow_left_size_y=1.0, eyebrow_left_pos_x=0.0, eyebrow_left_pos_y=0.0, eyebrow_left_rotation=0.0, eyebrow_right_size_x=1.0, eyebrow_right_size_y=1.0, eyebrow_right_pos_x=0.0, eyebrow_right_pos_y=0.0, eyebrow_right_rotation=0.0, + cheeks_enabled=True, cheek_left_pos_x=0.0, cheek_left_pos_y=0.0, cheek_right_pos_x=0.0, cheek_right_pos_y=0.0, nose_pos_y=0.0, nose_size_x=1.0, nose_size_y=1.0, @@ -182,6 +186,47 @@ def test_eye_rotation(): save_tensor_as_image(tensor, '/tmp/face_eye_rotation.png') print("✓ Eye rotation test passed") +def test_cheeks_disabled(): + """Test rendering with cheeks disabled.""" + print("\nTesting with cheeks disabled...") + node = ComfyUIFaceShaper() + + result = node.draw_face( + canvas_width=1024, + canvas_height=1024, + transparent_background=False, + line_thickness=2.0, + gender="female", + fov_mm=80.0, + camera_distance=1.0, + camera_pos_x=0.0, + camera_pos_y=0.0, + head_size_x=1.0, + head_size_y=1.0, + jaw_size_x=1.0, + fore_head_size_x=1.0, + eye_left_size_x=1.0, eye_left_size_y=1.0, eye_left_pos_x=0.0, eye_left_pos_y=0.0, + eye_left_rotation=0.0, + eye_right_size_x=1.0, eye_right_size_y=1.0, eye_right_pos_x=0.0, eye_right_pos_y=0.0, + eye_right_rotation=0.0, + iris_left_size=1.0, iris_left_pos_x=0.0, iris_left_pos_y=0.0, + iris_right_size=1.0, iris_right_pos_x=0.0, iris_right_pos_y=0.0, + eyebrow_left_size_x=1.0, eyebrow_left_size_y=1.0, eyebrow_left_pos_x=0.0, eyebrow_left_pos_y=0.0, eyebrow_left_rotation=0.0, + eyebrow_right_size_x=1.0, eyebrow_right_size_y=1.0, eyebrow_right_pos_x=0.0, eyebrow_right_pos_y=0.0, eyebrow_right_rotation=0.0, + cheeks_enabled=False, # Disable cheeks + cheek_left_pos_x=0.0, cheek_left_pos_y=0.0, + cheek_right_pos_x=0.0, cheek_right_pos_y=0.0, + nose_pos_y=0.0, nose_size_x=1.0, nose_size_y=1.0, + nose_tip_pos_y=0.0, + lips_pos_y=0.0, lip_size_x=1.0, + lip_upper_size_y=1.0, lip_lower_size_y=1.0, + settings_list=None + ) + + tensor, settings_list = result + save_tensor_as_image(tensor, '/tmp/face_no_cheeks.png') + print("✓ Cheeks disabled test passed") + if __name__ == "__main__": print("Running render tests...\n") @@ -189,6 +234,7 @@ if __name__ == "__main__": test_nose_tip_adjustment() test_debug_geometry() test_eye_rotation() + test_cheeks_disabled() print("\n✓ All render tests completed!") print("\nGenerated images:") @@ -196,3 +242,4 @@ if __name__ == "__main__": print(" /tmp/face_nose_tip_down.png - Nose tip adjusted down (+0.1)") print(" /tmp/face_no_debug.png - Normal render (debug_geometry removed)") print(" /tmp/face_eye_rotation.png - Eye rotation test (left +15°, right -15°)") + print(" /tmp/face_no_cheeks.png - Cheeks disabled test") diff --git a/web/js/faceShaperReset.js b/web/js/faceShaperReset.js index 3ab02b9..64d5c75 100644 --- a/web/js/faceShaperReset.js +++ b/web/js/faceShaperReset.js @@ -63,6 +63,7 @@ app.registerExtension({ "eyebrow_right_pos_x": 0.0, "eyebrow_right_pos_y": 0.0, "eyebrow_right_rotation": 0.0, + "cheeks_enabled": true, "cheek_left_pos_x": 0.0, "cheek_left_pos_y": 0.0, "cheek_right_pos_x": 0.0,