fp_pipe normalization across different workflows

This commit is contained in:
Rafał Tłuchowski
2025-02-23 19:34:27 +01:00
parent 2116b098d1
commit 2e4e530b78
2 changed files with 288 additions and 171 deletions
+203 -153
View File
@@ -8,6 +8,8 @@ from ..core.image_processor import ImageProcessor
class FaceFitAndRestore:
"""ComfyUI node for processing face images in Fit or Restore mode."""
def __init__(self):
self.face_detector = FaceDetector()
self.image_processor = ImageProcessor()
@@ -50,245 +52,293 @@ class FaceFitAndRestore:
def process_image(self, mode, workflow, padding_percent=0.0, bbox_size="1024",
image=None, fp_pipe=None, image_sequence=None):
"""Process images in either Fit or Restore mode with unified fp_pipe structure."""
# Validate inputs
if image is None and (workflow == "single_image" or mode == "Restore"):
print("Error: Image is required for single_image workflow and Restore mode")
return None, fp_pipe, None, int(bbox_size)
# Check if we should use workflow from fp_pipe in Restore mode
if mode == "Restore":
if fp_pipe is None:
print("Error: fp_pipe is required in Restore mode")
return None, {}, None, int(bbox_size)
if workflow == "image_sequence" and mode == "Fit" and (not image_sequence or "frames" not in image_sequence):
print("Error: Valid image sequence data is required for Fit mode")
return None, fp_pipe, None, int(bbox_size)
if "workflow" in fp_pipe:
workflow = fp_pipe["workflow"]
print(f"Using workflow from pipe: {workflow}")
if mode == "Restore" and (not fp_pipe or "frames" not in fp_pipe):
print("Error: Valid fp_pipe with frames is required for Restore mode")
return None, fp_pipe, None, int(bbox_size)
# Additional check for sequence restore
if workflow == "image_sequence" and "frames" not in fp_pipe:
print("Error: fp_pipe does not contain frames data required for image sequence restore")
return None, {}, None, int(bbox_size)
# Initialize fp_pipe if None
if fp_pipe is None:
fp_pipe = {
"workflow": workflow,
"current_frame": 0,
"padding_percent": padding_percent,
"target_lm": {}, # Will be populated by face_wrapper
"frames": {}
}
else:
# Update common parameters
fp_pipe["workflow"] = workflow
fp_pipe["padding_percent"] = padding_percent
if workflow == "single_image":
if image is None:
print("Error: Image is required for single_image workflow")
return None, {}, None, int(bbox_size)
try:
if workflow == "single_image":
return self._process_frame(
mode=mode,
image=image,
frame_number=0,
padding_percent=padding_percent,
bbox_size=bbox_size,
fp_pipe=fp_pipe
)
else: # image_sequence
# W trybie Restore używamy current_frame z fp_pipe
current_frame = (fp_pipe.get("current_frame", 0) if mode == "Restore"
else image_sequence.get("current_frame", 0))
fp_pipe["current_frame"] = current_frame
if mode == "Fit":
result = self._fit(image, padding_percent, bbox_size)
else: # Restore
result = self._restore(image, fp_pipe)
# W trybie Restore używamy liczby klatek z fp_pipe
if mode == "Restore":
total_frames = len(fp_pipe["frames"])
frames_to_process = {
idx: None for idx in range(total_frames)
}
else: # Fit mode
total_frames = len(image_sequence["frames"])
frames_to_process = image_sequence["frames"]
# Add workflow info to settings
result_settings = result[1]
result_settings["workflow"] = "single_image"
return result
elif workflow == "image_sequence":
# In Fit mode, we need image_sequence input
if mode == "Fit":
if image_sequence is None or not image_sequence.get("frames"):
print("Error: Valid image_sequence is required for image_sequence workflow in Fit mode")
return None, {}, None, int(bbox_size)
sequence_settings = {
"workflow": "image_sequence",
"frames": {},
"current_frame": image_sequence.get("current_frame", 0)
}
selected_result = None
total_frames = len(image_sequence["frames"])
print(f"Processing sequence of {total_frames} frames...")
print(f"Current frame index: {sequence_settings['current_frame']}")
for frame_idx, frame_path in image_sequence["frames"].items():
current_frame_result = None
# Process each frame
for frame_idx in range(total_frames):
print(f"Processing frame {frame_idx + 1}/{total_frames}")
try:
frame_tensor = self._load_image_from_path(frame_path)
if frame_tensor is None:
print(f"Warning: Could not load frame {frame_idx} from {frame_path}")
# Przygotowanie obrazu wejściowego
if mode == "Fit":
frame_path = frames_to_process[frame_idx]
frame_image = self._load_image_from_path(frame_path)
if frame_image is None:
print(f"Warning: Could not load frame {frame_idx}")
continue
else: # Restore
frame_image = image # Ten sam obraz dla każdej klatki w trybie Restore
frame_path = None
result = self._fit(frame_tensor, padding_percent, bbox_size)
# Process frame
result = self._process_frame(
mode=mode,
image=frame_image,
frame_number=frame_idx,
padding_percent=padding_percent,
bbox_size=bbox_size,
fp_pipe=fp_pipe,
original_path=frame_path if mode == "Fit" else None
)
if result: # Check if result was created successfully
result[1]["original_image_path"] = frame_path
sequence_settings["frames"][f"frame_{frame_idx}"] = result[1]
if frame_idx == sequence_settings["current_frame"]:
selected_result = result
# Store results for current frame
if frame_idx == current_frame:
current_frame_result = result
except Exception as e:
print(f"Error processing frame {frame_idx}: {str(e)}")
continue
# After processing all frames, return results for current frame
if current_frame_result is None:
print(f"Warning: Frame {current_frame} not found in sequence")
return None, fp_pipe, None, int(bbox_size)
return current_frame_result
except Exception as e:
print(f"Error in process_image: {str(e)}")
import traceback
traceback.print_exc()
return None, fp_pipe, None, int(bbox_size)
def _process_frame(self, mode, image, frame_number, padding_percent, bbox_size, fp_pipe, original_path=None):
"""Process a single frame in either Fit or Restore mode."""
try:
frame_key = f"frame_{frame_number}"
if mode == "Fit":
# Process frame in Fit mode
result_image, frame_settings, result_mask = self._fit(
image=image,
padding_percent=padding_percent,
bbox_size=bbox_size
)
if frame_settings:
# Create standardized frame data structure
frame_data = {
"bbox_size": int(bbox_size),
"crop_bbox": frame_settings["crop_bbox"],
"original_image_shape": frame_settings["original_image_shape"],
"rotation_angle": frame_settings["rotation_angle"],
"detected_lm": {} # Will be populated by face_wrapper
}
# Add original path for sequence processing
if original_path:
frame_data["original_image_path"] = original_path
# Store frame data
fp_pipe["frames"][frame_key] = frame_data
else: # Restore mode
if image is None:
print("Error: Input image is required for Restore mode")
return None, {}, None, int(bbox_size)
current_frame = fp_pipe.get("current_frame", 0)
frame_settings = fp_pipe.get("frames", {}).get(f"frame_{current_frame}")
frame_settings = fp_pipe["frames"].get(frame_key)
if frame_settings is None:
print(f"Error: No processor settings for frame {current_frame}")
return None, {}, None, int(bbox_size)
print(f"Processing restore for frame {current_frame} with settings: {frame_settings}")
result = self._restore(image, frame_settings)
if result is None:
print(f"Error: Failed to restore frame {current_frame}")
print(f"Error: No settings found for frame {frame_number}")
return None, fp_pipe, None, int(bbox_size)
return result[0], fp_pipe, result[2], int(bbox_size)
if selected_result is None:
print(f"Warning: Could not process current frame, using default empty result")
return None, sequence_settings, None, int(bbox_size)
result = self._restore(image, frame_settings)
if result is None:
return None, fp_pipe, None, int(bbox_size)
return selected_result[0], sequence_settings, selected_result[2], int(bbox_size)
result_image, result_mask = result[0], result[2]
return result_image, fp_pipe, result_mask, int(bbox_size)
except Exception as e:
print(f"Error processing frame {frame_number}: {str(e)}")
import traceback
traceback.print_exc()
return None, fp_pipe, None, int(bbox_size)
def _fit(self, image, padding_percent, bbox_size):
"""Fit mode: Crop and process the face."""
# Convert image to numpy array
image_np = self.image_processor.convert_to_numpy(image)
if image_np is None:
return image, {}, self._create_empty_mask(image), int(bbox_size)
return image, {}, self._create_empty_mask(image)
# Detect facial landmarks using mediapipe
# Detect facial landmarks
landmarks_df = self.face_detector.detect_landmarks_mp(image_np)
if landmarks_df is None:
print("No face detected, returning original image")
return image, {}, self._create_empty_mask(image), int(bbox_size)
return image, {}, self._create_empty_mask(image)
# Calculate rotation angle and rotate the image
# Calculate rotation and rotate image
rotation_angle = self.image_processor.calculate_rotation_angle(landmarks_df)
rotated_image, updated_landmarks = self.image_processor.rotate_image(image_np, landmarks_df)
if rotated_image is None:
print("Failed to rotate image, returning original")
return image, {}, self._create_empty_mask(image), int(bbox_size)
return image, {}, self._create_empty_mask(image)
# Crop face region to a square (1:1)
cropped_face, crop_bbox = self.image_processor.crop_face_to_square(rotated_image, updated_landmarks,
padding_percent)
# Crop face region
cropped_face, crop_bbox = self.image_processor.crop_face_to_square(
rotated_image, updated_landmarks, padding_percent
)
if cropped_face is None:
print("Failed to crop face, returning original image")
return image, {}, self._create_empty_mask(image), int(bbox_size)
return image, {}, self._create_empty_mask(image)
# Resize to target size
target_size = int(bbox_size)
final_image = self.image_processor.resize_image(cropped_face, target_size)
if final_image is None:
print("Failed to resize image, returning original")
return image, {}, self._create_empty_mask(image), int(bbox_size)
return image, {}, self._create_empty_mask(image)
# Convert to tensor format
# Convert to tensor
final_image = self.image_processor.numpy_to_tensor(final_image)
# Save processor settings for restoration
fp_pipe = {
# Create frame settings
frame_settings = {
"original_image_shape": image_np.shape,
"rotation_angle": rotation_angle,
"crop_bbox": crop_bbox, # (x, y, w, h)
"padding_percent": padding_percent,
"bbox_size": target_size,
"original_image_path": None
"crop_bbox": crop_bbox,
"bbox_size": target_size
}
# Create a mask for the face area
mask = np.ones((target_size, target_size), dtype=np.float32)
# Create mask
mask = self._create_mask(
image_shape=frame_settings["original_image_shape"],
crop_bbox=frame_settings["crop_bbox"],
rotation_angle=frame_settings["rotation_angle"]
)
if rotation_angle != 0:
original_mask = np.ones(image_np.shape[:2], dtype=np.float32)
height, width = image_np.shape[:2]
center = (width // 2, height // 2)
rotation_matrix = cv2.getRotationMatrix2D(center, rotation_angle, 1.0)
rotated_mask = cv2.warpAffine(original_mask, rotation_matrix, (width, height), flags=cv2.INTER_LINEAR)
x1, y1, w, h = crop_bbox
cropped_mask = rotated_mask[y1:y1 + h, x1:x1 + w]
resized_mask = cv2.resize(cropped_mask, (target_size, target_size), interpolation=cv2.INTER_LINEAR)
mask = resized_mask
mask = self.image_processor.convert_mask_to_tensor(mask)
return final_image, fp_pipe, mask, int(bbox_size)
def _restore(self, image, fp_pipe):
"""Restore mode: Restore the face to the original image."""
if fp_pipe is None:
print("Error in restore: fp_pipe is None")
return image, {}, self._create_empty_mask(image)
processed_face_np = self.image_processor.convert_to_numpy(image)
if processed_face_np is None:
print("Error in restore: Failed to convert image to numpy array")
return image, {}, self._create_empty_mask(image)
original_image_shape = fp_pipe.get("original_image_shape")
rotation_angle = fp_pipe.get("rotation_angle")
crop_bbox = fp_pipe.get("crop_bbox")
padding_percent = fp_pipe.get("padding_percent")
bbox_size = fp_pipe.get("bbox_size")
# Debug info
print(f"Restore settings: shape={original_image_shape}, rotation={rotation_angle}, bbox={crop_bbox}")
if not all([original_image_shape, crop_bbox]):
missing = []
if not original_image_shape:
missing.append("original_image_shape")
if not crop_bbox:
missing.append("crop_bbox")
print(f"Error in restore: Missing required settings: {', '.join(missing)}")
return image, {}, self._create_empty_mask(image)
return final_image, frame_settings, mask
def _restore(self, image, frame_settings):
"""Restore mode: Restore the face back to original position."""
try:
restored_image = np.zeros(original_image_shape, dtype=np.uint8)
# Validate frame settings
required_keys = ["original_image_shape", "rotation_angle", "crop_bbox"]
if not all(key in frame_settings for key in required_keys):
print("Error: Missing required frame settings")
return None
x1, y1, w, h = crop_bbox
# Convert input image to numpy
processed_face_np = self.image_processor.convert_to_numpy(image)
if processed_face_np is None:
return None
# Get settings
original_shape = frame_settings["original_image_shape"]
rotation_angle = frame_settings["rotation_angle"]
x1, y1, w, h = frame_settings["crop_bbox"]
# Create output image
restored_image = np.zeros(original_shape, dtype=np.uint8)
# Resize face to original crop size
resized_face = cv2.resize(processed_face_np, (w, h), interpolation=cv2.INTER_LANCZOS4)
# Place face in position
restored_image[y1:y1 + h, x1:x1 + w] = resized_face
# Rotate back if needed
if rotation_angle != 0:
height, width = original_image_shape[:2]
height, width = original_shape[:2]
center = (width // 2, height // 2)
rotation_matrix = cv2.getRotationMatrix2D(center, -rotation_angle, 1.0)
restored_image = cv2.warpAffine(restored_image, rotation_matrix, (width, height),
flags=cv2.INTER_LANCZOS4)
restored_image = cv2.warpAffine(
restored_image,
rotation_matrix,
(width, height),
flags=cv2.INTER_LANCZOS4
)
# Convert to tensor format
# Convert to tensor
restored_image = self.image_processor.numpy_to_tensor(restored_image)
mask = self._create_mask(original_image_shape, crop_bbox, rotation_angle)
return restored_image, fp_pipe, mask
# Create mask
mask = self._create_mask(original_shape, (x1, y1, w, h), rotation_angle)
return restored_image, frame_settings, mask
except Exception as e:
print(f"Error in restore: {str(e)}")
return image, fp_pipe, self._create_empty_mask(image)
return None
def _create_mask(self, image_shape, crop_bbox, rotation_angle):
"""Create a mask for the face area."""
mask = np.zeros(image_shape[:2], dtype=np.float32)
x1, y1, w, h = crop_bbox
# Draw a white rectangle for the face area
# Create face area mask
mask[y1:y1 + h, x1:x1 + w] = 1.0
# Rotate the mask back to the original orientation
# Rotate mask if needed
if rotation_angle != 0:
height, width = image_shape[:2]
center = (width // 2, height // 2)
rotation_matrix = cv2.getRotationMatrix2D(center, -rotation_angle, 1.0)
mask = cv2.warpAffine(mask, rotation_matrix, (width, height), flags=cv2.INTER_LINEAR)
mask = cv2.warpAffine(
mask,
rotation_matrix,
(width, height),
flags=cv2.INTER_LINEAR
)
return self.image_processor.convert_mask_to_tensor(mask)
def _create_empty_mask(self, image):
"""Create an empty mask with the same shape as the input image."""
"""Create an empty mask matching the image dimensions."""
if torch.is_tensor(image):
image_shape = image.shape[1:3] # (H, W)
shape = image.shape[1:3]
else:
image_shape = image.shape[:2] # (H, W)
mask = torch.zeros((1, *image_shape), dtype=torch.float32)
return mask
shape = image.shape[:2]
return torch.zeros((1, *shape), dtype=torch.float32)
def _load_image_from_path(self, image_path):
"""Load and convert image from file path to tensor format."""
+85 -18
View File
@@ -90,6 +90,7 @@ class FaceWrapper:
if det_overlay is not None:
overlays.append(det_overlay)
# Get base landmarks
base_landmarks = MediapipeBaseLandmarks.get_base_landmarks(
(width, height), x_scale=x_scale, y_translation=y_transform
)
@@ -117,12 +118,16 @@ class FaceWrapper:
output_image = self.image_processor.numpy_to_tensor(result_image)
output_mask = self.image_processor.convert_mask_to_tensor(mask_np)
landmarks_data = self._prepare_landmarks_data(landmarks_df, base_landmarks)
return output_image, self._update_pipe(fp_pipe, landmarks_data), output_mask
# Prepare and update landmarks data
landmarks_data = self._prepare_landmarks_data(landmarks_df, base_landmarks)
updated_pipe = self._update_pipe(fp_pipe, landmarks_data)
return output_image, updated_pipe, output_mask
def _unwrap_mode(self, image_np, landmarks_df, width, height, device,
x_scale, y_transform, fp_pipe, mask_np):
# Get base landmarks
base_landmarks = MediapipeBaseLandmarks.get_base_landmarks(
(width, height), x_scale=x_scale, y_translation=y_transform
)
@@ -141,8 +146,11 @@ class FaceWrapper:
output_image = self.image_processor.pil_to_tensor(warped_image)
# Prepare and update landmarks data
landmarks_data = self._prepare_landmarks_data(landmarks_df, base_landmarks)
return output_image, self._update_pipe(fp_pipe, landmarks_data), warped_mask
updated_pipe = self._update_pipe(fp_pipe, landmarks_data)
return output_image, updated_pipe, warped_mask
def _wrap_mode(self, image_np, landmarks_df, width, height, device,
x_scale, y_transform, fp_pipe, mask_np):
@@ -152,12 +160,25 @@ class FaceWrapper:
output_mask = self.image_processor.convert_mask_to_tensor(mask_np)
return output_image, {}, output_mask
current_frame = fp_pipe.get("current_frame", 0)
frame_key = f"frame_{current_frame}"
# Check if we have required data
if frame_key not in fp_pipe["frames"] or "detected_lm" not in fp_pipe["frames"][frame_key]:
print(f"No detected landmarks found for frame {current_frame}")
output_image = self.image_processor.numpy_to_tensor(image_np)
output_mask = self.image_processor.convert_mask_to_tensor(mask_np)
return output_image, fp_pipe, output_mask
# Get source landmarks from target_lm at root level
source_x = fp_pipe['target_lm']['x']
source_y = fp_pipe['target_lm']['y']
source_landmarks = np.column_stack((source_x, source_y))[:468]
detected_x = fp_pipe['detected_lm']['x']
detected_y = fp_pipe['detected_lm']['y']
# Get target landmarks from detected_lm in current frame
frame_data = fp_pipe["frames"][frame_key]
detected_x = frame_data['detected_lm']['x']
detected_y = frame_data['detected_lm']['y']
target_landmarks = np.column_stack((detected_x, detected_y))[:468]
# Process image
@@ -183,18 +204,64 @@ class FaceWrapper:
return CPUDeformer.warp_face(image, source_landmarks, target_landmarks)
def _prepare_landmarks_data(self, detected_df, target_lm):
return {
'detected_lm': {
'x': detected_df['x'].tolist(),
'y': detected_df['y'].tolist(),
'indices': detected_df['index'].tolist()
},
'target_lm': {
'x': target_lm[:, 0].tolist(),
'y': target_lm[:, 1].tolist(),
'indices': list(range(len(target_lm)))
}
"""
Prepare landmarks data in the correct format for fp_pipe
Args:
detected_df: DataFrame with detected landmarks
target_lm: Target landmarks array
Returns:
dict: Landmarks data in the format compatible with fp_pipe
"""
# Prepare detected landmarks data
detected_data = {
'x': detected_df['x'].tolist(),
'y': detected_df['y'].tolist(),
'indices': detected_df['index'].tolist()
}
def _update_pipe(self, pipe, new_data):
return {**(pipe or {}), **new_data}
# Prepare target landmarks data
target_data = {
'x': target_lm[:, 0].tolist(),
'y': target_lm[:, 1].tolist(),
'indices': list(range(len(target_lm)))
}
return detected_data, target_data
def _update_pipe(self, pipe, landmarks_data):
"""
Update fp_pipe with new landmarks data
Args:
pipe: Existing fp_pipe dictionary or None
landmarks_data: Tuple of (detected_landmarks, target_landmarks)
Returns:
dict: Updated fp_pipe structure
"""
if pipe is None:
pipe = {
"workflow": "single_image",
"current_frame": 0,
"frames": {}
}
detected_lm, target_lm = landmarks_data
# Update target landmarks at the root level
pipe["target_lm"] = target_lm
# Update detected landmarks for the current frame
current_frame = pipe.get("current_frame", 0)
frame_key = f"frame_{current_frame}"
# Ensure the frame exists in the structure
if frame_key not in pipe["frames"]:
pipe["frames"][frame_key] = {}
# Update detected landmarks for the current frame
pipe["frames"][frame_key]["detected_lm"] = detected_lm
return pipe