Refactor image processing and cutout functionalities
- Updated the naming convention for upscaled images to include underscores and file extensions. - Renamed properties related to cutout pixel size to point spacing for clarity. - Adjusted alpha region calculations to use a half-alpha threshold for better accuracy. - Enhanced the handling of foreground mesh masks to consider half-alpha values. - Introduced new functions for unbounded selections and cutout images, ensuring proper handling of source and selection images. - Improved test coverage for cutout functionalities, including bounds mapping and alpha preservation. - Updated tests to reflect changes in naming conventions and functionality adjustments.
This commit is contained in:
@@ -267,11 +267,12 @@ def extract_balloon_object(
|
||||
depth_data=None,
|
||||
normal_path=None,
|
||||
material_image=None,
|
||||
local_bounds=None,
|
||||
bake_normal_to_tangent=False,
|
||||
):
|
||||
image = material_image or source_object.data
|
||||
image_size = tuple(image.size)
|
||||
local_bounds = plane_bounds(source_object)
|
||||
local_bounds = local_bounds or plane_bounds(source_object)
|
||||
world_metric, world_z_scale = world_plane_metric(matrix_world)
|
||||
point_spacing, inflation = balloon_geometry_parameters(
|
||||
thickness,
|
||||
|
||||
@@ -26,9 +26,9 @@ CUTOUT_DETAIL_MIN = 4
|
||||
CUTOUT_DETAIL_MAX = 1024
|
||||
|
||||
|
||||
def cutout_mesh_detail(image_size, pixel_size):
|
||||
def cutout_mesh_detail(image_size, point_spacing):
|
||||
longest_edge = max(int(value) for value in image_size)
|
||||
detail = round(longest_edge / max(int(pixel_size), 1))
|
||||
detail = round(longest_edge / max(int(point_spacing), 1))
|
||||
return max(CUTOUT_DETAIL_MIN, min(CUTOUT_DETAIL_MAX, detail))
|
||||
|
||||
|
||||
@@ -69,9 +69,9 @@ def _open_shape_pie(
|
||||
source_name = source_object.name
|
||||
selection_data = json.dumps(polygon, separators=(",", ":"))
|
||||
settings = context.scene.anyimage_settings
|
||||
pixel_size = settings.cutout_pixel_size
|
||||
point_spacing = settings.cutout_point_spacing
|
||||
normal_analysis = bool(getattr(settings, "cutout_normal_analysis", False))
|
||||
mesh_detail = cutout_mesh_detail(source_object.data.size, pixel_size)
|
||||
mesh_detail = cutout_mesh_detail(source_object.data.size, point_spacing)
|
||||
|
||||
def draw(menu, _context):
|
||||
pie = menu.layout.menu_pie()
|
||||
@@ -147,6 +147,7 @@ class SelectCutoutRegion(ImageMaskGesture, bpy.types.Operator):
|
||||
gesture: gesture_property()
|
||||
full_image: bpy.props.BoolProperty(default=False, options={"HIDDEN", "SKIP_SAVE"})
|
||||
|
||||
preview_fill_rule = 'NONZERO'
|
||||
_pie_event = None
|
||||
|
||||
def _set_status(self, context):
|
||||
@@ -250,6 +251,6 @@ class CutoutTool(bpy.types.WorkSpaceTool):
|
||||
|
||||
def draw_settings(context, layout, _tool):
|
||||
settings = context.scene.anyimage_settings
|
||||
layout.prop(settings, "cutout_pixel_size")
|
||||
layout.prop(settings, "cutout_point_spacing")
|
||||
layout.prop(settings, "cutout_normal_analysis")
|
||||
draw_image_mask_settings(context, layout)
|
||||
|
||||
@@ -141,11 +141,11 @@ def image_base_name(image):
|
||||
|
||||
|
||||
def depth_data_name(source_object):
|
||||
return f"{image_base_name(source_object.data)}.depth"
|
||||
return f"{image_base_name(source_object.data)}_depth.exr"
|
||||
|
||||
|
||||
def normal_data_name(source_object):
|
||||
return f"{image_base_name(source_object.data)}.normal"
|
||||
return f"{image_base_name(source_object.data)}_normal.png"
|
||||
|
||||
|
||||
def copy_depth_image(source_object):
|
||||
@@ -155,8 +155,8 @@ def copy_depth_image(source_object):
|
||||
return depth_image
|
||||
|
||||
|
||||
def color_data_name(source_object, suffix=".color"):
|
||||
return f"{image_base_name(source_object.data)}{suffix}"
|
||||
def color_data_name(source_object):
|
||||
return f"{image_base_name(source_object.data)}_color.png"
|
||||
|
||||
|
||||
def material_data_name(source_object):
|
||||
|
||||
@@ -286,11 +286,16 @@ def dashed_line_segments(vertices, dash_length=4.0, gap_length=4.0):
|
||||
return tuple(segments)
|
||||
|
||||
|
||||
def draw_dashed_line(vertices, color=(1.0, 1.0, 1.0, 0.9)):
|
||||
def draw_dashed_line(
|
||||
vertices,
|
||||
color=(1.0, 1.0, 1.0, 0.9),
|
||||
segments=None,
|
||||
):
|
||||
import gpu
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
|
||||
segments = dashed_line_segments(vertices)
|
||||
if segments is None:
|
||||
segments = dashed_line_segments(vertices)
|
||||
if not segments:
|
||||
return
|
||||
shader = gpu.shader.from_builtin("UNIFORM_COLOR")
|
||||
@@ -303,19 +308,244 @@ def draw_dashed_line(vertices, color=(1.0, 1.0, 1.0, 0.9)):
|
||||
gpu.state.blend_set("NONE")
|
||||
|
||||
|
||||
def triangle_fan_vertices(vertices):
|
||||
def _signed_area(vertices):
|
||||
return sum(
|
||||
start[0] * end[1] - end[0] * start[1]
|
||||
for start, end in zip(vertices, (*vertices[1:], vertices[0]))
|
||||
) * 0.5
|
||||
|
||||
|
||||
def _triangle_cross(start, middle, end):
|
||||
return (middle[0] - start[0]) * (end[1] - start[1]) - (
|
||||
middle[1] - start[1]
|
||||
) * (end[0] - start[0])
|
||||
|
||||
|
||||
def _point_in_triangle(point, start, middle, end, orientation, epsilon):
|
||||
return all(
|
||||
orientation * cross >= -epsilon
|
||||
for cross in (
|
||||
_triangle_cross(start, middle, point),
|
||||
_triangle_cross(middle, end, point),
|
||||
_triangle_cross(end, start, point),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _triangulated_polygon_vertices_python(vertices):
|
||||
vertices = tuple(tuple(float(value) for value in vertex) for vertex in vertices)
|
||||
if len(vertices) < 3:
|
||||
return ()
|
||||
area = _signed_area(vertices)
|
||||
if abs(area) <= 1e-8:
|
||||
return ()
|
||||
orientation = 1.0 if area > 0.0 else -1.0
|
||||
remaining = list(range(len(vertices)))
|
||||
triangles = []
|
||||
for index in range(1, len(vertices) - 1):
|
||||
triangles.extend((vertices[0], vertices[index], vertices[index + 1]))
|
||||
epsilon = 1e-8
|
||||
while len(remaining) > 3:
|
||||
for position, current in enumerate(remaining):
|
||||
previous = remaining[position - 1]
|
||||
following = remaining[(position + 1) % len(remaining)]
|
||||
start = vertices[previous]
|
||||
middle = vertices[current]
|
||||
end = vertices[following]
|
||||
if orientation * _triangle_cross(start, middle, end) <= epsilon:
|
||||
continue
|
||||
if any(
|
||||
_point_in_triangle(
|
||||
vertices[index],
|
||||
start,
|
||||
middle,
|
||||
end,
|
||||
orientation,
|
||||
epsilon,
|
||||
)
|
||||
for index in remaining
|
||||
if index not in {previous, current, following}
|
||||
):
|
||||
continue
|
||||
triangles.extend((start, middle, end))
|
||||
del remaining[position]
|
||||
break
|
||||
else:
|
||||
return ()
|
||||
triangles.extend(vertices[index] for index in remaining)
|
||||
return tuple(triangles)
|
||||
|
||||
|
||||
def draw_polygon_fill(vertices, color=(1.0, 1.0, 1.0, 0.16)):
|
||||
def triangulated_polygon_vertices(vertices):
|
||||
vertices = tuple(tuple(float(value) for value in vertex) for vertex in vertices)
|
||||
try:
|
||||
import mathutils
|
||||
|
||||
points = [
|
||||
mathutils.Vector((vertex[0], vertex[1], 0.0)) for vertex in vertices
|
||||
]
|
||||
triangles = mathutils.geometry.tessellate_polygon([points])
|
||||
return tuple(
|
||||
(float(point[0]), float(point[1]))
|
||||
for triangle in triangles
|
||||
for point in triangle
|
||||
)
|
||||
except (AttributeError, ImportError, RuntimeError, TypeError, ValueError):
|
||||
return _triangulated_polygon_vertices_python(vertices)
|
||||
|
||||
|
||||
def scanline_fill_triangles(vertices, fill_rule='EVEN_ODD', step=2.0):
|
||||
vertices = tuple(tuple(float(value) for value in vertex) for vertex in vertices)
|
||||
if len(vertices) < 3:
|
||||
return ()
|
||||
if fill_rule not in {'EVEN_ODD', 'NONZERO'}:
|
||||
raise ValueError(f'Unknown polygon fill rule: {fill_rule}')
|
||||
minimum_y = min(vertex[1] for vertex in vertices)
|
||||
maximum_y = max(vertex[1] for vertex in vertices)
|
||||
band_bottom = minimum_y
|
||||
triangles = []
|
||||
edges = tuple(zip(vertices, (*vertices[1:], vertices[0])))
|
||||
while band_bottom < maximum_y:
|
||||
band_top = min(band_bottom + step, maximum_y)
|
||||
sample_y = (band_bottom + band_top) * 0.5
|
||||
events = []
|
||||
for start, end in edges:
|
||||
if (start[1] <= sample_y < end[1]) or (
|
||||
end[1] <= sample_y < start[1]
|
||||
):
|
||||
factor = (sample_y - start[1]) / (end[1] - start[1])
|
||||
events.append(
|
||||
(
|
||||
start[0] + (end[0] - start[0]) * factor,
|
||||
1 if end[1] > start[1] else -1,
|
||||
)
|
||||
)
|
||||
events.sort(key=lambda event: event[0])
|
||||
intervals = []
|
||||
winding = 0
|
||||
interval_start = None
|
||||
index = 0
|
||||
while index < len(events):
|
||||
x = events[index][0]
|
||||
delta = 0
|
||||
count = 0
|
||||
while index < len(events) and abs(events[index][0] - x) <= 1e-8:
|
||||
delta += events[index][1]
|
||||
count += 1
|
||||
index += 1
|
||||
was_inside = winding != 0
|
||||
if fill_rule == 'EVEN_ODD':
|
||||
if count % 2:
|
||||
winding = 0 if was_inside else 1
|
||||
else:
|
||||
winding += delta
|
||||
is_inside = winding != 0
|
||||
if not was_inside and is_inside:
|
||||
interval_start = x
|
||||
elif was_inside and not is_inside and interval_start is not None:
|
||||
intervals.append((interval_start, x))
|
||||
interval_start = None
|
||||
for left, right in intervals:
|
||||
if right - left <= 1e-8:
|
||||
continue
|
||||
triangles.extend(
|
||||
(
|
||||
(left, band_bottom),
|
||||
(right, band_bottom),
|
||||
(right, band_top),
|
||||
(left, band_bottom),
|
||||
(right, band_top),
|
||||
(left, band_top),
|
||||
)
|
||||
)
|
||||
band_bottom = band_top
|
||||
return tuple(triangles)
|
||||
|
||||
|
||||
def _point_on_segment(point, start, end, epsilon=1e-8):
|
||||
return (
|
||||
min(start[0], end[0]) - epsilon
|
||||
<= point[0]
|
||||
<= max(start[0], end[0]) + epsilon
|
||||
and min(start[1], end[1]) - epsilon
|
||||
<= point[1]
|
||||
<= max(start[1], end[1]) + epsilon
|
||||
)
|
||||
|
||||
|
||||
def _segments_intersect(first_start, first_end, second_start, second_end):
|
||||
crosses = (
|
||||
_triangle_cross(first_start, first_end, second_start),
|
||||
_triangle_cross(first_start, first_end, second_end),
|
||||
_triangle_cross(second_start, second_end, first_start),
|
||||
_triangle_cross(second_start, second_end, first_end),
|
||||
)
|
||||
if (crosses[0] > 1e-8) != (crosses[1] > 1e-8) and (
|
||||
crosses[2] > 1e-8
|
||||
) != (crosses[3] > 1e-8):
|
||||
return True
|
||||
return any(
|
||||
abs(cross) <= 1e-8 and _point_on_segment(point, start, end)
|
||||
for cross, point, start, end in (
|
||||
(crosses[0], second_start, first_start, first_end),
|
||||
(crosses[1], second_end, first_start, first_end),
|
||||
(crosses[2], first_start, second_start, second_end),
|
||||
(crosses[3], first_end, second_start, second_end),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def polygon_self_intersects(vertices, cell_size=32.0):
|
||||
vertices = tuple(tuple(float(value) for value in vertex) for vertex in vertices)
|
||||
if len(vertices) < 4:
|
||||
return False
|
||||
edges = tuple(zip(vertices, (*vertices[1:], vertices[0])))
|
||||
cells = {}
|
||||
last_edge = len(edges) - 1
|
||||
for edge_index, (start, end) in enumerate(edges):
|
||||
left = math.floor(min(start[0], end[0]) / cell_size)
|
||||
right = math.floor(max(start[0], end[0]) / cell_size)
|
||||
bottom = math.floor(min(start[1], end[1]) / cell_size)
|
||||
top = math.floor(max(start[1], end[1]) / cell_size)
|
||||
edge_cells = tuple(
|
||||
(x, y)
|
||||
for x in range(left, right + 1)
|
||||
for y in range(bottom, top + 1)
|
||||
)
|
||||
candidates = {
|
||||
candidate
|
||||
for cell in edge_cells
|
||||
for candidate in cells.get(cell, ())
|
||||
}
|
||||
for candidate in candidates:
|
||||
if abs(edge_index - candidate) <= 1 or {
|
||||
edge_index,
|
||||
candidate,
|
||||
} == {0, last_edge}:
|
||||
continue
|
||||
if _segments_intersect(start, end, *edges[candidate]):
|
||||
return True
|
||||
for cell in edge_cells:
|
||||
cells.setdefault(cell, []).append(edge_index)
|
||||
return False
|
||||
|
||||
|
||||
def preview_fill_triangles(vertices, fill_rule='EVEN_ODD'):
|
||||
if not polygon_self_intersects(vertices):
|
||||
triangles = triangulated_polygon_vertices(vertices)
|
||||
if triangles:
|
||||
return triangles
|
||||
return scanline_fill_triangles(vertices, fill_rule)
|
||||
|
||||
|
||||
def draw_polygon_fill(
|
||||
vertices,
|
||||
triangles=None,
|
||||
color=(1.0, 1.0, 1.0, 0.16),
|
||||
):
|
||||
import gpu
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
|
||||
triangles = triangle_fan_vertices(vertices)
|
||||
if triangles is None:
|
||||
triangles = triangulated_polygon_vertices(vertices)
|
||||
if not triangles:
|
||||
return
|
||||
shader = gpu.shader.from_builtin("UNIFORM_COLOR")
|
||||
|
||||
@@ -13,7 +13,6 @@ from ..balloon.operator import (
|
||||
hidden_string_property,
|
||||
screen_path_to_image_pixels,
|
||||
serialize_matrix,
|
||||
simplify_screen_path,
|
||||
)
|
||||
from ..common import HIDDEN, cleanup_temporary_input, push_undo
|
||||
from ..image import (
|
||||
@@ -28,12 +27,14 @@ from .common import (
|
||||
active_view3d_tool_id,
|
||||
apply_image_mask_settings,
|
||||
crop_pixels_to_alpha,
|
||||
dashed_line_segments,
|
||||
draw_dashed_line,
|
||||
draw_image_mask_operator_settings,
|
||||
draw_polygon_fill,
|
||||
image_edit_poll,
|
||||
image_mask_refine_request,
|
||||
polygon_bounds,
|
||||
preview_fill_triangles,
|
||||
replace_empty_image_pixels,
|
||||
report_image_edit_exception,
|
||||
resolve_image_edit_click,
|
||||
@@ -47,6 +48,45 @@ GESTURE_TOOL_IDS = {
|
||||
}
|
||||
LINE_SNAP_RADIANS = math.radians(5.0)
|
||||
POLYLINE_CLOSE_RADIUS = 8.0
|
||||
LASSO_POINT_DISTANCE = 2.0
|
||||
LASSO_SIMPLIFY_TOLERANCE = 0.75
|
||||
|
||||
|
||||
def append_lasso_point(path, point):
|
||||
point = (float(point[0]), float(point[1]))
|
||||
if not path:
|
||||
path.append(point)
|
||||
return True
|
||||
if math.dist(point, path[-1]) < LASSO_POINT_DISTANCE:
|
||||
return False
|
||||
if len(path) >= 2:
|
||||
anchor = path[-2]
|
||||
previous = path[-1]
|
||||
segment_x = point[0] - anchor[0]
|
||||
segment_y = point[1] - anchor[1]
|
||||
segment_length_squared = segment_x * segment_x + segment_y * segment_y
|
||||
if segment_length_squared > 0.0:
|
||||
factor = (
|
||||
(previous[0] - anchor[0]) * segment_x
|
||||
+ (previous[1] - anchor[1]) * segment_y
|
||||
) / segment_length_squared
|
||||
projected = (
|
||||
anchor[0] + segment_x * factor,
|
||||
anchor[1] + segment_y * factor,
|
||||
)
|
||||
forward = (
|
||||
(previous[0] - anchor[0]) * (point[0] - previous[0])
|
||||
+ (previous[1] - anchor[1]) * (point[1] - previous[1])
|
||||
)
|
||||
if (
|
||||
0.0 <= factor <= 1.0
|
||||
and forward >= 0.0
|
||||
and math.dist(previous, projected) <= LASSO_SIMPLIFY_TOLERANCE
|
||||
):
|
||||
path[-1] = point
|
||||
return True
|
||||
path.append(point)
|
||||
return True
|
||||
|
||||
|
||||
def points_inside_polygon(points, polygon):
|
||||
@@ -265,9 +305,14 @@ def image_line_half_plane_polygon(line, image_size):
|
||||
|
||||
class ImageMaskGesture:
|
||||
active_tool_id = None
|
||||
preview_fill_rule = 'EVEN_ODD'
|
||||
_path = None
|
||||
_cursor = None
|
||||
_handle = None
|
||||
_fill_polygon = None
|
||||
_fill_triangles = ()
|
||||
_outline_polygon = None
|
||||
_outline_segments = ()
|
||||
_area_pointer = 0
|
||||
_region_pointer = 0
|
||||
_region_size = (0, 0)
|
||||
@@ -283,7 +328,7 @@ class ImageMaskGesture:
|
||||
if self.gesture == "BOX":
|
||||
return box_screen_path(self._path[0], self._cursor)
|
||||
if self.gesture == "LASSO":
|
||||
return tuple(simplify_screen_path(self._path))
|
||||
return tuple(self._path)
|
||||
if self.gesture == "LINE":
|
||||
return line_half_plane_polygon(
|
||||
self._path[0], self._cursor, self._region_size
|
||||
@@ -299,16 +344,29 @@ class ImageMaskGesture:
|
||||
):
|
||||
return
|
||||
polygon = self._screen_polygon()
|
||||
if polygon != self._outline_polygon:
|
||||
self._outline_polygon = polygon
|
||||
if self.gesture == 'LINE':
|
||||
outline = (self._path[0], self._cursor)
|
||||
else:
|
||||
outline = (*polygon, polygon[0]) if len(polygon) > 1 else ()
|
||||
self._outline_segments = dashed_line_segments(outline)
|
||||
if len(polygon) >= 3:
|
||||
if polygon != self._fill_polygon:
|
||||
self._fill_polygon = polygon
|
||||
self._fill_triangles = preview_fill_triangles(
|
||||
polygon, self.preview_fill_rule
|
||||
)
|
||||
draw_polygon_fill(
|
||||
polygon,
|
||||
self._fill_triangles,
|
||||
(1.0, 0.25, 0.2, 0.16) if self.invert else (1.0, 1.0, 1.0, 0.16),
|
||||
)
|
||||
if self.gesture == "LINE":
|
||||
draw_dashed_line((self._path[0], self._cursor))
|
||||
draw_dashed_line((), segments=self._outline_segments)
|
||||
return
|
||||
if len(polygon) > 1:
|
||||
draw_dashed_line((*polygon, polygon[0]))
|
||||
draw_dashed_line((), segments=self._outline_segments)
|
||||
if (
|
||||
self.gesture == "POLYLINE"
|
||||
and len(self._path) >= 3
|
||||
@@ -382,6 +440,10 @@ class ImageMaskGesture:
|
||||
point = (float(event.mouse_region_x), float(event.mouse_region_y))
|
||||
self._path = [point]
|
||||
self._cursor = point
|
||||
self._fill_polygon = None
|
||||
self._fill_triangles = ()
|
||||
self._outline_polygon = None
|
||||
self._outline_segments = ()
|
||||
self._area_pointer = context.area.as_pointer()
|
||||
self._region_pointer = context.region.as_pointer()
|
||||
self._region_size = (context.region.width, context.region.height)
|
||||
@@ -447,8 +509,7 @@ class ImageMaskGesture:
|
||||
if event.type == "MOUSEMOVE":
|
||||
point = (float(event.mouse_region_x), float(event.mouse_region_y))
|
||||
if self.gesture == "LASSO":
|
||||
if math.dist(point, self._path[-1]) >= 2.0:
|
||||
self._path.append(point)
|
||||
append_lasso_point(self._path, point)
|
||||
self._cursor = point
|
||||
elif self.gesture == "LINE":
|
||||
self._cursor = snapped_line_endpoint(self._path[0], point, event.ctrl)
|
||||
@@ -459,8 +520,7 @@ class ImageMaskGesture:
|
||||
if event.type == "LEFTMOUSE" and event.value == "RELEASE":
|
||||
point = (float(event.mouse_region_x), float(event.mouse_region_y))
|
||||
if self.gesture == "LASSO":
|
||||
if math.dist(point, self._path[-1]) >= 2.0:
|
||||
self._path.append(point)
|
||||
append_lasso_point(self._path, point)
|
||||
self._cursor = point
|
||||
elif self.gesture == "LINE":
|
||||
self._cursor = snapped_line_endpoint(self._path[0], point, event.ctrl)
|
||||
|
||||
@@ -27,13 +27,16 @@ from .common import (
|
||||
)
|
||||
from .image import (
|
||||
analysis_source_image,
|
||||
color_data_name,
|
||||
create_color_image,
|
||||
image_base_name,
|
||||
image_user_settings,
|
||||
is_image_empty,
|
||||
material_data_name,
|
||||
normal_data_name,
|
||||
plane_bounds,
|
||||
prepare_image_analysis_inputs,
|
||||
prepare_image_input,
|
||||
remove_temporary_input,
|
||||
scaled_process_resolution,
|
||||
video_total_frames,
|
||||
@@ -114,11 +117,11 @@ def _depth_field_image(source_object, anchored):
|
||||
return image
|
||||
|
||||
|
||||
def _cutout_domain(regions_pixels, image_size, bounds, detail, world_metric):
|
||||
def _cutout_domain(regions_pixels, image_size, bounds, spacing, world_metric):
|
||||
import numpy as np
|
||||
|
||||
x_min, x_max, y_min, y_max = bounds
|
||||
spacing = max(x_max - x_min, y_max - y_min) / max(int(detail), 1)
|
||||
|
||||
regions, spacing = prepare_planar_regions(
|
||||
regions_pixels,
|
||||
image_size,
|
||||
@@ -168,12 +171,12 @@ def _load_field(path):
|
||||
return points, intrinsics, valid, image_size, reference_depth
|
||||
|
||||
|
||||
def _anchored_point_field(source_object, field):
|
||||
def _anchored_point_field(source_object, field, bounds=None):
|
||||
import numpy as np
|
||||
|
||||
points, intrinsics, valid, image_size, reference_depth = field
|
||||
width, height = image_size
|
||||
x_min, x_max, y_min, y_max = plane_bounds(source_object)
|
||||
x_min, x_max, y_min, y_max = bounds or plane_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])
|
||||
@@ -950,17 +953,6 @@ def _mesh_node_group(shape):
|
||||
return group
|
||||
|
||||
|
||||
def _unique_color_name(source_object, current=None):
|
||||
base = image_base_name(source_object.data)
|
||||
index = 0
|
||||
while True:
|
||||
number = "" if index == 0 else f".{index:03d}"
|
||||
candidate = f"{base}{number}.color"
|
||||
existing = bpy.data.images.get(candidate)
|
||||
if existing is None or existing is current:
|
||||
return candidate
|
||||
index += 1
|
||||
|
||||
|
||||
def _copy_color_image(source_object):
|
||||
source = source_object.data
|
||||
@@ -976,7 +968,7 @@ def _copy_color_image(source_object):
|
||||
source.pixels.foreach_get(pixels)
|
||||
image.pixels.foreach_set(pixels)
|
||||
image.update()
|
||||
image.name = _unique_color_name(source_object, image)
|
||||
image.name = color_data_name(source_object)
|
||||
return image
|
||||
|
||||
|
||||
@@ -989,9 +981,81 @@ def _image_rgba(image):
|
||||
return np.flipud(pixels.reshape((height, width, 4)))
|
||||
|
||||
|
||||
def _cutout_regions(alpha, mesh_detail):
|
||||
def _mask_crop_bounds(alpha):
|
||||
import numpy as np
|
||||
|
||||
values = np.asarray(alpha)
|
||||
visible_y, visible_x = np.nonzero(values > 1.0 / 255.0)
|
||||
if not len(visible_x):
|
||||
raise ValueError("The Cutout selection contains no visible pixels")
|
||||
return (
|
||||
int(visible_x.min()),
|
||||
int(visible_y.min()),
|
||||
int(visible_x.max()) + 1,
|
||||
int(visible_y.max()) + 1,
|
||||
)
|
||||
|
||||
|
||||
def _crop_plane_bounds(source_object, crop_bounds, source_size):
|
||||
source_width, source_height = (float(value) for value in source_size)
|
||||
left, top, right, bottom = (float(value) for value in crop_bounds)
|
||||
x_min, x_max, y_min, y_max = plane_bounds(source_object)
|
||||
width = x_max - x_min
|
||||
height = y_max - y_min
|
||||
return (
|
||||
x_min + left * width / source_width,
|
||||
x_min + right * width / source_width,
|
||||
y_max - bottom * height / source_height,
|
||||
y_max - top * height / source_height,
|
||||
)
|
||||
|
||||
|
||||
def _cropped_color_image(source_object, crop_bounds):
|
||||
import numpy as np
|
||||
|
||||
source = source_object.data
|
||||
rgba = _image_rgba(source)
|
||||
left, top, right, bottom = crop_bounds
|
||||
width = right - left
|
||||
height = bottom - top
|
||||
if width <= 0 or height <= 0:
|
||||
raise ValueError("The Cutout crop dimensions must be positive")
|
||||
cropped = np.zeros((height, width, 4), dtype=np.float32)
|
||||
source_height, source_width = rgba.shape[:2]
|
||||
source_left = max(0, left)
|
||||
source_top = max(0, top)
|
||||
source_right = min(source_width, right)
|
||||
source_bottom = min(source_height, bottom)
|
||||
if source_right > source_left and source_bottom > source_top:
|
||||
cropped[
|
||||
source_top - top : source_bottom - top,
|
||||
source_left - left : source_right - left,
|
||||
] = rgba[source_top:source_bottom, source_left:source_right]
|
||||
image = bpy.data.images.new(
|
||||
color_data_name(source_object),
|
||||
width=width,
|
||||
height=height,
|
||||
alpha=True,
|
||||
)
|
||||
try:
|
||||
image.colorspace_settings.name = source.colorspace_settings.name
|
||||
image.alpha_mode = source.alpha_mode
|
||||
image.pixels.foreach_set(np.flipud(cropped).ravel())
|
||||
image.pack()
|
||||
image.update()
|
||||
except Exception:
|
||||
bpy.data.images.remove(image, do_unlink=True)
|
||||
raise
|
||||
return image
|
||||
|
||||
|
||||
def _cutout_regions(alpha, mesh_detail, threshold=0.5):
|
||||
contour_resolution = max(512, min(int(mesh_detail) * 8, 1024))
|
||||
return alpha_regions(alpha, max_resolution=contour_resolution)
|
||||
return alpha_regions(
|
||||
alpha,
|
||||
max_resolution=contour_resolution,
|
||||
threshold=threshold,
|
||||
)
|
||||
|
||||
|
||||
def _antialias_selection(selection):
|
||||
@@ -1089,7 +1153,7 @@ def _local_cutout_rgba(source_object, polygon, invert=False):
|
||||
|
||||
|
||||
def _rename_color_image(image, source_object):
|
||||
image.name = _unique_color_name(source_object, image)
|
||||
image.name = color_data_name(source_object)
|
||||
return image
|
||||
|
||||
|
||||
@@ -1120,18 +1184,29 @@ def _build_mesh_object(
|
||||
field=None,
|
||||
regions=None,
|
||||
image_size=None,
|
||||
bounds=None,
|
||||
keep_original=False,
|
||||
):
|
||||
import numpy as np
|
||||
|
||||
bounds = plane_bounds(source_object)
|
||||
bounds = bounds or plane_bounds(source_object)
|
||||
world_metric, _world_z_scale = _world_plane_metric(source_object.matrix_world)
|
||||
domain = (
|
||||
_plane_domain(bounds, detail, world_metric)
|
||||
if shape in {PLANE, DEPTH_PLANE}
|
||||
else _cutout_domain(regions, image_size, bounds, detail, world_metric)
|
||||
else _cutout_domain(
|
||||
regions,
|
||||
image_size,
|
||||
bounds,
|
||||
_cutout_point_spacing(source_object, detail),
|
||||
world_metric,
|
||||
)
|
||||
)
|
||||
anchored = (
|
||||
_anchored_point_field(source_object, field, bounds)
|
||||
if field
|
||||
else None
|
||||
)
|
||||
anchored = _anchored_point_field(source_object, field) if field else None
|
||||
depth_image = None
|
||||
face_discontinuity = None
|
||||
if shape in {PLANE, DEPTH_PLANE}:
|
||||
@@ -1223,13 +1298,13 @@ def _build_mesh_object(
|
||||
|
||||
def _load_normal_image(path, source_object):
|
||||
image = bpy.data.images.load(str(path), check_existing=False)
|
||||
image.name = f"{image_base_name(source_object.data)}.normal"
|
||||
image.name = normal_data_name(source_object)
|
||||
image.colorspace_settings.name = "Non-Color"
|
||||
image.pack()
|
||||
return image
|
||||
|
||||
|
||||
def _balloon_resolution(source_object, detail):
|
||||
def _cutout_point_spacing(source_object, detail):
|
||||
import math
|
||||
|
||||
x_min, x_max, y_min, y_max = plane_bounds(source_object)
|
||||
@@ -1252,19 +1327,40 @@ def _build_local_cutout(
|
||||
polygon = json.loads(selection_data) if selection_data else None
|
||||
if not isinstance(polygon, list) or len(polygon) < 3:
|
||||
raise ValueError("The Cutout selection is invalid")
|
||||
rgba, selection = _local_cutout_rgba(source_object, polygon, invert)
|
||||
regions, image_size = _geometry_cutout_regions(
|
||||
rgba[:, :, 3],
|
||||
polygon,
|
||||
_rgba, selection = _local_cutout_rgba(source_object, polygon, invert)
|
||||
source_size = tuple(source_object.data.size)
|
||||
if invert:
|
||||
crop_bounds = _mask_crop_bounds(selection)
|
||||
left, top, right, bottom = crop_bounds
|
||||
cropped_selection = selection[top:bottom, left:right]
|
||||
else:
|
||||
crop_bounds = tuple(int(value) for value in polygon_bounds(polygon))
|
||||
left, top, right, bottom = crop_bounds
|
||||
relative_polygon = [
|
||||
(x - left, y - top)
|
||||
for x, y in polygon
|
||||
]
|
||||
cropped_selection = _antialias_selection(
|
||||
polygon_mask(
|
||||
(right - left, bottom - top),
|
||||
relative_polygon,
|
||||
"NONZERO",
|
||||
)
|
||||
)
|
||||
regions, image_size = _cutout_regions(
|
||||
cropped_selection,
|
||||
operator.mesh_detail,
|
||||
invert,
|
||||
refine_selection=False,
|
||||
selection=selection,
|
||||
threshold=0.5,
|
||||
)
|
||||
local_bounds = _crop_plane_bounds(
|
||||
source_object,
|
||||
crop_bounds,
|
||||
source_size,
|
||||
)
|
||||
color_image = None
|
||||
material = None
|
||||
try:
|
||||
color_image = _copy_color_image(source_object)
|
||||
color_image = _cropped_color_image(source_object, crop_bounds)
|
||||
if shape == BALLOON:
|
||||
result_object = extract_balloon_object(
|
||||
context,
|
||||
@@ -1273,12 +1369,15 @@ def _build_local_cutout(
|
||||
source_object.matrix_world.copy(),
|
||||
"POISSON",
|
||||
thickness=1.0,
|
||||
resolution=_balloon_resolution(source_object, operator.mesh_detail),
|
||||
resolution=_cutout_point_spacing(
|
||||
source_object, operator.mesh_detail
|
||||
),
|
||||
select_result=True,
|
||||
reuse_material=False,
|
||||
depth_data=None,
|
||||
normal_path=None,
|
||||
material_image=color_image,
|
||||
local_bounds=local_bounds,
|
||||
)
|
||||
result_object["anyimage_mesh_shape"] = shape
|
||||
else:
|
||||
@@ -1291,6 +1390,7 @@ def _build_local_cutout(
|
||||
material,
|
||||
regions=regions,
|
||||
image_size=image_size,
|
||||
bounds=local_bounds,
|
||||
keep_original=True,
|
||||
)
|
||||
except Exception:
|
||||
@@ -1305,27 +1405,38 @@ def _build_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")
|
||||
regions = image_size = None
|
||||
regions = image_size = local_bounds = None
|
||||
if operator.shape in CUTOUT_SHAPES:
|
||||
geometry_image = create_color_image(
|
||||
source_size = tuple(source_object.data.size)
|
||||
crop_bounds = tuple(
|
||||
int(value)
|
||||
for value in json.loads(
|
||||
result.file("bounds").read_text(encoding="utf-8")
|
||||
)
|
||||
)
|
||||
local_bounds = _crop_plane_bounds(
|
||||
source_object,
|
||||
crop_bounds,
|
||||
source_size,
|
||||
)
|
||||
selection_image = bpy.data.images.load(
|
||||
str(result.file("selection")),
|
||||
check_existing=False,
|
||||
)
|
||||
try:
|
||||
selection_alpha = _image_rgba(selection_image)[:, :, 3]
|
||||
regions, image_size = _cutout_regions(
|
||||
selection_alpha,
|
||||
operator.mesh_detail,
|
||||
threshold=0.5,
|
||||
)
|
||||
finally:
|
||||
bpy.data.images.remove(selection_image)
|
||||
color_image = create_color_image(
|
||||
source_object,
|
||||
result.directory,
|
||||
assign_to_source=False,
|
||||
)
|
||||
try:
|
||||
rgba = _image_rgba(geometry_image)
|
||||
finally:
|
||||
if geometry_image is not source_object.data:
|
||||
bpy.data.images.remove(geometry_image)
|
||||
polygon = json.loads(operator.selection_data)
|
||||
regions, image_size = _geometry_cutout_regions(
|
||||
rgba[:, :, 3],
|
||||
polygon,
|
||||
operator.mesh_detail,
|
||||
operator.invert,
|
||||
refine_selection=operator.refine_selection,
|
||||
)
|
||||
color_image = _copy_color_image(source_object)
|
||||
else:
|
||||
color_image = _rename_color_image(
|
||||
create_color_image(
|
||||
@@ -1350,12 +1461,13 @@ def _build_response(context, result, operator):
|
||||
source_object.matrix_world.copy(),
|
||||
"POISSON",
|
||||
thickness=1.0,
|
||||
resolution=_balloon_resolution(source_object, operator.mesh_detail),
|
||||
resolution=_cutout_point_spacing(source_object, operator.mesh_detail),
|
||||
select_result=True,
|
||||
reuse_material=False,
|
||||
depth_data=depth_data,
|
||||
normal_path=normal_path if normal_path.is_file() else None,
|
||||
material_image=color_image,
|
||||
local_bounds=local_bounds,
|
||||
bake_normal_to_tangent=normal_path.is_file(),
|
||||
)
|
||||
balloon["anyimage_mesh_shape"] = operator.shape
|
||||
@@ -1390,6 +1502,7 @@ def _build_response(context, result, operator):
|
||||
field=field,
|
||||
regions=regions,
|
||||
image_size=image_size,
|
||||
bounds=local_bounds,
|
||||
keep_original=operator.shape in CUTOUT_SHAPES,
|
||||
)
|
||||
if normal_image is not None and operator.shape == CUTOUT:
|
||||
@@ -1401,7 +1514,6 @@ def _build_response(context, result, operator):
|
||||
)
|
||||
push_undo(f"Convert to {operator.shape.replace('_', ' ').title()}")
|
||||
|
||||
|
||||
def _depth_settings(context, shape):
|
||||
preferences = addon_preferences(context)
|
||||
model = getattr(preferences, "mesh_depth_model", DEFAULT_MOGE2_MODEL_KEY)
|
||||
@@ -1528,16 +1640,25 @@ def _start_job(
|
||||
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
|
||||
)
|
||||
if shape in CUTOUT_SHAPES:
|
||||
source_path, delete_input = prepare_image_input(
|
||||
source_object.data,
|
||||
context.scene,
|
||||
)
|
||||
analysis_path = source_path
|
||||
delete_analysis_input = False
|
||||
analysis_image = source_object.data
|
||||
else:
|
||||
(
|
||||
source_path,
|
||||
delete_input,
|
||||
analysis_path,
|
||||
delete_analysis_input,
|
||||
) = prepare_image_analysis_inputs(
|
||||
source_object.data, context.scene
|
||||
)
|
||||
analysis_image = analysis_source_image(source_object.data)
|
||||
model, process_factor, resolution_level = _depth_settings(context, shape)
|
||||
analysis_image = analysis_source_image(source_object.data)
|
||||
process_res = (
|
||||
scaled_process_resolution(analysis_image, process_factor)
|
||||
if shape in DEPTH_SHAPES and not model.startswith("MOGE2_")
|
||||
|
||||
@@ -147,7 +147,7 @@ class UpscaleImage(JobOperatorBase, bpy.types.Operator):
|
||||
replaced_image.use_fake_user = False
|
||||
if int(replaced_image.users) == 0:
|
||||
bpy.data.images.remove(replaced_image, do_unlink=True)
|
||||
result_image.name = f"{image_base_name(source_image)}.upscale"
|
||||
result_image.name = f"{image_base_name(source_image)}_upscale.png"
|
||||
result_image.anyimage_upscale_source = source_image
|
||||
model_label = upscale_model_label(self.model)
|
||||
if getattr(self, "_rebuild_from_original", False):
|
||||
|
||||
@@ -332,11 +332,11 @@ def cutout_normal_analysis_property():
|
||||
default=False,
|
||||
)
|
||||
|
||||
def cutout_pixel_size_property():
|
||||
def cutout_point_spacing_property():
|
||||
return bpy.props.IntProperty(
|
||||
name="Pixel Size",
|
||||
name="Point Spacing",
|
||||
description=(
|
||||
"Approximate source image pixels per mesh step; smaller values "
|
||||
"Source image pixels between neighboring mesh points; smaller values "
|
||||
"create denser geometry"
|
||||
),
|
||||
default=32,
|
||||
@@ -372,7 +372,7 @@ def balloon_radial_zoom_property(scale):
|
||||
|
||||
|
||||
class AnyImageSettings(bpy.types.PropertyGroup):
|
||||
cutout_pixel_size: cutout_pixel_size_property()
|
||||
cutout_point_spacing: cutout_point_spacing_property()
|
||||
cutout_normal_analysis: cutout_normal_analysis_property()
|
||||
image_edit_refine_selection: image_edit_refine_selection_property()
|
||||
balloon_mode: balloon_mode_property()
|
||||
|
||||
@@ -186,7 +186,7 @@ def alpha_regions(alpha, max_resolution=512, threshold=0.5):
|
||||
sampled_size = tuple(max(1, round(value * scale)) for value in source_size)
|
||||
if sampled_size != source_size:
|
||||
values = _resize_alpha(values, sampled_size)
|
||||
mask = values > round(255.0 * threshold)
|
||||
mask = values >= 255.0 * threshold
|
||||
regions = mask_regions(mask)
|
||||
scale_x = source_size[0] / sampled_size[0]
|
||||
scale_y = source_size[1] / sampled_size[1]
|
||||
|
||||
@@ -116,7 +116,7 @@ def foreground_mesh_mask(foreground_path, depth_shape):
|
||||
resample=Image.Resampling.BILINEAR,
|
||||
)
|
||||
alpha = alpha.filter(ImageFilter.GaussianBlur(radius=0.75))
|
||||
return np.asarray(alpha) > 0
|
||||
return np.asarray(alpha) >= round(255.0 * 0.5)
|
||||
|
||||
|
||||
def confidence_percentile_threshold(depth, confidence, percentile):
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import json
|
||||
|
||||
from ..media import copy_color_images
|
||||
from ..models import geometry_adapter, moge2
|
||||
from .background import remove_image_background
|
||||
@@ -45,7 +47,7 @@ def _write_field(frame, output_path):
|
||||
|
||||
def _write_masked_color(input_path, polygon, output_path, invert=False):
|
||||
import numpy as np
|
||||
from PIL import Image, ImageChops, ImageFilter, ImageOps
|
||||
from PIL import Image, ImageFilter, ImageOps
|
||||
|
||||
with Image.open(input_path) as opened:
|
||||
source = opened.convert("RGBA")
|
||||
@@ -55,9 +57,8 @@ def _write_masked_color(input_path, polygon, output_path, invert=False):
|
||||
).filter(ImageFilter.GaussianBlur(radius=0.75))
|
||||
if invert:
|
||||
selection = ImageOps.invert(selection)
|
||||
alpha = ImageChops.multiply(source.getchannel("A"), selection)
|
||||
pixels = np.asarray(source, dtype=np.uint8).copy()
|
||||
pixels[:, :, 3] = np.asarray(alpha, dtype=np.uint8)
|
||||
pixels[:, :, 3] = np.asarray(selection, dtype=np.uint8)
|
||||
pixels[pixels[:, :, 3] == 0, :3] = 0
|
||||
Image.fromarray(pixels, mode="RGBA").save(
|
||||
output_path,
|
||||
@@ -67,6 +68,37 @@ def _write_masked_color(input_path, polygon, output_path, invert=False):
|
||||
return output_path
|
||||
|
||||
|
||||
def _write_unbounded_selection(
|
||||
input_path,
|
||||
polygon,
|
||||
output_path,
|
||||
invert=False,
|
||||
):
|
||||
import numpy as np
|
||||
from PIL import Image, ImageFilter
|
||||
|
||||
if invert:
|
||||
_write_masked_color(input_path, polygon, output_path, invert=True)
|
||||
with Image.open(input_path) as source:
|
||||
return output_path, (0, 0, source.width, source.height)
|
||||
canvas, selection_mask, bounds = create_selection_canvas(
|
||||
input_path, polygon, 'NONZERO'
|
||||
)
|
||||
selection = Image.fromarray(
|
||||
selection_mask.astype(np.uint8) * 255,
|
||||
mode='L',
|
||||
).filter(ImageFilter.GaussianBlur(radius=0.75))
|
||||
pixels = np.asarray(canvas, dtype=np.uint8).copy()
|
||||
pixels[:, :, 3] = np.asarray(selection, dtype=np.uint8)
|
||||
pixels[pixels[:, :, 3] == 0, :3] = 0
|
||||
Image.fromarray(pixels, mode='RGBA').save(
|
||||
output_path,
|
||||
format='PNG',
|
||||
compress_level=1,
|
||||
)
|
||||
return output_path, bounds
|
||||
|
||||
|
||||
def _write_refined_color(context, parameters, input_path, polygon, output_path):
|
||||
from PIL import Image, ImageChops
|
||||
|
||||
@@ -100,6 +132,77 @@ def _write_refined_color(context, parameters, input_path, polygon, output_path):
|
||||
return output_path
|
||||
|
||||
|
||||
def _write_cutout_images(
|
||||
input_path,
|
||||
selection_path,
|
||||
output_directory,
|
||||
selection_bounds=None,
|
||||
):
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
with Image.open(input_path) as opened:
|
||||
source = opened.convert("RGBA")
|
||||
with Image.open(selection_path) as opened:
|
||||
selection = opened.convert("RGBA")
|
||||
if selection_bounds is None and selection.size != source.size:
|
||||
raise ValueError("The Cutout selection does not match the source image")
|
||||
if selection_bounds is None:
|
||||
selection_bounds = (0, 0, source.width, source.height)
|
||||
selection_pixels = np.asarray(selection, dtype=np.uint8).copy()
|
||||
alpha = selection_pixels[:, :, 3]
|
||||
alpha[alpha <= 1] = 0
|
||||
selection_pixels[alpha == 0, :3] = 0
|
||||
selection = Image.fromarray(selection_pixels, mode="RGBA")
|
||||
visible_y, visible_x = np.nonzero(alpha >= 128)
|
||||
if not len(visible_x):
|
||||
raise ValueError("The Cutout selection contains no visible pixels")
|
||||
bounds = (
|
||||
int(visible_x.min()),
|
||||
int(visible_y.min()),
|
||||
int(visible_x.max()) + 1,
|
||||
int(visible_y.max()) + 1,
|
||||
)
|
||||
content_bounds = bounds
|
||||
selection_left, selection_top, _selection_right, _selection_bottom = (
|
||||
selection_bounds
|
||||
)
|
||||
bounds = (
|
||||
selection_left + content_bounds[0],
|
||||
selection_top + content_bounds[1],
|
||||
selection_left + content_bounds[2],
|
||||
selection_top + content_bounds[3],
|
||||
)
|
||||
color_canvas = Image.new('RGBA', selection.size, (0, 0, 0, 0))
|
||||
color_canvas.paste(source, (-selection_left, -selection_top))
|
||||
color = color_canvas.crop(content_bounds)
|
||||
color_path = output_directory / "image.0001.png"
|
||||
color.save(color_path, format="PNG", compress_level=1)
|
||||
|
||||
selection_crop = selection.crop(content_bounds)
|
||||
selection_path = output_directory / "selection.png"
|
||||
selection_crop.save(selection_path, format="PNG", compress_level=1)
|
||||
|
||||
analysis_pixels = np.asarray(color, dtype=np.uint8).copy()
|
||||
mask_alpha = np.asarray(selection_crop, dtype=np.uint8)[:, :, 3]
|
||||
analysis_pixels[:, :, 3] = np.minimum(
|
||||
analysis_pixels[:, :, 3],
|
||||
mask_alpha,
|
||||
)
|
||||
analysis_pixels[mask_alpha < 128] = 0
|
||||
analysis_path = output_directory / "analysis.png"
|
||||
Image.fromarray(analysis_pixels, mode="RGBA").save(
|
||||
analysis_path,
|
||||
format="PNG",
|
||||
compress_level=1,
|
||||
)
|
||||
bounds_path = output_directory / "bounds.json"
|
||||
bounds_path.write_text(
|
||||
json.dumps(bounds, separators=(",", ":")),
|
||||
encoding="utf-8",
|
||||
)
|
||||
return color_path, selection_path, analysis_path, bounds_path
|
||||
|
||||
def _write_camera_normal_from_prediction(prediction, color_path, output_path):
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
@@ -148,25 +251,15 @@ def _write_balloon_depth(
|
||||
context,
|
||||
parameters,
|
||||
input_path,
|
||||
polygon,
|
||||
selection_size=None,
|
||||
include_normal=False,
|
||||
):
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
with Image.open(input_path) as source:
|
||||
rgba = np.asarray(source.convert("RGBA"), dtype=np.uint8)
|
||||
source_size = source.size
|
||||
selection_size = selection_size or source_size
|
||||
scale_x = source_size[0] / selection_size[0]
|
||||
scale_y = source_size[1] / selection_size[1]
|
||||
analysis_polygon = tuple(
|
||||
(point[0] * scale_x, point[1] * scale_y) for point in polygon
|
||||
)
|
||||
selection_mask = polygon_selection_mask(
|
||||
source_size,
|
||||
analysis_polygon,
|
||||
"NONZERO",
|
||||
)
|
||||
selection_mask = rgba[:, :, 3] >= 128
|
||||
prediction = moge2.infer(
|
||||
context.resource("model_manager"),
|
||||
parameters,
|
||||
@@ -190,7 +283,6 @@ def _write_balloon_depth(
|
||||
)
|
||||
return depth_path, normal_path
|
||||
|
||||
|
||||
def _write_surface_depth(
|
||||
context, parameters, color_path, include_normal=False
|
||||
):
|
||||
@@ -230,49 +322,59 @@ def run(context, parameters):
|
||||
raise ValueError("Empty Image to Mesh currently supports one image")
|
||||
shape = parameters["shape"]
|
||||
polygon = parameters.get("polygon")
|
||||
analysis_path = None
|
||||
result = {}
|
||||
if polygon:
|
||||
selection_bounds = None
|
||||
full_selection_path = context.directory / "selection-full.png"
|
||||
if parameters.get("refine_selection"):
|
||||
color_path = _write_refined_color(
|
||||
_write_refined_color(
|
||||
context,
|
||||
parameters,
|
||||
images[0],
|
||||
polygon,
|
||||
context.directory / "image.0001.png",
|
||||
full_selection_path,
|
||||
)
|
||||
else:
|
||||
color_path = _write_masked_color(
|
||||
_selection_path, selection_bounds = _write_unbounded_selection(
|
||||
images[0],
|
||||
polygon,
|
||||
context.directory / "image.0001.png",
|
||||
full_selection_path,
|
||||
parameters.get("invert", False),
|
||||
)
|
||||
color_path, selection_path, analysis_path, bounds_path = (
|
||||
_write_cutout_images(
|
||||
images[0],
|
||||
full_selection_path,
|
||||
context.directory,
|
||||
selection_bounds,
|
||||
)
|
||||
)
|
||||
result.update(
|
||||
selection=selection_path.name,
|
||||
bounds=bounds_path.name,
|
||||
)
|
||||
else:
|
||||
copy_color_images(images, context.directory)
|
||||
color_path = min(context.directory.glob("image.0001.*"))
|
||||
result = {}
|
||||
|
||||
normal_analysis = bool(parameters.get("normal_analysis"))
|
||||
with analysis_frames(context, parameters, images) as model_images:
|
||||
if len(model_images) != 1:
|
||||
raise ValueError("Empty Image to Mesh currently supports one image")
|
||||
analysis_path = model_images[0]
|
||||
model_path = analysis_path or model_images[0]
|
||||
|
||||
if shape in {"CUTOUT", "BALLOON"} and normal_analysis:
|
||||
context.progress(0.4, "Estimating surface normal")
|
||||
result["normal"] = _write_camera_normal(
|
||||
context, parameters, analysis_path
|
||||
context, parameters, model_path
|
||||
).name
|
||||
elif shape == "DEPTH_BALLOON":
|
||||
from PIL import Image
|
||||
|
||||
with Image.open(images[0]) as selection_source:
|
||||
selection_size = selection_source.size
|
||||
context.progress(0.4, "Estimating Balloon depth")
|
||||
depth_path, normal_path = _write_balloon_depth(
|
||||
context,
|
||||
parameters,
|
||||
analysis_path,
|
||||
polygon,
|
||||
selection_size,
|
||||
model_path,
|
||||
include_normal=normal_analysis,
|
||||
)
|
||||
result["depth"] = depth_path.name
|
||||
@@ -283,7 +385,7 @@ def run(context, parameters):
|
||||
field_path, normal_path = _write_surface_depth(
|
||||
context,
|
||||
parameters,
|
||||
analysis_path,
|
||||
model_path,
|
||||
include_normal=normal_analysis,
|
||||
)
|
||||
result["field"] = field_path.name
|
||||
@@ -293,7 +395,7 @@ def run(context, parameters):
|
||||
and shape == "DEPTH_SURFACE"
|
||||
):
|
||||
normal_path = _write_camera_normal(
|
||||
context, parameters, analysis_path
|
||||
context, parameters, model_path
|
||||
)
|
||||
if normal_path is not None:
|
||||
result["normal"] = normal_path.name
|
||||
|
||||
Reference in New Issue
Block a user