Skip to content

Commit 3beb047

Browse files
chetanbarakkiivantha
authored andcommitted
feature: implement brightness and contrast operator
1 parent 63e1a37 commit 3beb047

9 files changed

Lines changed: 218 additions & 12 deletions

File tree

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import numpy as np
2+
3+
from app.operators.base import BaseOperator
4+
5+
6+
class BrightnessAndContrast(BaseOperator):
7+
"""
8+
Adjusts the brightness and contrast of an image.
9+
10+
Params:
11+
brightnessValue (int): Additive brightness offset [-100, 100]. Clamped if out of range.
12+
contrastValue (float): Multiplicative contrast scale [0.0, 3.0]. 1.0 = no change.
13+
"""
14+
15+
def compute(self, image: np.ndarray) -> np.ndarray:
16+
"""
17+
Apply brightness and contrast adjustment.
18+
19+
Args:
20+
image: Input uint8 NumPy ndarray (grayscale or multi-channel).
21+
22+
Returns:
23+
Adjusted image as a uint8 NumPy ndarray of the same shape.
24+
"""
25+
if image is None or not isinstance(image, np.ndarray) or image.size == 0:
26+
raise ValueError("BrightnessAndContrast.compute: received an invalid or empty image")
27+
try:
28+
brightness = int(self.params.get("brightnessValue", 0))
29+
except (TypeError, ValueError):
30+
brightness = 0
31+
try:
32+
contrast = float(self.params.get("contrastValue", 1.0))
33+
except (TypeError, ValueError):
34+
contrast = 1.0
35+
36+
# Ensure brightness and contrast values are within valid ranges
37+
# Clamp to match frontend field_number constraints in conversions.blocks.ts
38+
brightness = max(-100, min(100, brightness))
39+
contrast = max(0.0, min(3.0, contrast))
40+
41+
adjusted = contrast * (image.astype(np.float32) - 128.0) + 128.0 + brightness
42+
return np.clip(adjusted, 0, 255).astype(np.uint8)

imagelab-backend/app/operators/registry.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from app.operators.conversions.bgr_to_hsv import BgrToHsv
1111
from app.operators.conversions.bgr_to_lab import BgrToLab
1212
from app.operators.conversions.bgr_to_ycrcb import BgrToYcrcb
13+
from app.operators.conversions.brightness_and_contrast import BrightnessAndContrast
1314
from app.operators.conversions.channel_split import ChannelSplit
1415
from app.operators.conversions.clahe import claheImage
1516
from app.operators.conversions.color_maps import ColorMaps
@@ -79,6 +80,7 @@
7980
"imageconvertions_bgrtoycrcb": BgrToYcrcb,
8081
"imageconvertions_ycrcbtobgr": YcrcbToBgr,
8182
"imageconvertions_invertimage": InvertImage,
83+
"imageconvertions_brightnessandcontrast": BrightnessAndContrast,
8284
# Drawing
8385
"drawingoperations_drawline": DrawLine,
8486
"drawingoperations_drawcircle": DrawCircle,

imagelab-backend/app/utils/color.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import re
22

3-
43
HEX_COLOR_RE = re.compile(r"^[0-9a-fA-F]{6}$")
54

65

@@ -19,17 +18,13 @@ def hex_to_bgr(hex_color: str) -> tuple[int, int, int]:
1918
ValueError: If hex_color is not a valid 6-digit hex color string.
2019
"""
2120
if not isinstance(hex_color, str):
22-
raise TypeError(
23-
f"hex_to_bgr expects a str, got {type(hex_color).__name__!r}"
24-
)
21+
raise TypeError(f"hex_to_bgr expects a str, got {type(hex_color).__name__!r}")
2522

2623
original = hex_color
2724
normalized = hex_color.removeprefix("#")
2825

2926
if not HEX_COLOR_RE.fullmatch(normalized):
30-
raise ValueError(
31-
f"Invalid hex color: {original!r}. Expected format '#rrggbb' or 'rrggbb'."
32-
)
27+
raise ValueError(f"Invalid hex color: {original!r}. Expected format '#rrggbb' or 'rrggbb'.")
3328

3429
r = int(normalized[0:2], 16)
3530
g = int(normalized[2:4], 16)

imagelab-backend/tests/test_filtering_operators.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,7 @@ def test_asymmetric_kernel_uses_width_height_convention(self):
8080
image = np.arange(75, dtype=np.uint8).reshape(5, 5, 3)
8181
width, height = 1, 5
8282

83-
result = BoxFilter({"width": width, "height": height, "depth": -1}).compute(
84-
image
85-
)
83+
result = BoxFilter({"width": width, "height": height, "depth": -1}).compute(image)
8684
expected = cv2.boxFilter(
8785
image,
8886
-1,

imagelab-frontend/eslint.config.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,8 @@ export default defineConfig([
1919
ecmaVersion: 2020,
2020
globals: globals.browser,
2121
},
22+
rules: {
23+
'no-delete-var': 'off',
24+
},
2225
},
2326
])

imagelab-frontend/package-lock.json

Lines changed: 150 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

imagelab-frontend/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
"devDependencies": {
2828
"@eslint/js": "^9.39.1",
2929
"@tailwindcss/vite": "^4.2.0",
30+
"@testing-library/dom": "^10.4.1",
31+
"@testing-library/react": "^16.3.2",
3032
"@types/node": "^24.10.1",
3133
"@types/react": "^19.2.14",
3234
"@types/react-dom": "^19.2.3",
@@ -42,4 +44,4 @@
4244
"vite": "^7.3.1",
4345
"vitest": "~4.0.17"
4446
}
45-
}
47+
}

imagelab-frontend/src/blocks/categories.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export const categories: CategoryInfo[] = [
5151
{ type: "imageconvertions_bgrtoycrcb", label: "BGR to YCrCb" },
5252
{ type: "imageconvertions_ycrcbtobgr", label: "YCrCb to BGR" },
5353
{ type: "imageconvertions_invertimage", label: "Invert Image" },
54+
{ type: "imageconvertions_brightnessandcontrast", label: "Brightness and Contrast" },
5455
],
5556
},
5657
{

imagelab-frontend/src/blocks/definitions/conversions.blocks.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,20 @@ export const conversionsBlocks = [
9090
tooltip:
9191
"Apply different color maps to an image - Transforms the colors of an image using various color maps. This can enhance visual contrast and highlight specific features. For example, the 'JET' colormap transitions from blue to red, while 'HSV' represents hue, saturation, and value. Choose a colormap that best suits your image analysis needs.",
9292
},
93+
{
94+
type: "imageconvertions_brightnessandcontrast",
95+
message0: "Adjust image brightness by %1 and contrast scale by %2",
96+
args0: [
97+
{ type: "field_number", name: "brightnessValue", value: 0, min: -100, max: 100 },
98+
{ type: "field_number", name: "contrastValue", value: 1.0, min: 0.0, max: 3.0 },
99+
],
100+
inputsInline: false,
101+
previousStatement: null,
102+
nextStatement: null,
103+
style: "conversions_style",
104+
tooltip:
105+
"Adjusts the brightness and contrast of an image - Brightness controls the overall lightness or darkness of an image, while contrast controls the difference in intensity between different parts of the image. Increasing brightness makes the image lighter, and increasing contrast makes the differences between light and dark areas more pronounced.",
106+
},
93107
{
94108
type: "imageconvertions_colortobinary",
95109
message0:

0 commit comments

Comments
 (0)