diff --git a/face_shaper.py b/face_shaper.py index 2d4f2b1..c99fd76 100644 --- a/face_shaper.py +++ b/face_shaper.py @@ -305,7 +305,36 @@ class ComfyUIFaceShaper: "FLOAT", {"default": 1.0, "min": 0.5, "max": 2.0, "step": 0.01}, ), + # Cheeks + "cheek_left_pos_x": ( + "FLOAT", + {"default": 0.0, "min": -0.3, "max": 0.3, "step": 0.005}, + ), + "cheek_left_pos_y": ( + "FLOAT", + {"default": 0.0, "min": -0.3, "max": 0.3, "step": 0.005}, + ), + "cheek_right_pos_x": ( + "FLOAT", + {"default": 0.0, "min": -0.3, "max": 0.3, "step": 0.005}, + ), + "cheek_right_pos_y": ( + "FLOAT", + {"default": 0.0, "min": -0.3, "max": 0.3, "step": 0.005}, + ), # Eyebrows + "eyebrow_left_size_x": ( + "FLOAT", + {"default": 1.0, "min": 0.5, "max": 2.0, "step": 0.01}, + ), + "eyebrow_left_size_y": ( + "FLOAT", + {"default": 1.0, "min": 0.5, "max": 2.0, "step": 0.01}, + ), + "eyebrow_left_rotation": ( + "FLOAT", + {"default": 0.0, "min": -45.0, "max": 45.0, "step": 0.1}, + ), "eyebrow_left_pos_x": ( "FLOAT", {"default": 0.0, "min": -0.5, "max": 0.5, "step": 0.01}, @@ -314,6 +343,18 @@ class ComfyUIFaceShaper: "FLOAT", {"default": 0.0, "min": -0.5, "max": 0.5, "step": 0.01}, ), + "eyebrow_right_size_x": ( + "FLOAT", + {"default": 1.0, "min": 0.5, "max": 2.0, "step": 0.01}, + ), + "eyebrow_right_size_y": ( + "FLOAT", + {"default": 1.0, "min": 0.5, "max": 2.0, "step": 0.01}, + ), + "eyebrow_right_rotation": ( + "FLOAT", + {"default": 0.0, "min": -45.0, "max": 45.0, "step": 0.1}, + ), "eyebrow_right_pos_x": ( "FLOAT", {"default": 0.0, "min": -0.5, "max": 0.5, "step": 0.01}, @@ -388,8 +429,18 @@ class ComfyUIFaceShaper: lip_lower_size_y: float, chin_size_x: float, chin_size_y: float, + cheek_left_pos_x: float, + cheek_left_pos_y: float, + cheek_right_pos_x: float, + cheek_right_pos_y: float, + eyebrow_left_size_x: float, + eyebrow_left_size_y: float, + eyebrow_left_rotation: float, eyebrow_left_pos_x: float, eyebrow_left_pos_y: float, + eyebrow_right_size_x: float, + eyebrow_right_size_y: float, + eyebrow_right_rotation: float, eyebrow_right_pos_x: float, eyebrow_right_pos_y: float, nose_pos_y: float, @@ -403,7 +454,7 @@ class ComfyUIFaceShaper: ): """Render the facial mask image and return it as a tensor.""" # Handle settings_list import: override all adjustable parameters if provided - if settings_list is not None and len(settings_list) >= 35: + if settings_list is not None and len(settings_list) >= 47: eye_left_size_x = settings_list[0] eye_left_size_y = settings_list[1] eye_left_pos_x = settings_list[2] @@ -428,18 +479,28 @@ class ComfyUIFaceShaper: lip_lower_size_y = settings_list[21] chin_size_x = settings_list[22] chin_size_y = settings_list[23] - eyebrow_left_pos_x = settings_list[24] - eyebrow_left_pos_y = settings_list[25] - eyebrow_right_pos_x = settings_list[26] - eyebrow_right_pos_y = settings_list[27] - nose_pos_y = settings_list[28] - nose_size_x = settings_list[29] - nose_size_y = settings_list[30] - camera_distance = settings_list[31] - camera_pos_x = settings_list[32] - camera_pos_y = settings_list[33] - line_thickness = settings_list[34] - # Note: canvas_width/canvas_height (indices 35-36) are exported but not imported + cheek_left_pos_x = settings_list[24] + cheek_left_pos_y = settings_list[25] + cheek_right_pos_x = settings_list[26] + cheek_right_pos_y = settings_list[27] + eyebrow_left_size_x = settings_list[28] + eyebrow_left_size_y = settings_list[29] + eyebrow_left_rotation = settings_list[30] + eyebrow_left_pos_x = settings_list[31] + eyebrow_left_pos_y = settings_list[32] + eyebrow_right_size_x = settings_list[33] + eyebrow_right_size_y = settings_list[34] + eyebrow_right_rotation = settings_list[35] + eyebrow_right_pos_x = settings_list[36] + eyebrow_right_pos_y = settings_list[37] + nose_pos_y = settings_list[38] + nose_size_x = settings_list[39] + nose_size_y = settings_list[40] + camera_distance = settings_list[41] + camera_pos_x = settings_list[42] + camera_pos_y = settings_list[43] + line_thickness = settings_list[44] + # Note: canvas_width/canvas_height (indices 45-46) are exported but not imported # as they are always provided as direct parameters to the method face_points = _face_data_for_gender(gender) @@ -488,6 +549,32 @@ class ComfyUIFaceShaper: ) -> List[Tuple[float, float]]: return [(rx + offset_x, ry + offset_y) for rx, ry in points] + # Helper to rotate points around a center by angle in degrees. + def rotate_polygon( + points: List[Tuple[float, float]], + angle_degrees: float, + cx: float, + cy: float, + ) -> List[Tuple[float, float]]: + """Rotate points around (cx, cy) by angle_degrees using 2D rotation.""" + if angle_degrees == 0.0: + return points + import math + angle_rad = math.radians(angle_degrees) + cos_a = math.cos(angle_rad) + sin_a = math.sin(angle_rad) + rotated = [] + for rx, ry in points: + # Translate to origin + dx = rx - cx + dy = ry - cy + # Rotate + new_x = dx * cos_a - dy * sin_a + new_y = dx * sin_a + dy * cos_a + # Translate back + rotated.append((cx + new_x, cy + new_y)) + return rotated + # Draw outer head outline with per-region horizontal scaling if "outer_head" in face_points: # Calculate centroid for scaling @@ -571,22 +658,48 @@ class ComfyUIFaceShaper: pixel_points = [to_pixel(pt) for pt in chin] draw.line(pixel_points, fill=(0, 0, 0), width=stroke_width) - # Draw eyebrows with translation + # Draw eyebrows with scale → rotation → translation transform order if "eyebrow_left" in face_points: - eyebrow_left = translate_polygon( - face_points["eyebrow_left"], - eyebrow_left_pos_x, - eyebrow_left_pos_y, - ) + # Calculate centroid + eyebrow_left_points = face_points["eyebrow_left"] + cx = sum(px for px, _ in eyebrow_left_points) / len(eyebrow_left_points) + cy = sum(py for _, py in eyebrow_left_points) / len(eyebrow_left_points) + + # Step 1: Scale around centroid + scaled = [] + for rx, ry in eyebrow_left_points: + dx = (rx - cx) * eyebrow_left_size_x + dy = (ry - cy) * eyebrow_left_size_y + scaled.append((cx + dx, cy + dy)) + + # Step 2: Rotate around centroid + rotated = rotate_polygon(scaled, eyebrow_left_rotation, cx, cy) + + # Step 3: Apply positional offset + eyebrow_left = translate_polygon(rotated, eyebrow_left_pos_x, eyebrow_left_pos_y) + pixel_points = [to_pixel(pt) for pt in eyebrow_left] draw.line(pixel_points, fill=(0, 0, 0), width=stroke_width) if "eyebrow_right" in face_points: - eyebrow_right = translate_polygon( - face_points["eyebrow_right"], - eyebrow_right_pos_x, - eyebrow_right_pos_y, - ) + # Calculate centroid + eyebrow_right_points = face_points["eyebrow_right"] + cx = sum(px for px, _ in eyebrow_right_points) / len(eyebrow_right_points) + cy = sum(py for _, py in eyebrow_right_points) / len(eyebrow_right_points) + + # Step 1: Scale around centroid + scaled = [] + for rx, ry in eyebrow_right_points: + dx = (rx - cx) * eyebrow_right_size_x + dy = (ry - cy) * eyebrow_right_size_y + scaled.append((cx + dx, cy + dy)) + + # Step 2: Rotate around centroid + rotated = rotate_polygon(scaled, eyebrow_right_rotation, cx, cy) + + # Step 3: Apply positional offset + eyebrow_right = translate_polygon(rotated, eyebrow_right_pos_x, eyebrow_right_pos_y) + pixel_points = [to_pixel(pt) for pt in eyebrow_right] draw.line(pixel_points, fill=(0, 0, 0), width=stroke_width) @@ -602,20 +715,30 @@ class ComfyUIFaceShaper: pixel_points = [to_pixel(pt) for pt in nose] draw.line(pixel_points, fill=(0, 0, 0), width=stroke_width) - # Draw cheeks and connect them to outer head + # Draw cheeks with position offsets and connect them to outer head if "cheek_left" in face_points: - pixel_points = [to_pixel(pt) for pt in face_points["cheek_left"]] + 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((0.316923, 0.729073)) + cheek_left_end = to_pixel((0.316923 + cheek_left_pos_x, 0.729073 + cheek_left_pos_y)) outer_head_point = to_pixel((0.320690, 0.854875)) draw.line([cheek_left_end, outer_head_point], fill=(0, 0, 0), width=stroke_width) if "cheek_right" in face_points: - pixel_points = [to_pixel(pt) for pt in face_points["cheek_right"]] + 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((0.683077, 0.729073)) + cheek_right_end = to_pixel((0.683077 + cheek_right_pos_x, 0.729073 + cheek_right_pos_y)) outer_head_point = to_pixel((0.679310, 0.854875)) draw.line([cheek_right_end, outer_head_point], fill=(0, 0, 0), width=stroke_width) @@ -693,8 +816,18 @@ class ComfyUIFaceShaper: lip_lower_size_y, chin_size_x, chin_size_y, + cheek_left_pos_x, + cheek_left_pos_y, + cheek_right_pos_x, + cheek_right_pos_y, + eyebrow_left_size_x, + eyebrow_left_size_y, + eyebrow_left_rotation, eyebrow_left_pos_x, eyebrow_left_pos_y, + eyebrow_right_size_x, + eyebrow_right_size_y, + eyebrow_right_rotation, eyebrow_right_pos_x, eyebrow_right_pos_y, nose_pos_y, diff --git a/web/js/faceShaperReset.js b/web/js/faceShaperReset.js index 1a0e722..6256efd 100644 --- a/web/js/faceShaperReset.js +++ b/web/js/faceShaperReset.js @@ -51,8 +51,18 @@ app.registerExtension({ "lip_lower_size_y": 1.0, "chin_size_x": 1.0, "chin_size_y": 1.0, + "cheek_left_pos_x": 0.0, + "cheek_left_pos_y": 0.0, + "cheek_right_pos_x": 0.0, + "cheek_right_pos_y": 0.0, + "eyebrow_left_size_x": 1.0, + "eyebrow_left_size_y": 1.0, + "eyebrow_left_rotation": 0.0, "eyebrow_left_pos_x": 0.0, "eyebrow_left_pos_y": 0.0, + "eyebrow_right_size_x": 1.0, + "eyebrow_right_size_y": 1.0, + "eyebrow_right_rotation": 0.0, "eyebrow_right_pos_x": 0.0, "eyebrow_right_pos_y": 0.0, "nose_pos_y": 0.0,