Skip to content

Commit

Permalink
Crop & Mask updates
Browse files Browse the repository at this point in the history
  • Loading branch information
hipsterusername committed Sep 18, 2024
1 parent 7db4d26 commit dad9fc9
Show file tree
Hide file tree
Showing 2 changed files with 103 additions and 5 deletions.
82 changes: 82 additions & 0 deletions invokeai/app/invocations/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -1072,3 +1072,85 @@ def invoke(self, context: InvocationContext) -> CanvasV2MaskAndCropOutput:
width=image_dto.width,
height=image_dto.height,
)



@invocation(
"crop_to_object",
title="Crop to Mask Object",
tags=["image", "crop"],
category="image",
version="1.0.0",
)
class CropToObjectInvocation(BaseInvocation, WithMetadata, WithBoard):
"""Crops an image to a specified box around the object of specified color."""

image: ImageField = InputField(description="An input mask image with black and white content")
margin: int = InputField(default=0, ge=0, description="The desired margin around the object, as measured in pixels")
object_color: Literal['white', 'black'] = InputField(
default='white',
description="The color of the object to crop around (either 'white' or 'black')"
)

def invoke(self, context: InvocationContext) -> CropToObjectOutput:

Check failure on line 1095 in invokeai/app/invocations/image.py

View workflow job for this annotation

GitHub Actions / python-checks

Ruff (F821)

invokeai/app/invocations/image.py:1095:53: F821 Undefined name `CropToObjectOutput`
# Load the image
image = context.images.get_pil(self.image.image_name)

# Convert image to grayscale
grayscale_image = image.convert("L")

# Convert to numpy array
np_image = np.array(grayscale_image)

Check failure on line 1103 in invokeai/app/invocations/image.py

View workflow job for this annotation

GitHub Actions / python-checks

Ruff (F821)

invokeai/app/invocations/image.py:1103:20: F821 Undefined name `np`

# Depending on the object color, find the object pixels
if self.object_color == 'white':
# Find white pixels (value > 0)
object_pixels = np.argwhere(np_image > 0)

Check failure on line 1108 in invokeai/app/invocations/image.py

View workflow job for this annotation

GitHub Actions / python-checks

Ruff (F821)

invokeai/app/invocations/image.py:1108:29: F821 Undefined name `np`
else:
# Find black pixels (value < 255)
object_pixels = np.argwhere(np_image < 255)

Check failure on line 1111 in invokeai/app/invocations/image.py

View workflow job for this annotation

GitHub Actions / python-checks

Ruff (F821)

invokeai/app/invocations/image.py:1111:29: F821 Undefined name `np`

# If no object pixels are found, return the original image and zero offsets
if object_pixels.size == 0:
image_dto = context.images.save(image=image.copy())
return CropToObjectOutput(

Check failure on line 1116 in invokeai/app/invocations/image.py

View workflow job for this annotation

GitHub Actions / python-checks

Ruff (F821)

invokeai/app/invocations/image.py:1116:20: F821 Undefined name `CropToObjectOutput`
image=ImageField(image_name=image_dto.image_name),
width=image.width,
height=image.height,
offset_top=0,
offset_left=0,
offset_right=0,
offset_bottom=0,
)

# Get bounding box of object pixels
y_min, x_min = object_pixels.min(axis=0)
y_max, x_max = object_pixels.max(axis=0)

# Expand bounding box by margin
x_min_expanded = max(x_min - self.margin, 0)
y_min_expanded = max(y_min - self.margin, 0)
x_max_expanded = min(x_max + self.margin, np_image.shape[1] - 1)
y_max_expanded = min(y_max + self.margin, np_image.shape[0] - 1)

# Crop the image
cropped_image = image.crop((x_min_expanded, y_min_expanded, x_max_expanded + 1, y_max_expanded + 1))

# Calculate offsets
offset_top = y_min_expanded
offset_left = x_min_expanded
offset_right = np_image.shape[1] - x_max_expanded - 1
offset_bottom = np_image.shape[0] - y_max_expanded - 1

# Save the cropped image
image_dto = context.images.save(image=cropped_image)

return CropToObjectOutput(

Check failure on line 1148 in invokeai/app/invocations/image.py

View workflow job for this annotation

GitHub Actions / python-checks

Ruff (F821)

invokeai/app/invocations/image.py:1148:16: F821 Undefined name `CropToObjectOutput`
image=ImageField(image_name=image_dto.image_name),
width=cropped_image.width,
height=cropped_image.height,
offset_top=offset_top,
offset_left=offset_left,
offset_right=offset_right,
offset_bottom=offset_bottom,
)

Check failure on line 1156 in invokeai/app/invocations/image.py

View workflow job for this annotation

GitHub Actions / python-checks

Ruff (W292)

invokeai/app/invocations/image.py:1156:10: W292 No newline at end of file
26 changes: 21 additions & 5 deletions invokeai/app/invocations/mask.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,23 +96,37 @@ def invoke(self, context: InvocationContext) -> MaskOutput:
title="Image Mask to Tensor",
tags=["conditioning"],
category="conditioning",
version="1.0.0",
version="1.0.1",
)
class ImageMaskToTensorInvocation(BaseInvocation, WithMetadata):
"""Convert a mask image to a tensor. Converts the image to grayscale and uses thresholding at the specified value."""

image: ImageField = InputField(description="The mask image to convert.")
cutoff: int = InputField(ge=0, le=255, description="Cutoff (<)", default=128)
invert: bool = InputField(default=False, description="Whether to invert the mask.")
invert: bool = InputField(default=False, descimription="Whether to invert the mask.")

def invoke(self, context: InvocationContext) -> MaskOutput:
image = context.images.get_pil(self.image.image_name, mode="L")
image = context.images.get_pil(self.image.image_name)
np_image = np.array(image)

Check failure on line 111 in invokeai/app/invocations/mask.py

View workflow job for this annotation

GitHub Actions / python-checks

Ruff (W293)

invokeai/app/invocations/mask.py:111:1: W293 Blank line contains whitespace
# Handle different image modes
if image.mode == 'RGBA':
alpha_channel = np_image[:, :, 3] # Extract alpha channel
elif image.mode == 'RGB':
# For RGB images, treat all non-black pixels as opaque.
non_black_mask = np.any(np_image > 0, axis=2) # True for any non-black pixels
alpha_channel = non_black_mask.astype(np.uint8) * 255 # Convert to a mask of 0 or 255
elif image.mode == 'L': # Grayscale images
alpha_channel = np_image # Grayscale image, so we directly use it
else:
raise ValueError(f"Unsupported image mode: {image.mode}")

mask = torch.zeros((1, image.height, image.width), dtype=torch.bool)

if self.invert:
mask[0] = torch.tensor(np.array(image)[:, :] >= self.cutoff, dtype=torch.bool)
mask[0] = torch.tensor(alpha_channel == 0, dtype=torch.bool) # Transparent where alpha or brightness is 0
else:
mask[0] = torch.tensor(np.array(image)[:, :] < self.cutoff, dtype=torch.bool)
mask[0] = torch.tensor(alpha_channel > 0, dtype=torch.bool) # Opaque where alpha or brightness is > 0

return MaskOutput(
mask=TensorField(tensor_name=context.tensors.save(mask)),
Expand All @@ -121,6 +135,8 @@ def invoke(self, context: InvocationContext) -> MaskOutput:
)




@invocation(
"tensor_mask_to_image",
title="Tensor Mask to Image",
Expand Down

0 comments on commit dad9fc9

Please sign in to comment.