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:
@@ -562,7 +562,85 @@ class ServerTest(unittest.TestCase):
|
||||
self.assertEqual(tuple(result[1, 1]), (20, 40, 60, 255))
|
||||
self.assertEqual(tuple(result[5, 7]), (0, 0, 0, 0))
|
||||
|
||||
def test_mesh_job_keeps_original_ai_input_separate_from_geometry_mask(self):
|
||||
def test_mesh_cutout_images_crop_color_and_preserve_source_alpha(self):
|
||||
import json
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
mesh_job = sys.modules["server.jobs.mesh"]
|
||||
source_path = self.storage_root / "analysis-source.png"
|
||||
selection_path = self.storage_root / "selection-source.png"
|
||||
output_directory = self.storage_root / "cutout-images"
|
||||
output_directory.mkdir()
|
||||
source = np.full((6, 8, 4), (10, 80, 160, 255), dtype=np.uint8)
|
||||
source[2, 4, 3] = 64
|
||||
Image.fromarray(source, mode="RGBA").save(source_path)
|
||||
selection = np.zeros((6, 8, 4), dtype=np.uint8)
|
||||
selection[1:5, 2:7] = (200, 20, 40, 255)
|
||||
selection[1, 2] = (200, 20, 40, 0)
|
||||
Image.fromarray(selection, mode="RGBA").save(selection_path)
|
||||
|
||||
color_path, mask_path, analysis_path, bounds_path = (
|
||||
mesh_job._write_cutout_images(
|
||||
source_path,
|
||||
selection_path,
|
||||
output_directory,
|
||||
)
|
||||
)
|
||||
color = np.asarray(Image.open(color_path).convert("RGBA"))
|
||||
mask = np.asarray(Image.open(mask_path).convert("RGBA"))
|
||||
analysis = np.asarray(Image.open(analysis_path).convert("RGBA"))
|
||||
|
||||
self.assertEqual(json.loads(bounds_path.read_text()), [2, 1, 7, 5])
|
||||
self.assertEqual(color.shape, (4, 5, 4))
|
||||
self.assertEqual(tuple(color[1, 2]), (10, 80, 160, 64))
|
||||
self.assertEqual(int(mask[0, 0, 3]), 0)
|
||||
self.assertEqual(tuple(color[0, 0]), (10, 80, 160, 255))
|
||||
self.assertEqual(tuple(analysis[0, 0]), (0, 0, 0, 0))
|
||||
self.assertEqual(tuple(analysis[1, 2]), (10, 80, 160, 64))
|
||||
|
||||
def test_mesh_unrefined_cutout_keeps_selection_outside_the_source(self):
|
||||
import json
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
mesh_job = sys.modules['server.jobs.mesh']
|
||||
source_path = self.storage_root / 'outside-source.png'
|
||||
full_selection_path = self.storage_root / 'outside-selection-full.png'
|
||||
output_directory = self.storage_root / 'outside-cutout'
|
||||
output_directory.mkdir()
|
||||
Image.new('RGBA', (8, 6), (10, 80, 160, 255)).save(source_path)
|
||||
polygon = ((-2, 1), (10, 1), (10, 5), (-2, 5))
|
||||
|
||||
_selection_path, selection_bounds = (
|
||||
mesh_job._write_unbounded_selection(
|
||||
source_path,
|
||||
polygon,
|
||||
full_selection_path,
|
||||
)
|
||||
)
|
||||
color_path, mask_path, analysis_path, bounds_path = (
|
||||
mesh_job._write_cutout_images(
|
||||
source_path,
|
||||
full_selection_path,
|
||||
output_directory,
|
||||
selection_bounds,
|
||||
)
|
||||
)
|
||||
color = np.asarray(Image.open(color_path).convert('RGBA'))
|
||||
mask = np.asarray(Image.open(mask_path).convert('RGBA'))
|
||||
analysis = np.asarray(Image.open(analysis_path).convert('RGBA'))
|
||||
|
||||
self.assertEqual(selection_bounds, (-2, 1, 10, 5))
|
||||
self.assertEqual(json.loads(bounds_path.read_text()), [-2, 1, 10, 5])
|
||||
self.assertEqual(color.shape, (4, 12, 4))
|
||||
self.assertEqual(tuple(color[1, 0]), (0, 0, 0, 0))
|
||||
self.assertEqual(tuple(color[1, 2]), (10, 80, 160, 255))
|
||||
self.assertGreater(int(mask[1, 0, 3]), 127)
|
||||
self.assertEqual(tuple(analysis[1, 0]), (0, 0, 0, 0))
|
||||
|
||||
def test_mesh_job_keeps_unrefined_outside_selection_with_normal(self):
|
||||
import json
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
@@ -577,7 +655,7 @@ class ServerTest(unittest.TestCase):
|
||||
check_cancelled=lambda: None,
|
||||
resource=lambda _name: object(),
|
||||
)
|
||||
normal = np.zeros((1, 6, 8, 3), dtype=np.float32)
|
||||
normal = np.zeros((1, 4, 12, 3), dtype=np.float32)
|
||||
normal[..., 0] = 1.0
|
||||
prediction = SimpleNamespace(normal=normal)
|
||||
received = []
|
||||
@@ -594,7 +672,7 @@ class ServerTest(unittest.TestCase):
|
||||
{
|
||||
"input": str(source_path),
|
||||
"shape": "BALLOON",
|
||||
"polygon": ((2, 1), (7, 1), (7, 5), (2, 5)),
|
||||
"polygon": ((-2, 1), (10, 1), (10, 5), (-2, 5)),
|
||||
"mesh_detail": 64,
|
||||
"invert": False,
|
||||
"device": "cpu",
|
||||
@@ -609,16 +687,36 @@ class ServerTest(unittest.TestCase):
|
||||
normal_pixels = np.asarray(
|
||||
Image.open(job_directory / result["normal"]).convert("RGB")
|
||||
)
|
||||
self.assertEqual(result, {"normal": "normal.png"})
|
||||
self.assertEqual(
|
||||
result,
|
||||
{
|
||||
"selection": "selection.png",
|
||||
"bounds": "bounds.json",
|
||||
"normal": "normal.png",
|
||||
},
|
||||
)
|
||||
self.assertTrue(color_path.is_file())
|
||||
self.assertFalse((job_directory / "contour.json").exists())
|
||||
self.assertEqual(received[0]["model"], "MOGE2_VITS_NORMAL")
|
||||
self.assertEqual(received_images, [source_path])
|
||||
self.assertEqual(tuple(color_pixels[3, 4, :3]), (20, 40, 60))
|
||||
self.assertGreater(int(color_pixels[3, 4, 3]), 240)
|
||||
self.assertLess(int(color_pixels[0, 0, 3]), 5)
|
||||
self.assertEqual(tuple(normal_pixels[0, 0]), (255, 127, 127))
|
||||
self.assertEqual(tuple(normal_pixels[3, 4]), (255, 127, 127))
|
||||
self.assertEqual(received_images, [job_directory / "analysis.png"])
|
||||
analysis_pixels = np.asarray(
|
||||
Image.open(received_images[0]).convert("RGBA")
|
||||
)
|
||||
self.assertEqual(
|
||||
json.loads((job_directory / "bounds.json").read_text()),
|
||||
[-2, 1, 10, 5],
|
||||
)
|
||||
self.assertEqual(tuple(analysis_pixels[2, 2, :3]), (20, 40, 60))
|
||||
self.assertGreater(int(analysis_pixels[2, 2, 3]), 240)
|
||||
self.assertEqual(tuple(analysis_pixels[2, 0]), (0, 0, 0, 0))
|
||||
self.assertEqual(tuple(color_pixels[2, 2, :3]), (20, 40, 60))
|
||||
self.assertGreater(int(color_pixels[2, 2, 3]), 240)
|
||||
self.assertEqual(tuple(color_pixels[2, 0]), (0, 0, 0, 0))
|
||||
self.assertEqual(color_pixels.shape[:2], (4, 12))
|
||||
self.assertEqual(analysis_pixels.shape[:2], (4, 12))
|
||||
self.assertEqual(normal_pixels.shape[:2], (4, 12))
|
||||
self.assertEqual(tuple(normal_pixels[0, 0]), (128, 128, 255))
|
||||
self.assertEqual(tuple(normal_pixels[2, 2]), (255, 127, 127))
|
||||
|
||||
def test_mesh_job_skips_moge_normal_when_option_is_disabled(self):
|
||||
from PIL import Image
|
||||
@@ -648,17 +746,22 @@ class ServerTest(unittest.TestCase):
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(result, {})
|
||||
self.assertEqual(
|
||||
result,
|
||||
{"selection": "selection.png", "bounds": "bounds.json"},
|
||||
)
|
||||
write_normal.assert_not_called()
|
||||
self.assertFalse((job_directory / "normal.png").exists())
|
||||
|
||||
def test_mesh_depth_balloon_analyzes_full_source_then_applies_selection(self):
|
||||
def test_mesh_depth_balloon_uses_cropped_analysis_alpha(self):
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
mesh_job = sys.modules["server.jobs.mesh"]
|
||||
source_path = self.storage_root / "mesh-depth-balloon-source.png"
|
||||
Image.new("RGBA", (6, 6), (20, 40, 60, 255)).save(source_path)
|
||||
pixels = np.full((6, 6, 4), (20, 40, 60, 0), dtype=np.uint8)
|
||||
pixels[2:5, 2:5, 3] = 255
|
||||
Image.fromarray(pixels, mode="RGBA").save(source_path)
|
||||
job_directory = self.storage_root / "mesh-depth-balloon-job"
|
||||
job_directory.mkdir()
|
||||
context = SimpleNamespace(
|
||||
@@ -684,8 +787,6 @@ class ServerTest(unittest.TestCase):
|
||||
context,
|
||||
{"device": "cpu"},
|
||||
source_path,
|
||||
((2, 2), (10, 2), (2, 10)),
|
||||
selection_size=(12, 12),
|
||||
include_normal=True,
|
||||
)
|
||||
|
||||
@@ -696,7 +797,7 @@ class ServerTest(unittest.TestCase):
|
||||
self.assertFalse(bool(data["valid"][0, 0]))
|
||||
self.assertTrue(bool(data["valid"][2, 2]))
|
||||
normal = np.asarray(Image.open(normal_path).convert("RGB"))
|
||||
self.assertEqual(tuple(normal[0, 0]), (255, 127, 127))
|
||||
self.assertEqual(tuple(normal[0, 0]), (128, 128, 255))
|
||||
self.assertEqual(tuple(normal[2, 2]), (255, 127, 127))
|
||||
|
||||
def test_mesh_job_uses_upscale_source_only_for_analysis(self):
|
||||
|
||||
Reference in New Issue
Block a user