From ba17490555ed7f1d7a9c370e6027bf88d6825db3 Mon Sep 17 00:00:00 2001 From: Felipe Daragon Date: Sat, 26 Apr 2025 16:22:10 +0100 Subject: [PATCH] Add support for TIFF and improve output --- LICENSE | 4 +- README.md | 10 +++-- app.py | 97 ++++++++++++++++++++++++++++++++++++++++- codeformer_wrapper.py | 60 +++++++++++-------------- refacer.py | 82 ++++++++++++++++++++++++++-------- requirements-COREML.txt | 1 + requirements-CPU.txt | 1 + requirements-GPU.txt | 1 + 8 files changed, 195 insertions(+), 61 deletions(-) diff --git a/LICENSE b/LICENSE index b3c04f6..9461280 100644 --- a/LICENSE +++ b/LICENSE @@ -72,8 +72,8 @@ Please follow the instructions provided below: - Remove the subdirectories basicsr and facelib - Remove within weights subdirectory Codeformer and facelib. - Remove codeformer_wrapper.py -- Edit refacer.py and remove the import: codeformer_wrapper import enhance_image -- Within def reface_image, comment the line: output_path = enhance_image(output_path) +- Edit refacer.py and remove the import: codeformer_wrapper +- Adjust the code so that it doesn't calls the enhance functions from the commented wrapper - That's all! Failure to remove `codeformer` when required may violate the terms of its license. diff --git a/README.md b/README.md index 8f32505..0c4659b 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ -# NeoRefacer: Images. GIFs. Full-length videos. +# NeoRefacer: Images. GIFs. TIFFs. Full-length videos. In a future where identity flows like data and reality is just another layer, NeoRefacer gives you the power to transform. -Images. GIFs. Full-length videos. +Images. GIFs. TIFFs. Full-length videos. All yours to reface and reimagine - with a single pulse of electricity. @@ -20,7 +20,7 @@ Evolved from the foundations of the [Refacer](https://github.com/xaviviro/reface [OFFICIAL WEBSITE](https://www.mechas.ai/projects-neorefacer.php) ## Core DNA of NeoRefacer -* **Instant Identity Shift** - Swap faces in images, GIFs, and movies faster than your neural implants can blink. +* **Instant Identity Shift** - Swap faces in images, GIFs, multi-page TIFFs and movies faster than your neural implants can blink. * **Overclocked Engine** - Optimized for CPU rebels and GPU warlords. * **Feature Film Reface** - Not just TikToks. Full two-hour cinematic overthrows. * **Targeted Strike Modes** - Single-face raids, multi-face takeovers, or precision-targeted matchups. @@ -37,7 +37,7 @@ Evolved from the foundations of the [Refacer](https://github.com/xaviviro/reface ## What's New (Since Refacer) -* Image, GIF and Video reface modes +* Image, GIF, TIFF and Video reface modes * Significantly faster processing * Automatic image enhancing (Image mode) * Improved video output quality @@ -48,8 +48,10 @@ Evolved from the foundations of the [Refacer](https://github.com/xaviviro/reface * **Multiple Faces** (Fast): faces are replaced with the faces you provide based on their order from left to right * **Faces by Match** (Slower): faces are first detected and replaced with the faces you provide. * Improved GPU detection +* Support for multi-page TIFF * Uses local Gradio cache with auto-cleanup on startup * Includes a bulk image refacer utility (refacer_bulk.py) +* Videos and images are saved to the root of /output, and GIFs are saved to /output/gifs and previews are saved to /output/preview subdirectory NeoRefacer, just like the original Refacer project, requires no training - just one photo and you're ready to go. diff --git a/app.py b/app.py index 609eee9..9919968 100644 --- a/app.py +++ b/app.py @@ -106,17 +106,29 @@ def extract_faces_auto(filepath, refacer_instance, max_faces=5, isvideo=False): if filepath is None: return [None] * max_faces - # Check if video + # Check if video is too large if isvideo: if os.path.getsize(filepath) > 5 * 1024 * 1024: # larger than 5MB print("Video too large for auto-extract, skipping face extraction.") return [None] * max_faces + # Load first frame frame = load_first_frame(filepath) if frame is None: return [None] * max_faces - # Create manual temp image inside ./tmp + print("Loaded frame shape:", frame.shape) + + # Handle weird TIFF/multipage dimensions + while len(frame.shape) > 3: + frame = frame[0] # Keep taking the first slice until (H, W, C) + + print("Fixed frame shape:", frame.shape) + + if frame.shape[-1] != 3: + raise ValueError(f"Expected last dimension to be 3 (RGB), but got {frame.shape[-1]}") + + # Create temp image inside ./tmp temp_image_path = os.path.join("./tmp", f"temp_face_extract_{int(time.time() * 1000)}.png") Image.fromarray(frame).save(temp_image_path) @@ -270,6 +282,87 @@ with gr.Blocks(theme=theme, title="NeoRefacer - AI Refacer") as demo: inputs=gif_input, outputs=[gif_preview] ) + + # --- TIF MODE --- + with gr.Tab("TIFF Mode"): + with gr.Row(): + tif_input = gr.File(label="Original TIF", file_types=[".tif", ".tiff"]) + tif_preview = gr.Image(label="TIF Preview (Cover Page)", type="filepath") + tif_output_preview = gr.Image(label="Refaced TIF Preview (Cover Page)", type="filepath") + tif_output_file = gr.File(label="Refaced TIF (Download)", interactive=False) + + with gr.Row(): + face_mode_tif = gr.Radio( + choices=["Single Face", "Multiple Faces", "Faces By Match"], + value="Single Face", + label="Replacement Mode" + ) + tif_btn = gr.Button("Reface TIF", variant="primary") + + origin_tif, destination_tif, thresholds_tif, face_tabs_tif = [], [], [], [] + + for i in range(num_faces): + with gr.Tab(f"Face #{i+1}") as tab: + with gr.Row(): + origin = gr.Image(label="Face to replace") + destination = gr.Image(label="Destination face") + threshold = gr.Slider(label="Threshold", minimum=0.0, maximum=1.0, value=0.2) + origin_tif.append(origin) + destination_tif.append(destination) + thresholds_tif.append(threshold) + face_tabs_tif.append(tab) + + face_mode_tif.change( + fn=lambda mode: toggle_tabs_and_faces(mode, face_tabs_tif, origin_tif), + inputs=[face_mode_tif], + outputs=face_tabs_tif + origin_tif + ) + + demo.load( + fn=lambda: toggle_tabs_and_faces("Single Face", face_tabs_tif, origin_tif), + inputs=None, + outputs=face_tabs_tif + origin_tif + ) + + def process_tif(tif_path, *vars): + original_img = Image.open(tif_path) + if hasattr(original_img, "n_frames") and original_img.n_frames > 1: + original_img.seek(0) + temp_preview_path = os.path.join("./tmp", f"tif_preview_{int(time.time() * 1000)}.jpg") + original_img.convert('RGB').save(temp_preview_path) + + refaced_path = run_image(tif_path, *vars) + + refaced_img = Image.open(refaced_path) + if hasattr(refaced_img, "n_frames") and refaced_img.n_frames > 1: + refaced_img.seek(0) + temp_refaced_preview_path = os.path.join("./tmp", f"refaced_tif_preview_{int(time.time() * 1000)}.jpg") + refaced_img.convert('RGB').save(temp_refaced_preview_path) + + return temp_preview_path, temp_refaced_preview_path, refaced_path + + tif_btn.click( + fn=lambda tif_path, *args: process_tif(tif_path, *args), + inputs=[tif_input] + origin_tif + destination_tif + thresholds_tif + [face_mode_tif], + outputs=[tif_preview, tif_output_preview, tif_output_file] + ) + + tif_input.change( + fn=lambda filepath: extract_faces_auto(filepath, refacer, max_faces=num_faces), + inputs=tif_input, + outputs=origin_tif + ) + + tif_input.change( + fn=lambda filepath: ( + Image.open(filepath).convert('RGB').save( + (preview_path := os.path.join("./tmp", f"tif_preview_{int(time.time() * 1000)}.jpg")) + ) or preview_path + ), + inputs=tif_input, + outputs=tif_preview + ) + # --- VIDEO MODE --- with gr.Tab("Video Mode"): diff --git a/codeformer_wrapper.py b/codeformer_wrapper.py index b5c4ee8..81b18d4 100644 --- a/codeformer_wrapper.py +++ b/codeformer_wrapper.py @@ -1,13 +1,7 @@ -# codeformer_wrapper.py -# Copyright (c) 2022 Shangchen Zhou -# Modifications and additions copyright (c) 2025 Felipe Daragon - -# License: CC BY-NC-SA 4.0 (https://github.com/felipedaragon/codeformer/blob/main/README.md) -# Same as the original code by Shangchen Zhou. - import os import torch import cv2 +import numpy as np from pathlib import Path from torchvision.transforms.functional import normalize from basicsr.utils import img2tensor, tensor2img @@ -15,10 +9,8 @@ from basicsr.utils.download_util import load_file_from_url from facelib.utils.face_restoration_helper import FaceRestoreHelper from basicsr.utils.registry import ARCH_REGISTRY -# Prepare device device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') -# Load CodeFormer model once pretrain_model_url = { 'restoration': 'https://github.com/sczhou/CodeFormer/releases/download/v0.1.0/codeformer.pth', } @@ -32,9 +24,8 @@ checkpoint = torch.load(ckpt_path)['params_ema'] net.load_state_dict(checkpoint) net.eval() -# Load helper face_helper = FaceRestoreHelper( - upscale_factor=1, # No background upscaling + upscale_factor=1, face_size=512, crop_ratio=(1, 1), det_model='retinaface_resnet50', @@ -43,36 +34,18 @@ face_helper = FaceRestoreHelper( device=device ) -def enhance_image(input_image_path: str, w: float = 0.5) -> str: +def _enhance_img(img: np.ndarray, w: float = 0.5) -> np.ndarray: """ - Enhances an input image using CodeFormer and saves it with a '.enhanced.jpg' suffix. - - Args: - input_image_path (str): Path to the input image (JPG or PNG). - w (float): Balance quality and fidelity (default=0.5). - - Returns: - str: Path to the enhanced image. + Internal helper to enhance a numpy image with CodeFormer. """ - input_path = Path(input_image_path) - output_path = input_path.with_name(f"{input_path.stem}.enhanced.jpg") - - # Clean previous state face_helper.clean_all() - - # Load image - img = cv2.imread(str(input_path), cv2.IMREAD_COLOR) - if img is None: - raise ValueError(f"Cannot read image: {input_image_path}") - face_helper.read_image(img) num_faces = face_helper.get_face_landmarks_5(only_center_face=False, resize=640, eye_dist_threshold=5) if num_faces == 0: - raise ValueError(f"No faces detected in: {input_image_path}") + return img # Return original if no faces detected face_helper.align_warp_face() - # Enhance each face for cropped_face in face_helper.cropped_faces: cropped_face_t = img2tensor(cropped_face / 255., bgr2rgb=True, float32=True) normalize(cropped_face_t, (0.5, 0.5, 0.5), (0.5, 0.5, 0.5), inplace=True) @@ -85,13 +58,30 @@ def enhance_image(input_image_path: str, w: float = 0.5) -> str: restored_face = restored_face.astype('uint8') face_helper.add_restored_face(restored_face) - # Paste faces back face_helper.get_inverse_affine(None) restored_img = face_helper.paste_faces_to_input_image() + return restored_img + +def enhance_image(input_image_path: str, w: float = 0.5) -> str: + """ + Enhances an input image using CodeFormer and saves it with a '.enhanced.jpg' suffix. + """ + input_path = Path(input_image_path) + output_path = input_path.with_name(f"{input_path.stem}.enhanced.jpg") + + img = cv2.imread(str(input_path), cv2.IMREAD_COLOR) + if img is None: + raise ValueError(f"Cannot read image: {input_image_path}") + + restored_img = _enhance_img(img, w=w) - # Save output os.makedirs(output_path.parent, exist_ok=True) cv2.imwrite(str(output_path), restored_img) - print(f"Enhanced image saved to: {output_path}") return str(output_path) + +def enhance_image_memory(img: np.ndarray, w: float = 0.5) -> np.ndarray: + """ + Enhances an input image entirely in memory and returns the enhanced image. + """ + return _enhance_img(img, w=w) diff --git a/refacer.py b/refacer.py index 26f7fdc..7777317 100644 --- a/refacer.py +++ b/refacer.py @@ -22,7 +22,7 @@ import subprocess from PIL import Image import numpy as np import time -from codeformer_wrapper import enhance_image +from codeformer_wrapper import enhance_image, enhance_image_memory import tempfile gc = __import__('gc') @@ -235,8 +235,14 @@ class Refacer: filename = f"{original_name}_preview.mp4" if preview else f"{original_name}_{timestamp}.mp4" self.__check_video_has_audio(video_path) - os.makedirs("output", exist_ok=True) - output_video_path = os.path.join('output', filename) + + if preview: + os.makedirs("output/preview", exist_ok=True) + output_video_path = os.path.join('output', 'preview', filename) + else: + os.makedirs("output", exist_ok=True) + output_video_path = os.path.join('output', filename) + self.prepare_faces(faces, disable_similarity=disable_similarity, multiple_faces_mode=multiple_faces_mode) self.first_face = False if multiple_faces_mode else (faces[0].get("origin") is None or disable_similarity) @@ -275,13 +281,18 @@ class Refacer: converted_path = self.__convert_video(video_path, output_video_path, preview=preview) if video_path.lower().endswith(".gif"): - gif_output_path = converted_path.replace(".mp4", ".gif") + if preview: + gif_output_path = os.path.join("output", "preview", os.path.basename(converted_path).replace(".mp4", ".gif")) + else: + gif_output_path = os.path.join("output", "gifs", os.path.basename(converted_path).replace(".mp4", ".gif")) + self.__generate_gif(converted_path, gif_output_path) return converted_path, gif_output_path return converted_path, None def __generate_gif(self, video_path, gif_output_path): + os.makedirs(os.path.dirname(gif_output_path), exist_ok=True) print(f"Generating GIF at {gif_output_path}") ( ffmpeg @@ -307,22 +318,57 @@ class Refacer: self.prepare_faces(faces, disable_similarity=disable_similarity, multiple_faces_mode=multiple_faces_mode) self.first_face = False if multiple_faces_mode else (faces[0].get("origin") is None or disable_similarity) - bgr_image = cv2.imread(image_path) - if bgr_image is None: - raise ValueError("Failed to read input image") - - refaced_bgr = self.process_first_face(bgr_image.copy()) if self.first_face else self.process_faces(bgr_image.copy()) - refaced_rgb = cv2.cvtColor(refaced_bgr, cv2.COLOR_BGR2RGB) - pil_img = Image.fromarray(refaced_rgb) + ext = osp.splitext(image_path)[1].lower() os.makedirs("output", exist_ok=True) original_name = osp.splitext(osp.basename(image_path))[0] timestamp = str(int(time.time())) - filename = f"{original_name}_{timestamp}.jpg" - output_path = os.path.join("output", filename) - pil_img.save(output_path, format='JPEG', quality=100, subsampling=0) - output_path = enhance_image(output_path) - print(f"Saved refaced image to {output_path}") - return output_path + + if ext in ['.tif', '.tiff']: + pil_img = Image.open(image_path) + frames = [] + + # First, count pages + page_count = 0 + try: + while True: + pil_img.seek(page_count) + page_count += 1 + except EOFError: + pass # End of pages + + # Re-open to start real processing + pil_img = Image.open(image_path) + + with tqdm(total=page_count, desc="Processing TIFF pages") as pbar: + for page in range(page_count): + pil_img.seek(page) + bgr_image = cv2.cvtColor(np.array(pil_img.convert('RGB')), cv2.COLOR_RGB2BGR) + refaced_bgr = self.process_first_face(bgr_image.copy()) if self.first_face else self.process_faces(bgr_image.copy()) + enhanced_bgr = enhance_image_memory(refaced_bgr) + enhanced_rgb = cv2.cvtColor(enhanced_bgr, cv2.COLOR_BGR2RGB) + enhanced_pil = Image.fromarray(enhanced_rgb) + frames.append(enhanced_pil) + pbar.update(1) + + output_path = os.path.join("output", f"{original_name}_{timestamp}.tif") + frames[0].save(output_path, save_all=True, append_images=frames[1:], compression="tiff_deflate") + print(f"Saved multipage refaced TIFF to {output_path}") + return output_path + + else: + bgr_image = cv2.imread(image_path) + if bgr_image is None: + raise ValueError("Failed to read input image") + + refaced_bgr = self.process_first_face(bgr_image.copy()) if self.first_face else self.process_faces(bgr_image.copy()) + refaced_rgb = cv2.cvtColor(refaced_bgr, cv2.COLOR_BGR2RGB) + pil_img = Image.fromarray(refaced_rgb) + filename = f"{original_name}_{timestamp}.jpg" + output_path = os.path.join("output", filename) + pil_img.save(output_path, format='JPEG', quality=100, subsampling=0) + output_path = enhance_image(output_path) + print(f"Saved refaced image to {output_path}") + return output_path def extract_faces_from_image(self, image_path, max_faces=5): frame = cv2.imread(image_path) @@ -381,4 +427,4 @@ class Refacer: 'h264_videotoolbox': '0', 'h264_nvenc': '0', 'libx264': '0' - } + } \ No newline at end of file diff --git a/requirements-COREML.txt b/requirements-COREML.txt index d234538..d9b8ad1 100644 --- a/requirements-COREML.txt +++ b/requirements-COREML.txt @@ -1,5 +1,6 @@ ffmpeg_python==0.2.0 imageio[ffmpeg]==2.37.0 +imagecodecs==2025.3.30 gradio==5.22.0 insightface==0.7.3 numpy==1.24.3 diff --git a/requirements-CPU.txt b/requirements-CPU.txt index 4462627..4546d67 100644 --- a/requirements-CPU.txt +++ b/requirements-CPU.txt @@ -1,5 +1,6 @@ ffmpeg_python==0.2.0 imageio[ffmpeg]==2.37.0 +imagecodecs==2025.3.30 gradio==5.22.0 insightface==0.7.3 numpy==1.24.3 diff --git a/requirements-GPU.txt b/requirements-GPU.txt index 7e083ff..0335f76 100644 --- a/requirements-GPU.txt +++ b/requirements-GPU.txt @@ -1,5 +1,6 @@ ffmpeg_python==0.2.0 imageio[ffmpeg]==2.37.0 +imagecodecs==2025.3.30 gradio==5.22.0 insightface==0.7.3 numpy==1.24.3