Skip to content

Commit 26be867

Browse files
committed
support outline thiness via client settings
1 parent 1c9b3be commit 26be867

File tree

9 files changed

+129
-27
lines changed

9 files changed

+129
-27
lines changed

README.md

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -198,12 +198,23 @@ This command discovers and runs all test files (`test_*.py`) in the `server/test
198198
3. The annotations and other interactions will be handled by the Flask server running at [http://localhost:5000](http://localhost:5000).
199199

200200
## Configurations (Optional)
201-
You can customize some aspects of Annotate-Lab through configuration settings.
202-
To do this, modify the `config.py` file in the `server` directory:
201+
You can customize various aspects of Annotate-Lab through configuration settings. To do this, modify the `config.py` file in the `server` directory or the `config.js` file in the `client` directory.
203202
```python
204203
# config.py
205204
MASK_BACKGROUND_COLOR = (0, 0, 0) # Black background for masks
206-
OUTLINE_THICKNESS = 5 # Thicker outlines (5 pixels)
205+
```
206+
207+
```Javascript
208+
# config.js
209+
const config = {
210+
SERVER_URL, # url of server
211+
UPLOAD_LIMIT: 5, # image upload limit
212+
OUTLINE_THICKNESS_CONFIG : { # outline thickness of tools
213+
POLYGON: 2,
214+
CIRCLE: 2,
215+
BOUNDING_BOX: 2
216+
}
217+
};
207218
```
208219

209220
## Outputs

client/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@
7373
"semi": false
7474
},
7575
"jest":{
76-
"testEnvironment": "jsdom"
76+
"testEnvironment": "jsdom",
77+
"moduleNameMapper": {
78+
"^color-alpha$": "<rootDir>/node_modules/color-alpha"
79+
}
7780
}
7881
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import React from 'react';
2+
import { render } from '@testing-library/react';
3+
import RegionShapes, { WrappedRegionList, getStrokeWidth } from './index';
4+
import '@testing-library/jest-dom'
5+
6+
jest.mock('color-alpha', () => jest.fn((color, alpha) => `rgba(${color},${alpha})`));
7+
jest.mock('../config', () => ({
8+
OUTLINE_THICKNESS_CONFIG: {
9+
POLYGON: 3,
10+
CIRCLE: 2,
11+
BOUNDING_BOX: 2,
12+
},
13+
}));
14+
15+
16+
const mockRegions = [
17+
{ type: 'box', x: 0.1, y: 0.1, w: 0.5, h: 0.5, color: 'red', id: '1' },
18+
{ type: 'circle', x: 0.2, y: 0.2, w: 0.3, h: 0.3, color: 'blue', id: '2' },
19+
{ type: 'polygon', points: [[0.1, 0.1], [0.2, 0.2], [0.3, 0.1]], color: 'green', id: '3' },
20+
];
21+
22+
describe('RegionShapes Component', () => {
23+
it('renders without crashing', () => {
24+
const imagePosition = { topLeft: { x: 0, y: 0 }, bottomRight: { x: 100, y: 100 } };
25+
const { container } = render(
26+
<RegionShapes
27+
mat={null}
28+
imagePosition={imagePosition}
29+
regions={mockRegions}
30+
keypointDefinitions={{}}
31+
fullSegmentationMode={false}
32+
/>
33+
);
34+
expect(container).toBeInTheDocument();
35+
});
36+
37+
it('renders the correct number of region components', () => {
38+
const imagePosition = { topLeft: { x: 0, y: 0 }, bottomRight: { x: 100, y: 100 } };
39+
const { container } = render(
40+
<RegionShapes
41+
mat={null}
42+
imagePosition={imagePosition}
43+
regions={mockRegions}
44+
keypointDefinitions={{}}
45+
fullSegmentationMode={false}
46+
/>
47+
);
48+
const boxes = container.querySelectorAll('rect');
49+
const circles = container.querySelectorAll('ellipse');
50+
const polygons = container.querySelectorAll('polygon');
51+
expect(boxes.length).toBe(1);
52+
expect(circles.length).toBe(1);
53+
expect(polygons.length).toBe(1);
54+
});
55+
});
56+
57+
describe('WrappedRegionList Component', () => {
58+
it('renders without crashing', () => {
59+
const { container } = render(
60+
<WrappedRegionList
61+
regions={mockRegions}
62+
iw={100}
63+
ih={100}
64+
keypointDefinitions={{}}
65+
fullSegmentationMode={false}
66+
/>
67+
);
68+
expect(container).toBeInTheDocument();
69+
});
70+
71+
it('applies the correct stroke width from config', () => {
72+
expect(getStrokeWidth({ type: 'box' })).toBe(2);
73+
expect(getStrokeWidth({ type: 'circle' })).toBe(2);
74+
expect(getStrokeWidth({ type: 'polygon' })).toBe(3);
75+
expect(getStrokeWidth({ type: 'line' })).toBe(2); // Default case
76+
});
77+
});

client/src/RegionShapes/index.jsx

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import React, {memo} from "react"
44
import colorAlpha from "color-alpha"
55
import clamp from "../utils/clamp"
6+
import config from "../config.js"
67

78
const RegionComponents = {
89
point: memo(({region, iw, ih}) => (
@@ -15,12 +16,12 @@ const RegionComponents = {
1516
/>
1617
</g>
1718
)),
18-
line: memo(({region, iw, ih}) => {
19+
line: memo(({region, iw, ih, strokeWidth}) => {
1920
return (
2021
<g transform={`translate(${region.x1 * iw} ${region.y1 * ih})`}>
2122
<path
2223
id={region.id}
23-
strokeWidth={3}
24+
strokeWidth={strokeWidth}
2425
d={`M0,0 L${(region.x2 - region.x1) * iw},${(region.y2 - region.y1) * ih}`}
2526
stroke={colorAlpha(region.color, 0.9)}
2627
fill={colorAlpha(region.color, 0.25)}
@@ -33,10 +34,10 @@ const RegionComponents = {
3334
</text>
3435
</g>
3536
)}),
36-
box: memo(({region, iw, ih}) => (
37+
box: memo(({region, iw, ih, strokeWidth}) => (
3738
<g transform={`translate(${region.x * iw} ${region.y * ih})`}>
3839
<rect
39-
strokeWidth={2}
40+
strokeWidth={strokeWidth}
4041
x={0}
4142
y={0}
4243
width={Math.max(region.w * iw, 0)}
@@ -46,10 +47,10 @@ const RegionComponents = {
4647
/>
4748
</g>
4849
)),
49-
circle: memo(({ region, iw, ih }) => (
50+
circle: memo(({ region, iw, ih , strokeWidth}) => (
5051
<g transform={`translate(${region.x * iw} ${region.y * ih})`}>
5152
<ellipse
52-
strokeWidth={2}
53+
strokeWidth={strokeWidth}
5354
cx={Math.max(region.w * iw / 2, 0)}
5455
cy={Math.max(region.h * ih / 2, 0)}
5556
rx={Math.max(region.w * iw / 2, 0)}
@@ -59,15 +60,15 @@ const RegionComponents = {
5960
/>
6061
</g>
6162
)),
62-
polygon: memo(({region, iw, ih, fullSegmentationMode}) => {
63+
polygon: memo(({region, iw, ih, strokeWidth, fullSegmentationMode}) => {
6364
const Component = region.open ? "polyline" : "polygon"
6465
return (
6566
<Component
6667
points={region.points
6768
.map(([x, y]) => [x * iw, y * ih])
6869
.map((a) => a.join(" "))
6970
.join(" ")}
70-
strokeWidth={2}
71+
strokeWidth={strokeWidth}
7172
stroke={colorAlpha(region.color, 0.75)}
7273
fill={colorAlpha(region.color, 0.25)}
7374
/>
@@ -189,6 +190,13 @@ const RegionComponents = {
189190
pixel: () => null,
190191
}
191192

193+
export const getStrokeWidth = (region) => {
194+
const { type } = region;
195+
if(type === 'box') {
196+
return config.OUTLINE_THICKNESS_CONFIG.BOUNDING_BOX || 2;
197+
}
198+
return config.OUTLINE_THICKNESS_CONFIG[type.toUpperCase()] || 2;
199+
};
192200
export const WrappedRegionList = memo(
193201
({regions, keypointDefinitions, iw, ih, fullSegmentationMode}) => {
194202
return regions
@@ -201,13 +209,14 @@ export const WrappedRegionList = memo(
201209
region={r}
202210
iw={iw}
203211
ih={ih}
212+
strokeWidth = {getStrokeWidth(r)}
204213
keypointDefinitions={keypointDefinitions}
205214
fullSegmentationMode={fullSegmentationMode}
206215
/>
207216
)
208217
})
209218
},
210-
(n, p) => n.regions === p.regions && n.iw === p.iw && n.ih === p.ih
219+
(n, p) => n.regions === p.regions && n.iw === p.iw && n.ih === p.ih && n.strokeWidth === p.strokeWidth
211220
)
212221

213222
export const RegionShapes = ({

client/src/SettingsProvider/SettingsProvider.test.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,6 @@ test('should change setting and update state', () => {
5757
const TestComponent = () => {
5858
const settings = React.useContext(SettingsContext);
5959

60-
console.log(settings, 'masimis'); // Check what 'settings' actually contains
61-
6260
return (
6361
<div>
6462
<span data-testid="showCrosshairs">{settings && settings.showCrosshairs?.toString()}</span>

client/src/config.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ const config = {
1111
DOCS_URL:"https://annotate-docs.dwaste.live/",
1212
SERVER_URL,
1313
UPLOAD_LIMIT: 5,
14+
OUTLINE_THICKNESS_CONFIG : {
15+
POLYGON: 2,
16+
CIRCLE: 2,
17+
BOUNDING_BOX: 2
18+
}
1419
};
1520

1621
export default config;

client/src/workspace/DownloadButton/index.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { useSnackbar} from "../../SnackbarContext/index.jsx"
1111
import { hexToRgbTuple } from "../../utils/color-utils.js";
1212
import HeaderButton from "../HeaderButton/index.jsx";
1313
import { useTranslation } from "react-i18next"
14+
import config from "../../config.js";
1415

1516
const DownloadButton = ({selectedImageName, classList, hideHeaderText, disabled}) => {
1617
const [anchorEl, setAnchorEl] = useState(null);
@@ -34,6 +35,7 @@ const DownloadButton = ({selectedImageName, classList, hideHeaderText, disabled}
3435
const config_data = {}
3536
config_data['image_name'] = selectedImageName
3637
config_data['colorMap'] = classColorMap
38+
config_data['outlineThickness'] = config.OUTLINE_THICKNESS_CONFIG
3739
let url = ""
3840
switch (format) {
3941
case "configuration":

server/app.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,7 @@ def download_image_with_annotations():
275275
images = json.loads(json_str).get("configuration", [])
276276

277277
color_map = data.get("colorMap", {})
278+
outlineThickness = data.get("outlineThickness", {})
278279

279280
# Convert color map values to tuples
280281
for key in color_map.keys():
@@ -295,7 +296,7 @@ def download_image_with_annotations():
295296
points = region['points']
296297
scaled_points = [(x * width, y * height) for x, y in points]
297298
# Draw polygon with thicker outline
298-
draw.line(scaled_points + [scaled_points[0]], fill=color, width=app.config['OUTLINE_THICKNESS_CONFIG']['POLYGON']) # Change width as desired
299+
draw.line(scaled_points + [scaled_points[0]], fill=color, width=outlineThickness.get('POLYGON', 2)) # Change width as desired
299300
elif all(key in region for key in ('x', 'y', 'w', 'h')):
300301
try:
301302
x = float(region['x'][1:-1]) * width if isinstance(region['x'], str) else float(region['x'][0]) * width
@@ -305,7 +306,7 @@ def download_image_with_annotations():
305306
except (ValueError, TypeError) as e:
306307
raise ValueError(f"Invalid format in region dimensions: {region}, Error: {e}")
307308
# Draw rectangle with thicker outline
308-
draw.rectangle([x, y, x + w, y + h], outline=color, width=app.config['OUTLINE_THICKNESS_CONFIG']['BOUNDING_BOX'])
309+
draw.rectangle([x, y, x + w, y + h], outline=color, width=outlineThickness.get('BOUNDING_BOX', 2))
309310
elif all(key in region for key in ('rx', 'ry', 'rw', 'rh')):
310311
try:
311312
rx = float(region['rx'][1:-1]) * width if isinstance(region['rx'], str) else float(region['rx'][0]) * width
@@ -315,7 +316,7 @@ def download_image_with_annotations():
315316
except (ValueError, TypeError) as e:
316317
raise ValueError(f"Invalid format in region dimensions: {region}, Error: {e}")
317318
# Draw ellipse (circle if rw and rh are equal)
318-
draw.ellipse([rx, ry, rx + rw, ry + rh], outline=color, width=app.config['OUTLINE_THICKNESS_CONFIG']['CIRCLE'])
319+
draw.ellipse([rx, ry, rx + rw, ry + rh], outline=color, width=outlineThickness.get('CIRCLE', 2))
319320

320321

321322

@@ -356,6 +357,7 @@ def download_image_mask():
356357
images = json.loads(json_str).get("configuration", [])
357358

358359
color_map = data.get("colorMap", {})
360+
outlineThickness = data.get("outlineThickness", {})
359361

360362
# Convert color map values to tuples
361363
for key in color_map.keys():
@@ -375,7 +377,7 @@ def download_image_mask():
375377
if 'points' in region and region['points']:
376378
points = region['points']
377379
scaled_points = [(int(x * width), int(y * height)) for x, y in points]
378-
draw.polygon(scaled_points, outline=color, fill=color, width=app.config['OUTLINE_THICKNESS_CONFIG']['POLYGON'])
380+
draw.polygon(scaled_points, outline=color, fill=color, width=outlineThickness.get('POLYGON', 2))
379381
elif all(key in region for key in ('x', 'y', 'w', 'h')):
380382
try:
381383
x = float(region['x'][1:-1]) * width if isinstance(region['x'], str) else float(region['x'][0]) * width
@@ -385,7 +387,7 @@ def download_image_mask():
385387
except (ValueError, TypeError) as e:
386388
raise ValueError(f"Invalid format in region dimensions: {region}, Error: {e}")
387389
# Draw rectangle for bounding box
388-
draw.rectangle([x, y, x + w, y + h], outline=color, fill=color, width=app.config['OUTLINE_THICKNESS_CONFIG']['BOUNDING_BOX'])
390+
draw.rectangle([x, y, x + w, y + h], outline=color, fill=color, width=outlineThickness.get('BOUNDING_BOX', 2))
389391
elif all(key in region for key in ('rx', 'ry', 'rw', 'rh')):
390392
try:
391393
rx = float(region['rx'][1:-1]) * width if isinstance(region['rx'], str) else float(region['rx'][0]) * width
@@ -395,7 +397,7 @@ def download_image_mask():
395397
except (ValueError, TypeError) as e:
396398
raise ValueError(f"Invalid format in region dimensions: {region}, Error: {e}")
397399
# Draw ellipse (circle if rw and rh are equal)
398-
draw.ellipse([rx, ry, rx + rw, ry + rh], outline=color, width=app.config['OUTLINE_THICKNESS_CONFIG']['CIRCLE'], fill=color)
400+
draw.ellipse([rx, ry, rx + rw, ry + rh], outline=color,width=outlineThickness.get('CIRCLE', 2), fill=color)
399401

400402
mask_byte_arr = BytesIO()
401403
mask.save(mask_byte_arr, format='PNG')

server/config.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1 @@
1-
MASK_BACKGROUND_COLOR = (0, 0, 0)
2-
OUTLINE_THICKNESS_CONFIG = {
3-
"POLYGON": 3,
4-
"CIRCLE": 3,
5-
"BOUNDING_BOX": 3
6-
} # change outline thickness (currently only for downloaded files)
1+
MASK_BACKGROUND_COLOR = (0, 0, 0)

0 commit comments

Comments
 (0)