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 the subdirectories basicsr and facelib
- Remove within weights subdirectory Codeformer and facelib. - Remove within weights subdirectory Codeformer and facelib.
- Remove codeformer_wrapper.py - Remove codeformer_wrapper.py
- Edit refacer.py and remove the import: codeformer_wrapper import enhance_image - Edit refacer.py and remove the import: codeformer_wrapper
- Within def reface_image, comment the line: output_path = enhance_image(output_path) - Adjust the code so that it doesn't calls the enhance functions from the commented wrapper
- That's all! - That's all!
Failure to remove `codeformer` when required may violate the terms of its license. 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"/> <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. 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. 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) [OFFICIAL WEBSITE](https://www.mechas.ai/projects-neorefacer.php)
## Core DNA of NeoRefacer ## 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. * **Overclocked Engine** - Optimized for CPU rebels and GPU warlords.
* **Feature Film Reface** - Not just TikToks. Full two-hour cinematic overthrows. * **Feature Film Reface** - Not just TikToks. Full two-hour cinematic overthrows.
* **Targeted Strike Modes** - Single-face raids, multi-face takeovers, or precision-targeted matchups. * **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) ## What's New (Since Refacer)
* Image, GIF and Video reface modes * Image, GIF, TIFF and Video reface modes
* Significantly faster processing * Significantly faster processing
* Automatic image enhancing (Image mode) * Automatic image enhancing (Image mode)
* Improved video output quality * 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 * **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. * **Faces by Match** (Slower): faces are first detected and replaced with the faces you provide.
* Improved GPU detection * Improved GPU detection
* Support for multi-page TIFF
* Uses local Gradio cache with auto-cleanup on startup * Uses local Gradio cache with auto-cleanup on startup
* Includes a bulk image refacer utility (refacer_bulk.py) * 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. 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: if filepath is None:
return [None] * max_faces return [None] * max_faces
# Check if video # Check if video is too large
if isvideo: if isvideo:
if os.path.getsize(filepath) > 5 * 1024 * 1024: # larger than 5MB if os.path.getsize(filepath) > 5 * 1024 * 1024: # larger than 5MB
print("Video too large for auto-extract, skipping face extraction.") print("Video too large for auto-extract, skipping face extraction.")
return [None] * max_faces return [None] * max_faces
# Load first frame
frame = load_first_frame(filepath) frame = load_first_frame(filepath)
if frame is None: if frame is None:
return [None] * max_faces 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") temp_image_path = os.path.join("./tmp", f"temp_face_extract_{int(time.time() * 1000)}.png")
Image.fromarray(frame).save(temp_image_path) 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] 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 --- # --- VIDEO MODE ---
with gr.Tab("Video Mode"): with gr.Tab("Video Mode"):
with gr.Row(): 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 os
import torch import torch
import cv2 import cv2
import numpy as np
from pathlib import Path from pathlib import Path
from torchvision.transforms.functional import normalize from torchvision.transforms.functional import normalize
from basicsr.utils import img2tensor, tensor2img 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 facelib.utils.face_restoration_helper import FaceRestoreHelper
from basicsr.utils.registry import ARCH_REGISTRY from basicsr.utils.registry import ARCH_REGISTRY
# Prepare device
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# Load CodeFormer model once
pretrain_model_url = { pretrain_model_url = {
'restoration': 'https://github.com/sczhou/CodeFormer/releases/download/v0.1.0/codeformer.pth', '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.load_state_dict(checkpoint)
net.eval() net.eval()
# Load helper
face_helper = FaceRestoreHelper( face_helper = FaceRestoreHelper(
upscale_factor=1, # No background upscaling upscale_factor=1,
face_size=512, face_size=512,
crop_ratio=(1, 1), crop_ratio=(1, 1),
det_model='retinaface_resnet50', det_model='retinaface_resnet50',
@@ -43,36 +34,18 @@ face_helper = FaceRestoreHelper(
device=device 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. Internal helper to enhance a numpy image with CodeFormer.
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.
""" """
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() 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) face_helper.read_image(img)
num_faces = face_helper.get_face_landmarks_5(only_center_face=False, resize=640, eye_dist_threshold=5) num_faces = face_helper.get_face_landmarks_5(only_center_face=False, resize=640, eye_dist_threshold=5)
if num_faces == 0: 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() face_helper.align_warp_face()
# Enhance each face
for cropped_face in face_helper.cropped_faces: for cropped_face in face_helper.cropped_faces:
cropped_face_t = img2tensor(cropped_face / 255., bgr2rgb=True, float32=True) 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) 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') restored_face = restored_face.astype('uint8')
face_helper.add_restored_face(restored_face) face_helper.add_restored_face(restored_face)
# Paste faces back
face_helper.get_inverse_affine(None) face_helper.get_inverse_affine(None)
restored_img = face_helper.paste_faces_to_input_image() 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) os.makedirs(output_path.parent, exist_ok=True)
cv2.imwrite(str(output_path), restored_img) cv2.imwrite(str(output_path), restored_img)
print(f"Enhanced image saved to: {output_path}") print(f"Enhanced image saved to: {output_path}")
return str(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 from PIL import Image
import numpy as np import numpy as np
import time import time
from codeformer_wrapper import enhance_image from codeformer_wrapper import enhance_image, enhance_image_memory
import tempfile import tempfile
gc = __import__('gc') gc = __import__('gc')
@@ -235,8 +235,14 @@ class Refacer:
filename = f"{original_name}_preview.mp4" if preview else f"{original_name}_{timestamp}.mp4" filename = f"{original_name}_preview.mp4" if preview else f"{original_name}_{timestamp}.mp4"
self.__check_video_has_audio(video_path) 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) os.makedirs("output", exist_ok=True)
output_video_path = os.path.join('output', filename) output_video_path = os.path.join('output', filename)
self.prepare_faces(faces, disable_similarity=disable_similarity, multiple_faces_mode=multiple_faces_mode) 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) 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) converted_path = self.__convert_video(video_path, output_video_path, preview=preview)
if video_path.lower().endswith(".gif"): 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) self.__generate_gif(converted_path, gif_output_path)
return converted_path, gif_output_path return converted_path, gif_output_path
return converted_path, None return converted_path, None
def __generate_gif(self, video_path, gif_output_path): 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}") print(f"Generating GIF at {gif_output_path}")
( (
ffmpeg ffmpeg
@@ -307,6 +318,44 @@ class Refacer:
self.prepare_faces(faces, disable_similarity=disable_similarity, multiple_faces_mode=multiple_faces_mode) 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) 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) bgr_image = cv2.imread(image_path)
if bgr_image is None: if bgr_image is None:
raise ValueError("Failed to read input image") 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_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) refaced_rgb = cv2.cvtColor(refaced_bgr, cv2.COLOR_BGR2RGB)
pil_img = Image.fromarray(refaced_rgb) 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" filename = f"{original_name}_{timestamp}.jpg"
output_path = os.path.join("output", filename) output_path = os.path.join("output", filename)
pil_img.save(output_path, format='JPEG', quality=100, subsampling=0) pil_img.save(output_path, format='JPEG', quality=100, subsampling=0)

View File

@@ -1,5 +1,6 @@
ffmpeg_python==0.2.0 ffmpeg_python==0.2.0
imageio[ffmpeg]==2.37.0 imageio[ffmpeg]==2.37.0
imagecodecs==2025.3.30
gradio==5.22.0 gradio==5.22.0
insightface==0.7.3 insightface==0.7.3
numpy==1.24.3 numpy==1.24.3

View File

@@ -1,5 +1,6 @@
ffmpeg_python==0.2.0 ffmpeg_python==0.2.0
imageio[ffmpeg]==2.37.0 imageio[ffmpeg]==2.37.0
imagecodecs==2025.3.30
gradio==5.22.0 gradio==5.22.0
insightface==0.7.3 insightface==0.7.3
numpy==1.24.3 numpy==1.24.3

View File

@@ -1,5 +1,6 @@
ffmpeg_python==0.2.0 ffmpeg_python==0.2.0
imageio[ffmpeg]==2.37.0 imageio[ffmpeg]==2.37.0
imagecodecs==2025.3.30
gradio==5.22.0 gradio==5.22.0
insightface==0.7.3 insightface==0.7.3
numpy==1.24.3 numpy==1.24.3