Skip to content

support outline thickness via client settings #58

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,12 +198,23 @@ This command discovers and runs all test files (`test_*.py`) in the `server/test
3. The annotations and other interactions will be handled by the Flask server running at [http://localhost:5000](http://localhost:5000).

## Configurations (Optional)
You can customize some aspects of Annotate-Lab through configuration settings.
To do this, modify the `config.py` file in the `server` directory:
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.
```python
# config.py
MASK_BACKGROUND_COLOR = (0, 0, 0) # Black background for masks
OUTLINE_THICKNESS = 5 # Thicker outlines (5 pixels)
```

```Javascript
# config.js
const config = {
SERVER_URL, # url of server
UPLOAD_LIMIT: 5, # image upload limit
OUTLINE_THICKNESS_CONFIG : { # outline thickness of tools
POLYGON: 2,
CIRCLE: 2,
BOUNDING_BOX: 2
}
};
```

## Outputs
Expand Down
5 changes: 4 additions & 1 deletion client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@
"semi": false
},
"jest":{
"testEnvironment": "jsdom"
"testEnvironment": "jsdom",
"moduleNameMapper": {
"^color-alpha$": "<rootDir>/node_modules/color-alpha"
}
}
}
77 changes: 77 additions & 0 deletions client/src/RegionShapes/RegionShapes.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import React from 'react';
import { render } from '@testing-library/react';
import RegionShapes, { WrappedRegionList, getStrokeWidth } from './index';
import '@testing-library/jest-dom'

jest.mock('color-alpha', () => jest.fn((color, alpha) => `rgba(${color},${alpha})`));
jest.mock('../config', () => ({
OUTLINE_THICKNESS_CONFIG: {
POLYGON: 3,
CIRCLE: 2,
BOUNDING_BOX: 2,
},
}));


const mockRegions = [
{ type: 'box', x: 0.1, y: 0.1, w: 0.5, h: 0.5, color: 'red', id: '1' },
{ type: 'circle', x: 0.2, y: 0.2, w: 0.3, h: 0.3, color: 'blue', id: '2' },
{ type: 'polygon', points: [[0.1, 0.1], [0.2, 0.2], [0.3, 0.1]], color: 'green', id: '3' },
];

describe('RegionShapes Component', () => {
it('renders without crashing', () => {
const imagePosition = { topLeft: { x: 0, y: 0 }, bottomRight: { x: 100, y: 100 } };
const { container } = render(
<RegionShapes
mat={null}
imagePosition={imagePosition}
regions={mockRegions}
keypointDefinitions={{}}
fullSegmentationMode={false}
/>
);
expect(container).toBeInTheDocument();
});

it('renders the correct number of region components', () => {
const imagePosition = { topLeft: { x: 0, y: 0 }, bottomRight: { x: 100, y: 100 } };
const { container } = render(
<RegionShapes
mat={null}
imagePosition={imagePosition}
regions={mockRegions}
keypointDefinitions={{}}
fullSegmentationMode={false}
/>
);
const boxes = container.querySelectorAll('rect');
const circles = container.querySelectorAll('ellipse');
const polygons = container.querySelectorAll('polygon');
expect(boxes.length).toBe(1);
expect(circles.length).toBe(1);
expect(polygons.length).toBe(1);
});
});

describe('WrappedRegionList Component', () => {
it('renders without crashing', () => {
const { container } = render(
<WrappedRegionList
regions={mockRegions}
iw={100}
ih={100}
keypointDefinitions={{}}
fullSegmentationMode={false}
/>
);
expect(container).toBeInTheDocument();
});

it('applies the correct stroke width from config', () => {
expect(getStrokeWidth({ type: 'box' })).toBe(2);
expect(getStrokeWidth({ type: 'circle' })).toBe(2);
expect(getStrokeWidth({ type: 'polygon' })).toBe(3);
expect(getStrokeWidth({ type: 'line' })).toBe(2); // Default case
});
});
27 changes: 18 additions & 9 deletions client/src/RegionShapes/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import React, {memo} from "react"
import colorAlpha from "color-alpha"
import clamp from "../utils/clamp"
import config from "../config.js"

const RegionComponents = {
point: memo(({region, iw, ih}) => (
Expand All @@ -15,12 +16,12 @@ const RegionComponents = {
/>
</g>
)),
line: memo(({region, iw, ih}) => {
line: memo(({region, iw, ih, strokeWidth}) => {
return (
<g transform={`translate(${region.x1 * iw} ${region.y1 * ih})`}>
<path
id={region.id}
strokeWidth={3}
strokeWidth={strokeWidth}
d={`M0,0 L${(region.x2 - region.x1) * iw},${(region.y2 - region.y1) * ih}`}
stroke={colorAlpha(region.color, 0.9)}
fill={colorAlpha(region.color, 0.25)}
Expand All @@ -33,10 +34,10 @@ const RegionComponents = {
</text>
</g>
)}),
box: memo(({region, iw, ih}) => (
box: memo(({region, iw, ih, strokeWidth}) => (
<g transform={`translate(${region.x * iw} ${region.y * ih})`}>
<rect
strokeWidth={2}
strokeWidth={strokeWidth}
x={0}
y={0}
width={Math.max(region.w * iw, 0)}
Expand All @@ -46,10 +47,10 @@ const RegionComponents = {
/>
</g>
)),
circle: memo(({ region, iw, ih }) => (
circle: memo(({ region, iw, ih , strokeWidth}) => (
<g transform={`translate(${region.x * iw} ${region.y * ih})`}>
<ellipse
strokeWidth={2}
strokeWidth={strokeWidth}
cx={Math.max(region.w * iw / 2, 0)}
cy={Math.max(region.h * ih / 2, 0)}
rx={Math.max(region.w * iw / 2, 0)}
Expand All @@ -59,15 +60,15 @@ const RegionComponents = {
/>
</g>
)),
polygon: memo(({region, iw, ih, fullSegmentationMode}) => {
polygon: memo(({region, iw, ih, strokeWidth, fullSegmentationMode}) => {
const Component = region.open ? "polyline" : "polygon"
return (
<Component
points={region.points
.map(([x, y]) => [x * iw, y * ih])
.map((a) => a.join(" "))
.join(" ")}
strokeWidth={2}
strokeWidth={strokeWidth}
stroke={colorAlpha(region.color, 0.75)}
fill={colorAlpha(region.color, 0.25)}
/>
Expand Down Expand Up @@ -189,6 +190,13 @@ const RegionComponents = {
pixel: () => null,
}

export const getStrokeWidth = (region) => {
const { type } = region;
if(type === 'box') {
return config.OUTLINE_THICKNESS_CONFIG.BOUNDING_BOX || 2;
}
return config.OUTLINE_THICKNESS_CONFIG[type.toUpperCase()] || 2;
};
export const WrappedRegionList = memo(
({regions, keypointDefinitions, iw, ih, fullSegmentationMode}) => {
return regions
Expand All @@ -201,13 +209,14 @@ export const WrappedRegionList = memo(
region={r}
iw={iw}
ih={ih}
strokeWidth = {getStrokeWidth(r)}
keypointDefinitions={keypointDefinitions}
fullSegmentationMode={fullSegmentationMode}
/>
)
})
},
(n, p) => n.regions === p.regions && n.iw === p.iw && n.ih === p.ih
(n, p) => n.regions === p.regions && n.iw === p.iw && n.ih === p.ih && n.strokeWidth === p.strokeWidth
)

export const RegionShapes = ({
Expand Down
2 changes: 0 additions & 2 deletions client/src/SettingsProvider/SettingsProvider.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,6 @@ test('should change setting and update state', () => {
const TestComponent = () => {
const settings = React.useContext(SettingsContext);

console.log(settings, 'masimis'); // Check what 'settings' actually contains

return (
<div>
<span data-testid="showCrosshairs">{settings && settings.showCrosshairs?.toString()}</span>
Expand Down
5 changes: 5 additions & 0 deletions client/src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ const config = {
DOCS_URL:"https://annotate-docs.dwaste.live/",
SERVER_URL,
UPLOAD_LIMIT: 5,
OUTLINE_THICKNESS_CONFIG : {
POLYGON: 2,
CIRCLE: 2,
BOUNDING_BOX: 2
}
};

export default config;
2 changes: 2 additions & 0 deletions client/src/workspace/DownloadButton/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { useSnackbar} from "../../SnackbarContext/index.jsx"
import { hexToRgbTuple } from "../../utils/color-utils.js";
import HeaderButton from "../HeaderButton/index.jsx";
import { useTranslation } from "react-i18next"
import config from "../../config.js";

const DownloadButton = ({selectedImageName, classList, hideHeaderText, disabled}) => {
const [anchorEl, setAnchorEl] = useState(null);
Expand All @@ -34,6 +35,7 @@ const DownloadButton = ({selectedImageName, classList, hideHeaderText, disabled}
const config_data = {}
config_data['image_name'] = selectedImageName
config_data['colorMap'] = classColorMap
config_data['outlineThickness'] = config.OUTLINE_THICKNESS_CONFIG
let url = ""
switch (format) {
case "configuration":
Expand Down
14 changes: 8 additions & 6 deletions server/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ def download_image_with_annotations():
images = json.loads(json_str).get("configuration", [])

color_map = data.get("colorMap", {})
outlineThickness = data.get("outlineThickness", {})

# Convert color map values to tuples
for key in color_map.keys():
Expand All @@ -295,7 +296,7 @@ def download_image_with_annotations():
points = region['points']
scaled_points = [(x * width, y * height) for x, y in points]
# Draw polygon with thicker outline
draw.line(scaled_points + [scaled_points[0]], fill=color, width=app.config['OUTLINE_THICKNESS_CONFIG']['POLYGON']) # Change width as desired
draw.line(scaled_points + [scaled_points[0]], fill=color, width=outlineThickness.get('POLYGON', 2)) # Change width as desired
elif all(key in region for key in ('x', 'y', 'w', 'h')):
try:
x = float(region['x'][1:-1]) * width if isinstance(region['x'], str) else float(region['x'][0]) * width
Expand All @@ -305,7 +306,7 @@ def download_image_with_annotations():
except (ValueError, TypeError) as e:
raise ValueError(f"Invalid format in region dimensions: {region}, Error: {e}")
# Draw rectangle with thicker outline
draw.rectangle([x, y, x + w, y + h], outline=color, width=app.config['OUTLINE_THICKNESS_CONFIG']['BOUNDING_BOX'])
draw.rectangle([x, y, x + w, y + h], outline=color, width=outlineThickness.get('BOUNDING_BOX', 2))
elif all(key in region for key in ('rx', 'ry', 'rw', 'rh')):
try:
rx = float(region['rx'][1:-1]) * width if isinstance(region['rx'], str) else float(region['rx'][0]) * width
Expand All @@ -315,7 +316,7 @@ def download_image_with_annotations():
except (ValueError, TypeError) as e:
raise ValueError(f"Invalid format in region dimensions: {region}, Error: {e}")
# Draw ellipse (circle if rw and rh are equal)
draw.ellipse([rx, ry, rx + rw, ry + rh], outline=color, width=app.config['OUTLINE_THICKNESS_CONFIG']['CIRCLE'])
draw.ellipse([rx, ry, rx + rw, ry + rh], outline=color, width=outlineThickness.get('CIRCLE', 2))



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

color_map = data.get("colorMap", {})
outlineThickness = data.get("outlineThickness", {})

# Convert color map values to tuples
for key in color_map.keys():
Expand All @@ -375,7 +377,7 @@ def download_image_mask():
if 'points' in region and region['points']:
points = region['points']
scaled_points = [(int(x * width), int(y * height)) for x, y in points]
draw.polygon(scaled_points, outline=color, fill=color, width=app.config['OUTLINE_THICKNESS_CONFIG']['POLYGON'])
draw.polygon(scaled_points, outline=color, fill=color, width=outlineThickness.get('POLYGON', 2))
elif all(key in region for key in ('x', 'y', 'w', 'h')):
try:
x = float(region['x'][1:-1]) * width if isinstance(region['x'], str) else float(region['x'][0]) * width
Expand All @@ -385,7 +387,7 @@ def download_image_mask():
except (ValueError, TypeError) as e:
raise ValueError(f"Invalid format in region dimensions: {region}, Error: {e}")
# Draw rectangle for bounding box
draw.rectangle([x, y, x + w, y + h], outline=color, fill=color, width=app.config['OUTLINE_THICKNESS_CONFIG']['BOUNDING_BOX'])
draw.rectangle([x, y, x + w, y + h], outline=color, fill=color, width=outlineThickness.get('BOUNDING_BOX', 2))
elif all(key in region for key in ('rx', 'ry', 'rw', 'rh')):
try:
rx = float(region['rx'][1:-1]) * width if isinstance(region['rx'], str) else float(region['rx'][0]) * width
Expand All @@ -395,7 +397,7 @@ def download_image_mask():
except (ValueError, TypeError) as e:
raise ValueError(f"Invalid format in region dimensions: {region}, Error: {e}")
# Draw ellipse (circle if rw and rh are equal)
draw.ellipse([rx, ry, rx + rw, ry + rh], outline=color, width=app.config['OUTLINE_THICKNESS_CONFIG']['CIRCLE'], fill=color)
draw.ellipse([rx, ry, rx + rw, ry + rh], outline=color,width=outlineThickness.get('CIRCLE', 2), fill=color)

mask_byte_arr = BytesIO()
mask.save(mask_byte_arr, format='PNG')
Expand Down
7 changes: 1 addition & 6 deletions server/config.py
Original file line number Diff line number Diff line change
@@ -1,6 +1 @@
MASK_BACKGROUND_COLOR = (0, 0, 0)
OUTLINE_THICKNESS_CONFIG = {
"POLYGON": 3,
"CIRCLE": 3,
"BOUNDING_BOX": 3
} # change outline thickness (currently only for downloaded files)
MASK_BACKGROUND_COLOR = (0, 0, 0)
Loading