Add support for TIFF and improve output

This commit is contained in:
Felipe Daragon
2025-04-26 16:22:10 +01:00
parent ef87bffe7d
commit ba17490555
8 changed files with 195 additions and 61 deletions

View File

@@ -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.

View File

@@ -1,10 +1,10 @@
<img src="https://raw.githubusercontent.com/MechasAI/NeoRefacer/main/icon.png"/>
# 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.

97
app.py
View File

@@ -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)
@@ -271,6 +283,87 @@ with gr.Blocks(theme=theme, title="NeoRefacer - AI Refacer") as demo:
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"):
with gr.Row():

View File

@@ -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)

View File

@@ -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)
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,6 +318,44 @@ 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)
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()))
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")
@@ -314,9 +363,6 @@ class Refacer:
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)
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)

View File

@@ -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

View File

@@ -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

View File

@@ -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