UNPKG

label-studio

Version:

Data Labeling Tool that is backend agnostic and can be embedded into your applications

340 lines (281 loc) 9.53 kB
import React, { Fragment } from "react"; import { Rect } from "react-konva"; import { observer, inject } from "mobx-react"; import { types, getParentOfType, getParent, getRoot } from "mobx-state-tree"; import Constants from "../core/Constants"; import DisabledMixin from "../mixins/Normalization"; import NormalizationMixin from "../mixins/Normalization"; import RegionsMixin from "../mixins/Regions"; import Registry from "../core/Registry"; import Utils from "../utils"; import { ImageModel } from "../tags/object/Image"; import { LabelsModel } from "../tags/control/Labels"; import { RatingModel } from "../tags/control/Rating"; import { RectangleLabelsModel } from "../tags/control/RectangleLabels"; import { guidGenerator } from "../core/Helpers"; /** * Rectangle object for Bounding Box * */ const Model = types .model({ id: types.identifier, pid: types.optional(types.string, guidGenerator), type: "rectangleregion", x: types.number, y: types.number, relativeX: types.optional(types.number, 0), relativeY: types.optional(types.number, 0), relativeWidth: types.optional(types.number, 0), relativeHeight: types.optional(types.number, 0), _start_x: types.optional(types.number, 0), _start_y: types.optional(types.number, 0), width: types.number, height: types.number, scaleX: types.optional(types.number, 1), scaleY: types.optional(types.number, 1), rotation: types.optional(types.number, 0), opacity: types.number, fill: types.optional(types.boolean, true), fillcolor: types.optional(types.string, Constants.FILL_COLOR), strokeColor: types.optional(types.string, Constants.STROKE_COLOR), strokeWidth: types.optional(types.number, Constants.STROKE_WIDTH), states: types.maybeNull(types.array(types.union(LabelsModel, RatingModel, RectangleLabelsModel))), wp: types.maybeNull(types.number), hp: types.maybeNull(types.number), sw: types.maybeNull(types.number), sh: types.maybeNull(types.number), coordstype: types.optional(types.enumeration(["px", "perc"]), "px"), supportsTransform: true, }) .views(self => ({ get parent() { return getParentOfType(self, ImageModel); }, get completion() { return getRoot(self).completionStore.selected; }, })) .actions(self => ({ afterCreate() { self._start_x = self.x; self._start_y = self.y; if (self.coordstype === "perc") { self.relativeX = self.x; self.relativeY = self.y; self.relativeWidth = self.width; self.relativeHeight = self.height; } }, unselectRegion() { self.selected = false; self.parent.setSelected(undefined); self.completion.setHighlightedNode(null); }, coordsInside(x, y) { // check if x and y are inside the rectangle const rx = self.x; const ry = self.y; const rw = self.width * (self.scaleX || 1); const rh = self.height * (self.scaleY || 1); if (x > rx && x < rx + rw && y > ry && y < ry + rh) return true; return false; }, selectRegion() { self.selected = true; self.completion.setHighlightedNode(self); self.parent.setSelected(self.id); }, /** * Boundg Box set position on canvas * @param {number} x * @param {number} y * @param {number} width * @param {number} height * @param {number} rotation */ setPosition(x, y, width, height, rotation) { self.x = x; self.y = y; self.width = width; self.height = height; self.relativeX = (x / self.parent.stageWidth) * 100; self.relativeY = (y / self.parent.stageHeight) * 100; self.relativeWidth = (width / self.parent.stageWidth) * 100; self.relativeHeight = (height / self.parent.stageHeight) * 100; if (rotation < 0) { self.rotation = (rotation % 360) + 360; } else { self.rotation = rotation % 360; } }, setScale(x, y) { self.scaleX = x; self.scaleY = y; }, addState(state) { self.states.push(state); }, setFill(color) { self.fill = color; }, updateImageSize(wp, hp, sw, sh) { self.wp = wp; self.hp = hp; self.sw = sw; self.sh = sh; if (self.coordstype === "px") { self.x = (sw * self.relativeX) / 100; self.y = (sh * self.relativeY) / 100; self.width = (sw * self.relativeWidth) / 100; self.height = (sh * self.relativeHeight) / 100; } else if (self.coordstype === "perc") { self.x = (sw * self.x) / 100; self.y = (sh * self.y) / 100; self.width = (sw * self.width) / 100; self.height = (sh * self.height) / 100; self.coordstype = "px"; } }, /** * Format for sending to server */ toStateJSON() { const parent = self.parent; let fromEl = parent.states()[0]; if (parent.states().length > 1) { parent.states().forEach(state => { if (state.type === "rectanglelabels") { fromEl = state; } }); } const buildTree = obj => { const tree = { id: self.id, from_name: fromEl.name, to_name: parent.name, source: parent.value, type: "rectangle", value: { x: (self.x * 100) / self.parent.stageWidth, y: (self.y * 100) / self.parent.stageHeight, width: (self.width * (self.scaleX || 1) * 100) / self.parent.stageWidth, // * (self.scaleX || 1) height: (self.height * (self.scaleY || 1) * 100) / self.parent.stageHeight, rotation: self.rotation, }, }; if (self.normalization) tree["normalization"] = self.normalization; return tree; }; if (self.states && self.states.length) { return self.states.map(s => { const tree = buildTree(s); // in case of labels it's gonna be, labels: ["label1", "label2"] tree["value"][s.type] = s.getSelectedNames(); tree["type"] = s.type; return tree; }); } else { return buildTree(parent); } }, })); const RectRegionModel = types.compose("RectRegionModel", RegionsMixin, NormalizationMixin, DisabledMixin, Model); const HtxRectangleView = ({ store, item }) => { let { strokeColor, strokeWidth } = item; if (item.highlighted) { strokeColor = Constants.HIGHLIGHTED_STROKE_COLOR; strokeWidth = Constants.HIGHLIGHTED_STROKE_WIDTH; } return ( <Fragment> <Rect x={item.x} y={item.y} width={item.width} height={item.height} fill={item.fill ? Utils.Colors.convertToRGBA(item.fillcolor, 0.4) : null} stroke={strokeColor} strokeWidth={strokeWidth} strokeScaleEnabled={false} shadowBlur={0} scaleX={item.scaleX} scaleY={item.scaleY} opacity={item.opacity} rotation={item.rotation} name={item.id} onTransformEnd={e => { const t = e.target; item.setPosition( t.getAttr("x"), t.getAttr("y"), t.getAttr("width") * t.getAttr("scaleX"), t.getAttr("height") * t.getAttr("scaleY"), t.getAttr("rotation"), ); t.setAttr("scaleX", 1); t.setAttr("scaleY", 1); }} onDragEnd={e => { const t = e.target; item.setPosition( t.getAttr("x"), t.getAttr("y"), t.getAttr("width"), t.getAttr("height"), t.getAttr("rotation"), ); item.setScale(t.getAttr("scaleX"), t.getAttr("scaleY")); }} dragBoundFunc={(pos, e) => { let { x, y } = pos; let { stageHeight, stageWidth } = getParent(item, 2); if (x <= 0) { x = 0; } else if (x + item.width >= stageWidth) { x = stageWidth - item.width; } if (y < 0) { y = 0; } else if (y + item.height >= stageHeight) { y = stageHeight - item.height; } return { x: x, y: y, }; }} onMouseOver={e => { const stage = item.parent.stageRef; if (store.completionStore.selected.relationMode) { item.setHighlight(true); stage.container().style.cursor = Constants.RELATION_MODE_CURSOR; } else { stage.container().style.cursor = Constants.POINTER_CURSOR; } }} onMouseOut={e => { const stage = item.parent.stageRef; stage.container().style.cursor = Constants.DEFAULT_CURSOR; if (store.completionStore.selected.relationMode) { item.setHighlight(false); } }} onClick={e => { const stage = item.parent.stageRef; if (!item.completion.edittable) return; if (store.completionStore.selected.relationMode) { stage.container().style.cursor = Constants.DEFAULT_CURSOR; } item.setHighlight(false); item.onClickRegion(); }} draggable={item.completion.edittable} /> </Fragment> ); }; const HtxRectangle = inject("store")(observer(HtxRectangleView)); Registry.addTag("rectangleregion", RectRegionModel, HtxRectangle); export { RectRegionModel, HtxRectangle };