UNPKG

lost-sia

Version:

Single Image Annotation Tool

479 lines (456 loc) 15.5 kB
import React, { useEffect, useRef, useState } from "react"; import Canvas from "./Canvas"; import { noAnnos } from "./siaDummyData"; import ToolBar from "./ToolBar"; import * as annoActions from "./types/canvasActions"; import * as tbe from "./types/toolbarEvents"; /** * SIA element that handles annotations within an image * * @param {boolean} isStaticPosition - Use static positioning instead of "fixed" * * @param {integer} fixedImageSize - Use fixed image size if specified * * @param {object} annos - A json object containing all annotation * information for an image * { * bBoxes: [{ * id: int, // -> Not required if status === annoStatus.NEW * data: {}, * labelIds: list of int, // -> optional * status: see annoStatus, // -> optional * annoTime: float, // -> optional * },...], * points: [] * lines: [] * polygons: [] * } * @param {object} annoSaveResponse - Backend response when updating an annotation in backend * { * tempId: int or str, // temporal frontend Id * dbId: int, // Id from backend * newStatus: str // new Status for the annotation * } * @param {object} possibleLabels - Possible labels that can be assigned to * an annotation. * [{ * id: int, * description: str, * label: str, (name of the label) * color: str (color is optional) * }, ...] * @param {blob} imageBlob - The actual image blob that will be displayed * @param {object} imageMeta - Meta information for the current image * { * "id": int, * "number": int, // -> number of image in current annotask * "amount": int, // -> total number of images in current annotask * "isFirst": bool, // -> True if this image is the first image * "isLast": bool, // -> True if current image is the last image in annotask * "labelIds": list of int, // -> List of label for the current image * "isJunk": bool, // -> Indicates wether current image is a junk image * "annoTime": float, // -> Total annotation time for the current image * "description": str or null // -> Description or comment for the current image * } * @param {object} exampleImg - Example for a selected label * { * "anno": { * "id": int, // -> ID of the example annotation * "comment": null or str // -> Comment that has been assigned to this example * }, * "img": image blob * } * @param {bool} isJunk - Indicates wether the current image is junk or not * @param {object} uiConfig - User interface configs * { * nodesRadius: int, strokeWidth: int, * layoutOffset: {left:int, top:int, right:int, bottom:int}, -> Offset of the canvas inside the container * imgBarVisible: bool, * imgLabelInputVisible: bool, * centerCanvasInContainer: bool, -> Center the canvas in the middle of the container. * maxCanvas: bool -> Maximize Canvas Size. Do not fit canvas to image size. * } * @param {int} layoutUpdate - A counter that triggers a layout update * everytime it is incremented. * @param {string} selectedTool - The tool that is selected to draw an * annotation. Possible choices are: 'bBox', 'point', 'line', 'polygon' * @param {object} canvasConfig - Configuration for this canvas * { * annos:{ * tools: { * point: bool, * line: bool, * polygon: bool, * bbox: bool * }, * multilabels: bool, * actions: { * draw: bool, * label: bool, * edit: bool, * }, * maxAnnos: null or int, * minArea: int * }, * img: { * multilabels: bool, * actions: { * label: bool, * } * }, * allowedToMarkExample: bool, -> Indicates wether the current user is allowed to mark an annotation as example. * } * @param {str or int} defaultLabel (optional) - Name or ID of the default label that is used * when no label was selected by the annotator. If not set "no label" will be used. * If ID is used, it needs to be one of the possible label ids. * @param {bool} blocked Block canvas view with loading dimmer. * @param {bool} fullscreen Set fullscreen mode if provided * @param {bool} preventScrolling Prevent scrolling on mouseEnter * @param {bool} lockedAnnos A list of AnnoIds of annos that should only be displayed. * Such annos can not be edited in any way. * @param {object} filter Information for the filter Popup * { * "clahe": { * "clipLimit": int, * "active": bool * }, * } * @param {bool | object} toolbarEnabled Defines which toolbar buttons are * displayed or if toolbar is shown at all. * false | { * imgLabel: bool, * nextPrev: bool, * toolSelection: bool, * fullscreen: bool, * junk: bool, * deleteAll: bool, * settings: bool | {infoBoxes: bool, annoStyle: bool}, * filter: bool | {rotate: bool, clahe:bool}, * help: bool * } * @event onAnnoSaveEvent - Callback with update information for a single * annotation or the current image that can be used for backend updates * args: { * action: the action that was performed in frontend, * anno: anno information, * img: image information * } * @event onNotification - Callback for Notification messages * args: {title: str, message: str, type: str} * @event onCanvasKeyDown - Fires for keyDown on canvas * @event onAnnoEvent - Fires when an anno performed an action * args: {anno: annoObject, newAnnos: list of annoObjects, pAction: str} * @event onGetAnnoExample - Fires when anno example is requested by canvas * { * id: int, // -> ID of the annotation that will be requested as example * comment: null or str * } * @event onCanvasEvent - Fires on canvas event * args: {action: action, data: dataObject} * action -> CANVAS_SVG_UPDATE * data: {width: int, height: int, scale: float, translateX: float, * translateY:float} * action -> CANVAS_UI_CONFIG_UPDATE * action -> CANVAS_LABEL_INPUT_CLOSE * action -> CANVAS_IMG_LOADED * action -> CANVAS_IMGBAR_CLOSE * @event onToolBarEvent - Fires on Toolbar event * args: {e: event, data: data object} * * e -> DELETE_ALL_ANNOS * e -> TOOL_SELECTED * data: 'bbox', 'point', 'line', 'polygon' * e -> GET_NEXT_IMAGE * data: int // -> Image ID * e -> GET_PREV_IMAGE * data: int // -> Image ID * e -> TASK_FINISHED * data: null * e -> SHOW_IMAGE_LABEL_INPUT * data: null * e -> IMG_IS_JUNK * data: null * e -> APPLY_FILTER * data: { * "clahe": { * "clipLimit": int, * "active": bool * }, * "rotate": { * "angle": 90 | -90 | 180, * "active": bool * } * } * e -> SHOW_ANNO_DETAILS * data: null * e -> SHOW_LABEL_INFO * data: null * e -> SHOW_ANNO_STATS * data: null * e -> EDIT_STROKE_WIDTH * data: int // -> Stroke width * e -> EDIT_NODE_RADIUS * data: int // -> Radius * @event onGetFunction - Get special canvas functions for manipulation from outside canvas * deleteAllAnnos() * unloadImage() * resetZoom() * getAnnos(annos,removeFrontendIds) */ const Sia = (props) => { const [fullscreenCSS, setFullscreenCSS] = useState(""); const [fullscreen, setFullscreen] = useState(); const [annos, setAnnos] = useState(noAnnos); const [layoutUpdate, setLayoutUpdate] = useState(0); const [svg, setSvg] = useState(); const [externalConfigUpdate, setExternalConfigUpdate] = useState(false); const [uiConfig, setUiConfig] = useState({ nodeRadius: 4, strokeWidth: 4, annoDetails: { visible: false, }, labelInfo: { visible: false, }, annoStats: { visible: false, }, layoutOffset: { left: 20, top: 0, bottom: 5, right: 5, }, imgBarVisible: true, imgLabelInputVisible: false, centerCanvasInContainer: true, maxCanvas: true, }); const containerRef = useRef(); useEffect(() => { doLayoutUpdate(); }, [props.layoutUpdate]); useEffect(() => { // console.log(annos); }, [annos]); useEffect(() => { // console.log("props.annos", props.annos); if (props.annos) { setAnnos(props.annos); } else { setAnnos({ ...noAnnos }); } }, [props.annos]); useEffect(() => { // console.log("props.fullscreen", props.fullscreen); // console.log("fullscreen", fullscreen); if (typeof props.fullscreen === "boolean") { if (fullscreen !== props.fullscreen) { setFullscreen(props.fullscreen); } } }, [props.fullscreen]); useEffect(() => { if (fullscreen !== undefined) { // console.log("effect fullscreen", fullscreen); // toggleFullscreen() applyFullscreen(fullscreen); } }, [fullscreen]); useEffect(() => { setExternalConfigUpdate(true); setUiConfig({ ...uiConfig, ...props.uiConfig }); }, [props.uiConfig]); useEffect(() => { if (externalConfigUpdate) { setExternalConfigUpdate(false); } else { if (props.onCanvasEvent) { props.onCanvasEvent(annoActions.CANVAS_UI_CONFIG_UPDATE, uiConfig); } } }, [uiConfig]); const doLayoutUpdate = () => { setLayoutUpdate(layoutUpdate + 1); }; const handleAnnoEvent = (anno, annos, action) => { // console.log("handleAnnoEvent anno, annos, action", anno, annos, action); if (props.onAnnoEvent) { props.onAnnoEvent(anno, annos, action); } }; const handleNotification = (msg) => { if (props.onNotification) { props.onNotification(msg); } }; const handleCanvasKeyDown = (e) => { if (props.onCanvasKeyDown) { props.onCanvasKeyDown(e); } }; const handleCanvasEvent = (e, data) => { switch (e) { case annoActions.CANVAS_SVG_UPDATE: setSvg(data); break; case annoActions.CANVAS_UI_CONFIG_UPDATE: setUiConfig({ ...uiConfig, ...data }); break; default: break; } if (props.onCanvasEvent) { props.onCanvasEvent(e, data); } }; const handleGetFunction = (canvasFunction) => { if (props.onGetFunction) { props.onGetFunction(canvasFunction); } }; const handleAnnoSaveEvent = (action, saveData) => { if (props.onAnnoSaveEvent) { props.onAnnoSaveEvent(action, saveData); } }; const applyFullscreen = (full) => { if (full) { setFullscreenCSS("sia-fullscreen"); setUiConfig({ ...uiConfig, layoutOffset: { ...uiConfig.layoutOffset, left: 50, top: 5, }, }); doLayoutUpdate(); } else { setFullscreenCSS(""); setUiConfig({ ...uiConfig, layoutOffset: { ...uiConfig.layoutOffset, left: 20, top: 0, }, }); doLayoutUpdate(); } }; const toggleFullscreen = () => { if (fullscreen) { setFullscreen(false); } else { setFullscreen(true); } }; const handleToolBarEvent = (e, data) => { switch (e) { case tbe.SET_FULLSCREEN: toggleFullscreen(); break; case tbe.SHOW_ANNO_DETAILS: setUiConfig({ ...uiConfig, annoDetails: { ...uiConfig.annoDetails, visible: !uiConfig.annoDetails.visible, }, }); break; case tbe.SHOW_LABEL_INFO: setUiConfig({ ...uiConfig, labelInfo: { ...uiConfig.labelInfo, visible: !uiConfig.labelInfo.visible, }, }); break; case tbe.SHOW_ANNO_STATS: setUiConfig({ ...uiConfig, annoStats: { ...uiConfig.annoStats, visible: !uiConfig.annoStats.visible, }, }); break; case tbe.EDIT_STROKE_WIDTH: setUiConfig({ ...uiConfig, strokeWidth: data }); break; case tbe.EDIT_NODE_RADIUS: setUiConfig({ ...uiConfig, nodeRadius: data }); break; default: break; } if (props.onToolBarEvent) { props.onToolBarEvent(e, data); } }; const allowedTools = props.canvasConfig.tools; return ( <div className={`sia-app ${fullscreenCSS}`} ref={containerRef}> <Canvas container={containerRef} onAnnoEvent={(anno, annos, action) => handleAnnoEvent(anno, annos, action) } onNotification={(messageObj) => handleNotification(messageObj)} onKeyDown={(e) => handleCanvasKeyDown(e)} onCanvasEvent={(action, data) => handleCanvasEvent(action, data)} onGetAnnoExample={(exampleArgs) => props.onGetAnnoExample ? props.onGetAnnoExample(exampleArgs) : {} } onGetFunction={(canvasFunc) => handleGetFunction(canvasFunc)} onAnnoSaveEvent={(saveData) => handleAnnoSaveEvent(saveData)} annoSaveResponse={props.annoSaveResponse} canvasConfig={props.canvasConfig} uiConfig={uiConfig} annos={annos} annoTaskId={props.annoTaskId} imageMeta={props.imageMeta} imageBlob={props.imageBlob} possibleLabels={props.possibleLabels} exampleImg={props.exampleImg} lockedAnnos={props.lockedAnnos} layoutUpdate={layoutUpdate} selectedTool={props.selectedTool} isJunk={props.isJunk} blocked={props.blockCanvas} defaultLabel={props.defaultLabel} preventScrolling={props.preventScrolling} isImageChanging={props.isImageChanging} isStaticPosition={props.isStaticPosition} fixedImageSize={props.fixedImageSize} samPoints={props.samPoints || []} onSamPointClick={props.onSamPointClick} samBBox={props.samBBox || null} onUpdateSamBBox={props.onUpdateSamBBox} /> <ToolBar onToolBarEvent={(e, data) => handleToolBarEvent(e, data)} imageMeta={props.imageMeta} layoutUpdate={layoutUpdate} svg={svg} active={{ isJunk: props.isJunk, selectedTool: props.selectedTool, fullscreen: props.fullscreenMode, }} enabled={props.toolbarEnabled} canvasConfig={props.canvasConfig} uiConfig={uiConfig} filter={props.filter} onImgageSearchClicked={() => { if (props.onImgageSearchClicked) return props.onImgageSearchClicked(); }} /> </div> ); }; export default Sia;