label-studio
Version:
Data Labeling Tool that is backend agnostic and can be embedded into your applications
521 lines (421 loc) • 13.8 kB
JavaScript
import Konva from "konva";
import React from "react";
import { Group, Line } from "react-konva";
import { observer, inject } from "mobx-react";
import { types, getParentOfType, getRoot, destroy, detach } from "mobx-state-tree";
import Constants from "../core/Constants";
import Hotkey from "../core/Hotkey";
import NormalizationMixin from "../mixins/Normalization";
import RegionsMixin from "../mixins/Regions";
import Registry from "../core/Registry";
import { ImageModel } from "../tags/object/Image";
import { LabelsModel } from "../tags/control/Labels";
import { PolygonLabelsModel } from "../tags/control/PolygonLabels";
import { PolygonPoint, PolygonPointView } from "./PolygonPoint";
import { RatingModel } from "../tags/control/Rating";
import { green } from "@ant-design/colors";
import { guidGenerator } from "../core/Helpers";
const Model = types
.model({
id: types.identifier,
pid: types.optional(types.string, guidGenerator),
type: "polygonregion",
opacity: types.number,
fillcolor: types.maybeNull(types.string),
strokewidth: types.number,
strokecolor: types.string,
pointsize: types.string,
pointstyle: types.string,
closed: types.optional(types.boolean, false),
points: types.array(PolygonPoint, []),
states: types.maybeNull(types.array(types.union(LabelsModel, RatingModel, PolygonLabelsModel))),
mouseOverStartPoint: types.optional(types.boolean, false),
selectedPoint: types.maybeNull(types.safeReference(PolygonPoint)),
coordstype: types.optional(types.enumeration(["px", "perc"]), "px"),
fromName: types.maybeNull(types.string),
wp: types.maybeNull(types.number),
hp: types.maybeNull(types.number),
})
.views(self => ({
get parent() {
return getParentOfType(self, ImageModel);
},
get completion() {
return getRoot(self).completionStore.selected;
},
}))
.actions(self => ({
/**
* Handler for mouse on start point of Polygon
* @param {boolean} val
*/
setMouseOverStartPoint(value) {
self.mouseOverStartPoint = value;
},
setSelectedPoint(point) {
if (self.selectedPoint) {
self.selectedPoint.selected = false;
}
point.selected = true;
self.selectedPoint = point;
},
handleMouseMove({ e, flattenedPoints }) {
let { offsetX: cursorX, offsetY: cursorY } = e.evt;
const [x, y] = getAnchorPoint({ flattenedPoints, cursorX, cursorY });
const group = e.currentTarget;
const layer = e.currentTarget.getLayer();
moveHoverAnchor({ point: [x, y], group, layer });
},
handleMouseLeave({ e }) {
removeHoverAnchor({ layer: e.currentTarget.getLayer() });
},
handleLineClick({ e, flattenedPoints, insertIdx }) {
e.cancelBubble = true;
if (!self.closed || !self.selected) return;
removeHoverAnchor({ layer: e.currentTarget.getLayer() });
const { offsetX: cursorX, offsetY: cursorY } = e.evt;
const point = getAnchorPoint({ flattenedPoints, cursorX, cursorY });
self.insertPoint(insertIdx, point[0], point[1]);
},
addPoint(x, y) {
if (self.closed) return;
self._addPoint(x, y);
},
insertPoint(insertIdx, x, y) {
const p = {
id: guidGenerator(),
x: x,
y: y,
size: self.pointsize,
style: self.pointstyle,
index: self.points.length,
};
self.points.splice(insertIdx, 0, p);
},
_addPoint(x, y) {
const index = self.points.length;
self.points.push({
id: guidGenerator(),
x: x,
y: y,
size: self.pointsize,
style: self.pointstyle,
index: index,
});
},
closePoly() {
self.closed = true;
self.selectRegion();
},
canClose(x, y) {
if (self.points.length < 2) return false;
const p1 = self.points[0];
const p2 = { x: x, y: y };
var r = 50;
var dist_points = (p1["x"] - p2["x"]) * (p1["x"] - p2["x"]) + (p1["y"] - p2["y"]) * (p2["y"] - p2["y"]);
if (dist_points < r) {
return true;
} else {
return false;
}
},
destroyRegion() {
detach(self.points);
destroy(self.points);
},
unselectRegion() {
if (self.selectedPoint) {
self.selectedPoint.selected = false;
}
// self.points.forEach(p => p.computeOffset());
self.selected = false;
self.parent.setSelected(undefined);
self.completion.setHighlightedNode(null);
},
selectRegion() {
// self.points.forEach(p => p.computeOffset());
self.selected = true;
self.completion.setHighlightedNode(self);
self.parent.setSelected(self.id);
},
setScale(x, y) {
self.scaleX = x;
self.scaleY = y;
},
addState(state) {
self.states.push(state);
},
setFill(color) {
self.fill = color;
},
updateOffset() {
self.points.map(p => p.computeOffset());
},
updateImageSize(wp, hp, sw, sh) {
self.wp = wp;
self.hp = hp;
if (self.coordstype === "px") {
self.points.forEach(p => {
const x = (sw * p.relativeX) / 100;
const y = (sh * p.relativeY) / 100;
p._movePoint(x, y);
});
}
if (!self.completion.sentUserGenerate && self.coordstype === "perc") {
self.points.forEach(p => {
const x = (sw * p.x) / 100;
const y = (sh * p.y) / 100;
self.coordstype = "px";
p._movePoint(x, y);
});
}
},
toStateJSON() {
const { naturalWidth, naturalHeight, stageWidth, stageHeight } = self.parent;
const perc_w = (stageWidth * 100) / naturalWidth;
const perc_h = (stageHeight * 100) / naturalHeight;
const perc_points = self.points.map(p => {
const orig_w = (p.x * 100) / perc_w;
const res_w = (orig_w * 100) / naturalWidth;
const orig_h = (p.y * 100) / perc_h;
const res_h = (orig_h * 100) / naturalHeight;
return [res_w, res_h];
});
const parent = self.parent;
const buildTree = obj => {
const tree = {
id: self.id,
from_name: obj.name,
to_name: parent.name,
source: parent.value,
type: "polygon",
value: {
points: perc_points,
},
};
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 PolygonRegionModel = types.compose("PolygonRegionModel", RegionsMixin, NormalizationMixin, Model);
/**
* Get coordinates of anchor point
* @param {array} flattenedPoints
* @param {number} cursorX coordinates of cursor X
* @param {number} cursorY coordinates of cursor Y
*/
function getAnchorPoint({ flattenedPoints, cursorX, cursorY }) {
const [point1X, point1Y, point2X, point2Y] = flattenedPoints;
const y =
((point2X - point1X) * (point2X * point1Y - point1X * point2Y) +
(point2X - point1X) * (point2Y - point1Y) * cursorX +
(point2Y - point1Y) * (point2Y - point1Y) * cursorY) /
((point2Y - point1Y) * (point2Y - point1Y) + (point2X - point1X) * (point2X - point1X));
const x =
cursorX -
((point2Y - point1Y) *
(point2X * point1Y - point1X * point2Y + cursorX * (point2Y - point1Y) - cursorY * (point2X - point1X))) /
((point2Y - point1Y) * (point2Y - point1Y) + (point2X - point1X) * (point2X - point1X));
return [x, y];
}
function getFlattenedPoints(points) {
const p = points.map(p => [p.x, p.y]);
return p.reduce(function(flattenedPoints, point) {
return flattenedPoints.concat(point);
}, []);
}
function getHoverAnchor({ layer }) {
return layer.findOne(".hoverAnchor");
}
/**
* Create new anchor for current polygon
*/
function createHoverAnchor({ point, group, layer }) {
const hoverAnchor = new Konva.Circle({
name: "hoverAnchor",
x: point[0],
y: point[1],
stroke: green.primary,
fill: green[0],
strokeWidth: 2,
radius: 5,
});
group.add(hoverAnchor);
layer.draw();
return hoverAnchor;
}
function moveHoverAnchor({ point, group, layer }) {
const hoverAnchor = getHoverAnchor({ layer }) || createHoverAnchor({ point, group, layer });
hoverAnchor.to({ x: point[0], y: point[1], duration: 0 });
}
function removeHoverAnchor({ layer }) {
const hoverAnchor = getHoverAnchor({ layer });
if (!hoverAnchor) return;
hoverAnchor.destroy();
layer.draw();
}
const HtxPolygonView = ({ store, item }) => {
/**
* Render line between 2 points
*/
function renderLine({ points, idx1, idx2 }) {
const name = `border_${idx1}_${idx2}`;
let { strokecolor, strokewidth } = item;
if (item.highlighted) {
strokecolor = Constants.HIGHLIGHTED_STROKE_COLOR;
strokewidth = Constants.HIGHLIGHTED_STROKE_WIDTH;
}
const insertIdx = idx1 + 1; // idx1 + 1 or idx2
const flattenedPoints = getFlattenedPoints([points[idx1], points[idx2]]);
return (
<Group
key={name}
name={name}
onClick={e => item.handleLineClick({ e, flattenedPoints, insertIdx })}
onMouseMove={e => {
if (!item.closed || !item.selected) return;
item.handleMouseMove({ e, flattenedPoints });
}}
onMouseLeave={e => item.handleMouseLeave({ e })}
>
<Line
points={flattenedPoints}
stroke={strokecolor}
opacity={item.opacity}
lineJoin="bevel"
strokeWidth={strokewidth}
/>
</Group>
);
}
function renderLines(points) {
const name = "borders";
return (
<Group key={name} name={name}>
{points.map((p, idx) => {
const idx1 = idx;
const idx2 = idx === points.length - 1 ? 0 : idx + 1;
return renderLine({ points, idx1, idx2 });
})}
</Group>
);
}
function renderPoly(points) {
const name = "poly";
return (
<Group key={name} name={name}>
<Line
lineJoin="bevel"
points={getFlattenedPoints(points)}
fill={item.strokecolor}
closed={true}
opacity={0.2}
/>
</Group>
);
}
function renderCircle({ points, idx }) {
const name = `anchor_${points.length}_${idx}`;
const point = points[idx];
if (!item.closed || (item.closed && item.selected)) {
return <PolygonPointView item={point} name={name} key={name} />;
}
}
function renderCircles(points) {
const name = "anchors";
return (
<Group key={name} name={name}>
{points.map((p, idx) => renderCircle({ points, idx }))}
</Group>
);
}
function minMax(items) {
return items.reduce((acc, val) => {
acc[0] = acc[0] === undefined || val < acc[0] ? val : acc[0];
acc[1] = acc[1] === undefined || val > acc[1] ? val : acc[1];
return acc;
}, []);
}
let minX = 0,
maxX = 0,
minY = 0,
maxY = 0;
return (
<Group
key={item.id ? item.id : guidGenerator(5)}
onDragStart={e => {
item.completion.setDragMode(true);
var arrX = item.points.map(p => p.x);
var arrY = item.points.map(p => p.y);
[minX, maxX] = minMax(arrX);
[minY, maxY] = minMax(arrY);
}}
dragBoundFunc={function(pos) {
let { x, y } = pos;
const sw = item.parent.stageWidth;
const sh = item.parent.stageHeight;
if (minY + y < 0) y = -1 * minY;
if (minX + x < 0) x = -1 * minX;
if (maxY + y > sh) y = sh - maxY;
if (maxX + x > sw) x = sw - maxX;
return { x: x, y: y };
}}
onDragEnd={e => {
const t = e.target;
item.completion.setDragMode(false);
if (!item.closed) item.closePoly();
item.points.forEach(p => p.movePoint(t.getAttr("x"), t.getAttr("y")));
t.setAttr("x", 0);
t.setAttr("y", 0);
}}
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 => {
e.cancelBubble = true;
if (!item.completion.edittable) return;
if (!item.closed) return;
const stage = item.parent.stageRef;
if (store.completionStore.selected.relationMode) {
stage.container().style.cursor = Constants.DEFAULT_CURSOR;
}
item.setHighlight(false);
item.onClickRegion();
}}
draggable={item.completion.edittable && item.parent.zoomScale === 1}
>
{item.mouseOverStartPoint}
{item.points ? renderPoly(item.points) : null}
{item.points ? renderLines(item.points) : null}
{item.points ? renderCircles(item.points) : null}
</Group>
);
};
const HtxPolygon = inject("store")(observer(HtxPolygonView));
Registry.addTag("polygonregion", PolygonRegionModel, HtxPolygon);
export { PolygonRegionModel, HtxPolygon };