Skip to content

Commit

Permalink
support outline thiness via client settings
Browse files Browse the repository at this point in the history
  • Loading branch information
sumn2u committed Jun 19, 2024
1 parent 1c9b3be commit 26be867
Show file tree
Hide file tree
Showing 9 changed files with 129 additions and 27 deletions.
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)

0 comments on commit 26be867

Please sign in to comment.