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:
@@ -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