feat: Implement upscale functionality with ONNX models
- Added upscale operator to handle image upscaling by 4x using AI models. - Introduced new jobs for upscale processing in the server, including debug and standard upscale jobs. - Created a new ONNX model handler for upscale operations, supporting tiled inference and resource management. - Updated tests to cover new upscale functionality, including input validation and output verification. - Added documentation for the upscale feature, detailing usage and model options.
This commit is contained in:
@@ -81,6 +81,9 @@ class FakeModels:
|
||||
def get_da3(self, _model_directory, _device):
|
||||
return object()
|
||||
|
||||
def get_upscale(self, _model_directory, _device):
|
||||
return object()
|
||||
|
||||
|
||||
class FakeJobContext:
|
||||
def __init__(self, directory, job_type, models=None):
|
||||
@@ -88,6 +91,7 @@ class FakeJobContext:
|
||||
self.job_type = job_type
|
||||
self.models = models or FakeModels()
|
||||
self.status = {}
|
||||
self.logs = []
|
||||
|
||||
def resource(self, name):
|
||||
if name != "model_manager":
|
||||
@@ -101,6 +105,9 @@ class FakeJobContext:
|
||||
**details,
|
||||
}
|
||||
|
||||
def log(self, message, *, level="INFO"):
|
||||
self.logs.append((level, message))
|
||||
|
||||
def check_cancelled(self):
|
||||
return None
|
||||
|
||||
@@ -115,7 +122,8 @@ class WorkerTest(unittest.TestCase):
|
||||
cls.ben2 = load_server_module("models.ben2")
|
||||
cls.da3_model = load_server_module("models.da3")
|
||||
cls.moge2_model = load_server_module("models.moge2")
|
||||
cls.realesrgan_model = load_server_module("models.realesrgan")
|
||||
cls.upscale_model = load_server_module("models.upscale")
|
||||
cls.onnx_upscale = load_server_module("models.onnx_upscale")
|
||||
cls.surface_outputs = load_server_module("outputs.surface")
|
||||
cls.debug_da3_outputs = load_server_module("outputs.debug_da3")
|
||||
cls.debug_moge2_outputs = load_server_module("outputs.debug_moge2")
|
||||
@@ -125,9 +133,10 @@ class WorkerTest(unittest.TestCase):
|
||||
cls.debug_da3_job = load_server_module("jobs.debug_da3")
|
||||
cls.debug_moge2_job = load_server_module("jobs.debug_moge2")
|
||||
cls.debug_ben2_job = load_server_module("jobs.debug_ben2")
|
||||
cls.debug_realesrgan_job = load_server_module(
|
||||
"jobs.debug_realesrgan"
|
||||
cls.debug_upscale_job = load_server_module(
|
||||
"jobs.debug_upscale"
|
||||
)
|
||||
cls.upscale_job = load_server_module("jobs.upscale")
|
||||
cls.frames_job = load_server_module("jobs.frames")
|
||||
cls.foreground_job = load_server_module("jobs.foreground")
|
||||
cls.download = load_server_module("model_download")
|
||||
@@ -276,7 +285,7 @@ class WorkerTest(unittest.TestCase):
|
||||
self.media.copy_color_images([source], output)
|
||||
|
||||
self.assertEqual(
|
||||
(output / "color.0001.png").read_bytes(),
|
||||
(output / "image.0001.png").read_bytes(),
|
||||
b"frame",
|
||||
)
|
||||
|
||||
@@ -572,7 +581,7 @@ class WorkerTest(unittest.TestCase):
|
||||
{"input": str(input_path), "model": "BEN2", "device": "cpu"},
|
||||
)
|
||||
|
||||
self.assertTrue((root / "result" / "color.0001.png").is_file())
|
||||
self.assertTrue((root / "result" / "image.0001.png").is_file())
|
||||
|
||||
self.assertEqual(
|
||||
inference_options[0][1],
|
||||
@@ -721,10 +730,10 @@ class WorkerTest(unittest.TestCase):
|
||||
|
||||
result_directory = context.directory
|
||||
self.assertTrue(
|
||||
(result_directory / "color.0001.png").is_file()
|
||||
(result_directory / "image.0001.png").is_file()
|
||||
)
|
||||
self.assertTrue(
|
||||
(result_directory / "color.0002.png").is_file()
|
||||
(result_directory / "image.0002.png").is_file()
|
||||
)
|
||||
|
||||
self.assertEqual(len(inference_options), 2)
|
||||
@@ -755,7 +764,7 @@ class WorkerTest(unittest.TestCase):
|
||||
for path in result_directory.iterdir()
|
||||
if path.is_file()
|
||||
),
|
||||
["color.0001.jpg", "color.0002.jpg", "color.0003.jpg"],
|
||||
["image.0001.jpg", "image.0002.jpg", "image.0003.jpg"],
|
||||
)
|
||||
self.assertEqual(context.status["progress"], 1.0)
|
||||
self.assertIn("3 frame", context.status["message"])
|
||||
@@ -890,7 +899,7 @@ class WorkerTest(unittest.TestCase):
|
||||
b"foreground",
|
||||
)
|
||||
|
||||
def test_realesrgan_debug_saves_each_direct_api_prediction(self):
|
||||
def test_upscale_debug_saves_each_direct_api_prediction(self):
|
||||
from PIL import Image
|
||||
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
@@ -899,17 +908,17 @@ class WorkerTest(unittest.TestCase):
|
||||
frames.mkdir()
|
||||
(frames / "a.png").touch()
|
||||
(frames / "b.png").touch()
|
||||
context = FakeJobContext(root / "result", "debug-realesrgan")
|
||||
context = FakeJobContext(root / "result", "debug-upscale")
|
||||
with patch.object(
|
||||
self.debug_realesrgan_job,
|
||||
self.debug_upscale_job,
|
||||
"infer_one",
|
||||
return_value=Image.new("RGB", (8, 8), (10, 20, 30)),
|
||||
) as infer_one:
|
||||
self.debug_realesrgan_job.run(
|
||||
self.debug_upscale_job.run(
|
||||
context,
|
||||
{
|
||||
"input": str(frames),
|
||||
"model": "REALESRGAN_GENERAL_X4V3",
|
||||
"model": "REALESRGAN_GENERAL_WDN_X4V3",
|
||||
"video_frames": 2,
|
||||
"device": "cpu",
|
||||
},
|
||||
@@ -922,6 +931,146 @@ class WorkerTest(unittest.TestCase):
|
||||
self.assertEqual(prediction.size, (8, 8))
|
||||
self.assertEqual(prediction.mode, "RGB")
|
||||
|
||||
def test_upscale_job_saves_importable_upscale_sequence(self):
|
||||
from PIL import Image
|
||||
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
frames = root / "frames"
|
||||
frames.mkdir()
|
||||
(frames / "a.png").touch()
|
||||
(frames / "b.png").touch()
|
||||
context = FakeJobContext(root / "result", "upscale-image")
|
||||
with patch.object(
|
||||
self.upscale_job,
|
||||
"infer_one",
|
||||
return_value=Image.new("RGB", (8, 8), (10, 20, 30)),
|
||||
) as infer_one:
|
||||
result = self.upscale_job.run(
|
||||
context,
|
||||
{
|
||||
"input": str(frames),
|
||||
"model": "REALESRGAN_GENERAL_WDN_X4V3",
|
||||
"video_frames": 2,
|
||||
"device": "cpu",
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(infer_one.call_count, 2)
|
||||
self.assertEqual(result, {"upscale": "."})
|
||||
colors = sorted(context.directory.glob("upscale.*.png"))
|
||||
self.assertEqual(
|
||||
[path.name for path in colors],
|
||||
["upscale.0001.png", "upscale.0002.png"],
|
||||
)
|
||||
self.assertIn(
|
||||
(
|
||||
"INFO",
|
||||
"Upscale model: REALESRGAN_GENERAL_WDN_X4V3; "
|
||||
"maximum output size: 4096 px",
|
||||
),
|
||||
context.logs,
|
||||
)
|
||||
|
||||
def test_upscale_job_preserves_source_alpha(self):
|
||||
from PIL import Image
|
||||
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
source_path = root / "source.png"
|
||||
pixels = np.zeros((3, 5, 4), dtype=np.uint8)
|
||||
pixels[..., :3] = (10, 20, 30)
|
||||
pixels[..., 3] = np.arange(5, dtype=np.uint8) * 60
|
||||
Image.fromarray(pixels, mode="RGBA").save(source_path)
|
||||
context = FakeJobContext(root / "result", "upscale-image")
|
||||
prediction = Image.new("RGB", (20, 12), (10, 20, 30))
|
||||
|
||||
with patch.object(
|
||||
self.onnx_upscale,
|
||||
"infer",
|
||||
return_value=prediction,
|
||||
):
|
||||
self.upscale_job.run(
|
||||
context,
|
||||
{
|
||||
"input": str(source_path),
|
||||
"model": "REALESRGAN_GENERAL_WDN_X4V3",
|
||||
"video_frames": 1,
|
||||
"device": "cpu",
|
||||
},
|
||||
)
|
||||
|
||||
with Image.open(context.directory / "upscale.0001.png") as result:
|
||||
self.assertEqual(result.mode, "RGBA")
|
||||
self.assertEqual(result.size, (20, 12))
|
||||
alpha = np.asarray(result.getchannel("A"))
|
||||
self.assertLess(
|
||||
int(alpha[:, 0].max()),
|
||||
int(alpha[:, -1].min()),
|
||||
)
|
||||
|
||||
def test_upscale_input_is_limited_before_four_x_inference(self):
|
||||
from PIL import Image
|
||||
|
||||
source = Image.new("RGB", (2000, 1000))
|
||||
limited = self.upscale_model._limited_input(source, 4096)
|
||||
|
||||
self.assertEqual(limited.size, (1024, 512))
|
||||
square = self.upscale_model._limited_input(
|
||||
Image.new("RGB", (3000, 3000)),
|
||||
8192,
|
||||
)
|
||||
self.assertLessEqual(
|
||||
square.width * square.height * 16,
|
||||
self.upscale_model.MAX_OUTPUT_PIXELS,
|
||||
)
|
||||
with self.assertRaisesRegex(ValueError, "between 1 and 8192"):
|
||||
self.upscale_model._limited_input(source, 9000)
|
||||
|
||||
def test_upscale_resource_error_releases_session_and_is_readable(self):
|
||||
from PIL import Image
|
||||
|
||||
class Models:
|
||||
def __init__(self, directory):
|
||||
self.model_directory = directory
|
||||
self.released = False
|
||||
|
||||
def directory(self, _model):
|
||||
return self.model_directory
|
||||
|
||||
def get_upscale(self, _directory, _device):
|
||||
return object()
|
||||
|
||||
def release_upscale(self):
|
||||
self.released = True
|
||||
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory) / "input.png"
|
||||
Image.new("RGB", (8, 8)).save(path)
|
||||
models = Models(Path(directory))
|
||||
with patch.object(
|
||||
self.onnx_upscale,
|
||||
"infer",
|
||||
side_effect=self.onnx_upscale.UpscaleResourceError(
|
||||
"provider memory exhausted"
|
||||
),
|
||||
):
|
||||
with self.assertRaisesRegex(
|
||||
RuntimeError,
|
||||
"Reduce Max Output Size or select CPU",
|
||||
):
|
||||
self.upscale_model.infer_one(
|
||||
models,
|
||||
{
|
||||
"model": "REALESRGAN_GENERAL_WDN_X4V3",
|
||||
"device": "directml",
|
||||
"max_output_size": 4096,
|
||||
},
|
||||
path,
|
||||
)
|
||||
|
||||
self.assertTrue(models.released)
|
||||
|
||||
def test_da3_removes_background_after_depth_using_original_input(self):
|
||||
order = []
|
||||
inference_inputs = []
|
||||
@@ -1047,10 +1196,10 @@ class WorkerTest(unittest.TestCase):
|
||||
|
||||
result_directory = context.directory
|
||||
self.assertTrue(
|
||||
(result_directory / "color.0001.jpg").is_file()
|
||||
(result_directory / "image.0001.jpg").is_file()
|
||||
)
|
||||
self.assertTrue(
|
||||
(result_directory / "color.0002.jpg").is_file()
|
||||
(result_directory / "image.0002.jpg").is_file()
|
||||
)
|
||||
self.assertEqual(len(inference_inputs), 2)
|
||||
self.assertTrue(all(len(paths) == 1 for paths in inference_inputs))
|
||||
|
||||
Reference in New Issue
Block a user