Skip to content

Commit 1cee223

Browse files
authored
Adding image crop and adjustments methods (color, contrast, brightness, sharpness) (#154)
* Adding image adjustments methods (color, contrast, brightness, sharpness) * Typing * Improving adjustment tests * lint
1 parent ba36688 commit 1cee223

File tree

7 files changed

+229
-10
lines changed

7 files changed

+229
-10
lines changed

landingai/pipeline/frameset.py

Lines changed: 116 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33

44
import logging
55
from datetime import datetime
6-
from typing import Any, Callable, Dict, List, Optional, Union, cast
6+
from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union, cast
77
import warnings
88

99
import cv2
1010
import imageio
1111
import numpy as np
12-
from PIL import Image
12+
from PIL import Image, ImageEnhance
1313
from pydantic import BaseModel
1414

1515
from landingai.common import ClassificationPrediction, OcrPrediction, Prediction
@@ -256,6 +256,59 @@ def downsize(
256256
self.image = self.image.resize((width, height))
257257
return self
258258

259+
def crop(self, bbox: Tuple[int, int, int, int]) -> "Frame":
260+
"""Crop the image based on the bounding box
261+
262+
Parameters
263+
----------
264+
bbox: A tuple with the bounding box coordinates (xmin, ymin, xmax, ymax)
265+
"""
266+
self.image = self.image.crop(bbox)
267+
return self
268+
269+
def adjust_sharpness(self, factor: float) -> "Frame":
270+
"""Adjust the sharpness of the image
271+
272+
Parameters
273+
----------
274+
factor: The enhancement factor
275+
"""
276+
return self._apply_enhancement(ImageEnhance.Sharpness, factor)
277+
278+
def adjust_brightness(self, factor: float) -> "Frame":
279+
"""Adjust the brightness of the image
280+
281+
Parameters
282+
----------
283+
factor: The enhancement factor
284+
"""
285+
return self._apply_enhancement(ImageEnhance.Brightness, factor)
286+
287+
def adjust_contrast(self, factor: float) -> "Frame":
288+
"""Adjust the contrast of the image
289+
290+
Parameters
291+
----------
292+
factor: The enhancement factor
293+
"""
294+
return self._apply_enhancement(ImageEnhance.Contrast, factor)
295+
296+
def adjust_color(self, factor: float) -> "Frame":
297+
"""Adjust the color of the image
298+
299+
Parameters
300+
----------
301+
factor: The enhancement factor
302+
"""
303+
return self._apply_enhancement(ImageEnhance.Color, factor)
304+
305+
def _apply_enhancement(
306+
self, enhancement: Type[ImageEnhance._Enhance], factor: float
307+
) -> "Frame":
308+
enhancer = enhancement(self.image) # type: ignore
309+
self.image = enhancer.enhance(factor)
310+
return self
311+
259312
class Config:
260313
arbitrary_types_allowed = True
261314

@@ -380,6 +433,67 @@ def downsize(
380433
frame.downsize(width, height)
381434
return self
382435

436+
def crop(self, bbox: Tuple[int, int, int, int]) -> "FrameSet":
437+
"""Crop the images based on the bounding box
438+
439+
Parameters
440+
----------
441+
bbox: A tuple with the bounding box coordinates (xmin, ymin, xmax, ymax)
442+
"""
443+
for frame in self.frames:
444+
frame.crop(bbox)
445+
return self
446+
447+
def adjust_sharpness(self, factor: float) -> "FrameSet":
448+
"""Adjust the sharpness of the image
449+
450+
Parameters
451+
----------
452+
factor: The enhancement factor
453+
"""
454+
for f in self.frames:
455+
f.adjust_sharpness(factor)
456+
return self
457+
458+
def adjust_brightness(self, factor: float) -> "FrameSet":
459+
"""Adjust the brightness of the image
460+
461+
Parameters
462+
----------
463+
factor: The enhancement factor
464+
"""
465+
for f in self.frames:
466+
f.adjust_brightness(factor)
467+
return self
468+
469+
def adjust_contrast(self, factor: float) -> "FrameSet":
470+
"""Adjust the contrast of the image
471+
472+
Parameters
473+
----------
474+
factor: The enhancement factor
475+
"""
476+
for f in self.frames:
477+
f.adjust_contrast(factor)
478+
return self
479+
480+
def adjust_color(self, factor: float) -> "FrameSet":
481+
"""Adjust the color of the image
482+
483+
Parameters
484+
----------
485+
factor: The enhancement factor
486+
"""
487+
for f in self.frames:
488+
f.adjust_color(factor)
489+
return self
490+
491+
def copy(self, *args: Any, **kwargs: Any) -> "FrameSet":
492+
"""Returns a copy of this FrameSet, with all the frames copied"""
493+
frameset = super().copy(*args, **kwargs)
494+
frameset.frames = [frame.copy() for frame in self.frames]
495+
return frameset
496+
383497
def save_image(
384498
self,
385499
filename_prefix: str,
Loading
32.5 KB
Loading
35.2 KB
Loading
18.7 KB
Loading
34.2 KB
Loading

tests/unit/landingai/pipeline/test_frameset.py

Lines changed: 113 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from pathlib import Path
33
from unittest import mock
44

5+
from numpy.testing import assert_array_equal
56
from PIL import Image
67
import pytest
78

@@ -10,16 +11,18 @@
1011
from landingai.pipeline.frameset import FrameSet, Frame, PredictionList
1112

1213

13-
def get_frameset() -> FrameSet:
14-
return FrameSet.from_image("tests/data/images/cereal1.jpeg")
14+
def get_frameset(image_path: str = "tests/data/images/cereal1.jpeg") -> FrameSet:
15+
return FrameSet.from_image(image_path)
1516

1617

17-
def get_frame() -> Frame:
18-
return Frame.from_image("tests/data/images/cereal1.jpeg")
18+
def get_frame(image_path: str = "tests/data/images/cereal1.jpeg") -> Frame:
19+
return Frame.from_image(image_path)
1920

2021

21-
def get_frameset_with_od_coffee_prediction() -> FrameSet:
22-
frameset = get_frameset()
22+
def get_frameset_with_od_coffee_prediction(
23+
image_path: str = "tests/data/images/cereal1.jpeg",
24+
) -> FrameSet:
25+
frameset = get_frameset(image_path)
2326
frameset.frames[0].predictions = PredictionList(
2427
[
2528
ObjectDetectionPrediction(
@@ -34,8 +37,10 @@ def get_frameset_with_od_coffee_prediction() -> FrameSet:
3437
return frameset
3538

3639

37-
def get_frame_with_od_coffee_prediction() -> Frame:
38-
frame = get_frame()
40+
def get_frame_with_od_coffee_prediction(
41+
image_path: str = "tests/data/images/cereal1.jpeg",
42+
) -> Frame:
43+
frame = get_frame(image_path)
3944
frame.predictions = PredictionList(
4045
[
4146
ObjectDetectionPrediction(
@@ -234,6 +239,106 @@ def test_frameset_downsize_bigger_than_original(frame_getter):
234239
assert get_image_from_frame_or_frameset(frame).size == (width, height)
235240

236241

242+
@pytest.mark.parametrize(
243+
"frame_getter",
244+
[
245+
get_frameset_with_od_coffee_prediction,
246+
get_frame_with_od_coffee_prediction,
247+
],
248+
)
249+
def test_crop(frame_getter):
250+
frame = frame_getter()
251+
image = get_image_from_frame_or_frameset(frame)
252+
width, height = image.size
253+
# Crop 5px from each side
254+
frame.crop((5, 5, width - 5, height - 5))
255+
assert get_image_from_frame_or_frameset(frame).size == (width - 10, height - 10)
256+
257+
258+
@pytest.mark.parametrize(
259+
"frame_getter",
260+
[
261+
get_frameset_with_od_coffee_prediction,
262+
get_frame_with_od_coffee_prediction,
263+
],
264+
)
265+
@pytest.mark.parametrize(
266+
"enhancement",
267+
[
268+
("adjust_sharpness", 1.0),
269+
("adjust_brightness", 1.0),
270+
("adjust_contrast", 1.0),
271+
("adjust_color", 1.0),
272+
],
273+
)
274+
def test_enhancements_no_op(enhancement, frame_getter):
275+
"""Checks each enhancement method with it's no-op factor, making sure the frame
276+
will be exactly the same after the operation"""
277+
enhancement_method, enhancement_factor = enhancement
278+
frame = frame_getter()
279+
original_frame = frame.copy()
280+
getattr(frame, enhancement_method)(enhancement_factor)
281+
282+
original_image = get_image_from_frame_or_frameset(original_frame)
283+
image = get_image_from_frame_or_frameset(frame)
284+
assert original_image is not image
285+
assert_array_equal(original_image, image)
286+
287+
288+
@pytest.mark.parametrize(
289+
"frame_getter",
290+
[
291+
get_frameset_with_od_coffee_prediction,
292+
get_frame_with_od_coffee_prediction,
293+
],
294+
)
295+
@pytest.mark.parametrize(
296+
"enhancement",
297+
[
298+
("adjust_sharpness", 1.5, "sharpness-1.5.jpeg"),
299+
("adjust_brightness", 1.5, "brightness-1.5.jpeg"),
300+
("adjust_contrast", 1.5, "contrast-1.5.jpeg"),
301+
("adjust_color", 1.5, "color-1.5.jpeg"),
302+
],
303+
)
304+
def test_enhancements(enhancement, frame_getter):
305+
"""Checks each enhancement method with it's factor, making sure the frame
306+
wil be changed after the operation"""
307+
enhancement_method, enhancement_factor, expected_img_file = enhancement
308+
img_folder = "tests/data/images/cereal-tiny/"
309+
frame = frame_getter(image_path=f"{img_folder}/original.jpeg")
310+
original_frame = frame.copy()
311+
getattr(frame, enhancement_method)(enhancement_factor)
312+
313+
original_image = get_image_from_frame_or_frameset(original_frame)
314+
image = get_image_from_frame_or_frameset(frame)
315+
assert original_image is not image
316+
317+
expected_content = Image.open(f"{img_folder}/{expected_img_file}")
318+
assert_array_equal(image, expected_content)
319+
320+
321+
@pytest.mark.parametrize(
322+
"frame_getter",
323+
[
324+
get_frameset_with_od_coffee_prediction,
325+
get_frame_with_od_coffee_prediction,
326+
],
327+
)
328+
def test_copy_operation(frame_getter):
329+
frame = frame_getter()
330+
width, height = get_image_from_frame_or_frameset(frame).size
331+
new_frame = frame.copy()
332+
# Do any operation on the new frame, so we can test if the original frame is preserved
333+
new_frame.crop((0, 0, 1, 1))
334+
335+
original_image = get_image_from_frame_or_frameset(frame)
336+
new_image = get_image_from_frame_or_frameset(new_frame)
337+
assert frame.predictions == new_frame.predictions
338+
assert original_image is not new_image
339+
assert original_image.size == (width, height)
340+
341+
237342
@pytest.mark.parametrize(
238343
"frame_getter",
239344
[

0 commit comments

Comments
 (0)