feat: Implement cutout tool and depth generation features

- Added a new cutout tool for selecting regions in images, allowing users to choose mesh shapes based on selected areas.
- Introduced depth generation capabilities for rasterized images, including normal data extraction.
- Created jobs for generating depth planes and region data, enhancing the AI data generation workflow.
- Developed a selection refinement process for cutout regions, improving accuracy in generated outputs.
- Established a new region module for handling rasterized image regions and their associated AI generation tasks.
- Enhanced node graph builders for asset generation, facilitating smoother integration of new features.
This commit is contained in:
2026-08-30 23:09:13 +08:00
parent 32bcd45d7b
commit 08309f719d
88 changed files with 4145 additions and 5639 deletions

View File

@@ -1,19 +1,20 @@
import bpy
from ..common.image import (
from ..common.mesh_result import load_image_geometry
from ..common.image_data import (
color_data_name,
create_color_image,
depth_data_name,
is_image_empty,
material_data_name,
normal_data_name,
plane_bounds,
image_pixels,
load_color_result_image,
load_normal_result_image,
image_empty_bounds,
require_image_empty,
)
from ..common.material import create_material
from ..common.node_asset import modifier_input_identifier, set_modifier_input
from ..common.normal_map import bake_tangent_normal_image
from ..common.operator import push_undo
from ..common.mesh_result import create_image_material
from ..common.mesh_result import modifier_input_identifier, set_modifier_input
from ..common.mesh_result import finalize_object_result
from .mesh_plane import create_mesh_plane_mesh, create_mesh_plane_object
from .normal_map import bake_tangent_normal_image
from .nodes import depth_plane_node_group
@@ -23,10 +24,7 @@ def copy_color_image(source_object):
if getattr(source, "source", None) == "GENERATED" or getattr(
source, "is_dirty", False
):
import numpy as np
pixels = np.empty(len(source.pixels), dtype=np.float32)
source.pixels.foreach_get(pixels)
pixels = image_pixels(source)
image.pixels.foreach_set(pixels)
image.update()
image.name = color_data_name(source_object)
@@ -38,7 +36,7 @@ def build_mesh_plane(context, source_object, subdivisions, material):
plane = create_mesh_plane_object(
context,
source_name,
plane_bounds(source_object),
image_empty_bounds(source_object),
material,
matrix_world=source_object.matrix_world,
subdivisions=subdivisions,
@@ -46,58 +44,32 @@ def build_mesh_plane(context, source_object, subdivisions, material):
for polygon in plane.data.polygons:
polygon.use_smooth = True
plane["anyimage_mesh_shape"] = "PLANE"
for selected in context.selected_objects:
selected.select_set(False)
plane.select_set(True)
context.view_layer.objects.active = plane
bpy.data.objects.remove(source_object, do_unlink=True)
plane.name = source_name
return plane
def load_depth_field(path):
import numpy as np
with np.load(path, allow_pickle=False) as archive:
points = np.asarray(archive["points"], dtype=np.float32)
intrinsics = np.asarray(archive["intrinsics"], dtype=np.float32)
valid = np.asarray(archive["valid"], dtype=bool)
calibration_valid = np.asarray(archive["calibration_valid"], dtype=bool)
image_size = tuple(int(value) for value in archive["image_size"])
reference_depth = float(archive["reference_depth"])
if (
points.ndim != 3
or points.shape[2] != 3
or valid.shape != points.shape[:2]
or calibration_valid.shape != points.shape[:2]
):
raise ValueError("The mesh point field is invalid")
if intrinsics.shape != (3, 3) or not reference_depth > 0.0:
raise ValueError("The mesh camera field is invalid")
return points, intrinsics, valid, calibration_valid, image_size, reference_depth
return finalize_object_result(context, source_object, plane)
def anchored_point_field(source_object, field):
import numpy as np
points, intrinsics, valid, _calibration, image_size, reference_depth = field
width, height = image_size
x_min, x_max, y_min, y_max = plane_bounds(source_object)
points = field.points
intrinsics = field.intrinsics
valid = field.valid
width, height = field.image_size
x_min, x_max, y_min, y_max = image_empty_bounds(source_object)
plane_width = x_max - x_min
plane_height = y_max - y_min
fx, fy = float(intrinsics[0, 0]), float(intrinsics[1, 1])
cx, cy = float(intrinsics[0, 2]), float(intrinsics[1, 2])
if fx <= 0.0 or fy <= 0.0:
raise ValueError("Camera intrinsics contain an invalid focal length")
scale_x = plane_width * fx / (max(width - 1, 1) * reference_depth)
scale_y = plane_height * fy / (max(height - 1, 1) * reference_depth)
scale_x = plane_width * fx / (max(width - 1, 1) * field.reference_depth)
scale_y = plane_height * fy / (max(height - 1, 1) * field.reference_depth)
scale_z = float(np.sqrt(scale_x * scale_y))
camera_x = x_min + plane_width * cx / max(width - 1, 1)
camera_y = y_max - plane_height * cy / max(height - 1, 1)
anchored = np.empty_like(points, dtype=np.float32)
anchored[..., 0] = camera_x + points[..., 0] * scale_x
anchored[..., 1] = camera_y - points[..., 1] * scale_y
anchored[..., 2] = (reference_depth - points[..., 2]) * scale_z
anchored[..., 2] = (field.reference_depth - points[..., 2]) * scale_z
return anchored, valid, points[..., 2]
@@ -122,17 +94,9 @@ def create_depth_image(source_object, anchored_points):
return image
def load_normal_image(path, source_object):
image = bpy.data.images.load(str(path), check_existing=False)
image.name = normal_data_name(source_object)
image.colorspace_settings.name = "Non-Color"
image.pack()
return image
def build_depth_plane(context, source_object, subdivisions, material, field):
source_name = source_object.name
mesh = create_mesh_plane_mesh(source_name, plane_bounds(source_object))
mesh = create_mesh_plane_mesh(source_name, image_empty_bounds(source_object))
for polygon in mesh.polygons:
polygon.use_smooth = True
mesh.materials.append(material)
@@ -160,33 +124,21 @@ def build_depth_plane(context, source_object, subdivisions, material, field):
modifier_input_identifier(node_group, name),
value,
)
for selected in context.selected_objects:
selected.select_set(False)
plane.select_set(True)
context.view_layer.objects.active = plane
bpy.data.objects.remove(source_object, do_unlink=True)
plane.name = source_name
return plane
return finalize_object_result(context, source_object, plane)
def build_depth_plane_response(context, result, operator):
source_object = bpy.data.objects.get(operator.source_object_name)
if not is_image_empty(source_object):
raise RuntimeError("The source Image Empty no longer exists")
color_image = create_color_image(
source_object,
result.directory,
assign_to_source=False,
)
source_object = require_image_empty(operator.source_object_name)
color_image = load_color_result_image(source_object, result.directory)
color_image.name = color_data_name(source_object)
normal_path = result.directory / "normal.png"
normal_image = (
load_normal_image(normal_path, source_object)
load_normal_result_image(normal_path, source_object)
if normal_path.is_file()
else None
)
material = create_material(
material_data_name(source_object),
material = create_image_material(
source_object,
color_image,
0.0,
normal_image=normal_image,
@@ -197,7 +149,7 @@ def build_depth_plane_response(context, result, operator):
source_object,
operator.mesh_detail,
material,
load_depth_field(result.file("field")),
load_image_geometry(result.file("image_geometry")),
)
if normal_image is not None:
bake_tangent_normal_image(
@@ -206,4 +158,5 @@ def build_depth_plane_response(context, result, operator):
plane,
normal_image,
)
push_undo("Convert to Depth Plane")
if "FINISHED" not in bpy.ops.ed.undo_push(message="Convert to Depth Plane"):
raise RuntimeError("Unable to create the Convert to Depth Plane Undo step")

View File

@@ -8,30 +8,29 @@ from ...server.media.resolution import (
MAX_MAX_AI_INPUT_SIZE,
MIN_MAX_AI_INPUT_SIZE,
)
from ..common.image import (
analysis_source_image,
from ..common.image_data import (
cleanup_image_input,
image_user_settings,
is_image_empty,
material_data_name,
prepare_image_analysis_inputs,
remove_temporary_input,
scaled_process_resolution,
prepare_image_input,
source_start_frame,
video_total_frames,
)
from ..common.material import create_material
from ..common.operator import (
HIDDEN,
cleanup_temporary_input,
from ..common.mesh_result import create_image_material
from ..common.ai_job import (
da3_parameters,
input_path,
require_input_path,
invoke_ai_setup_if_needed,
moge2_parameters,
production_device,
require_environment,
require_model,
source_start_frame,
)
from .build import build_depth_plane_response, build_mesh_plane, copy_color_image
from .resolution import scaled_process_resolution
_HIDDEN = {"HIDDEN", "SKIP_SAVE"}
def depth_plane_settings(context):
@@ -46,48 +45,39 @@ def depth_plane_settings(context):
class GenerateDepthPlane(JobOperatorBase, bpy.types.Operator):
bl_idname = "anyimage.generate_depth_plane"
bl_label = "Generate Depth Plane"
job_type = "prepare-geometry-data"
job_type = "generate-depth-plane"
starting_message = "Preparing Depth Plane"
input_path: bpy.props.StringProperty(options=HIDDEN)
analysis_input_path: bpy.props.StringProperty(options=HIDDEN)
source_object_name: bpy.props.StringProperty(options=HIDDEN)
mesh_detail: bpy.props.IntProperty(default=0, min=0, max=10, options=HIDDEN)
model: bpy.props.StringProperty(default=DEFAULT_MOGE2_MODEL_KEY, options=HIDDEN)
process_res: bpy.props.IntProperty(default=1008, min=1, options=HIDDEN)
resolution_level: bpy.props.IntProperty(default=5, min=0, max=9, options=HIDDEN)
input_path: bpy.props.StringProperty(options=_HIDDEN)
source_object_name: bpy.props.StringProperty(options=_HIDDEN)
mesh_detail: bpy.props.IntProperty(default=0, min=0, max=10, options=_HIDDEN)
model: bpy.props.StringProperty(default=DEFAULT_MOGE2_MODEL_KEY, options=_HIDDEN)
process_res: bpy.props.IntProperty(default=1008, min=1, options=_HIDDEN)
resolution_level: bpy.props.IntProperty(default=5, min=0, max=9, options=_HIDDEN)
max_input_size: bpy.props.IntProperty(
default=DEFAULT_MAX_AI_INPUT_SIZE,
min=MIN_MAX_AI_INPUT_SIZE,
max=MAX_MAX_AI_INPUT_SIZE,
options=HIDDEN,
options=_HIDDEN,
)
delete_input: bpy.props.BoolProperty(default=False, options=HIDDEN)
delete_analysis_input: bpy.props.BoolProperty(default=False, options=HIDDEN)
delete_input: bpy.props.BoolProperty(default=False, options=_HIDDEN)
def request(self, context):
require_environment()
require_model(self.model)
parameters = {
"input": str(input_path(self.input_path)),
"analysis_input": str(input_path(self.analysis_input_path)),
"shape": "DEPTH_PLANE",
"polygon": None,
"mesh_detail": self.mesh_detail,
"input": str(require_input_path(self.input_path)),
"device": production_device().lower(),
"video_frames": 1,
"start_frame": source_start_frame(self.source_object_name),
"max_input_size": int(self.max_input_size),
"refine_selection": False,
"normal_analysis": False,
"invert": False,
}
settings = context.scene.anyimage_settings
if self.model.startswith("MOGE2_"):
parameters.update(
moge2_parameters(
settings,
input_path(self.input_path),
require_input_path(self.input_path),
self.model,
self.resolution_level,
source_start_frame(self.source_object_name),
@@ -97,13 +87,12 @@ class GenerateDepthPlane(JobOperatorBase, bpy.types.Operator):
parameters.update(
da3_parameters(
settings,
input_path(self.input_path),
require_input_path(self.input_path),
self.model,
self.process_res,
source_start_frame(self.source_object_name),
)
)
parameters.update(shape="DEPTH_PLANE", polygon=None)
return parameters
def response(self, context, result):
@@ -111,11 +100,7 @@ class GenerateDepthPlane(JobOperatorBase, bpy.types.Operator):
return "Depth Plane is ready"
def cleanup(self):
cleanup_temporary_input(self.input_path, self.delete_input)
cleanup_temporary_input(
self.analysis_input_path,
self.delete_analysis_input,
)
cleanup_image_input(self.input_path, self.delete_input)
def start_depth_plane_job(operator, context):
@@ -127,13 +112,13 @@ def start_depth_plane_job(operator, context):
if setup_result is not None:
return setup_result
try:
source_path, delete_input, analysis_path, delete_analysis_input = (
prepare_image_analysis_inputs(source_object.data, context.scene)
source_path, delete_input = prepare_image_input(
source_object.data,
context.scene,
)
analysis_image = analysis_source_image(source_object.data)
model, process_factor, resolution_level = depth_plane_settings(context)
process_res = (
scaled_process_resolution(analysis_image, process_factor)
scaled_process_resolution(source_object.data, process_factor)
if not model.startswith("MOGE2_")
else 1
)
@@ -142,7 +127,6 @@ def start_depth_plane_job(operator, context):
return {"CANCELLED"}
result = bpy.ops.anyimage.generate_depth_plane(
input_path=str(source_path),
analysis_input_path=str(analysis_path),
source_object_name=source_object.name,
mesh_detail=operator.mesh_detail,
model=model,
@@ -150,13 +134,10 @@ def start_depth_plane_job(operator, context):
resolution_level=resolution_level,
max_input_size=configured_max_ai_input_size(context),
delete_input=delete_input,
delete_analysis_input=delete_analysis_input,
)
if "CANCELLED" in result:
if delete_input:
remove_temporary_input(source_path)
if delete_analysis_input:
remove_temporary_input(analysis_path)
cleanup_image_input(source_path, True)
return {"CANCELLED"} if "CANCELLED" in result else {"FINISHED"}
@@ -178,8 +159,8 @@ class ConvertToPlane(bpy.types.Operator):
frame_start, frame_offset, frame_duration, use_cyclic = image_user_settings(
source_object
)
material = create_material(
material_data_name(source_object),
material = create_image_material(
source_object,
color_image,
1.0,
frame_start=frame_start,

View File

@@ -2,11 +2,8 @@
import bpy
from ..common.node_asset import (
load_mesh_plane_node_group,
modifier_input_identifier,
set_modifier_input,
)
from ..common.mesh_result import modifier_input_identifier, set_modifier_input
from .nodes import mesh_plane_node_group
MESH_PLANE_NODE_GROUP_NAME = "O Mesh Plane"
@@ -44,7 +41,7 @@ def add_mesh_plane_processing(
subdivisions=0,
thickness=0.0,
):
node_group = load_mesh_plane_node_group()
node_group = mesh_plane_node_group()
modifier = plane_object.modifiers.new(
name=MESH_PLANE_NODE_GROUP_NAME,
type="NODES",

View File

@@ -2,8 +2,33 @@
import bpy
from ..common.node_asset import load_mesh_plane_node_group
from ..common.node_graph import group_input, interface_socket
from ..common.mesh_result import load_asset_node_group
MESH_PLANE_NODE_GROUP_NAME = "O Mesh Plane"
def mesh_plane_node_group():
return load_asset_node_group(MESH_PLANE_NODE_GROUP_NAME)
def _interface_socket(group, name, in_out, socket_type):
socket = group.interface.new_socket(
name=name,
in_out=in_out,
socket_type=socket_type,
)
if in_out == "INPUT" and hasattr(socket, "structure_type"):
socket.structure_type = "SINGLE"
return socket
def _group_input(nodes, location, visible_names):
node = nodes.new("NodeGroupInput")
node.location = location
visible = set(visible_names)
for output in node.outputs:
output.hide = output.name not in visible
return node
def depth_plane_node_group():
group_name = "O Image Depth Plane"
@@ -16,42 +41,42 @@ def depth_plane_node_group():
group.nodes.clear()
group.interface.clear()
group.is_modifier = True
interface_socket(group, "Geometry", "INPUT", "NodeSocketGeometry")
subdivide = interface_socket(group, "Subdivide", "INPUT", "NodeSocketInt")
_interface_socket(group, "Geometry", "INPUT", "NodeSocketGeometry")
subdivide = _interface_socket(group, "Subdivide", "INPUT", "NodeSocketInt")
subdivide.default_value = 7
subdivide.min_value = 0
subdivide.max_value = 10
thickness = interface_socket(group, "Thickness", "INPUT", "NodeSocketFloat")
thickness = _interface_socket(group, "Thickness", "INPUT", "NodeSocketFloat")
thickness.default_value = 0.1
thickness.min_value = 0.0
thickness.subtype = "DISTANCE"
depth_amount = interface_socket(group, "Depth Amount", "INPUT", "NodeSocketFloat")
depth_amount = _interface_socket(group, "Depth Amount", "INPUT", "NodeSocketFloat")
depth_amount.default_value = 1.0
depth_amount.min_value = 0.0
depth_image = interface_socket(group, "Depth Image", "INPUT", "NodeSocketImage")
depth_image = _interface_socket(group, "Depth Image", "INPUT", "NodeSocketImage")
depth_image.hide_in_modifier = True
material = interface_socket(group, "Material", "INPUT", "NodeSocketMaterial")
material = _interface_socket(group, "Material", "INPUT", "NodeSocketMaterial")
material.hide_in_modifier = True
interface_socket(group, "Geometry", "OUTPUT", "NodeSocketGeometry")
_interface_socket(group, "Geometry", "OUTPUT", "NodeSocketGeometry")
nodes = group.nodes
links = group.links
geometry_input = group_input(
geometry_input = _group_input(
nodes,
(-1850.0, 450.0),
{"Geometry", "Subdivide", "Material"},
)
shape_input = group_input(
shape_input = _group_input(
nodes,
(-1250.0, 850.0),
{"Thickness", "Depth Amount"},
)
image_input = group_input(nodes, (-1050.0, 50.0), {"Depth Image"})
cleanup_input = group_input(nodes, (650.0, -200.0), {"Thickness"})
image_input = _group_input(nodes, (-1050.0, 50.0), {"Depth Image"})
cleanup_input = _group_input(nodes, (650.0, -200.0), {"Thickness"})
group_output = nodes.new("NodeGroupOutput")
group_output.location = (1600.0, 450.0)
mesh_plane = nodes.new("GeometryNodeGroup")
mesh_plane.node_tree = load_mesh_plane_node_group()
mesh_plane.node_tree = mesh_plane_node_group()
mesh_plane.inputs["Thickness"].default_value = 0.0
mesh_plane.location = (-1550.0, 450.0)
links.new(geometry_input.outputs["Geometry"], mesh_plane.inputs["Geometry"])
@@ -150,7 +175,7 @@ def depth_plane_node_group():
def plane_node_group(shape):
if shape == "PLANE":
return load_mesh_plane_node_group()
return mesh_plane_node_group()
if shape == "DEPTH_PLANE":
return depth_plane_node_group()
raise ValueError(f"Unsupported Plane shape: {shape}")

View File

@@ -0,0 +1,159 @@
from ..common.image_data import image_pixels
def _normalized(values, fallback):
import numpy as np
values = np.asarray(values, dtype=np.float64)
lengths = np.linalg.norm(values, axis=-1)
result = np.empty_like(values)
usable = lengths > 1e-10
result[usable] = values[usable] / lengths[usable, None]
result[~usable] = fallback
return result
def rasterize_tangent_normal(
object_normal,
triangle_uv,
triangle_normal,
triangle_tangent,
triangle_sign,
return_coverage=False,
):
"""Convert object-space pixels through a smooth per-corner UV tangent frame."""
import numpy as np
object_normal = _normalized(object_normal, (0.0, 0.0, 1.0))
height, width = object_normal.shape[:2]
output = np.zeros((height, width, 3), dtype=np.float32)
output[..., 2] = 1.0
coverage = np.zeros((height, width), dtype=bool)
scale = np.asarray((max(width - 1, 1), max(height - 1, 1)))
for uv, corner_normal, corner_tangent, corner_sign in zip(
triangle_uv,
triangle_normal,
triangle_tangent,
triangle_sign,
strict=True,
):
pixel = np.asarray(uv, dtype=np.float64) * scale
x_min = max(int(np.ceil(np.min(pixel[:, 0]))), 0)
x_max = min(int(np.floor(np.max(pixel[:, 0]))), width - 1)
y_min = max(int(np.ceil(np.min(pixel[:, 1]))), 0)
y_max = min(int(np.floor(np.max(pixel[:, 1]))), height - 1)
if x_min > x_max or y_min > y_max:
continue
a, b, c = pixel
denominator = (b[1] - c[1]) * (a[0] - c[0]) + (
c[0] - b[0]
) * (a[1] - c[1])
if abs(denominator) <= 1e-12:
continue
grid_x, grid_y = np.meshgrid(
np.arange(x_min, x_max + 1, dtype=np.float64),
np.arange(y_min, y_max + 1, dtype=np.float64),
)
first = (
(b[1] - c[1]) * (grid_x - c[0])
+ (c[0] - b[0]) * (grid_y - c[1])
) / denominator
second = (
(c[1] - a[1]) * (grid_x - c[0])
+ (a[0] - c[0]) * (grid_y - c[1])
) / denominator
third = 1.0 - first - second
inside = (first >= -1e-6) & (second >= -1e-6) & (third >= -1e-6)
if not inside.any():
continue
weights = np.stack((first[inside], second[inside], third[inside]), axis=-1)
normal = _normalized(weights @ corner_normal, (0.0, 0.0, 1.0))
tangent = weights @ corner_tangent
tangent -= normal * np.sum(tangent * normal, axis=-1, keepdims=True)
tangent = _normalized(tangent, (1.0, 0.0, 0.0))
sign = np.where(weights @ corner_sign >= 0.0, 1.0, -1.0)
bitangent = np.cross(normal, tangent) * sign[:, None]
rows = grid_y[inside].astype(np.int32)
columns = grid_x[inside].astype(np.int32)
source = object_normal[rows, columns]
output[rows, columns, 0] = np.sum(source * tangent, axis=-1)
output[rows, columns, 1] = np.sum(source * bitangent, axis=-1)
output[rows, columns, 2] = np.sum(source * normal, axis=-1)
coverage[rows, columns] = True
output[coverage] = _normalized(output[coverage], (0.0, 0.0, 1.0))
return (output, coverage) if return_coverage else output
def bake_tangent_normal_image(context, source_object, target_object, image):
"""Bake a MoGe object-space image against the evaluated target mesh TBN."""
import numpy as np
width, height = (int(value) for value in image.size)
rgba = image_pixels(image).reshape((height, width, 4))
source_normal = rgba[..., :3].astype(np.float64) * 2.0 - 1.0
source_to_target = (
target_object.matrix_world.inverted().to_3x3()
@ source_object.matrix_world.to_3x3()
)
normal_matrix = source_to_target.inverted().transposed()
transform = np.asarray(normal_matrix, dtype=np.float64)
target_normal = source_normal @ transform.T
target_normal = _normalized(target_normal, (0.0, 0.0, 1.0))
front = _normalized(
np.asarray((0.0, 0.0, 1.0), dtype=np.float64)[None, :] @ transform.T,
(0.0, 0.0, 1.0),
)[0]
context.view_layer.update()
depsgraph = context.evaluated_depsgraph_get()
evaluated = target_object.evaluated_get(depsgraph)
mesh = evaluated.to_mesh(preserve_all_data_layers=True, depsgraph=depsgraph)
try:
uv_layer = mesh.uv_layers.active
if uv_layer is None:
raise ValueError("The target mesh has no active UV map")
mesh.calc_loop_triangles()
mesh.calc_tangents(uvmap=uv_layer.name)
loop_count = len(mesh.loops)
uv = np.empty((loop_count, 2), dtype=np.float64)
uv_layer.data.foreach_get("uv", uv.ravel())
tangent = np.empty((loop_count, 3), dtype=np.float64)
sign = np.empty(loop_count, dtype=np.float64)
mesh.loops.foreach_get("tangent", tangent.ravel())
mesh.loops.foreach_get("bitangent_sign", sign)
normal = np.asarray(
[tuple(loop.normal) for loop in mesh.loops],
dtype=np.float64,
)
triangles = np.asarray(
[tuple(triangle.loops) for triangle in mesh.loop_triangles],
dtype=np.int32,
)
triangle_normal = np.asarray(
[tuple(triangle.normal) for triangle in mesh.loop_triangles],
dtype=np.float64,
)
front_facing = triangle_normal @ front > 1e-6
triangles = triangles[front_facing]
tangent_normal, coverage = rasterize_tangent_normal(
target_normal,
uv[triangles],
normal[triangles],
tangent[triangles],
sign[triangles],
return_coverage=True,
)
finally:
evaluated.to_mesh_clear()
rgba[coverage, :3] = tangent_normal[coverage] * 0.5 + 0.5
image.pixels.foreach_set(rgba.ravel())
image.update()
image.pack()
return image

View File

@@ -0,0 +1,16 @@
DA3_PATCH_SIZE = 14
DA3_MIN_PROCESS_RES = 252
DA3_MAX_PROCESS_RES = 1008
def scaled_process_resolution(image, scale_percent):
"""Return the scaled long edge clamped and aligned for DA3 inference."""
width, height = image.size
if width <= 0 or height <= 0:
raise ValueError("The source image has no readable dimensions")
if scale_percent < 1.0 or scale_percent > 100.0:
raise ValueError("Resolution Scale must be between 1% and 100%")
process_res = max(width, height) * scale_percent / 100.0
process_res = max(DA3_MIN_PROCESS_RES, process_res)
process_res = min(DA3_MAX_PROCESS_RES, process_res)
return int(process_res / DA3_PATCH_SIZE + 0.5) * DA3_PATCH_SIZE