|
25 | 25 | from invokeai.app.services.image_records.image_records_common import ImageCategory |
26 | 26 | from invokeai.app.services.shared.invocation_context import InvocationContext |
27 | 27 | from invokeai.app.util.misc import SEED_MAX |
| 28 | +from invokeai.backend.image_util.color_conversion import ( |
| 29 | + linear_srgb_from_oklab, |
| 30 | + linear_srgb_from_oklch, |
| 31 | + linear_srgb_from_srgb, |
| 32 | + oklab_from_linear_srgb, |
| 33 | + oklch_from_oklab, |
| 34 | + srgb_from_linear_srgb, |
| 35 | +) |
28 | 36 | from invokeai.backend.image_util.invisible_watermark import InvisibleWatermark |
29 | 37 | from invokeai.backend.image_util.safety_checker import SafetyChecker |
30 | 38 |
|
@@ -397,6 +405,51 @@ def invoke(self, context: InvocationContext) -> ImageOutput: |
397 | 405 | ) |
398 | 406 |
|
399 | 407 |
|
| 408 | +@invocation( |
| 409 | + "unsharp_mask_oklab", |
| 410 | + title="Unsharp Mask (Oklab)", |
| 411 | + tags=["image", "unsharp_mask", "oklab"], |
| 412 | + category="image", |
| 413 | + version="1.0.0", |
| 414 | +) |
| 415 | +class OklabUnsharpMaskInvocation(BaseInvocation, WithMetadata, WithBoard): |
| 416 | + """Applies an unsharp mask filter to an image in the Oklab color space""" |
| 417 | + |
| 418 | + image: ImageField = InputField(description="The image to use") |
| 419 | + radius: float = InputField(gt=0, description="Unsharp mask radius", default=2) |
| 420 | + strength: float = InputField(ge=0, description="Unsharp mask strength", default=50) |
| 421 | + |
| 422 | + def pil_from_array(self, arr: numpy.ndarray) -> Image.Image: |
| 423 | + return Image.fromarray((numpy.clip(arr, 0.0, 1.0) * 255).astype("uint8")) |
| 424 | + |
| 425 | + def array_from_pil(self, img: Image.Image) -> numpy.ndarray: |
| 426 | + return numpy.array(img, dtype=numpy.float32) / 255.0 |
| 427 | + |
| 428 | + def invoke(self, context: InvocationContext) -> ImageOutput: |
| 429 | + image = context.images.get_pil(self.image.image_name) |
| 430 | + mode = image.mode |
| 431 | + |
| 432 | + alpha_channel = image.getchannel("A") if mode == "RGBA" else None |
| 433 | + image = image.convert("RGB") |
| 434 | + |
| 435 | + image_blurred = self.array_from_pil(image.filter(ImageFilter.GaussianBlur(radius=self.radius))) |
| 436 | + image_arr = self.array_from_pil(image) |
| 437 | + |
| 438 | + image_oklab = oklab_from_linear_srgb(linear_srgb_from_srgb(image_arr)) |
| 439 | + image_blurred_oklab = oklab_from_linear_srgb(linear_srgb_from_srgb(image_blurred)) |
| 440 | + |
| 441 | + image_oklab += (image_oklab - image_blurred_oklab) * (self.strength / 100.0) |
| 442 | + image_oklab = numpy.clip(image_oklab, -1.0, 1.0) |
| 443 | + |
| 444 | + image = self.pil_from_array(srgb_from_linear_srgb(linear_srgb_from_oklab(image_oklab))).convert(mode) |
| 445 | + |
| 446 | + if alpha_channel is not None: |
| 447 | + image.putalpha(alpha_channel) |
| 448 | + |
| 449 | + image_dto = context.images.save(image=image) |
| 450 | + return ImageOutput.build(image_dto) |
| 451 | + |
| 452 | + |
400 | 453 | PIL_RESAMPLING_MODES = Literal[ |
401 | 454 | "nearest", |
402 | 455 | "box", |
@@ -802,6 +855,40 @@ def invoke(self, context: InvocationContext) -> ImageOutput: |
802 | 855 | return ImageOutput.build(image_dto) |
803 | 856 |
|
804 | 857 |
|
| 858 | +@invocation( |
| 859 | + "img_hue_adjust_oklch", |
| 860 | + title="Adjust Image Hue (Oklch)", |
| 861 | + tags=["image", "hue", "oklch"], |
| 862 | + category="image", |
| 863 | + version="1.0.0", |
| 864 | +) |
| 865 | +class OklchImageHueAdjustmentInvocation(BaseInvocation, WithMetadata, WithBoard): |
| 866 | + """Adjusts the hue of an image in Oklch space.""" |
| 867 | + |
| 868 | + image: ImageField = InputField(description="The image to adjust") |
| 869 | + hue: int = InputField(default=0, description="The degrees by which to rotate the hue, 0-360") |
| 870 | + |
| 871 | + def invoke(self, context: InvocationContext) -> ImageOutput: |
| 872 | + image = context.images.get_pil(self.image.image_name) |
| 873 | + mode = image.mode |
| 874 | + alpha_channel = image.getchannel("A") if mode == "RGBA" else None |
| 875 | + |
| 876 | + rgb = numpy.asarray(image.convert("RGB"), dtype=numpy.float32) / 255.0 |
| 877 | + oklch = oklch_from_oklab(oklab_from_linear_srgb(linear_srgb_from_srgb(rgb))) |
| 878 | + oklch[..., 2] = (oklch[..., 2] + self.hue) % 360.0 |
| 879 | + |
| 880 | + image = Image.fromarray( |
| 881 | + numpy.clip(srgb_from_linear_srgb(linear_srgb_from_oklch(oklch)) * 255.0, 0.0, 255.0).astype(numpy.uint8), |
| 882 | + mode="RGB", |
| 883 | + ).convert(mode) |
| 884 | + |
| 885 | + if alpha_channel is not None: |
| 886 | + image.putalpha(alpha_channel) |
| 887 | + |
| 888 | + image_dto = context.images.save(image=image) |
| 889 | + return ImageOutput.build(image_dto) |
| 890 | + |
| 891 | + |
805 | 892 | COLOR_CHANNELS = Literal[ |
806 | 893 | "Red (RGBA)", |
807 | 894 | "Green (RGBA)", |
|
0 commit comments