Skip to content

Commit d728eb6

Browse files
feat: Add find_edges method in Image (#531)
Closes #523 ### Summary of Changes Added find_edges method for the new image class. Works just like the find_edges method from the old Image class with pillow. Fixed images with one channel for both repr and both to_file methods. Added grayscale images that only contain one channel each. Re-enabled Image.adjust_color_balance & Image.find_edges in the Image tutorial. Added warning for adjust_color_balance if it is used on a grayscale image. --------- Co-authored-by: megalinter-bot <129584137+megalinter-bot@users.noreply.github.com>
1 parent dba23f9 commit d728eb6

File tree

99 files changed

+183
-28
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

99 files changed

+183
-28
lines changed

docs/tutorials/image_processing.ipynb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@
175175
"execution_count": null,
176176
"outputs": [],
177177
"source": [
178-
"# plane.adjust_color_balance(0.5)"
178+
"plane.adjust_color_balance(0.5)"
179179
],
180180
"metadata": {
181181
"collapsed": false
@@ -280,7 +280,7 @@
280280
"execution_count": null,
281281
"outputs": [],
282282
"source": [
283-
"# plane.find_edges()\n"
283+
"plane.find_edges()\n"
284284
],
285285
"metadata": {
286286
"collapsed": false

src/safeds/data/image/containers/_image.py

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@ class Image:
3434

3535
_pil_to_tensor = PILToTensor()
3636
_default_device = _get_device()
37+
_FILTER_EDGES_KERNEL = (
38+
torch.tensor([[-1.0, -1.0, -1.0], [-1.0, 8.0, -1.0], [-1.0, -1.0, -1.0]])
39+
.unsqueeze(dim=0)
40+
.unsqueeze(dim=0)
41+
.to(_default_device)
42+
)
3743

3844
@staticmethod
3945
def from_file(path: str | Path, device: Device = _default_device) -> Image:
@@ -116,7 +122,10 @@ def _repr_jpeg_(self) -> bytes | None:
116122
if self.channel == 4:
117123
return None
118124
buffer = io.BytesIO()
119-
save_image(self._image_tensor.to(torch.float32) / 255, buffer, format="jpeg")
125+
if self.channel == 1:
126+
func2.to_pil_image(self._image_tensor, mode="L").save(buffer, format="jpeg")
127+
else:
128+
save_image(self._image_tensor.to(torch.float32) / 255, buffer, format="jpeg")
120129
buffer.seek(0)
121130
return buffer.read()
122131

@@ -130,7 +139,10 @@ def _repr_png_(self) -> bytes:
130139
The image as PNG.
131140
"""
132141
buffer = io.BytesIO()
133-
save_image(self._image_tensor.to(torch.float32) / 255, buffer, format="png")
142+
if self.channel == 1:
143+
func2.to_pil_image(self._image_tensor, mode="L").save(buffer, format="png")
144+
else:
145+
save_image(self._image_tensor.to(torch.float32) / 255, buffer, format="png")
134146
buffer.seek(0)
135147
return buffer.read()
136148

@@ -213,7 +225,10 @@ def to_jpeg_file(self, path: str | Path) -> None:
213225
if self.channel == 4:
214226
raise IllegalFormatError("png")
215227
Path(path).parent.mkdir(parents=True, exist_ok=True)
216-
save_image(self._image_tensor.to(torch.float32) / 255, path, format="jpeg")
228+
if self.channel == 1:
229+
func2.to_pil_image(self._image_tensor, mode="L").save(path, format="jpeg")
230+
else:
231+
save_image(self._image_tensor.to(torch.float32) / 255, path, format="jpeg")
217232

218233
def to_png_file(self, path: str | Path) -> None:
219234
"""
@@ -225,7 +240,10 @@ def to_png_file(self, path: str | Path) -> None:
225240
The path to the PNG file.
226241
"""
227242
Path(path).parent.mkdir(parents=True, exist_ok=True)
228-
save_image(self._image_tensor.to(torch.float32) / 255, path, format="png")
243+
if self.channel == 1:
244+
func2.to_pil_image(self._image_tensor, mode="L").save(path, format="png")
245+
else:
246+
save_image(self._image_tensor.to(torch.float32) / 255, path, format="png")
229247

230248
# ------------------------------------------------------------------------------------------------------------------
231249
# Transformations
@@ -457,6 +475,12 @@ def adjust_color_balance(self, factor: float) -> Image:
457475
UserWarning,
458476
stacklevel=2,
459477
)
478+
elif self.channel == 1:
479+
warnings.warn(
480+
"Color adjustment will not have an affect on grayscale images with only one channel.",
481+
UserWarning,
482+
stacklevel=2,
483+
)
460484
return Image(
461485
self.convert_to_grayscale()._image_tensor * (1.0 - factor * 1.0) + self._image_tensor * (factor * 1.0),
462486
device=self.device,
@@ -568,3 +592,38 @@ def rotate_left(self) -> Image:
568592
The image rotated 90 degrees counter-clockwise.
569593
"""
570594
return Image(func2.rotate(self._image_tensor, 90, expand=True), device=self.device)
595+
596+
def find_edges(self) -> Image:
597+
"""
598+
Return a grayscale version of the image with the edges highlighted.
599+
600+
The original image is not modified.
601+
602+
Returns
603+
-------
604+
result : Image
605+
The image with edges found.
606+
"""
607+
kernel = (
608+
Image._FILTER_EDGES_KERNEL
609+
if self.device.type == Image._default_device
610+
else Image._FILTER_EDGES_KERNEL.to(self.device)
611+
)
612+
edges_tensor = torch.clamp(
613+
torch.nn.functional.conv2d(
614+
self.convert_to_grayscale()._image_tensor.float()[0].unsqueeze(dim=0),
615+
kernel,
616+
padding="same",
617+
).squeeze(dim=1),
618+
0,
619+
255,
620+
).to(torch.uint8)
621+
if self.channel == 3:
622+
return Image(edges_tensor.repeat(3, 1, 1), device=self.device)
623+
elif self.channel == 4:
624+
return Image(
625+
torch.cat([edges_tensor.repeat(3, 1, 1), self._image_tensor[3].unsqueeze(dim=0)]),
626+
device=self.device,
627+
)
628+
else:
629+
return Image(edges_tensor, device=self.device)

tests/resources/image/grayscale.jpg

976 Bytes

tests/resources/image/grayscale.png

492 Bytes

0 commit comments

Comments
 (0)