Add support for TIFF and improve output
This commit is contained in:
4
LICENSE
4
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.
|
||||
|
||||
10
README.md
10
README.md
@@ -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
97
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)
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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)
|
||||
|
||||
56
refacer.py
56
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)
|
||||
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user