masking.py 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
  1. from PIL import Image, ImageFilter, ImageOps
  2. def get_crop_region_v2(mask, pad=0):
  3. """
  4. Finds a rectangular region that contains all masked ares in a mask.
  5. Returns None if mask is completely black mask (all 0)
  6. Parameters:
  7. mask: PIL.Image.Image L mode or numpy 1d array
  8. pad: int number of pixels that the region will be extended on all sides
  9. Returns: (x1, y1, x2, y2) | None
  10. Introduced post 1.9.0
  11. """
  12. mask = mask if isinstance(mask, Image.Image) else Image.fromarray(mask)
  13. if box := mask.getbbox():
  14. x1, y1, x2, y2 = box
  15. return (max(x1 - pad, 0), max(y1 - pad, 0), min(x2 + pad, mask.size[0]), min(y2 + pad, mask.size[1])) if pad else box
  16. def get_crop_region(mask, pad=0):
  17. """
  18. Same function as get_crop_region_v2 but handles completely black mask (all 0) differently
  19. when mask all black still return coordinates but the coordinates may be invalid ie x2>x1 or y2>y1
  20. Notes: it is possible for the coordinates to be "valid" again if pad size is sufficiently large
  21. (mask_size.x-pad, mask_size.y-pad, pad, pad)
  22. Extension developer should use get_crop_region_v2 instead unless for compatibility considerations.
  23. """
  24. mask = mask if isinstance(mask, Image.Image) else Image.fromarray(mask)
  25. if box := get_crop_region_v2(mask, pad):
  26. return box
  27. x1, y1 = mask.size
  28. x2 = y2 = 0
  29. return max(x1 - pad, 0), max(y1 - pad, 0), min(x2 + pad, mask.size[0]), min(y2 + pad, mask.size[1])
  30. def expand_crop_region(crop_region, processing_width, processing_height, image_width, image_height):
  31. """expands crop region get_crop_region() to match the ratio of the image the region will processed in; returns expanded region
  32. for example, if user drew mask in a 128x32 region, and the dimensions for processing are 512x512, the region will be expanded to 128x128."""
  33. x1, y1, x2, y2 = crop_region
  34. ratio_crop_region = (x2 - x1) / (y2 - y1)
  35. ratio_processing = processing_width / processing_height
  36. if ratio_crop_region > ratio_processing:
  37. desired_height = (x2 - x1) / ratio_processing
  38. desired_height_diff = int(desired_height - (y2-y1))
  39. y1 -= desired_height_diff//2
  40. y2 += desired_height_diff - desired_height_diff//2
  41. if y2 >= image_height:
  42. diff = y2 - image_height
  43. y2 -= diff
  44. y1 -= diff
  45. if y1 < 0:
  46. y2 -= y1
  47. y1 -= y1
  48. if y2 >= image_height:
  49. y2 = image_height
  50. else:
  51. desired_width = (y2 - y1) * ratio_processing
  52. desired_width_diff = int(desired_width - (x2-x1))
  53. x1 -= desired_width_diff//2
  54. x2 += desired_width_diff - desired_width_diff//2
  55. if x2 >= image_width:
  56. diff = x2 - image_width
  57. x2 -= diff
  58. x1 -= diff
  59. if x1 < 0:
  60. x2 -= x1
  61. x1 -= x1
  62. if x2 >= image_width:
  63. x2 = image_width
  64. return x1, y1, x2, y2
  65. def fill(image, mask):
  66. """fills masked regions with colors from image using blur. Not extremely effective."""
  67. image_mod = Image.new('RGBA', (image.width, image.height))
  68. image_masked = Image.new('RGBa', (image.width, image.height))
  69. image_masked.paste(image.convert("RGBA").convert("RGBa"), mask=ImageOps.invert(mask.convert('L')))
  70. image_masked = image_masked.convert('RGBa')
  71. for radius, repeats in [(256, 1), (64, 1), (16, 2), (4, 4), (2, 2), (0, 1)]:
  72. blurred = image_masked.filter(ImageFilter.GaussianBlur(radius)).convert('RGBA')
  73. for _ in range(repeats):
  74. image_mod.alpha_composite(blurred)
  75. return image_mod.convert("RGB")