diff --git a/README.md b/README.md index 1c0b70c..0044cf7 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/client/package.json b/client/package.json index 92562c1..5edd77d 100644 --- a/client/package.json +++ b/client/package.json @@ -73,6 +73,9 @@ "semi": false }, "jest":{ - "testEnvironment": "jsdom" + "testEnvironment": "jsdom", + "moduleNameMapper": { + "^color-alpha$": "/node_modules/color-alpha" + } } } diff --git a/client/src/RegionShapes/RegionShapes.test.js b/client/src/RegionShapes/RegionShapes.test.js new file mode 100644 index 0000000..6cdaad7 --- /dev/null +++ b/client/src/RegionShapes/RegionShapes.test.js @@ -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( + + ); + 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( + + ); + 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( + + ); + 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 + }); + }); \ No newline at end of file diff --git a/client/src/RegionShapes/index.jsx b/client/src/RegionShapes/index.jsx index f281fdd..7065eff 100644 --- a/client/src/RegionShapes/index.jsx +++ b/client/src/RegionShapes/index.jsx @@ -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}) => ( @@ -15,12 +16,12 @@ const RegionComponents = { /> )), - line: memo(({region, iw, ih}) => { + line: memo(({region, iw, ih, strokeWidth}) => { return ( )}), - box: memo(({region, iw, ih}) => ( + box: memo(({region, iw, ih, strokeWidth}) => ( )), - circle: memo(({ region, iw, ih }) => ( + circle: memo(({ region, iw, ih , strokeWidth}) => ( )), - polygon: memo(({region, iw, ih, fullSegmentationMode}) => { + polygon: memo(({region, iw, ih, strokeWidth, fullSegmentationMode}) => { const Component = region.open ? "polyline" : "polygon" return ( [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)} /> @@ -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 @@ -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 = ({ diff --git a/client/src/SettingsProvider/SettingsProvider.test.js b/client/src/SettingsProvider/SettingsProvider.test.js index 29b7194..6d3ad9a 100644 --- a/client/src/SettingsProvider/SettingsProvider.test.js +++ b/client/src/SettingsProvider/SettingsProvider.test.js @@ -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 (
{settings && settings.showCrosshairs?.toString()} diff --git a/client/src/config.js b/client/src/config.js index 751c425..d33bdd8 100644 --- a/client/src/config.js +++ b/client/src/config.js @@ -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; diff --git a/client/src/workspace/DownloadButton/index.jsx b/client/src/workspace/DownloadButton/index.jsx index 43952f9..cad2e0e 100644 --- a/client/src/workspace/DownloadButton/index.jsx +++ b/client/src/workspace/DownloadButton/index.jsx @@ -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); @@ -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": diff --git a/server/app.py b/server/app.py index d9445c6..bafe413 100644 --- a/server/app.py +++ b/server/app.py @@ -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(): @@ -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 @@ -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 @@ -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)) @@ -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(): @@ -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 @@ -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 @@ -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') diff --git a/server/config.py b/server/config.py index 1e23bc8..a709cf7 100644 --- a/server/config.py +++ b/server/config.py @@ -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) \ No newline at end of file +MASK_BACKGROUND_COLOR = (0, 0, 0) \ No newline at end of file