Skip to content

Commit ae2a495

Browse files
Merge branch 'main' into feature/sidebar-ide-style
2 parents 507643a + be49940 commit ae2a495

7 files changed

Lines changed: 32 additions & 29 deletions

File tree

imagelab-backend/app/operators/conversions/gray_to_binary.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
class GrayToBinary(BaseOperator):
88
def compute(self, image: np.ndarray) -> np.ndarray:
9-
threshold_value = float(self.params.get("thresholdValue", 0))
9+
threshold_value = float(self.params.get("thresholdValue", 127))
1010
max_value = float(self.params.get("maxValue", 255))
1111
_, dst = cv2.threshold(image, threshold_value, max_value, cv2.THRESH_BINARY)
1212
return dst

imagelab-backend/app/services/pipeline_executor.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ def execute_pipeline(request: PipelineRequest) -> PipelineResponse:
4343
t_fail = time.perf_counter()
4444
return PipelineResponse(
4545
success=False,
46-
error=f"Unknown operator '{step.type}' at step {i}",
47-
step=i,
46+
error=f"Unknown operator '{step.type}' at step {i + 1}",
47+
step=i + 1,
4848
timings=PipelineTimings(total_ms=(t_fail - t_start_total) * 1000, steps=step_timings),
4949
)
5050

@@ -60,8 +60,8 @@ def execute_pipeline(request: PipelineRequest) -> PipelineResponse:
6060
t_fail = time.perf_counter()
6161
return PipelineResponse(
6262
success=False,
63-
error=f"Error in step {i} ({step.type}): {type(e).__name__}: {e}",
64-
step=i,
63+
error=f"Error in step {i + 1} ({step.type}): {type(e).__name__}: {e}",
64+
step=i + 1,
6565
timings=PipelineTimings(total_ms=(t_fail - t_start_total) * 1000, steps=step_timings),
6666
)
6767

imagelab-backend/tests/operators/filtering/test_contour_detection.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,11 @@ def test_contour_detection_supports_gray_bgr_bgra(channels):
5050

5151
result = op.compute(image.copy())
5252

53-
if channels == 4:
54-
assert result.shape == (100, 100, 3) # BGRA -> BGR conversion path
53+
if channels == 1:
54+
# Grayscale input is promoted to BGR so the contour can be drawn in colour.
55+
assert result.shape == (100, 100, 3)
5556
else:
57+
# BGR (3) and BGRA (4) inputs keep their channel count.
5658
assert result.shape == image.shape
5759

5860

imagelab-backend/tests/test_blurring_operators.py

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import numpy as np
2+
import pytest
23

34
from app.operators.blurring.blur import Blur
45
from app.operators.blurring.gaussian_blur import GaussianBlur
@@ -56,11 +57,10 @@ def test_custom_params_output_shape(self, color_image):
5657
result = GaussianBlur({"widthSize": 5, "heightSize": 5}).compute(color_image)
5758
assert result.shape == color_image.shape
5859

59-
def test_even_kernel_corrected_to_odd(self, color_image):
60-
result_even = GaussianBlur({"widthSize": 4, "heightSize": 4}).compute(color_image)
61-
result_odd = GaussianBlur({"widthSize": 5, "heightSize": 5}).compute(color_image)
62-
assert result_even.shape == color_image.shape
63-
np.testing.assert_array_equal(result_even, result_odd)
60+
def test_even_kernel_raises_value_error(self, color_image):
61+
# Validator rejects even kernels with a "Did you mean 3 or 5?" message.
62+
with pytest.raises(ValueError, match="odd"):
63+
GaussianBlur({"widthSize": 4, "heightSize": 4}).compute(color_image)
6464

6565
def test_grayscale_input(self, grayscale_image):
6666
result = GaussianBlur({}).compute(grayscale_image)
@@ -102,11 +102,10 @@ def test_custom_params_output_shape(self, color_image):
102102
result = MedianBlur({"kernelSize": 5}).compute(color_image)
103103
assert result.shape == color_image.shape
104104

105-
def test_even_kernel_corrected_to_odd(self, color_image):
106-
result_even = MedianBlur({"kernelSize": 4}).compute(color_image)
107-
result_odd = MedianBlur({"kernelSize": 5}).compute(color_image)
108-
assert result_even.shape == color_image.shape
109-
np.testing.assert_array_equal(result_even, result_odd)
105+
def test_even_kernel_raises_value_error(self, color_image):
106+
# Validator rejects even kernels with a "Did you mean 3 or 5?" message.
107+
with pytest.raises(ValueError, match="odd"):
108+
MedianBlur({"kernelSize": 4}).compute(color_image)
110109

111110
def test_grayscale_input(self, grayscale_image):
112111
result = MedianBlur({}).compute(grayscale_image)
@@ -129,9 +128,11 @@ def test_median_blur_removes_salt_pepper_noise(self):
129128
np.abs(result.astype(int) - clean.astype(int)).mean() < np.abs(noisy.astype(int) - clean.astype(int)).mean()
130129
)
131130

132-
def test_kernel_size_1_is_identity(self, color_image):
133-
result = MedianBlur({"kernelSize": 1}).compute(color_image)
134-
np.testing.assert_array_equal(result, color_image)
131+
def test_kernel_size_1_rejected(self, color_image):
132+
# MedianBlur's validator deliberately forbids ksize=1 (a no-op) even though
133+
# OpenCV would accept it. See app/operators/blurring/validation.py.
134+
with pytest.raises(ValueError, match=">= 3"):
135+
MedianBlur({"kernelSize": 1}).compute(color_image)
135136

136137
def test_small_image_no_crash(self):
137138
img = np.zeros((3, 3, 3), dtype=np.uint8)

imagelab-backend/tests/test_pipeline_executor.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ def test_multi_step_pipeline(make_request, sample_image_b64):
4545
def test_unknown_operator_gives_clear_error(make_request):
4646
res = execute_pipeline(make_request([PipelineStep(type="not_a_real_op")]))
4747
assert res.success is False
48-
assert res.step == 0
49-
assert "at step 0" in res.error
48+
assert res.step == 1
49+
assert "at step 1" in res.error
5050
assert "not_a_real_op" in res.error
5151
assert "Unknown operator" in res.error
5252

@@ -58,8 +58,8 @@ def test_error_includes_correct_step_index(make_request):
5858
]
5959
res = execute_pipeline(make_request(steps))
6060
assert res.success is False
61-
assert res.step == 1 # first step succeeds, second should fail
62-
assert "at step 1" in res.error
61+
assert res.step == 2 # first step succeeds, second (1-indexed: 2) should fail
62+
assert "at step 2" in res.error
6363
assert "bad_operator_step_one" in res.error
6464

6565

imagelab-backend/tests/test_thresholding_operators.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import numpy as np
2-
import pytest
32

43
from app.operators.thresholding.adaptive_threshold import AdaptiveThreshold
54
from app.operators.thresholding.apply_threshold import ApplyThreshold
@@ -15,9 +14,6 @@ def test_output_is_uint8(self, color_image):
1514
result = ApplyThreshold({}).compute(color_image)
1615
assert result.dtype == np.uint8
1716

18-
@pytest.mark.xfail(
19-
strict=True, reason="maxValue defaults to 0 — known bug, fix in fix/apply-threshold-default-max-value"
20-
)
2117
def test_default_params_produces_non_empty_output(self, color_image):
2218
result = ApplyThreshold({}).compute(color_image)
2319
assert result.max() > 0

imagelab-frontend/src/hooks/useBlocklyWorkspace.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
/* eslint-disable react-hooks/preserve-manual-memoization */
21
import { useRef, useEffect, useState, useCallback } from "react";
32
import * as Blockly from "blockly";
43

@@ -150,6 +149,11 @@ export function useBlocklyWorkspace({ isDark = false }: UseBlocklyWorkspaceOptio
150149
resizeObserverRef.current = observer;
151150
}
152151
updateBlockStats(ws); // Initial stats calculation if any blocks loaded
152+
// isDark is intentionally omitted from deps: initWorkspace is a one-shot
153+
// initializer (guarded by workspaceRef.current). Live theme toggles are
154+
// handled by the setTheme useEffect above, so adding isDark here would
155+
// dispose and recreate the workspace on every toggle, losing user blocks.
156+
// eslint-disable-next-line react-hooks/exhaustive-deps
153157
}, [setSelectedBlock, updateBlockStats]);
154158

155159
useEffect(() => {

0 commit comments

Comments
 (0)