Update SnapForm to version 0.10.10 and add technical documentation
- Bump SnapForm version in uv.lock from 0.9.30 to 0.10.10. - Introduce comprehensive technical documentation for SnapForm, detailing features, usage instructions, and development guidelines.
This commit is contained in:
@@ -1,13 +1,14 @@
|
||||
import argparse
|
||||
import fnmatch
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
from urllib.parse import quote, urlencode
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
|
||||
IMAGE_SUFFIXES = {".jpg", ".jpeg", ".png", ".webp"}
|
||||
@@ -24,9 +25,10 @@ VIDEO_SUFFIXES = {
|
||||
}
|
||||
SCENE_FILENAME = "scene.glb"
|
||||
EXPORT_FORMATS = ("glb", "gs_ply", "depth_vis", "mini_npz")
|
||||
HF_ENDPOINT = "https://huggingface.co/"
|
||||
HF_MIRRORS = ("https://hf-mirror.com",)
|
||||
HF_PROBE_TIMEOUT = 5.0
|
||||
HF_ENDPOINT = "https://huggingface.co"
|
||||
MODELSCOPE_ENDPOINT = "https://modelscope.cn"
|
||||
MODELSCOPE_REVISION = "master"
|
||||
MODELSCOPE_TIMEOUT = 60.0
|
||||
|
||||
|
||||
class TaskCancelled(RuntimeError):
|
||||
@@ -38,34 +40,6 @@ def check_cancelled(cancel_check):
|
||||
raise TaskCancelled("Task cancelled")
|
||||
|
||||
|
||||
def reachable(url, timeout=HF_PROBE_TIMEOUT):
|
||||
request = urllib.request.Request(
|
||||
url,
|
||||
method="HEAD",
|
||||
headers={"User-Agent": "SnapForm-Runtime-Installer"},
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=timeout):
|
||||
return True
|
||||
except urllib.error.HTTPError:
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def resolve_hf_endpoint():
|
||||
"""Probe Hugging Face reachability and fall back to a mirror when direct access fails; returns (endpoint, used_mirror)."""
|
||||
configured = os.environ.get("HF_ENDPOINT")
|
||||
if configured:
|
||||
return configured, False
|
||||
if reachable(HF_ENDPOINT):
|
||||
return HF_ENDPOINT, False
|
||||
for mirror in HF_MIRRORS:
|
||||
if reachable(mirror):
|
||||
return mirror, True
|
||||
return HF_ENDPOINT, False
|
||||
|
||||
|
||||
def write_status(
|
||||
path,
|
||||
progress,
|
||||
@@ -106,56 +80,66 @@ def collect_images(directory):
|
||||
)
|
||||
|
||||
|
||||
def first_frames(images, maximum_frames, start_frame=1):
|
||||
"""Return up to maximum_frames image paths in order from start_frame."""
|
||||
if maximum_frames <= 0:
|
||||
return images
|
||||
start_index = max(start_frame - 1, 0)
|
||||
return images[start_index:start_index + maximum_frames]
|
||||
|
||||
|
||||
def extract_video_frames(
|
||||
video_path,
|
||||
output_directory,
|
||||
maximum_frames,
|
||||
cv2,
|
||||
start_frame=1,
|
||||
cancel_check=None,
|
||||
):
|
||||
output_directory = Path(output_directory)
|
||||
output_directory.mkdir(parents=True, exist_ok=True)
|
||||
capture = cv2.VideoCapture(str(video_path))
|
||||
if not capture.isOpened():
|
||||
raise RuntimeError(f"Unable to open video: {video_path}")
|
||||
|
||||
output_directory = Path(output_directory)
|
||||
output_directory.mkdir(parents=True, exist_ok=True)
|
||||
total_frames = int(capture.get(cv2.CAP_PROP_FRAME_COUNT))
|
||||
if total_frames <= 0:
|
||||
capture.release()
|
||||
raise RuntimeError("The video does not report a readable frame count")
|
||||
|
||||
import numpy as np
|
||||
|
||||
sample_count = min(maximum_frames, total_frames)
|
||||
frame_indices = np.linspace(
|
||||
0,
|
||||
total_frames - 1,
|
||||
sample_count,
|
||||
dtype=np.int64,
|
||||
)
|
||||
image_paths = []
|
||||
skipped = 0
|
||||
try:
|
||||
for output_index, frame_index in enumerate(frame_indices):
|
||||
while len(image_paths) < maximum_frames:
|
||||
check_cancelled(cancel_check)
|
||||
capture.set(cv2.CAP_PROP_POS_FRAMES, int(frame_index))
|
||||
success, frame = capture.read()
|
||||
if not success:
|
||||
raise RuntimeError(
|
||||
f"Unable to read video frame {int(frame_index)}"
|
||||
)
|
||||
image_path = output_directory / f"frame-{output_index:04d}.jpg"
|
||||
break
|
||||
if skipped < start_frame - 1:
|
||||
skipped += 1
|
||||
continue
|
||||
image_path = (
|
||||
output_directory / f"frame.{len(image_paths) + 1:04d}.jpg"
|
||||
)
|
||||
if not cv2.imwrite(str(image_path), frame):
|
||||
raise RuntimeError(f"Unable to write video frame: {image_path}")
|
||||
image_paths.append(image_path)
|
||||
finally:
|
||||
capture.release()
|
||||
|
||||
if not image_paths:
|
||||
raise RuntimeError(f"Unable to read any video frame from: {video_path}")
|
||||
return image_paths
|
||||
|
||||
|
||||
def prepare_input(input_path, video_frames, temporary_root, cancel_check=None):
|
||||
def prepare_input(
|
||||
input_path,
|
||||
video_frames,
|
||||
temporary_root,
|
||||
start_frame=1,
|
||||
cancel_check=None,
|
||||
):
|
||||
check_cancelled(cancel_check)
|
||||
input_path = Path(input_path)
|
||||
if input_path.is_dir():
|
||||
images = collect_images(input_path)
|
||||
if not images:
|
||||
raise RuntimeError(f"No image frames found in: {input_path}")
|
||||
return first_frames(images, video_frames, start_frame), None
|
||||
suffix = input_path.suffix.lower()
|
||||
if suffix in IMAGE_SUFFIXES:
|
||||
return [input_path], None
|
||||
@@ -174,6 +158,7 @@ def prepare_input(input_path, video_frames, temporary_root, cancel_check=None):
|
||||
temporary_directory,
|
||||
video_frames,
|
||||
cv2,
|
||||
start_frame=start_frame,
|
||||
cancel_check=cancel_check,
|
||||
)
|
||||
except Exception:
|
||||
@@ -182,6 +167,43 @@ def prepare_input(input_path, video_frames, temporary_root, cancel_check=None):
|
||||
return images, temporary_directory
|
||||
|
||||
|
||||
def prepare_background_input(
|
||||
input_path,
|
||||
video_frames,
|
||||
temporary_root,
|
||||
start_frame=1,
|
||||
cancel_check=None,
|
||||
):
|
||||
"""Resolve a BEN2 input to frame images; returns (images, temporary_directory)."""
|
||||
input_path = Path(input_path)
|
||||
if input_path.is_dir():
|
||||
images = collect_images(input_path)
|
||||
if not images:
|
||||
raise RuntimeError(f"No image frames found in: {input_path}")
|
||||
return first_frames(images, video_frames, start_frame), None
|
||||
if input_path.suffix.lower() in IMAGE_SUFFIXES:
|
||||
return [input_path], None
|
||||
return prepare_input(
|
||||
input_path,
|
||||
video_frames,
|
||||
temporary_root,
|
||||
start_frame=start_frame,
|
||||
cancel_check=cancel_check,
|
||||
)
|
||||
|
||||
|
||||
def copy_frames(images, output_directory):
|
||||
"""Copy a multi-frame input as numbered frame files for the Blender sequence."""
|
||||
if len(images) <= 1:
|
||||
return
|
||||
for index, image_path in enumerate(images):
|
||||
suffix = Path(image_path).suffix.lower() or ".jpg"
|
||||
shutil.copy2(
|
||||
image_path,
|
||||
Path(output_directory) / f"frame.{index + 1:04d}{suffix}",
|
||||
)
|
||||
|
||||
|
||||
class DownloadProgressReporter:
|
||||
def __init__(
|
||||
self,
|
||||
@@ -318,21 +340,8 @@ def download_model(args):
|
||||
"stages": getattr(args, "stages", None),
|
||||
"stage_label": getattr(args, "stage_label", None),
|
||||
}
|
||||
write_status(
|
||||
args.status,
|
||||
start,
|
||||
"checking download source",
|
||||
**stage_kwargs,
|
||||
)
|
||||
endpoint, used_mirror = resolve_hf_endpoint()
|
||||
if endpoint != os.environ.get("HF_ENDPOINT"):
|
||||
os.environ["HF_ENDPOINT"] = endpoint
|
||||
from huggingface_hub import snapshot_download
|
||||
|
||||
message = f"downloading {label}"
|
||||
if used_mirror:
|
||||
message = f"{message} (mirror)"
|
||||
write_status(args.status, start, message, **stage_kwargs)
|
||||
progress_class = create_download_progress_class(
|
||||
args.status,
|
||||
args.repository,
|
||||
@@ -341,18 +350,213 @@ def download_model(args):
|
||||
label=label,
|
||||
**stage_kwargs,
|
||||
)
|
||||
snapshot_download(
|
||||
repo_id=args.repository,
|
||||
local_dir=model_dir,
|
||||
tqdm_class=progress_class,
|
||||
allow_patterns=args.allow_pattern or None,
|
||||
failures = []
|
||||
for endpoint in download_endpoints():
|
||||
if not hf_endpoint_reachable(endpoint):
|
||||
failures.append((endpoint, "endpoint unreachable"))
|
||||
continue
|
||||
os.environ["HF_ENDPOINT"] = endpoint
|
||||
message = f"downloading {label}"
|
||||
if endpoint != HF_ENDPOINT:
|
||||
message = f"{message} (mirror)"
|
||||
write_status(args.status, start, message, **stage_kwargs)
|
||||
try:
|
||||
snapshot_download(
|
||||
repo_id=args.repository,
|
||||
local_dir=model_dir,
|
||||
tqdm_class=progress_class,
|
||||
allow_patterns=args.allow_pattern or None,
|
||||
endpoint=endpoint,
|
||||
)
|
||||
_verify_model_complete(model_dir)
|
||||
except Exception as error:
|
||||
failures.append((endpoint, error))
|
||||
continue
|
||||
write_status(
|
||||
args.status,
|
||||
start + span,
|
||||
"model ready",
|
||||
**stage_kwargs,
|
||||
)
|
||||
return
|
||||
# Hugging Face 官方源与已配置镜像均失败时,回退 ModelScope(仓库名与 Hugging Face 一致)
|
||||
try:
|
||||
write_status(
|
||||
args.status,
|
||||
start,
|
||||
f"downloading {label} (mirror: modelscope.cn)",
|
||||
**stage_kwargs,
|
||||
)
|
||||
download_from_modelscope(
|
||||
args,
|
||||
model_dir,
|
||||
start=start,
|
||||
span=span,
|
||||
label=label,
|
||||
stage_kwargs=stage_kwargs,
|
||||
)
|
||||
_verify_model_complete(model_dir)
|
||||
except Exception as error:
|
||||
failures.append((MODELSCOPE_ENDPOINT, error))
|
||||
else:
|
||||
write_status(
|
||||
args.status,
|
||||
start + span,
|
||||
"model ready",
|
||||
**stage_kwargs,
|
||||
)
|
||||
return
|
||||
detail = "; ".join(
|
||||
f"{source}: {error}" for source, error in failures
|
||||
)
|
||||
write_status(
|
||||
raise RuntimeError(f"All download sources failed: {detail}")
|
||||
|
||||
|
||||
def download_endpoints():
|
||||
"""Hugging Face download candidates: configured mirror first, official endpoint otherwise."""
|
||||
endpoint = os.environ.get("HF_ENDPOINT") or HF_ENDPOINT
|
||||
return [endpoint.rstrip("/")]
|
||||
|
||||
|
||||
def hf_endpoint_reachable(endpoint, timeout=10.0):
|
||||
"""Quick connectivity probe so slow or blocked Hugging Face endpoints fail fast."""
|
||||
try:
|
||||
request = Request(
|
||||
f"{endpoint.rstrip('/')}/api/models",
|
||||
method="HEAD",
|
||||
headers={"User-Agent": "SnapForm"},
|
||||
)
|
||||
with urlopen(request, timeout=timeout):
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _verify_model_complete(model_dir):
|
||||
"""Refuse to report success when the required model files are missing."""
|
||||
model_dir = Path(model_dir)
|
||||
missing = []
|
||||
if not (model_dir / "config.json").is_file():
|
||||
missing.append("config.json")
|
||||
if not any(model_dir.glob("*.safetensors")):
|
||||
missing.append("*.safetensors")
|
||||
if missing:
|
||||
raise RuntimeError(
|
||||
"Model download reported success but files are missing: "
|
||||
+ ", ".join(missing)
|
||||
)
|
||||
|
||||
|
||||
def _hidden_repo_path(path):
|
||||
return any(part.startswith(".") for part in Path(path).parts)
|
||||
|
||||
|
||||
def _model_file_allowed(path, patterns):
|
||||
if not patterns:
|
||||
return True
|
||||
return any(fnmatch.fnmatch(path, pattern) for pattern in patterns)
|
||||
|
||||
|
||||
def modelscope_file_list(repository, revision=MODELSCOPE_REVISION):
|
||||
"""List (path, size) file entries of a ModelScope repository."""
|
||||
url = (
|
||||
f"{MODELSCOPE_ENDPOINT}/api/v1/models/{repository}/repo/files?"
|
||||
+ urlencode({"Revision": revision, "Recursive": "true"})
|
||||
)
|
||||
request = Request(url, headers={"User-Agent": "SnapForm"})
|
||||
with urlopen(request, timeout=MODELSCOPE_TIMEOUT) as response:
|
||||
payload = json.loads(response.read().decode("utf-8"))
|
||||
entries = []
|
||||
for entry in payload.get("Data", {}).get("Files", []):
|
||||
path = entry.get("Path")
|
||||
if path and entry.get("Type") == "blob" and not _hidden_repo_path(path):
|
||||
entries.append((path, int(entry.get("Size") or 0)))
|
||||
return entries
|
||||
|
||||
|
||||
def _download_modelscope_file(
|
||||
repository,
|
||||
path,
|
||||
destination,
|
||||
progress=None,
|
||||
retries=2,
|
||||
):
|
||||
"""Download one ModelScope file atomically with a per-chunk progress callback."""
|
||||
url = (
|
||||
f"{MODELSCOPE_ENDPOINT}/models/{repository}/resolve/"
|
||||
f"{MODELSCOPE_REVISION}/{quote(path)}"
|
||||
)
|
||||
request = Request(url, headers={"User-Agent": "SnapForm"})
|
||||
last_error = None
|
||||
for _attempt in range(retries):
|
||||
temporary = destination.with_name(f"{destination.name}.part")
|
||||
try:
|
||||
with urlopen(request, timeout=MODELSCOPE_TIMEOUT) as response:
|
||||
with open(temporary, "wb") as output:
|
||||
while True:
|
||||
chunk = response.read(1024 * 1024)
|
||||
if not chunk:
|
||||
break
|
||||
output.write(chunk)
|
||||
if progress is not None:
|
||||
progress(len(chunk))
|
||||
temporary.replace(destination)
|
||||
return
|
||||
except Exception as error:
|
||||
last_error = error
|
||||
try:
|
||||
temporary.unlink()
|
||||
except OSError:
|
||||
pass
|
||||
raise last_error
|
||||
|
||||
|
||||
def download_from_modelscope(
|
||||
args,
|
||||
model_dir,
|
||||
start=0.05,
|
||||
span=0.90,
|
||||
label=None,
|
||||
stage_kwargs=None,
|
||||
):
|
||||
"""Download a model from ModelScope using only standard library requests."""
|
||||
repository = args.repository
|
||||
files = modelscope_file_list(repository)
|
||||
patterns = args.allow_pattern or None
|
||||
selected = [
|
||||
(path, size)
|
||||
for path, size in files
|
||||
if _model_file_allowed(path, patterns)
|
||||
]
|
||||
if not selected:
|
||||
raise RuntimeError(
|
||||
f"No matching files in ModelScope repository: {repository}"
|
||||
)
|
||||
reporter = DownloadProgressReporter(
|
||||
args.status,
|
||||
start + span,
|
||||
"model ready",
|
||||
**stage_kwargs,
|
||||
repository,
|
||||
start=start,
|
||||
span=span,
|
||||
label=label or repository,
|
||||
**(stage_kwargs or {}),
|
||||
)
|
||||
total_size = sum(size for _path, size in selected)
|
||||
downloaded = 0
|
||||
|
||||
def on_chunk(count):
|
||||
nonlocal downloaded
|
||||
downloaded += count
|
||||
reporter.report(downloaded, total_size)
|
||||
|
||||
for path, _size in selected:
|
||||
destination = Path(model_dir) / path
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
_download_modelscope_file(
|
||||
repository,
|
||||
path,
|
||||
destination,
|
||||
progress=on_chunk,
|
||||
)
|
||||
|
||||
|
||||
def format_rate(rate):
|
||||
@@ -435,6 +639,49 @@ def infer_foreground(
|
||||
return foreground, load_ms, inference_ms
|
||||
|
||||
|
||||
def infer_foreground_frames(
|
||||
images,
|
||||
model_dir,
|
||||
device,
|
||||
torch,
|
||||
output_directory,
|
||||
status_path,
|
||||
start_progress,
|
||||
progress_span,
|
||||
model_cache=None,
|
||||
cancel_check=None,
|
||||
):
|
||||
"""Run BEN2 on every frame and write foreground.0001.png etc."""
|
||||
total_load_ms = 0.0
|
||||
total_inference_ms = 0.0
|
||||
for index, image_path in enumerate(images):
|
||||
check_cancelled(cancel_check)
|
||||
write_status(
|
||||
status_path,
|
||||
start_progress + progress_span * index / len(images),
|
||||
f"Removing background ({index + 1}/{len(images)})",
|
||||
)
|
||||
foreground, load_ms, inference_ms = infer_foreground(
|
||||
image_path,
|
||||
model_dir,
|
||||
device,
|
||||
torch,
|
||||
model_cache=model_cache,
|
||||
cancel_check=cancel_check,
|
||||
)
|
||||
output_path = output_directory / f"foreground.{index + 1:04d}.png"
|
||||
foreground.convert("RGBA").save(
|
||||
output_path,
|
||||
format="PNG",
|
||||
compress_level=1,
|
||||
)
|
||||
if not output_path.is_file():
|
||||
raise RuntimeError(f"BEN2 did not write: {output_path}")
|
||||
total_load_ms += load_ms
|
||||
total_inference_ms += inference_ms
|
||||
return total_load_ms, total_inference_ms
|
||||
|
||||
|
||||
def remove_background(args, model_cache=None, cancel_check=None):
|
||||
total_started = time.perf_counter()
|
||||
write_status(args.status, 0.02, "Starting BEN2 Runtime")
|
||||
@@ -446,41 +693,90 @@ def remove_background(args, model_cache=None, cancel_check=None):
|
||||
torch = model_cache.torch
|
||||
|
||||
output_directory = Path(args.output_dir)
|
||||
output_directory.mkdir(parents=True, exist_ok=False)
|
||||
output_directory.mkdir(parents=True, exist_ok=True)
|
||||
device = select_device(args.device, torch)
|
||||
|
||||
write_status(args.status, 0.12, f"Loading BEN2 on {device.type}")
|
||||
write_status(args.status, 0.35, "Removing background")
|
||||
foreground, load_ms, inference_ms = infer_foreground(
|
||||
video_frames = getattr(args, "video_frames", 16)
|
||||
start_frame = getattr(args, "start_frame", 1)
|
||||
images, temporary_directory = prepare_background_input(
|
||||
args.input,
|
||||
args.model_dir,
|
||||
device,
|
||||
torch,
|
||||
model_cache=model_cache,
|
||||
video_frames,
|
||||
output_directory.parent,
|
||||
start_frame=start_frame,
|
||||
cancel_check=cancel_check,
|
||||
)
|
||||
|
||||
check_cancelled(cancel_check)
|
||||
write_status(args.status, 0.92, "Writing transparent PNG")
|
||||
output_path = output_directory / "foreground.png"
|
||||
foreground.convert("RGBA").save(
|
||||
output_path,
|
||||
format="PNG",
|
||||
compress_level=1,
|
||||
)
|
||||
if not output_path.is_file():
|
||||
raise RuntimeError("BEN2 did not write a foreground image")
|
||||
try:
|
||||
write_status(args.status, 0.12, f"Loading BEN2 on {device.type}")
|
||||
if len(images) == 1:
|
||||
write_status(args.status, 0.35, "Removing background")
|
||||
foreground, load_ms, inference_ms = infer_foreground(
|
||||
images[0],
|
||||
args.model_dir,
|
||||
device,
|
||||
torch,
|
||||
model_cache=model_cache,
|
||||
cancel_check=cancel_check,
|
||||
)
|
||||
check_cancelled(cancel_check)
|
||||
write_status(args.status, 0.92, "Writing transparent PNG")
|
||||
output_path = output_directory / "foreground.png"
|
||||
foreground.convert("RGBA").save(
|
||||
output_path,
|
||||
format="PNG",
|
||||
compress_level=1,
|
||||
)
|
||||
if not output_path.is_file():
|
||||
raise RuntimeError("BEN2 did not write a foreground image")
|
||||
else:
|
||||
load_ms, inference_ms = infer_foreground_frames(
|
||||
images,
|
||||
args.model_dir,
|
||||
device,
|
||||
torch,
|
||||
output_directory,
|
||||
args.status,
|
||||
0.12,
|
||||
0.7,
|
||||
model_cache=model_cache,
|
||||
cancel_check=cancel_check,
|
||||
)
|
||||
copy_frames(images, output_directory)
|
||||
finally:
|
||||
if temporary_directory is not None:
|
||||
shutil.rmtree(temporary_directory, ignore_errors=True)
|
||||
|
||||
check_cancelled(cancel_check)
|
||||
total_ms = (time.perf_counter() - total_started) * 1000.0
|
||||
frame_text = "" if len(images) == 1 else f" from {len(images)} frame(s)"
|
||||
message = (
|
||||
f"Background removed in {inference_ms:.0f} ms "
|
||||
f"Background removed{frame_text} in {inference_ms:.0f} ms "
|
||||
f"(model load {load_ms:.0f} ms, total {total_ms:.0f} ms)"
|
||||
)
|
||||
print(message)
|
||||
write_status(args.status, 1.0, message)
|
||||
|
||||
|
||||
def extract_frames(args, cancel_check=None):
|
||||
"""Copy source frames into a sequence directory without loading any AI model."""
|
||||
output_directory = Path(args.output_dir)
|
||||
output_directory.mkdir(parents=True, exist_ok=True)
|
||||
images, temporary_directory = prepare_input(
|
||||
args.input,
|
||||
getattr(args, "video_frames", 16),
|
||||
output_directory.parent,
|
||||
start_frame=getattr(args, "start_frame", 1),
|
||||
cancel_check=cancel_check,
|
||||
)
|
||||
try:
|
||||
copy_frames(images, output_directory)
|
||||
finally:
|
||||
if temporary_directory is not None:
|
||||
shutil.rmtree(temporary_directory, ignore_errors=True)
|
||||
message = f"Extracted {len(images)} frame(s)"
|
||||
print(message)
|
||||
write_status(args.status, 1.0, message)
|
||||
|
||||
|
||||
def exported_paths(output_directory, export_format):
|
||||
output_directory = Path(output_directory)
|
||||
if export_format == "glb":
|
||||
@@ -491,7 +787,12 @@ def exported_paths(output_directory, export_format):
|
||||
if export_format == "depth_vis":
|
||||
return sorted((output_directory / "depth_vis").glob("*.jpg"))
|
||||
if export_format == "mini_npz":
|
||||
result_path = output_directory / "exports" / "mini_npz" / "results.npz"
|
||||
result_path = output_directory / "results.npz"
|
||||
if result_path.is_file():
|
||||
return [result_path]
|
||||
result_path = (
|
||||
output_directory / "exports" / "mini_npz" / "results.npz"
|
||||
)
|
||||
return [result_path] if result_path.is_file() else []
|
||||
return []
|
||||
|
||||
@@ -543,12 +844,13 @@ def reconstruct(args, model_cache=None, cancel_check=None):
|
||||
from depth_anything_3.utils.export import export
|
||||
|
||||
output_directory = Path(args.output_dir)
|
||||
output_directory.mkdir(parents=True, exist_ok=False)
|
||||
output_directory.mkdir(parents=True, exist_ok=True)
|
||||
write_status(args.status, 0.08, "Preparing input")
|
||||
images, temporary_directory = prepare_input(
|
||||
args.input,
|
||||
args.video_frames,
|
||||
output_directory.parent,
|
||||
start_frame=getattr(args, "start_frame", 1),
|
||||
cancel_check=cancel_check,
|
||||
)
|
||||
|
||||
@@ -562,22 +864,36 @@ def reconstruct(args, model_cache=None, cancel_check=None):
|
||||
)
|
||||
if remove_background_model_dir:
|
||||
write_status(args.status, 0.12, "Removing image background")
|
||||
foreground, load_ms, inference_ms = infer_foreground(
|
||||
images[0],
|
||||
remove_background_model_dir,
|
||||
device,
|
||||
torch,
|
||||
model_cache=model_cache,
|
||||
cancel_check=cancel_check,
|
||||
)
|
||||
check_cancelled(cancel_check)
|
||||
foreground.convert("RGBA").save(
|
||||
output_directory / "foreground.png",
|
||||
format="PNG",
|
||||
compress_level=1,
|
||||
)
|
||||
if not (output_directory / "foreground.png").is_file():
|
||||
raise RuntimeError("BEN2 did not write a foreground image")
|
||||
if len(images) == 1:
|
||||
foreground, load_ms, inference_ms = infer_foreground(
|
||||
images[0],
|
||||
remove_background_model_dir,
|
||||
device,
|
||||
torch,
|
||||
model_cache=model_cache,
|
||||
cancel_check=cancel_check,
|
||||
)
|
||||
check_cancelled(cancel_check)
|
||||
foreground.convert("RGBA").save(
|
||||
output_directory / "foreground.png",
|
||||
format="PNG",
|
||||
compress_level=1,
|
||||
)
|
||||
if not (output_directory / "foreground.png").is_file():
|
||||
raise RuntimeError("BEN2 did not write a foreground image")
|
||||
else:
|
||||
load_ms, inference_ms = infer_foreground_frames(
|
||||
images,
|
||||
remove_background_model_dir,
|
||||
device,
|
||||
torch,
|
||||
output_directory,
|
||||
args.status,
|
||||
0.12,
|
||||
0.05,
|
||||
model_cache=model_cache,
|
||||
cancel_check=cancel_check,
|
||||
)
|
||||
print(
|
||||
f"BEN2 inference {inference_ms:.0f} ms; "
|
||||
f"model load {load_ms:.0f} ms"
|
||||
@@ -636,7 +952,21 @@ def reconstruct(args, model_cache=None, cancel_check=None):
|
||||
raise RuntimeError(
|
||||
f"No {args.export_format} files were exported"
|
||||
)
|
||||
if args.export_format == "mini_npz":
|
||||
nested_path = (
|
||||
output_directory / "exports" / "mini_npz" / "results.npz"
|
||||
)
|
||||
result_path = output_directory / "results.npz"
|
||||
if nested_path.is_file():
|
||||
nested_path.replace(result_path)
|
||||
result_paths = [result_path]
|
||||
for directory in (nested_path.parent, nested_path.parent.parent):
|
||||
try:
|
||||
directory.rmdir()
|
||||
except OSError:
|
||||
pass
|
||||
check_cancelled(cancel_check)
|
||||
copy_frames(images, output_directory)
|
||||
write_status(
|
||||
args.status,
|
||||
1.0,
|
||||
@@ -673,6 +1003,15 @@ def parser():
|
||||
choices=("auto", "cuda", "mps", "cpu"),
|
||||
required=True,
|
||||
)
|
||||
background.add_argument("--video-frames", type=int, default=16)
|
||||
background.add_argument("--start-frame", type=int, default=1)
|
||||
|
||||
frames = commands.add_parser("extract-frames")
|
||||
frames.add_argument("--input", required=True)
|
||||
frames.add_argument("--output-dir", required=True)
|
||||
frames.add_argument("--status", required=True)
|
||||
frames.add_argument("--video-frames", type=int, default=16)
|
||||
frames.add_argument("--start-frame", type=int, default=1)
|
||||
|
||||
reconstruction = commands.add_parser("reconstruct")
|
||||
reconstruction.add_argument("--input", required=True)
|
||||
@@ -709,6 +1048,7 @@ def parser():
|
||||
required=True,
|
||||
)
|
||||
reconstruction.add_argument("--video-frames", type=int, required=True)
|
||||
reconstruction.add_argument("--start-frame", type=int, default=1)
|
||||
reconstruction.add_argument(
|
||||
"--device",
|
||||
choices=("auto", "cuda", "mps", "cpu"),
|
||||
@@ -735,6 +1075,8 @@ def main():
|
||||
download_model(args)
|
||||
elif args.command == "reconstruct":
|
||||
reconstruct(args)
|
||||
elif args.command == "extract-frames":
|
||||
extract_frames(args)
|
||||
else:
|
||||
remove_background(args)
|
||||
except Exception as error:
|
||||
|
||||
Reference in New Issue
Block a user