Remove obsolete implementation and simplify project structure
This commit is contained in:
5
src/anyimage/server/jobs/__init__.py
Normal file
5
src/anyimage/server/jobs/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from .depth import run as generate_depth
|
||||
from .surface import run as generate_surface
|
||||
|
||||
|
||||
__all__ = ("generate_depth", "generate_surface")
|
||||
76
src/anyimage/server/jobs/background.py
Normal file
76
src/anyimage/server/jobs/background.py
Normal file
@@ -0,0 +1,76 @@
|
||||
import shutil
|
||||
import time
|
||||
|
||||
from ..media import prepare_background_input
|
||||
from ..models.ben2 import infer_foreground, infer_foreground_frames
|
||||
|
||||
|
||||
def run(context, parameters):
|
||||
"""Run BEN2 and write transparent foreground images into the Job directory."""
|
||||
total_started = time.perf_counter()
|
||||
context.progress(0.02, "Starting Environment")
|
||||
context.check_cancelled()
|
||||
|
||||
output_directory = context.directory
|
||||
output_directory.mkdir(parents=True, exist_ok=True)
|
||||
device = parameters["device"]
|
||||
model_manager = context.resource("model_manager")
|
||||
model_directory = model_manager.directory(parameters["model"])
|
||||
|
||||
video_frames = parameters.get("video_frames", 16)
|
||||
start_frame = parameters.get("start_frame", 1)
|
||||
images, temporary_directory = prepare_background_input(
|
||||
parameters["input"],
|
||||
video_frames,
|
||||
output_directory,
|
||||
start_frame=start_frame,
|
||||
cancel_check=context.check_cancelled,
|
||||
)
|
||||
try:
|
||||
context.progress(0.12, f"Loading BEN2 for {device.upper()}")
|
||||
if len(images) == 1:
|
||||
context.progress(0.35, "Removing background")
|
||||
foreground, load_ms, inference_ms = infer_foreground(
|
||||
images[0],
|
||||
model_directory,
|
||||
device,
|
||||
model_cache=model_manager,
|
||||
cancel_check=context.check_cancelled,
|
||||
)
|
||||
context.check_cancelled()
|
||||
context.progress(0.92, "Writing transparent PNG")
|
||||
output_path = output_directory / "color.0001.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,
|
||||
model_directory,
|
||||
device,
|
||||
output_directory,
|
||||
context.progress,
|
||||
0.12,
|
||||
0.7,
|
||||
model_cache=model_manager,
|
||||
cancel_check=context.check_cancelled,
|
||||
output_prefix="color",
|
||||
)
|
||||
finally:
|
||||
if temporary_directory is not None:
|
||||
shutil.rmtree(temporary_directory, ignore_errors=True)
|
||||
|
||||
context.check_cancelled()
|
||||
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{frame_text} in {inference_ms:.0f} ms "
|
||||
f"(model load {load_ms:.0f} ms, total {total_ms:.0f} ms)"
|
||||
)
|
||||
print(message)
|
||||
context.progress(1.0, message)
|
||||
return {"color": "."}
|
||||
428
src/anyimage/server/jobs/balloon.py
Normal file
428
src/anyimage/server/jobs/balloon.py
Normal file
@@ -0,0 +1,428 @@
|
||||
import json
|
||||
import math
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from ..models import moge2
|
||||
from ..models.ben2 import infer_foreground
|
||||
|
||||
|
||||
ALPHA_THRESHOLD = 0.5
|
||||
|
||||
|
||||
def create_lasso_input(input_path, polygon, output_path):
|
||||
"""Crop a lasso and clear every RGBA channel outside its polygon."""
|
||||
import numpy as np
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
points = np.asarray(polygon, dtype=np.float64)
|
||||
if points.ndim != 2 or points.shape[0] < 3 or points.shape[1] != 2:
|
||||
raise ValueError("The lasso does not define a valid image region")
|
||||
if not np.isfinite(points).all():
|
||||
raise ValueError("The lasso contains invalid image coordinates")
|
||||
|
||||
with Image.open(input_path) as opened_image:
|
||||
source = opened_image.convert("RGBA")
|
||||
width, height = source.size
|
||||
left = max(0, int(math.floor(float(points[:, 0].min()))))
|
||||
top = max(0, int(math.floor(float(points[:, 1].min()))))
|
||||
right = min(width, int(math.ceil(float(points[:, 0].max()))))
|
||||
bottom = min(height, int(math.ceil(float(points[:, 1].max()))))
|
||||
if right <= left or bottom <= top:
|
||||
raise ValueError("The lasso does not overlap the source image")
|
||||
|
||||
relative = points - np.asarray((left, top), dtype=np.float64)
|
||||
cropped = source.crop((left, top, right, bottom))
|
||||
selection = Image.new("L", cropped.size, 0)
|
||||
ImageDraw.Draw(selection).polygon(
|
||||
[tuple(point) for point in relative],
|
||||
fill=255,
|
||||
)
|
||||
selection_mask = np.asarray(selection, dtype=np.uint8) > 0
|
||||
pixels = np.asarray(cropped, dtype=np.uint8).copy()
|
||||
pixels[~selection_mask] = 0
|
||||
masked = Image.fromarray(pixels, mode="RGBA")
|
||||
output_path = Path(output_path)
|
||||
masked.save(output_path, format="PNG", compress_level=1)
|
||||
return output_path, selection_mask, (left, top), (width, height)
|
||||
|
||||
|
||||
def largest_component(mask):
|
||||
"""Return the largest four-connected component of a boolean image."""
|
||||
import numpy as np
|
||||
|
||||
mask = np.asarray(mask, dtype=bool)
|
||||
if mask.ndim != 2:
|
||||
raise ValueError("The BEN2 Alpha mask must be two-dimensional")
|
||||
height, width = mask.shape
|
||||
visited = np.zeros_like(mask)
|
||||
largest = []
|
||||
for start_y, start_x in np.argwhere(mask):
|
||||
if visited[start_y, start_x]:
|
||||
continue
|
||||
visited[start_y, start_x] = True
|
||||
pending = [(int(start_x), int(start_y))]
|
||||
component = []
|
||||
while pending:
|
||||
x, y = pending.pop()
|
||||
component.append((x, y))
|
||||
for next_x, next_y in (
|
||||
(x - 1, y),
|
||||
(x + 1, y),
|
||||
(x, y - 1),
|
||||
(x, y + 1),
|
||||
):
|
||||
if (
|
||||
0 <= next_x < width
|
||||
and 0 <= next_y < height
|
||||
and mask[next_y, next_x]
|
||||
and not visited[next_y, next_x]
|
||||
):
|
||||
visited[next_y, next_x] = True
|
||||
pending.append((next_x, next_y))
|
||||
if len(component) > len(largest):
|
||||
largest = component
|
||||
if not largest:
|
||||
raise ValueError("BEN2 found no foreground inside the lasso")
|
||||
result = np.zeros_like(mask)
|
||||
component_x, component_y = zip(*largest)
|
||||
result[component_y, component_x] = True
|
||||
return result
|
||||
|
||||
|
||||
def _choose_boundary_edge(start, direction, candidates):
|
||||
right = (-direction[1], direction[0])
|
||||
left = (direction[1], -direction[0])
|
||||
back = (-direction[0], -direction[1])
|
||||
preference = {right: 0, direction: 1, left: 2, back: 3}
|
||||
return min(
|
||||
candidates,
|
||||
key=lambda end: preference.get(
|
||||
(end[0] - start[0], end[1] - start[1]),
|
||||
4,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _simplify_axis_aligned_contour(points):
|
||||
simplified = []
|
||||
for index, current in enumerate(points):
|
||||
previous = points[index - 1]
|
||||
following = points[(index + 1) % len(points)]
|
||||
first = (current[0] - previous[0], current[1] - previous[1])
|
||||
second = (following[0] - current[0], following[1] - current[1])
|
||||
if first[0] * second[1] - first[1] * second[0] != 0:
|
||||
simplified.append(current)
|
||||
return simplified
|
||||
|
||||
|
||||
def trace_component_contour(component):
|
||||
"""Trace the outer pixel-edge loop of one four-connected component."""
|
||||
import numpy as np
|
||||
|
||||
component = np.asarray(component, dtype=bool)
|
||||
height, width = component.shape
|
||||
edges = {}
|
||||
|
||||
def add_edge(start, end):
|
||||
edges.setdefault(start, []).append(end)
|
||||
|
||||
for y, x in np.argwhere(component):
|
||||
x = int(x)
|
||||
y = int(y)
|
||||
if y == 0 or not component[y - 1, x]:
|
||||
add_edge((x, y), (x + 1, y))
|
||||
if x == width - 1 or not component[y, x + 1]:
|
||||
add_edge((x + 1, y), (x + 1, y + 1))
|
||||
if y == height - 1 or not component[y + 1, x]:
|
||||
add_edge((x + 1, y + 1), (x, y + 1))
|
||||
if x == 0 or not component[y, x - 1]:
|
||||
add_edge((x, y + 1), (x, y))
|
||||
if not edges:
|
||||
raise ValueError("BEN2 foreground has no usable boundary")
|
||||
|
||||
start = min(edges, key=lambda point: (point[1], point[0]))
|
||||
current = start
|
||||
direction = (1, 0)
|
||||
contour = []
|
||||
edge_count = sum(len(values) for values in edges.values())
|
||||
for _index in range(edge_count + 1):
|
||||
contour.append(current)
|
||||
candidates = edges.get(current, ())
|
||||
if not candidates:
|
||||
raise ValueError("BEN2 foreground boundary is open")
|
||||
following = _choose_boundary_edge(current, direction, candidates)
|
||||
direction = (following[0] - current[0], following[1] - current[1])
|
||||
current = following
|
||||
if current == start:
|
||||
break
|
||||
else:
|
||||
raise ValueError("BEN2 foreground boundary could not be closed")
|
||||
contour = _simplify_axis_aligned_contour(contour)
|
||||
if len(contour) < 3:
|
||||
raise ValueError("BEN2 foreground is too small for a Balloon")
|
||||
return contour
|
||||
|
||||
|
||||
def contour_from_alpha(alpha, selection_mask, origin=(0, 0)):
|
||||
"""Build a lasso contour from BEN2 Alpha values strictly above 0.5."""
|
||||
import numpy as np
|
||||
|
||||
alpha = np.asarray(alpha, dtype=np.float32)
|
||||
selection_mask = np.asarray(selection_mask, dtype=bool)
|
||||
if alpha.shape != selection_mask.shape:
|
||||
raise ValueError("BEN2 Alpha does not match the lasso input")
|
||||
foreground = (alpha / 255.0 > ALPHA_THRESHOLD) & selection_mask
|
||||
component = largest_component(foreground)
|
||||
contour = trace_component_contour(component)
|
||||
offset_x, offset_y = origin
|
||||
return [[x + offset_x, y + offset_y] for x, y in contour]
|
||||
|
||||
|
||||
def resized_selection_mask(selection_mask, shape):
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
selection_mask = np.asarray(selection_mask, dtype=bool)
|
||||
if selection_mask.shape == tuple(shape):
|
||||
return selection_mask
|
||||
resized = Image.fromarray(selection_mask.astype(np.uint8) * 255, mode="L")
|
||||
resized = resized.resize(
|
||||
(int(shape[1]), int(shape[0])),
|
||||
Image.Resampling.NEAREST,
|
||||
)
|
||||
return np.asarray(resized, dtype=np.uint8) > 0
|
||||
|
||||
|
||||
def normalized_moge_depth(depth, model_mask, selection_mask):
|
||||
"""Normalize MoGe depth while preserving its adjustable fitted base plane."""
|
||||
import numpy as np
|
||||
|
||||
depth = np.asarray(depth, dtype=np.float32)
|
||||
model_mask = np.asarray(model_mask, dtype=bool)
|
||||
selection_mask = resized_selection_mask(selection_mask, depth.shape)
|
||||
if model_mask.shape != depth.shape:
|
||||
raise ValueError("MoGe-2 depth and mask dimensions do not match")
|
||||
valid = selection_mask & model_mask & np.isfinite(depth) & (depth > 0.0)
|
||||
if not valid.any():
|
||||
raise ValueError("MoGe-2 found no valid depth inside the lasso")
|
||||
|
||||
height, width = depth.shape
|
||||
y, x = np.indices(depth.shape, dtype=np.float64)
|
||||
x = x / max(width - 1, 1) * 2.0 - 1.0
|
||||
y = y / max(height - 1, 1) * 2.0 - 1.0
|
||||
design = np.stack((x[valid], y[valid], np.ones(valid.sum())), axis=1)
|
||||
selected = depth[valid].astype(np.float64)
|
||||
weights = np.ones_like(selected)
|
||||
coefficients = np.zeros(3, dtype=np.float64)
|
||||
for _iteration in range(4):
|
||||
weighted_design = design * np.sqrt(weights)[:, None]
|
||||
weighted_depth = selected * np.sqrt(weights)
|
||||
coefficients = np.linalg.lstsq(
|
||||
weighted_design,
|
||||
weighted_depth,
|
||||
rcond=None,
|
||||
)[0]
|
||||
residual = selected - design @ coefficients
|
||||
centered = residual - np.median(residual)
|
||||
scale = max(float(np.median(np.abs(centered))) * 1.4826, 1e-6)
|
||||
weights = np.minimum(
|
||||
1.0,
|
||||
(2.5 * scale) / np.maximum(np.abs(centered), 1e-12),
|
||||
)
|
||||
|
||||
baseline = coefficients[0] * x + coefficients[1] * y + coefficients[2]
|
||||
reference = float(np.median(baseline[valid]))
|
||||
residual = baseline[valid] - selected
|
||||
near_residual = np.maximum(residual, 0.0)
|
||||
near_total = float(np.sum(near_residual))
|
||||
relative_span = (
|
||||
float(np.sum(np.square(near_residual))) / near_total
|
||||
if near_total > 1e-12
|
||||
else float(np.sqrt(np.mean(np.square(residual))))
|
||||
)
|
||||
relative_span = max(relative_span, 1e-6)
|
||||
depth_signal = np.zeros(depth.shape, dtype=np.float32)
|
||||
depth_signal[valid] = (reference - selected) / relative_span
|
||||
baseline_vector = np.asarray(
|
||||
(
|
||||
-coefficients[0] / relative_span,
|
||||
-coefficients[1] / relative_span,
|
||||
(reference - coefficients[2]) / relative_span,
|
||||
),
|
||||
dtype=np.float32,
|
||||
)
|
||||
return depth_signal, valid, reference, baseline_vector
|
||||
|
||||
|
||||
def write_depth_analysis(
|
||||
prediction,
|
||||
selection_mask,
|
||||
origin,
|
||||
source_size,
|
||||
output_path,
|
||||
):
|
||||
import numpy as np
|
||||
|
||||
if prediction.depth.shape[0] != 1 or prediction.mask.shape[0] != 1:
|
||||
raise ValueError("Balloon Depth requires one MoGe-2 frame")
|
||||
depth_signal, valid, reference, baseline_vector = normalized_moge_depth(
|
||||
prediction.depth[0],
|
||||
prediction.mask[0],
|
||||
selection_mask,
|
||||
)
|
||||
crop_size = (selection_mask.shape[1], selection_mask.shape[0])
|
||||
np.savez_compressed(
|
||||
output_path,
|
||||
depth_signal=depth_signal,
|
||||
baseline_vector=baseline_vector,
|
||||
valid=valid,
|
||||
reference_depth=np.asarray(reference, dtype=np.float32),
|
||||
origin=np.asarray(origin, dtype=np.int32),
|
||||
crop_size=np.asarray(crop_size, dtype=np.int32),
|
||||
source_size=np.asarray(source_size, dtype=np.int32),
|
||||
)
|
||||
return Path(output_path)
|
||||
|
||||
|
||||
def write_balloon_normal_texture(
|
||||
normal,
|
||||
valid,
|
||||
origin,
|
||||
crop_size,
|
||||
source_size,
|
||||
output_path,
|
||||
):
|
||||
"""Write MoGe camera normals as a full-image tangent-space texture."""
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
normal = np.asarray(normal, dtype=np.float32)
|
||||
valid = np.asarray(valid, dtype=bool)
|
||||
if normal.ndim != 3 or normal.shape[2] != 3:
|
||||
raise ValueError("Balloon normal data must have three components")
|
||||
if valid.shape != normal.shape[:2]:
|
||||
valid = resized_selection_mask(valid, normal.shape[:2])
|
||||
encoded = np.clip(
|
||||
(normal * np.asarray((1.0, -1.0, -1.0), dtype=np.float32) + 1.0)
|
||||
* 127.5,
|
||||
0.0,
|
||||
255.0,
|
||||
).astype(np.uint8)
|
||||
encoded[~valid] = (128, 128, 255)
|
||||
crop_width, crop_height = (int(value) for value in crop_size)
|
||||
crop = Image.fromarray(encoded, mode="RGB")
|
||||
if crop.size != (crop_width, crop_height):
|
||||
crop = crop.resize(
|
||||
(crop_width, crop_height),
|
||||
Image.Resampling.BILINEAR,
|
||||
)
|
||||
source_width, source_height = (int(value) for value in source_size)
|
||||
full = Image.new("RGB", (source_width, source_height), (128, 128, 255))
|
||||
full.paste(crop, tuple(int(value) for value in origin))
|
||||
output_path = Path(output_path)
|
||||
full.save(output_path)
|
||||
return output_path
|
||||
|
||||
|
||||
def run(context, parameters):
|
||||
"""Analyze a Balloon lasso with optional BEN2 contour and MoGe-2 depth."""
|
||||
total_started = time.perf_counter()
|
||||
contour_analysis = bool(parameters.get("contour_analysis"))
|
||||
depth_analysis = bool(parameters.get("depth_analysis"))
|
||||
if not contour_analysis and not depth_analysis:
|
||||
raise ValueError("Select Contour or Depth")
|
||||
|
||||
context.progress(0.02, "Preparing lasso input")
|
||||
context.check_cancelled()
|
||||
output_directory = context.directory
|
||||
output_directory.mkdir(parents=True, exist_ok=True)
|
||||
input_path, selection_mask, origin, source_size = create_lasso_input(
|
||||
parameters["input"],
|
||||
parameters["polygon"],
|
||||
output_directory / "lasso-input.png",
|
||||
)
|
||||
context.check_cancelled()
|
||||
|
||||
device = parameters["device"]
|
||||
model_manager = context.resource("model_manager")
|
||||
result = {}
|
||||
if contour_analysis:
|
||||
model_directory = model_manager.directory(parameters["contour_model"])
|
||||
context.progress(0.15, f"Loading BEN2 for {device.upper()}")
|
||||
foreground, _load_ms, _inference_ms = infer_foreground(
|
||||
input_path,
|
||||
model_directory,
|
||||
device,
|
||||
model_cache=model_manager,
|
||||
cancel_check=context.check_cancelled,
|
||||
)
|
||||
context.check_cancelled()
|
||||
context.progress(
|
||||
0.55 if depth_analysis else 0.9,
|
||||
"Tracing BEN2 foreground",
|
||||
)
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
alpha = np.asarray(foreground.getchannel("A"), dtype=np.uint8)
|
||||
contour = contour_from_alpha(alpha, selection_mask, origin)
|
||||
gated_alpha = np.where(selection_mask, alpha, 0).astype(np.uint8)
|
||||
foreground_pixels = foreground.convert("RGBA")
|
||||
foreground_pixels.putalpha(Image.fromarray(gated_alpha, mode="L"))
|
||||
foreground_pixels.save(
|
||||
output_directory / "foreground.png",
|
||||
format="PNG",
|
||||
compress_level=1,
|
||||
)
|
||||
contour_path = output_directory / "contour.json"
|
||||
contour_path.write_text(
|
||||
json.dumps({"points": contour}, separators=(",", ":")),
|
||||
encoding="utf-8",
|
||||
)
|
||||
result["contour"] = "contour.json"
|
||||
|
||||
if depth_analysis:
|
||||
context.progress(
|
||||
0.12 if not contour_analysis else 0.58,
|
||||
"Estimating Balloon depth with MoGe-2",
|
||||
)
|
||||
prediction = moge2.infer(
|
||||
model_manager,
|
||||
{
|
||||
"model": parameters["depth_model"],
|
||||
"device": device,
|
||||
"resolution_level": parameters.get("resolution_level", 5),
|
||||
},
|
||||
[input_path],
|
||||
include_points=False,
|
||||
progress_callback=None,
|
||||
cancel_check=context.check_cancelled,
|
||||
)
|
||||
context.check_cancelled()
|
||||
context.progress(0.9, "Writing Balloon depth")
|
||||
depth_path = write_depth_analysis(
|
||||
prediction,
|
||||
selection_mask,
|
||||
origin,
|
||||
source_size,
|
||||
output_directory / "depth.npz",
|
||||
)
|
||||
result["depth"] = depth_path.name
|
||||
import numpy as np
|
||||
|
||||
with np.load(depth_path, allow_pickle=False) as data:
|
||||
normal_path = write_balloon_normal_texture(
|
||||
prediction.normal[0],
|
||||
data["valid"],
|
||||
origin,
|
||||
(selection_mask.shape[1], selection_mask.shape[0]),
|
||||
source_size,
|
||||
output_directory / "normal.png",
|
||||
)
|
||||
result["normal"] = normal_path.name
|
||||
|
||||
total_ms = (time.perf_counter() - total_started) * 1000.0
|
||||
message = f"Balloon AI analysis finished in {total_ms:.0f} ms"
|
||||
context.progress(1.0, message)
|
||||
return result
|
||||
26
src/anyimage/server/jobs/common.py
Normal file
26
src/anyimage/server/jobs/common.py
Normal file
@@ -0,0 +1,26 @@
|
||||
import shutil
|
||||
from contextlib import contextmanager
|
||||
|
||||
from ..media import prepare_input
|
||||
|
||||
|
||||
def frame_progress_message(message, index, total):
|
||||
if total > 1:
|
||||
return f"{message} ({index + 1}/{total})"
|
||||
return message
|
||||
|
||||
|
||||
@contextmanager
|
||||
def input_frames(context, parameters):
|
||||
images, temporary_directory = prepare_input(
|
||||
parameters["input"],
|
||||
parameters.get("video_frames", 16),
|
||||
context.directory,
|
||||
start_frame=parameters.get("start_frame", 1),
|
||||
cancel_check=context.check_cancelled,
|
||||
)
|
||||
try:
|
||||
yield images
|
||||
finally:
|
||||
if temporary_directory is not None:
|
||||
shutil.rmtree(temporary_directory, ignore_errors=True)
|
||||
26
src/anyimage/server/jobs/debug_ben2.py
Normal file
26
src/anyimage/server/jobs/debug_ben2.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from ..media import copy_frames
|
||||
from ..models.ben2 import infer_foreground
|
||||
from .common import input_frames
|
||||
|
||||
|
||||
def run(context, parameters):
|
||||
context.progress(0.02, "Preparing BEN2 API input")
|
||||
context.directory.mkdir(parents=True, exist_ok=True)
|
||||
model_manager = context.resource("model_manager")
|
||||
model_directory = model_manager.directory(parameters["model"])
|
||||
with input_frames(context, parameters) as images:
|
||||
for index, image in enumerate(images):
|
||||
context.check_cancelled()
|
||||
foreground, _load_ms, _inference_ms = infer_foreground(
|
||||
image,
|
||||
model_directory,
|
||||
parameters["device"],
|
||||
model_cache=model_manager,
|
||||
cancel_check=context.check_cancelled,
|
||||
)
|
||||
suffix = "" if len(images) == 1 else f".{index + 1:04d}"
|
||||
foreground.save(context.directory / f"foreground{suffix}.png")
|
||||
copy_frames(images, context.directory)
|
||||
context.check_cancelled()
|
||||
context.progress(1.0, "Saved raw BEN2 API foreground")
|
||||
return {"directory": "."}
|
||||
21
src/anyimage/server/jobs/debug_da3.py
Normal file
21
src/anyimage/server/jobs/debug_da3.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from ..media import copy_frames
|
||||
from ..models import da3
|
||||
from ..outputs.debug_da3 import write_prediction
|
||||
from .common import input_frames
|
||||
|
||||
|
||||
def run(context, parameters):
|
||||
context.progress(0.02, "Preparing DA3 API input")
|
||||
context.directory.mkdir(parents=True, exist_ok=True)
|
||||
with input_frames(context, parameters) as images:
|
||||
prediction = da3.infer(
|
||||
context.resource("model_manager"),
|
||||
parameters,
|
||||
images,
|
||||
cancel_check=context.check_cancelled,
|
||||
)
|
||||
write_prediction(prediction, context.directory / "prediction.npz")
|
||||
copy_frames(images, context.directory)
|
||||
context.check_cancelled()
|
||||
context.progress(1.0, "Saved raw DA3 API prediction")
|
||||
return {"directory": "."}
|
||||
26
src/anyimage/server/jobs/debug_moge2.py
Normal file
26
src/anyimage/server/jobs/debug_moge2.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from ..media import copy_frames
|
||||
from ..models.moge2 import infer_one
|
||||
from ..outputs.debug_moge2 import write_prediction
|
||||
from .common import input_frames
|
||||
|
||||
|
||||
def run(context, parameters):
|
||||
context.progress(0.02, "Preparing MoGe-2 API input")
|
||||
context.directory.mkdir(parents=True, exist_ok=True)
|
||||
with input_frames(context, parameters) as images:
|
||||
for index, image in enumerate(images):
|
||||
context.check_cancelled()
|
||||
prediction = infer_one(
|
||||
context.resource("model_manager"),
|
||||
parameters,
|
||||
image,
|
||||
)
|
||||
suffix = "" if len(images) == 1 else f".{index + 1:04d}"
|
||||
write_prediction(
|
||||
prediction,
|
||||
context.directory / f"prediction{suffix}.npz",
|
||||
)
|
||||
copy_frames(images, context.directory)
|
||||
context.check_cancelled()
|
||||
context.progress(1.0, "Saved raw MoGe-2 API prediction")
|
||||
return {"directory": "."}
|
||||
50
src/anyimage/server/jobs/depth.py
Normal file
50
src/anyimage/server/jobs/depth.py
Normal file
@@ -0,0 +1,50 @@
|
||||
from ..media import copy_color_images
|
||||
from ..models import da3, geometry_adapter, moge2
|
||||
from ..outputs.depth import write_da3_depth, write_moge_depth
|
||||
from .common import frame_progress_message, input_frames
|
||||
from .foreground import write_optional
|
||||
|
||||
|
||||
def run(context, parameters):
|
||||
context.progress(0.02, "Preparing depth input")
|
||||
context.directory.mkdir(parents=True, exist_ok=True)
|
||||
model_manager = context.resource("model_manager")
|
||||
adapter = geometry_adapter(parameters["model"])
|
||||
with input_frames(context, parameters) as images:
|
||||
if adapter is da3:
|
||||
prediction = adapter.infer(
|
||||
model_manager,
|
||||
parameters,
|
||||
images,
|
||||
progress_callback=lambda index, total: context.progress(
|
||||
0.15 + 0.65 * index / total,
|
||||
frame_progress_message("Estimating DA3 depth", index, total),
|
||||
),
|
||||
cancel_check=context.check_cancelled,
|
||||
)
|
||||
write_da3_depth(prediction, context.directory / "results.npz")
|
||||
else:
|
||||
prediction = adapter.infer(
|
||||
model_manager,
|
||||
parameters,
|
||||
images,
|
||||
include_points=False,
|
||||
progress_callback=lambda index, total: context.progress(
|
||||
0.15 + 0.65 * index / total,
|
||||
frame_progress_message("Estimating MoGe-2 depth", index, total),
|
||||
),
|
||||
cancel_check=context.check_cancelled,
|
||||
)
|
||||
write_moge_depth(context.directory, prediction)
|
||||
color_paths = write_optional(
|
||||
context,
|
||||
parameters,
|
||||
images,
|
||||
progress_start=0.84,
|
||||
progress_span=0.08,
|
||||
)
|
||||
if not color_paths:
|
||||
copy_color_images(images, context.directory)
|
||||
context.check_cancelled()
|
||||
context.progress(1.0, f"Exported depth for {len(images)} frame(s)")
|
||||
return {"depth": "results.npz"}
|
||||
52
src/anyimage/server/jobs/foreground.py
Normal file
52
src/anyimage/server/jobs/foreground.py
Normal file
@@ -0,0 +1,52 @@
|
||||
from ..models.ben2 import infer_foreground, infer_foreground_frames
|
||||
|
||||
|
||||
def write_optional(
|
||||
context,
|
||||
parameters,
|
||||
images,
|
||||
*,
|
||||
progress_start,
|
||||
progress_span,
|
||||
):
|
||||
model_key = parameters.get("remove_background_model", "")
|
||||
if not model_key:
|
||||
return []
|
||||
model_manager = context.resource("model_manager")
|
||||
model_directory = model_manager.directory(model_key)
|
||||
device = parameters["device"]
|
||||
if len(images) == 1:
|
||||
context.progress(progress_start, "Removing image background")
|
||||
foreground, load_ms, inference_ms = infer_foreground(
|
||||
images[0],
|
||||
model_directory,
|
||||
device,
|
||||
model_cache=model_manager,
|
||||
cancel_check=context.check_cancelled,
|
||||
)
|
||||
path = context.directory / "color.0001.png"
|
||||
foreground.convert("RGBA").save(
|
||||
path,
|
||||
format="PNG",
|
||||
compress_level=1,
|
||||
)
|
||||
paths = [path]
|
||||
else:
|
||||
load_ms, inference_ms = infer_foreground_frames(
|
||||
images,
|
||||
model_directory,
|
||||
device,
|
||||
context.directory,
|
||||
context.progress,
|
||||
progress_start,
|
||||
progress_span,
|
||||
model_cache=model_manager,
|
||||
cancel_check=context.check_cancelled,
|
||||
output_prefix="color",
|
||||
)
|
||||
paths = sorted(context.directory.glob("color.*.png"))
|
||||
print(
|
||||
f"BEN2 inference {inference_ms:.0f} ms; "
|
||||
f"model load {load_ms:.0f} ms"
|
||||
)
|
||||
return paths
|
||||
21
src/anyimage/server/jobs/frames.py
Normal file
21
src/anyimage/server/jobs/frames.py
Normal file
@@ -0,0 +1,21 @@
|
||||
import shutil
|
||||
|
||||
from ..media import copy_color_images, prepare_input
|
||||
|
||||
|
||||
def run(context, parameters):
|
||||
context.directory.mkdir(parents=True, exist_ok=True)
|
||||
images, temporary_directory = prepare_input(
|
||||
parameters["input"],
|
||||
parameters.get("video_frames", 16),
|
||||
context.directory,
|
||||
start_frame=parameters.get("start_frame", 1),
|
||||
cancel_check=context.check_cancelled,
|
||||
)
|
||||
try:
|
||||
copy_color_images(images, context.directory)
|
||||
finally:
|
||||
if temporary_directory is not None:
|
||||
shutil.rmtree(temporary_directory, ignore_errors=True)
|
||||
context.progress(1.0, f"Extracted {len(images)} frame(s)")
|
||||
return {"frames": len(images)}
|
||||
68
src/anyimage/server/jobs/surface.py
Normal file
68
src/anyimage/server/jobs/surface.py
Normal file
@@ -0,0 +1,68 @@
|
||||
from ..geometry import build_surface, foreground_mesh_mask
|
||||
from ..media import copy_color_images
|
||||
from ..models import geometry_adapter, moge2
|
||||
from ..outputs.surface import write_normal_texture, write_surface_archive
|
||||
from .common import frame_progress_message, input_frames
|
||||
from .foreground import write_optional
|
||||
|
||||
def run(context, parameters):
|
||||
context.progress(0.02, "Preparing surface input")
|
||||
context.directory.mkdir(parents=True, exist_ok=True)
|
||||
model_manager = context.resource("model_manager")
|
||||
adapter = geometry_adapter(parameters["model"])
|
||||
with input_frames(context, parameters) as images:
|
||||
if len(images) != 1:
|
||||
raise ValueError("Convert to Surface currently supports one image")
|
||||
context.progress(0.12, "Estimating surface geometry")
|
||||
if adapter is moge2:
|
||||
prediction = adapter.infer(
|
||||
model_manager,
|
||||
parameters,
|
||||
images,
|
||||
include_points=True,
|
||||
progress_callback=lambda index, total: context.progress(
|
||||
0.12 + 0.58 * index / total,
|
||||
frame_progress_message("Estimating MoGe-2 geometry", index, total),
|
||||
),
|
||||
cancel_check=context.check_cancelled,
|
||||
)
|
||||
else:
|
||||
prediction = adapter.infer(
|
||||
model_manager,
|
||||
parameters,
|
||||
images,
|
||||
progress_callback=lambda index, total: context.progress(
|
||||
0.12 + 0.58 * index / total,
|
||||
frame_progress_message("Estimating DA3 geometry", index, total),
|
||||
),
|
||||
cancel_check=context.check_cancelled,
|
||||
)
|
||||
context.check_cancelled()
|
||||
frame = adapter.surface_frame(prediction)
|
||||
color_paths = write_optional(
|
||||
context,
|
||||
parameters,
|
||||
images,
|
||||
progress_start=0.72,
|
||||
progress_span=0.08,
|
||||
)
|
||||
foreground_mask = None
|
||||
if color_paths:
|
||||
foreground_mask = foreground_mesh_mask(
|
||||
color_paths[0],
|
||||
frame.depth.shape,
|
||||
)
|
||||
context.progress(0.82, "Building surface")
|
||||
mesh = build_surface(frame, foreground_mask=foreground_mask)
|
||||
context.progress(0.92, "Writing surface")
|
||||
write_surface_archive(mesh, context.directory / "mesh.npz")
|
||||
if not color_paths:
|
||||
copy_color_images(images, context.directory)
|
||||
if adapter is moge2:
|
||||
write_normal_texture(
|
||||
prediction.normal[0],
|
||||
context.directory / "normal.png",
|
||||
)
|
||||
context.check_cancelled()
|
||||
context.progress(1.0, "Surface is ready")
|
||||
return {"surface": "mesh.npz"}
|
||||
Reference in New Issue
Block a user