+
-
-
+
+
-
+
{{ index + 1 }}
{{ name }} {{ annotation.name }}
- (Empty)
- (id: {{ annotation.id }})
+ (Empty)
+ (id: {{ annotation.id }})
-
-
-
+
+
-
+
+
+
+
+
+
@@ -33,22 +138,32 @@
-
+
@@ -60,16 +175,22 @@
import paper from "paper";
import axios from "axios";
import simplifyjs from "simplify-js";
+import JQuery from "jquery";
+import { Keypoint, Keypoints } from "@/libs/keypoints";
import { mapMutations } from "vuex";
import UndoAction from "@/undo";
+import TagsInput from "@/components/TagsInput";
import Metadata from "@/components/Metadata";
+let $ = JQuery;
+
export default {
name: "Annotaiton",
components: {
- Metadata
+ Metadata,
+ TagsInput
},
props: {
annotation: {
@@ -103,6 +224,18 @@ export default {
simplify: {
type: Number,
default: 1
+ },
+ keypointEdges: {
+ type: Array,
+ required: true
+ },
+ keypointLabels: {
+ type: Array,
+ required: true
+ },
+ activeTool: {
+ type: String,
+ required: true
}
},
data() {
@@ -110,16 +243,36 @@ export default {
isVisible: true,
color: this.annotation.color,
compoundPath: null,
+ keypoints: null,
metadata: [],
isEmpty: true,
name: "",
- pervious: []
+ uuid: "",
+ pervious: [],
+ count: 0,
+ currentKeypoint: null,
+ keypoint: {
+ tag: [],
+ visibility: 0,
+ next: {
+ label: -1,
+ visibility: 2
+ }
+ },
+ sessions: [],
+ session: {
+ start: Date.now(),
+ tools: [],
+ milliseconds: 0
+ },
+ tagRecomputeCounter: 0
};
},
methods: {
...mapMutations(["addUndo"]),
initAnnotation() {
let metaName = this.annotation.metadata.name;
+
if (metaName) {
this.name = metaName;
delete this.annotation.metadata["name"];
@@ -139,6 +292,9 @@ export default {
json = json || null;
segments = segments || null;
+ let width = this.annotation.width;
+ let height = this.annotation.height;
+
// Validate json
if (json != null) {
if (json.length !== 2) {
@@ -154,19 +310,37 @@ export default {
}
if (this.compoundPath != null) this.compoundPath.remove();
+ if (this.keypoints != null) this.keypoints.remove();
// Create new compoundpath
this.compoundPath = new paper.CompoundPath();
+ this.compoundPath.onDoubleClick = () => {
+ if (this.activeTool !== "Select") return;
+ $(`#annotationSettings${this.annotation.id}`).modal("show");
+ };
+ this.keypoints = new Keypoints(this.keypointEdges, this.keypointLabels);
+ this.keypoints.radius = this.scale * 6;
+ this.keypoints.lineWidth = this.scale * 2;
+
+ let keypoints = this.annotation.keypoints;
+ if (keypoints) {
+ for (let i = 0; i < keypoints.length; i += 3) {
+ let x = keypoints[i] - width / 2,
+ y = keypoints[i + 1] - height / 2,
+ v = keypoints[i + 2];
+
+ if (keypoints[i] === 0 && keypoints[i + 1] === 0 && v === 0) continue;
+
+ this.addKeypoint(new paper.Point(x, y), v, i / 3 + 1);
+ }
+ }
if (json != null) {
// Import data directroy from paperjs object
this.compoundPath.importJSON(json);
} else if (segments != null) {
// Load segments input compound path
- let center = new paper.Point(
- this.annotation.width / 2,
- this.annotation.height / 2
- );
+ let center = new paper.Point(width / 2, height / 2);
for (let i = 0; i < segments.length; i++) {
let polygon = segments[i];
@@ -182,21 +356,48 @@ export default {
}
this.compoundPath.data.annotationId = this.index;
+ this.compoundPath.data.categoryId = this.categoryIndex;
+
+ this.compoundPath.fullySelected = this.isCurrent;
+ this.compoundPath.opacity = this.opacity;
+
this.setColor();
+
+ this.compoundPath.onClick = () => {
+ this.$emit("click", this.index);
+ };
},
deleteAnnotation() {
axios.delete("/api/annotation/" + this.annotation.id).then(() => {
- this.$parent.category.annotations.splice(this.index, 1);
- this.$emit("deleted", this.index);
+ this.$socket.emit("annotation", {
+ action: "delete",
+ annotation: this.annotation
+ });
+ this.delete();
- if (this.compoundPath != null) this.compoundPath.remove();
+ this.$emit("deleted", this.index);
});
},
+ delete() {
+ this.$parent.category.annotations.splice(this.index, 1);
+ if (this.compoundPath != null) this.compoundPath.remove();
+ if (this.keypoints != null) this.keypoints.remove();
+ },
onAnnotationClick() {
if (this.isVisible) {
this.$emit("click", this.index);
}
},
+ onMouseEnter() {
+ if (this.compoundPath == null) return;
+
+ this.compoundPath.selected = true;
+ },
+ onMouseLeave() {
+ if (this.compoundPath == null) return;
+
+ this.compoundPath.selected = false;
+ },
getCompoundPath() {
if (this.compoundPath == null) {
this.createCompoundPath();
@@ -207,6 +408,7 @@ export default {
if (this.compoundPath == null) this.createCompoundPath();
let copy = this.compoundPath.clone();
+ copy.fullySelected = false;
copy.visible = false;
this.pervious.push(copy);
@@ -219,36 +421,112 @@ export default {
this.addUndo(action);
},
simplifyPath() {
- let flatten = 1;
let simplify = this.simplify;
- this.compoundPath.flatten(flatten);
+ this.compoundPath.flatten(1);
if (this.compoundPath instanceof paper.Path) {
this.compoundPath = new paper.CompoundPath(this.compoundPath);
this.compoundPath.data.annotationId = this.index;
+ this.compoundPath.data.categoryId = this.categoryIndex;
}
+ let newChildren = [];
this.compoundPath.children.forEach(path => {
let points = [];
path.segments.forEach(seg => {
points.push({ x: seg.point.x, y: seg.point.y });
});
-
points = simplifyjs(points, simplify, true);
- path.remove();
let newPath = new paper.Path(points);
newPath.closePath();
- this.compoundPath.addChild(newPath);
+ newChildren.push(newPath);
});
+
+ this.compoundPath.removeChildren();
+ this.compoundPath.addChildren(newChildren);
+
+ this.compoundPath.fullySelected = this.isCurrent;
+
+ this.emitModify();
},
undoCompound() {
if (this.pervious.length == 0) return;
this.compoundPath.remove();
this.compoundPath = this.pervious.pop();
+ this.compoundPath.fullySelected = this.isCurrent;
+ },
+ addKeypoint(point, visibility, label) {
+ if (label == null && this.keypoints.contains(point)) return;
+
+ visibility = visibility || parseInt(this.keypoint.next.visibility);
+ label = label || parseInt(this.keypoint.next.label);
+
+ let keypoint = new Keypoint(point.x, point.y, {
+ visibility: visibility || 0,
+ indexLabel: label || -1,
+ radius: this.scale * 6,
+ onClick: event => {
+ this.onAnnotationClick();
+
+ if (!["Select", "Keypoints"].includes(this.activeTool)) return;
+ let keypoint = event.target.keypoint;
+
+ // Remove if already selected
+ if (keypoint == this.currentKeypoint) {
+ this.currentKeypoint = null;
+ return;
+ }
+
+ if (this.currentKeypoint) {
+ let i1 = this.currentKeypoint.indexLabel;
+ let i2 = keypoint.indexLabel;
+ if (this.keypoints && i1 > 0 && i2 > 0) {
+ let edge = [i1, i2];
+
+ if (!this.keypoints.getLine(edge)) {
+ this.$parent.addKeypointEdge(edge);
+ } else {
+ this.$parent.removeKeypointEdge(edge);
+ }
+
+ this.currentKeypoint = null;
+ return;
+ }
+ }
+
+ this.currentKeypoint = event.target.keypoint;
+ },
+ onDoubleClick: event => {
+ if (!this.$parent.isCurrent) return;
+ if (!["Select", "Keypoints"].includes(this.activeTool)) return;
+ this.currentKeypoint = event.target.keypoint;
+ let id = `#keypointSettings${this.annotation.id}`;
+ let indexLabel = this.currentKeypoint.indexLabel;
+
+ this.keypoint.tag = indexLabel == -1 ? [] : [indexLabel.toString()];
+ this.keypoint.visibility = this.currentKeypoint.visibility;
+
+ $(id).modal("show");
+ },
+ onMouseDrag: event => {
+ let keypoint = event.target.keypoint;
+ if (!["Select", "Keypoints"].includes(this.activeTool)) return;
+
+ this.keypoints.moveKeypoint(event.point, keypoint);
+ }
+ });
+
+ this.keypoints.addKeypoint(keypoint);
+ this.isEmpty = this.compoundPath.isEmpty() && this.keypoints.isEmpty();
+
+ this.tagRecomputeCounter++;
+ },
+ deleteKeypoint(keypoint) {
+ this.keypoints.delete(keypoint);
},
/**
* Unites current annotation path with anyother path.
@@ -260,10 +538,16 @@ export default {
if (this.compoundPath == null) this.createCompoundPath();
let newCompound = this.compoundPath.unite(compound);
+ newCompound.strokeColor = null;
+ newCompound.strokeWidth = 0;
+ newCompound.onDoubleClick = this.compoundPath.onDoubleClick;
+ newCompound.onClick = this.compoundPath.onClick;
+
if (undoable) this.createUndoAction("Unite");
this.compoundPath.remove();
this.compoundPath = newCompound;
+ this.keypoints.bringToFront();
if (simplify) this.simplifyPath();
},
@@ -277,10 +561,12 @@ export default {
if (this.compoundPath == null) this.createCompoundPath();
let newCompound = this.compoundPath.subtract(compound);
+ newCompound.onDoubleClick = this.compoundPath.onDoubleClick;
if (undoable) this.createUndoAction("Subtract");
this.compoundPath.remove();
this.compoundPath = newCompound;
+ this.keypoints.bringToFront();
if (simplify) this.simplifyPath();
},
@@ -292,11 +578,9 @@ export default {
return;
}
+ this.compoundPath.opacity = this.opacity;
this.compoundPath.fillColor = this.color;
- let h = Math.round(this.compoundPath.fillColor.hue);
- let l = Math.round((this.compoundPath.fillColor.lightness - 0.2) * 100);
- let s = Math.round(this.compoundPath.fillColor.saturation * 100);
- this.compoundPath.strokeColor = "hsl(" + h + "," + s + "%," + l + "%)";
+ this.keypoints.color = this.darkHSL;
},
export() {
if (this.compoundPath == null) this.createCompoundPath();
@@ -309,19 +593,56 @@ export default {
metadata: metadata
};
+ this.simplifyPath();
+ this.compoundPath.fullySelected = false;
let json = this.compoundPath.exportJSON({
asString: false,
precision: 1
});
+ if (!this.keypoints.isEmpty()) {
+ annotationData.keypoints = this.keypoints.exportJSON(
+ this.keypointLabels,
+ this.annotation.width,
+ this.annotation.height
+ );
+ }
+
+ this.compoundPath.fullySelected = this.isCurrent;
if (this.annotation.paper_object !== json) {
annotationData.compoundPath = json;
}
+ // Export sessions and reset
+ annotationData.sessions = this.sessions;
+ this.sessions = [];
+
return annotationData;
+ },
+ emitModify() {
+ this.uuid = Math.random()
+ .toString(36)
+ .replace(/[^a-z]+/g, "");
+ this.annotation.paper_object = this.compoundPath.exportJSON({
+ asString: false,
+ precision: 1
+ });
+ this.$socket.emit("annotation", {
+ uuid: this.uuid,
+ action: "modify",
+ annotation: this.annotation
+ });
}
},
watch: {
+ activeTool(tool) {
+ if (this.isCurrent) {
+ this.session.tools.push(tool);
+ }
+ },
+ opacity(opacity) {
+ this.compoundPath.opacity = opacity;
+ },
color() {
this.setColor();
},
@@ -329,23 +650,71 @@ export default {
if (this.compoundPath == null) return;
this.compoundPath.visible = newVisible;
+ this.keypoints.visible = newVisible;
},
compoundPath() {
if (this.compoundPath == null) return;
this.compoundPath.visible = this.isVisible;
- this.$parent.group.addChild(this.compoundPath);
this.setColor();
- this.isEmpty = this.compoundPath.isEmpty();
+ this.isEmpty = this.compoundPath.isEmpty() && this.keypoints.isEmpty();
+ },
+ keypoints() {
+ this.isEmpty = this.compoundPath.isEmpty() && this.keypoints.isEmpty();
},
annotation() {
this.initAnnotation();
+ },
+ isCurrent(current, wasCurrent) {
+ if (current) {
+ // Start new session
+ this.session.start = Date.now();
+ this.session.tools = [this.activeTool];
+ }
+ if (wasCurrent) {
+ // Close session
+ this.session.milliseconds = Date.now() - this.session.start;
+ this.sessions.push(this.session);
+ }
+
+ if (this.compoundPath == null) return;
+ this.compoundPath.fullySelected = this.isCurrent;
+ },
+ currentKeypoint(point, old) {
+ if (old) old.selected = false;
+ if (point) point.selected = true;
+ },
+ "keypoint.tag"(newVal) {
+ let id = newVal.length === 0 ? -1 : newVal[0];
+ this.keypoints.setKeypointIndex(this.currentKeypoint, id);
+ this.tagRecomputeCounter++;
+ },
+ "keypoint.visibility"(newVal) {
+ if (!this.currentKeypoint) return;
+ this.currentKeypoint.visibility = newVal;
+ },
+ keypointEdges(newEdges) {
+ this.keypoints.color = this.darkHSL;
+ newEdges.forEach(e => this.keypoints.addEdge(e));
+ },
+ scale: {
+ immediate: true,
+ handler(scale) {
+ if (!this.keypoints) return;
+
+ this.keypoints.radius = scale * 6;
+ this.keypoints.lineWidth = scale * 2;
+ }
}
},
computed: {
+ categoryIndex() {
+ return this.$parent.index;
+ },
isCurrent() {
if (this.index === this.current && this.$parent.isCurrent) {
if (this.compoundPath != null) this.compoundPath.bringToFront();
+ if (this.keypoints != null) this.keypoints.bringToFront();
return true;
}
return false;
@@ -366,10 +735,76 @@ export default {
if (search === String(this.annotation.id)) return true;
if (search === String(this.index + 1)) return true;
return this.name.toLowerCase().includes(this.search);
+ },
+ darkHSL() {
+ let color = new paper.Color(this.color);
+ let h = Math.round(color.hue);
+ let l = Math.round(color.lightness * 50);
+ let s = Math.round(color.saturation * 100);
+ return "hsl(" + h + "," + s + "%," + l + "%)";
+ },
+ notUsedKeypointLabels() {
+ this.tagRecomputeCounter;
+ let tags = {};
+
+ for (let i = 0; i < this.keypointLabels.length; i++) {
+ // Include it tags if it is the current keypoint or not in use.
+ if (this.keypoints && !this.keypoints._labelled[i + 1]) {
+ tags[i + 1] = this.keypointLabels[i];
+ }
+ }
+
+ return tags;
+ },
+ usedKeypointLabels() {
+ this.tagRecomputeCounter;
+ let tags = {};
+
+ for (let i = 0; i < this.keypointLabels.length; i++) {
+ if (!this.keypoints || this.keypoints._labelled[i + 1]) {
+ tags[i + 1] = this.keypointLabels[i];
+ }
+ }
+
+ return tags;
+ },
+ keypointLabelTags() {
+ this.tagRecomputeCounter;
+ let tags = this.notUsedKeypointLabels;
+
+ Object.keys(this.usedKeypointLabels).forEach(i => {
+ if (this.currentKeypoint && i == this.currentKeypoint.indexLabel) {
+ tags[i] = this.usedKeypointLabels[i];
+ }
+ });
+
+ return tags;
+ }
+ },
+ sockets: {
+ annotation(data) {
+ let annotation = data.annotation;
+
+ if (this.uuid == data.uuid) return;
+ if (annotation.id != this.annotation.id) return;
+
+ if (data.action == "modify") {
+ this.createCompoundPath(
+ annotation.paper_object,
+ annotation.segmentation
+ );
+ }
+
+ if (data.action == "delete") {
+ this.delete();
+ }
}
},
mounted() {
this.initAnnotation();
+ $(`#keypointSettings${this.annotation.id}`).on("hidden.bs.modal", () => {
+ this.currentKeypoint = null;
+ });
}
};
diff --git a/client/src/components/annotator/Category.vue b/client/src/components/annotator/Category.vue
index b706c096..7b82be9a 100755
--- a/client/src/components/annotator/Category.vue
+++ b/client/src/components/annotator/Category.vue
@@ -1,37 +1,101 @@
-
-
+
-
+
@@ -40,29 +104,47 @@
+
+
+ Keypoint Labels
+
+
+
diff --git a/client/src/components/annotator/panels/EraserPanel.vue b/client/src/components/annotator/panels/EraserPanel.vue
index 8e3c1668..63061ca3 100755
--- a/client/src/components/annotator/panels/EraserPanel.vue
+++ b/client/src/components/annotator/panels/EraserPanel.vue
@@ -1,14 +1,22 @@
-
diff --git a/client/src/components/annotator/panels/MagicWandPanel.vue b/client/src/components/annotator/panels/MagicWandPanel.vue
index 8f097938..402d2f73 100755
--- a/client/src/components/annotator/panels/MagicWandPanel.vue
+++ b/client/src/components/annotator/panels/MagicWandPanel.vue
@@ -1,13 +1,24 @@
-
diff --git a/client/src/components/annotator/tools/BrushTool.vue b/client/src/components/annotator/tools/BrushTool.vue
index d34763bb..28a054fe 100755
--- a/client/src/components/annotator/tools/BrushTool.vue
+++ b/client/src/components/annotator/tools/BrushTool.vue
@@ -85,6 +85,12 @@ export default {
strokeColor: this.brush.pathOptions.strokeColor,
radius: this.brush.pathOptions.radius
};
+ },
+ setPreferences(pref) {
+ this.brush.pathOptions.strokeColor =
+ pref.strokeColor || this.brush.pathOptions.strokeColor;
+ this.brush.pathOptions.radius =
+ pref.radius || this.brush.pathOptions.radius;
}
},
computed: {
@@ -112,6 +118,7 @@ export default {
if (active) {
this.tool.activate();
+ localStorage.setItem("editorTool", this.name);
}
},
/**
diff --git a/client/src/components/annotator/tools/CopyAnnotationsButton.vue b/client/src/components/annotator/tools/CopyAnnotationsButton.vue
index 3dee306b..fb69bf8c 100755
--- a/client/src/components/annotator/tools/CopyAnnotationsButton.vue
+++ b/client/src/components/annotator/tools/CopyAnnotationsButton.vue
@@ -7,7 +7,7 @@
data-toggle="modal"
data-target="#copyAnnotations"
>
-
+
@@ -58,12 +77,19 @@
:typeahead-activation-threshold="0"
/>
-
@@ -140,7 +166,6 @@ export default {
}
)
.then(() => {
- this.removeProcess(process);
this.$parent.getData();
})
.catch(error => {
@@ -148,8 +173,8 @@ export default {
"Copying Annotations",
error.response.data.message
);
- this.removeProcess(process);
- });
+ })
+ .finally(() => this.removeProcess(process));
});
}
},
diff --git a/client/src/components/annotator/tools/DEXTRTool.vue b/client/src/components/annotator/tools/DEXTRTool.vue
new file mode 100644
index 00000000..7b69021e
--- /dev/null
+++ b/client/src/components/annotator/tools/DEXTRTool.vue
@@ -0,0 +1,92 @@
+
diff --git a/client/src/components/annotator/tools/EraserTool.vue b/client/src/components/annotator/tools/EraserTool.vue
index fbd4cf26..8be6591b 100755
--- a/client/src/components/annotator/tools/EraserTool.vue
+++ b/client/src/components/annotator/tools/EraserTool.vue
@@ -83,6 +83,12 @@ export default {
strokeColor: this.eraser.pathOptions.strokeColor,
radius: this.eraser.pathOptions.radius
};
+ },
+ setPreferences(pref) {
+ this.eraser.pathOptions.strokeColor =
+ pref.strokeColor || this.eraser.pathOptions.strokeColor;
+ this.eraser.pathOptions.radius =
+ pref.radius || this.eraser.pathOptions.radius;
}
},
computed: {
@@ -110,6 +116,7 @@ export default {
if (active) {
this.tool.activate();
+ localStorage.setItem("editorTool", this.name);
}
},
/**
diff --git a/client/src/components/annotator/tools/KeypointTool.vue b/client/src/components/annotator/tools/KeypointTool.vue
new file mode 100755
index 00000000..f85a896f
--- /dev/null
+++ b/client/src/components/annotator/tools/KeypointTool.vue
@@ -0,0 +1,41 @@
+
diff --git a/client/src/components/annotator/tools/MagicWandTool.vue b/client/src/components/annotator/tools/MagicWandTool.vue
index 152d0b4b..eb30b3d2 100755
--- a/client/src/components/annotator/tools/MagicWandTool.vue
+++ b/client/src/components/annotator/tools/MagicWandTool.vue
@@ -7,9 +7,17 @@ export default {
name: "MagicWand",
mixins: [tool],
props: {
- raster: {
- type: Object,
+ width: {
+ type: null,
required: true
+ },
+ height: {
+ type: null,
+ required: true
+ },
+ imageData: {
+ required: true,
+ validator: prop => typeof prop === "object" || prop === null
}
},
data() {
@@ -20,10 +28,18 @@ export default {
cursor: "crosshair",
wand: {
threshold: 30,
- blur: 5
+ blur: 40
}
};
},
+ watch: {
+ isActive(active) {
+ if (active) {
+ this.tool.activate();
+ localStorage.setItem("editorTool", this.name);
+ }
+ }
+ },
methods: {
/**
* Exports settings
@@ -35,6 +51,10 @@ export default {
blur: this.wand.blur
};
},
+ setPreferences(pref) {
+ this.wand.threshold = pref.threshold || this.wand.threshold;
+ this.wand.blur = pref.blur || this.wand.blur;
+ },
/**
* Creates MagicWand selection
* @param {number} x x position
@@ -44,10 +64,12 @@ export default {
* @returns {paper.CompoundPath} create selection
*/
flood(x, y, thr, rad) {
+ if (this.imageData == null) return;
+
let image = {
- data: this.imageInfo.data.data,
- width: this.imageInfo.width,
- height: this.imageInfo.height,
+ data: this.imageData.data,
+ width: this.width,
+ height: this.height,
bytes: 4
};
let mask = MagicWand.floodFill(image, x, y, thr);
@@ -77,23 +99,19 @@ export default {
// this.$parent.currentAnnotation.simplifyPath();
},
onMouseDown(event) {
- let x = Math.round(this.imageInfo.width / 2 + event.point.x);
- let y = Math.round(this.imageInfo.height / 2 + event.point.y);
+ let x = Math.round(this.width / 2 + event.point.x);
+ let y = Math.round(this.height / 2 + event.point.y);
// Check if valid coordinates
- if (
- x > this.imageInfo.width ||
- y > this.imageInfo.height ||
- x < 0 ||
- y < 0
- ) {
+ if (x > this.width || y > this.height || x < 0 || y < 0) {
return;
}
// Create shape and apply to current annotation
let path = this.flood(x, y, this.wand.threshold, this.wand.blur);
+
if (event.modifiers.shift) {
- this.$parent.currentAnnotation.unite(path);
+ this.$parent.currentAnnotation.subtract(path);
} else {
this.$parent.currentAnnotation.unite(path);
}
@@ -108,29 +126,6 @@ export default {
isDisabled() {
return this.$parent.current.annotation === -1;
}
- },
- watch: {
- // Generate data whenever the image changes
- raster(raster) {
- if (raster == null) return;
- if (Object.keys(raster).length === 0) return;
-
- this.imageInfo.width = raster.width;
- this.imageInfo.height = raster.height;
-
- // Create a copy of image data
- let tempCtx = document.createElement("canvas").getContext("2d");
- tempCtx.canvas.width = raster.width;
- tempCtx.canvas.height = raster.height;
- tempCtx.drawImage(raster.image, 0, 0);
-
- this.imageInfo.data = tempCtx.getImageData(
- 0,
- 0,
- raster.width,
- raster.height
- );
- }
}
};
diff --git a/client/src/components/annotator/tools/PolygonTool.vue b/client/src/components/annotator/tools/PolygonTool.vue
index 62b13044..e9afa985 100755
--- a/client/src/components/annotator/tools/PolygonTool.vue
+++ b/client/src/components/annotator/tools/PolygonTool.vue
@@ -54,15 +54,23 @@ export default {
...mapMutations(["addUndo", "removeUndos"]),
export() {
return {
+ guidance: this.polygon.guidance,
completeDistance: this.polygon.completeDistance,
minDistance: this.polygon.minDistance,
- strokeColor: this.polygon.strokeColor,
-
blackOrWhite: this.color.blackOrWhite,
auto: this.color.auto,
radius: this.color.radius
};
},
+ setPreferences(pref) {
+ this.polygon.guidance = pref.guidance || this.polygon.guidance;
+ this.polygon.completeDistance =
+ pref.completeDistance || this.polygon.completeDistance;
+ this.polygon.minDistance = pref.minDistance || this.polygon.minDistance;
+ this.color.blackOrWhite = pref.blackOrWhite || this.color.blackOrWhite;
+ this.color.auto = pref.auto || this.color.auto;
+ this.color.radius = pref.radius || this.color.radius;
+ },
/**
* Creates a new selection polygon path
*/
@@ -213,6 +221,12 @@ export default {
}
},
watch: {
+ isActive(active) {
+ if (active) {
+ this.tool.activate();
+ localStorage.setItem("editorTool", this.name);
+ }
+ },
/**
* Change width of stroke based on zoom of image
*/
diff --git a/client/src/components/annotator/tools/SelectTool.vue b/client/src/components/annotator/tools/SelectTool.vue
index e08bf815..fc0716c1 100755
--- a/client/src/components/annotator/tools/SelectTool.vue
+++ b/client/src/components/annotator/tools/SelectTool.vue
@@ -17,8 +17,15 @@ export default {
name: "Select",
cursor: "pointer",
movePath: false,
+ point: null,
segment: null,
scaleFactor: 15,
+ edit: {
+ indicatorWidth: 0,
+ indicatorSize: 0,
+ center: null,
+ canMove: false
+ },
hover: {
showText: true,
text: null,
@@ -33,10 +40,15 @@ export default {
annotation: null,
annotationText: null
},
+ keypoint: null,
hitOptions: {
segments: true,
stroke: true,
- tolerance: 2
+ fill: false,
+ tolerance: 5,
+ match: hit => {
+ return !hit.item.hasOwnProperty("indicator");
+ }
}
};
},
@@ -46,8 +58,43 @@ export default {
showText: this.hover.showText
};
},
- generateStringFromMetadata() {
+ setPreferences(pref) {
+ this.hover.showText = pref.showText || this.hover.showText;
+ },
+ generateTitle() {
let string = " ";
+ if (this.keypoint) {
+ let index = this.keypoint.keypoint.indexLabel;
+ let visibility = this.keypoint.keypoint.visibility;
+
+ string += "Keypoint \n";
+ string += "Visibility: " + visibility + " \n";
+ string +=
+ index == -1
+ ? "No Label \n"
+ : "Label: " + this.keypoint.keypoints.labels[index - 1] + " \n";
+ return string.replace(/\n/g, " \n ").slice(0, -2);
+ }
+
+ if (this.hover.category && this.hover.annotation) {
+ let id = this.hover.textId;
+ let category = this.hover.category.category.name;
+ string += "ID: " + id + " \n";
+ string += "Category: " + category + " \n";
+ }
+
+ if (this.$store.getters["user/loginEnabled"]) {
+ let creator = this.hover.annotation.annotation.creator;
+ if (creator != null) {
+ string += "Created by " + creator + "\n\n";
+ }
+ }
+
+ return string.replace(/\n/g, " \n ").slice(0, -2) + " \n ";
+ },
+ generateStringFromMetadata() {
+ if (this.keypoint) return "";
+ let string = "";
let metadata = this.hover.annotation.$refs.metadata.metadataList;
if (metadata == null || metadata.length === 0) {
@@ -61,39 +108,36 @@ export default {
});
}
- if (this.$store.getters["user/loginEnabled"]) {
- let creator = this.hover.annotation.annotation.creator;
- if (creator != null) {
- string += "Created by " + creator + "\n";
- }
- }
-
return string.replace(/\n/g, " \n ").slice(0, -2);
},
hoverText() {
if (!this.hover.showText) return;
-
- if (this.hover.category == null) return;
- if (this.hover.annotation == null) return;
+ if (!this.keypoint) {
+ if (this.hover.category == null) return;
+ if (this.hover.annotation == null) return;
+ }
let position = this.hover.position.add(this.hover.textShift, 0);
if (
this.hover.text == null ||
- this.hover.annotation.annotation.id !== this.hover.textId
+ this.hover.annotation.annotation.id !== this.hover.textId ||
+ this.keypoint != null
) {
if (this.hover.text !== null) {
this.hover.text.remove();
this.hover.box.remove();
}
-
- this.hover.textId = this.hover.annotation.annotation.id;
- let content = this.generateStringFromMetadata();
+ let content = this.generateTitle() + this.generateStringFromMetadata();
+ if (this.hover.annotation) {
+ this.hover.textId = this.hover.annotation.annotation.id;
+ }
this.hover.text = new paper.PointText(position);
this.hover.text.justification = "left";
this.hover.text.fillColor = "black";
this.hover.text.content = content;
+ this.hover.text.indicator = true;
this.hover.text.fontSize = this.hover.fontSize;
@@ -101,7 +145,7 @@ export default {
this.hover.text.bounds,
this.hover.rounded
);
-
+ this.hover.box.indicator = true;
this.hover.box.fillColor = "white";
this.hover.box.strokeColor = "white";
this.hover.box.opacity = 0.5;
@@ -115,12 +159,14 @@ export default {
2;
this.hover.box.position = position.add(this.hover.shift, 0);
this.hover.text.position = position.add(this.hover.shift, 0);
+
+ this.hover.box.bringToFront();
this.hover.text.bringToFront();
},
onMouseDown(event) {
let hitResult = this.$parent.paper.project.hitTest(
event.point,
- this.hitResult
+ this.hitOptions
);
if (!hitResult) return;
@@ -132,85 +178,159 @@ export default {
return;
}
- this.path = hitResult.item;
+ let path = hitResult.item;
if (hitResult.type === "segment") {
this.segment = hitResult.segment;
} else if (hitResult.type === "stroke") {
let location = hitResult.location;
- this.segment = this.path.insert(location.index + 1, event.point);
+ this.segment = path.insert(location.index + 1, event.point);
+ }
+
+ if (this.point != null) {
+ this.edit.canMove = this.point.contains(event.point);
+ } else {
+ this.edit.canMove = false;
+ }
+ },
+ clear() {
+ this.hover.category = null;
+ this.hover.annotation = null;
+
+ if (this.hover.text != null) {
+ this.hover.text.remove();
+ this.hover.box.remove();
+ this.hover.text = null;
+ this.hover.box = null;
}
},
+ createPoint(point) {
+ if (this.point != null) {
+ this.point.remove();
+ }
+
+ this.point = new paper.Path.Circle(point, this.edit.indicatorSize);
+ this.point.strokeColor = "white";
+ this.point.strokeWidth = this.edit.indicatorWidth;
+ this.point.indicator = true;
+ },
onMouseDrag(event) {
- if (this.segment) {
+ if (this.segment && this.edit.canMove) {
+ this.createPoint(event.point);
this.segment.point = event.point;
}
},
onMouseMove(event) {
+ let hitResult = this.$parent.paper.project.hitTest(
+ event.point,
+ this.hitOptions
+ );
+
+ if (hitResult) {
+ let point = null;
+
+ if (hitResult.type === "segment") {
+ point = hitResult.segment.location.point;
+ } else if (hitResult.type === "stroke") {
+ point = hitResult.location.point;
+ }
+
+ if (point != null) {
+ this.edit.center = point;
+ this.createPoint(point);
+ } else {
+ if (this.point != null) {
+ this.point.remove();
+ this.point = null;
+ }
+ }
+ }
+
this.$parent.hover.annotation = -1;
this.$parent.hover.category = -1;
this.$parent.paper.project.activeLayer.selected = false;
+ let item = event.item;
+
+ this.keypoint = null;
+
if (
event.item &&
- event.item.visible &&
- event.item.data.hasOwnProperty("categoryId") &&
- event.item.hasChildren()
+ event.item.data.hasOwnProperty("annotationId") &&
+ event.item.data.hasOwnProperty("categoryId")
) {
- let item = event.item;
- this.$parent.hover.category = item.data.categoryId;
- this.hover.category = this.$parent.getCategory(item.data.categoryId);
-
- if (this.hover.category == null) return;
-
- for (let i = 0; i < item.children.length; i++) {
- let child = item.children[i];
-
- if (
- child.visible &&
- child.contains(event.point) &&
- child.data.hasOwnProperty("annotationId")
- ) {
- this.hover.position = event.point;
- this.$parent.hover.annotation = child.data.annotationId;
+ this.hover.position = event.point;
- this.hover.annotation = this.hover.category.getAnnotation(
- child.data.annotationId
- );
- child.selected = true;
+ let categoryId = event.item.data.categoryId;
+ let annotationId = event.item.data.annotationId;
+ this.$parent.hover.categoryId = categoryId;
+ this.$parent.hover.annotation = annotationId;
- this.hoverText();
- break;
- }
+ this.hover.category = this.$parent.getCategory(categoryId);
+ if (this.hover.category != null) {
+ this.hover.annotation = this.hover.category.getAnnotation(annotationId);
+ event.item.selected = true;
+ this.hoverText();
}
+ } else if (event.item && event.item.hasOwnProperty("keypoint")) {
+ this.hover.position = event.point;
+ this.keypoint = item;
} else {
- this.hover.category = null;
- this.hover.annotation = -1;
+ this.clear();
+ }
+ }
+ },
+ watch: {
+ keypoint(keypoint) {
+ this.clear();
+ if (!keypoint) return;
+ this.hoverText();
+ },
+ scale: {
+ handler(newScale) {
+ this.hover.rounded = newScale * 5;
+ this.hover.textShift = newScale * 40;
+ this.hover.fontSize = newScale * this.scaleFactor;
+ this.edit.distance = newScale * 40;
+ this.edit.indicatorSize = newScale * 10;
+ this.edit.indicatorWidth = newScale * 2;
+
+ if (this.edit.center && this.point != null) {
+ this.createPoint(this.edit.center);
+ }
if (this.hover.text != null) {
+ this.hover.text.fontSize = this.hover.fontSize;
+ this.hover.shift =
+ (this.hover.text.bounds.bottomRight.x -
+ this.hover.text.bounds.bottomLeft.x) /
+ 2;
+ let totalShift = this.hover.shift + this.hover.textShift;
+ this.hover.text.position = this.hover.position.add(totalShift, 0);
+ this.hover.box.bounds = this.hover.text.bounds;
+ }
+ },
+ immediate: true
+ },
+ isActive(active) {
+ if (active) {
+ this.tool.activate();
+ } else {
+ if (this.hover.text) {
this.hover.text.remove();
this.hover.box.remove();
- this.hover.text = null;
+
this.hover.box = null;
+ this.hover.text = null;
+ }
+ if (this.point) {
+ this.point.remove();
+ this.point = null;
+ this.segment = null;
+ }
+ if (this.hover.annotation) {
+ this.hover.annotation.compoundPath.selected = false;
}
- }
- }
- },
- watch: {
- scale(newScale) {
- this.hover.rounded = newScale * 5;
- this.hover.textShift = newScale * 40;
- this.hover.fontSize = newScale * this.scaleFactor;
-
- if (this.hover.text != null) {
- this.hover.text.fontSize = this.hover.fontSize;
- this.hover.shift =
- (this.hover.text.bounds.bottomRight.x -
- this.hover.text.bounds.bottomLeft.x) /
- 2;
- let totalShift = this.hover.shift + this.hover.textShift;
- this.hover.text.position = this.hover.position.add(totalShift, 0);
- this.hover.box.bounds = this.hover.text.bounds;
}
}
}
diff --git a/client/src/components/annotator/tools/SettingsButton.vue b/client/src/components/annotator/tools/SettingsButton.vue
index ebbd472f..3b158b01 100755
--- a/client/src/components/annotator/tools/SettingsButton.vue
+++ b/client/src/components/annotator/tools/SettingsButton.vue
@@ -1,24 +1,56 @@
diff --git a/client/src/components/annotator/tools/UndoButton.vue b/client/src/components/annotator/tools/UndoButton.vue
index fea55f07..ad41bd8e 100755
--- a/client/src/components/annotator/tools/UndoButton.vue
+++ b/client/src/components/annotator/tools/UndoButton.vue
@@ -1,4 +1,3 @@
-
+
+
diff --git a/client/src/components/tasks/TaskGroup.vue b/client/src/components/tasks/TaskGroup.vue
new file mode 100644
index 00000000..b54bf42e
--- /dev/null
+++ b/client/src/components/tasks/TaskGroup.vue
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
diff --git a/client/src/libs/keypoints.js b/client/src/libs/keypoints.js
new file mode 100644
index 00000000..120b6d86
--- /dev/null
+++ b/client/src/libs/keypoints.js
@@ -0,0 +1,435 @@
+import paper from "paper";
+
+export class Keypoints extends paper.Group {
+ constructor(edges, labels, args) {
+ super();
+ args = args || {};
+
+ this._edges = {};
+
+ this._lines = {};
+ this._labelled = {};
+ this._keypoints = [];
+ this.labels = labels;
+
+ this.strokeColor = args.strokeColor || "red";
+ this.lineWidth = args.strokeWidth || 4;
+
+ edges = edges || [];
+ edges.forEach(e => this.addEdge(e));
+ }
+
+ isEmpty() {
+ return this._keypoints.length === 0;
+ }
+
+ setKeypointIndex(keypoint, newIndex) {
+ let oldIndex = keypoint.indexLabel;
+ if (newIndex == oldIndex) return;
+
+ keypoint.indexLabel = parseInt(newIndex);
+
+ if (oldIndex >= 0) {
+ delete this._labelled[oldIndex];
+
+ let otherIndices = this._edges[oldIndex];
+ if (otherIndices) {
+ otherIndices.forEach(i => this.removeLine([i, oldIndex]));
+ }
+ // TODO: Remove assoicated lines
+ }
+ if (newIndex >= 0) {
+ this._labelled[newIndex] = keypoint;
+ this._drawLines(keypoint);
+ }
+ }
+
+ bringToFront() {
+ super.bringToFront();
+ Object.values(this._lines).forEach(l => l.bringToFront());
+ this._keypoints.forEach(k => k.path.bringToFront());
+ }
+
+ addKeypoint(keypoint) {
+ keypoint.keypoints = this;
+ keypoint.path.keypoints = this;
+ keypoint.color = this.strokeColor;
+ keypoint.path.strokeWidth = this.strokeWidth;
+
+ let indexLabel = keypoint.indexLabel;
+ if (this._labelled.hasOwnProperty(indexLabel)) {
+ keypoint.indexLabel = -1;
+ } else {
+ this._labelled[indexLabel] = keypoint;
+ }
+
+ this._keypoints.push(keypoint);
+ this.addChild(keypoint.path);
+ this._drawLines(keypoint);
+ keypoint.path.bringToFront();
+ }
+
+ deleteKeypoint(keypoint) {
+ let indexLabel = keypoint.indexLabel;
+ if (this._labelled.hasOwnProperty(indexLabel)) {
+ delete this._labelled[indexLabel];
+ }
+ if (this._edges.hasOwnProperty(indexLabel)) {
+ this._edges[indexLabel].forEach(e => this.removeLine([e, indexLabel]));
+ }
+ let index = this._keypoints.findIndex(k => k == keypoint);
+ if (index > -1) this._keypoints.splice(index, 1);
+ keypoint.path.remove();
+ }
+
+ moveKeypoint(point, keypoint) {
+ let indexLabel = keypoint.indexLabel;
+ let edges = this._edges[indexLabel];
+
+ if (edges) {
+ edges.forEach(i => {
+ let line = this.getLine([i, indexLabel]);
+ if (line) {
+ // We need to move the line aswell
+ for (let i = 0; i < line.segments.length; i++) {
+ let segment = line.segments[i];
+ if (segment.point.isClose(keypoint, 0)) {
+ segment.point = point;
+ break;
+ }
+ }
+ }
+ });
+ }
+ keypoint.move(point);
+ keypoint.path.bringToFront();
+ }
+
+ set visible(val) {
+ this._visible = val;
+ this._keypoints.forEach(k => (k.visible = val));
+ Object.values(this._lines).forEach(l => (l.visible = val));
+ }
+
+ get visible() {
+ return this._visible;
+ }
+
+ set color(val) {
+ this._color = val;
+ this.strokeColor = val;
+ this._keypoints.forEach(k => (k.color = val));
+ Object.values(this._lines).forEach(l => (l.strokeColor = val));
+ }
+
+ get color() {
+ return this._color;
+ }
+
+ set lineWidth(val) {
+ this._lineWidth = val;
+ this.strokeWidth = val;
+ this._keypoints.forEach(k => (k.path.storkeWidth = val));
+ Object.values(this._lines).forEach(l => (l.strokeWidth = val));
+ }
+
+ get lineWidth() {
+ return this._lineWidth;
+ }
+
+ set radius(val) {
+ this._radius = val;
+ this._keypoints.forEach(k => (k.radius = val));
+ }
+
+ get radius() {
+ return this._radius;
+ }
+
+ exportJSON(labels, width, height) {
+ let array = [];
+ for (let i = 0; i < labels.length; i++) {
+ let j = i * 3;
+ array[j] = 0;
+ array[j + 1] = 0;
+ array[j + 2] = 0;
+ }
+
+ this._keypoints.forEach(k => {
+ let center = new paper.Point(width / 2, height / 2);
+ let point = k.clone().add(center);
+ let index = k.indexLabel;
+
+ if (index == -1) {
+ array.push(...[Math.round(point.x), Math.round(point.y), k.visibility]);
+ } else {
+ index = (index - 1) * 3;
+ array[index] = Math.round(point.x);
+ array[index + 1] = Math.round(point.y);
+ array[index + 2] = Math.round(k.visibility);
+ }
+ });
+
+ return array;
+ }
+
+ contains(point) {
+ return this._keypoints.findIndex(k => k.path.contains(point)) > -1;
+ }
+
+ edges() {
+ let edges = [];
+ let keys = Object.keys(this._edges);
+
+ for (let i = 0; i < keys.length; i++) {
+ let i1 = parseInt(keys[i]);
+ let otherIndices = Array.from(this._edges[i1]);
+
+ for (let j = 0; j < otherIndices.length; j++) {
+ let i2 = parseInt(otherIndices[j]);
+
+ if (i2 < i1) continue;
+ edges.push([i1, i2]);
+ }
+ }
+
+ return edges;
+ }
+
+ addEdge(edge) {
+ if (edge.length !== 2) return;
+
+ let i1 = edge[0];
+ let i2 = edge[1];
+
+ // If labels convert to indexs
+ if (typeof i1 == "string") i1 = this.getLabelIndex(i1);
+ if (typeof i2 == "string") i2 = this.getLabelIndex(i2);
+ if (i1 < 0 || i2 < 0) return;
+
+ this._addEdgeIndex(i1, i2);
+ this._addEdgeIndex(i2, i1);
+
+ // Draw line if points exist
+ let k1 = this._labelled[i1];
+ let k2 = this._labelled[i2];
+ if (k1 && k2) {
+ this._drawLine(edge, k1, k2);
+ }
+ }
+
+ getLabelIndex(label) {
+ return this.labels.find(l => l == label);
+ }
+
+ _addEdgeIndex(index1, index2) {
+ if (this._edges.hasOwnProperty(index1)) {
+ if (!this._edges[index1].has(index2)) this._edges[index1].add(index2);
+ } else {
+ this._edges[index1] = new Set([index2]);
+ }
+ }
+
+ /**
+ * Draws lines to other keypoints if they exist
+ */
+ _drawLines(keypoint) {
+ if (keypoint.indexLabel < 0) return;
+ if (!this._edges.hasOwnProperty(keypoint.indexLabel)) return;
+
+ let otherIndices = this._edges[keypoint.indexLabel];
+ otherIndices.forEach(i => {
+ let k2 = this._labelled[i];
+ if (!k2) return;
+
+ let edge = [keypoint.indexLabel, i];
+ this._drawLine(edge, keypoint, k2);
+ });
+ }
+
+ /**
+ * Draws a line between two keypoints and hashes to a table for quick look up
+ * @param {list} edge array of two elementings contain the index edges
+ * @param {Keypoint} firstKeypoint first keypoint object
+ * @param {Keypoint} secondKeypoint second keypoint object
+ */
+ _drawLine(edge, firstKeypoint, secondKeypoint) {
+ let h = this._hashEdge(edge);
+ if (this._lines[h]) return;
+
+ let line = new paper.Path.Line(firstKeypoint, secondKeypoint);
+ line.strokeColor = this.strokeColor;
+ line.strokeWidth = this.lineWidth;
+ line.indicator = true;
+
+ line.insertAbove(secondKeypoint.path);
+
+ this._lines[h] = line;
+ }
+
+ removeLine(edge) {
+ let h = this._hashEdge(edge);
+ let line = this._lines[h];
+ if (line) {
+ line.remove();
+ delete this._lines[h];
+ }
+ }
+
+ /**
+ * Returns paperjs path of line [O(1) lookup time]
+ * @param {list} edge array of two elementing contains the index edges
+ * @returns paperjs object path of the line or undefind if not found
+ */
+ getLine(edge) {
+ let h = this._hashEdge(edge);
+ return this._lines[h];
+ }
+
+ /**
+ * Uses cantor pairing function to has two numbers
+ * @param {list} edge array of two elementing contains the index edges
+ */
+ _hashEdge(edge) {
+ // Order doesn't matter so can sort first
+ let min = Math.min(edge[0], edge[1]);
+ let max = Math.max(edge[0], edge[1]);
+ // Cantor pairing function
+ let add = min + max;
+ return (1 / 2) * add * (add - 1) - max;
+ }
+}
+
+/**
+ * Keypoint visibility types as defined by the COCO format
+ */
+export let VisibilityType = {
+ NOT_LABELED: 0,
+ LABELED_NOT_VISIBLE: 1,
+ LABELED_VISIBLE: 2,
+ UNKNOWN: 3
+};
+
+export class Keypoint extends paper.Point {
+ constructor(x, y, args) {
+ super(x, y);
+ args = args || {};
+
+ this.path = null;
+
+ this.label = args.label || "";
+ this.radius = args.radius || 5;
+ this.indexLabel = args.indexLabel || -1;
+ this.visibility = args.visibility || VisibilityType.NOT_LABELED;
+ this.visible = args.visible || true;
+
+ this.onClick = args.onClick;
+ this.onDoubleClick = args.onDoubleClick;
+ this.onMouseDrag = args.onMouseDrag;
+
+ this._draw();
+ this.color = args.color || "red";
+ this.setFillColor();
+ }
+
+ setFillColor() {
+ if (this.path == null) return;
+
+ switch (this.visibility) {
+ case VisibilityType.NOT_LABELED:
+ this.path.fillColor = "black";
+ break;
+ case VisibilityType.LABELED_NOT_VISIBLE:
+ this.path.fillColor = "white";
+ break;
+ default:
+ this.path.fillColor = this.color;
+ }
+ }
+
+ move(point) {
+ this.x = point.x;
+ this.y = point.y;
+ this._draw();
+ }
+
+ _draw() {
+ let strokeColor = this.color;
+ if (this.path !== null) {
+ strokeColor = this.path.strokeColor;
+ this.path.remove();
+ }
+
+ this.path = new paper.Path.Circle(this, this.radius);
+
+ this.path.onMouseDown = this.onMouseDown;
+ this.path.onMouseUp = this.onMouseUp;
+ this.path.onMouseDrag = this.onMouseDrag;
+ this.path.onDoubleClick = this.onDoubleClick;
+ this.path.onClick = this.onClick;
+
+ this.path.indicator = true;
+ this.path.strokeColor = strokeColor;
+ this.path.strokeWidth = this.radius * 0.4;
+ this.path.visible = this.visible;
+ this.path.keypoint = this;
+ this.path.keypoints = this.keypoints;
+
+ this.setFillColor();
+ }
+
+ set visible(val) {
+ this._visible = val;
+ this.path.visible = val;
+ }
+
+ get visible() {
+ return this._visible;
+ }
+
+ set visibility(val) {
+ this._visibility = val;
+ this.setFillColor();
+ }
+
+ get visibility() {
+ return this._visibility;
+ }
+
+ set radius(val) {
+ this._radius = val;
+ this._draw();
+ }
+
+ get radius() {
+ return this._radius;
+ }
+
+ set color(val) {
+ this._color = val;
+ this.path.strokeColor = this.selected ? "white" : val;
+ this.setFillColor();
+ }
+
+ get color() {
+ return this._color;
+ }
+
+ set strokeColor(val) {
+ this.color = val;
+ }
+
+ get strokeColor() {
+ return this.color;
+ }
+
+ set selected(val) {
+ this._selected = val;
+ this.path.strokeColor = val ? "white" : this.color;
+ this.path.bringToFront();
+ }
+
+ get selected() {
+ return this._selected;
+ }
+}
diff --git a/client/src/main.js b/client/src/main.js
index c473e01a..51d2d64f 100755
--- a/client/src/main.js
+++ b/client/src/main.js
@@ -1,3 +1,5 @@
+import "intersection-observer";
+
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
@@ -6,6 +8,8 @@ import VueToastr2 from "vue-toastr-2";
import paper from "paper";
import VTooltip from "v-tooltip";
import Loading from "vue-loading-overlay";
+import VueSocketIO from "vue-socket.io";
+import { VLazyImagePlugin } from "v-lazy-image";
import "bootstrap";
import "bootstrap/dist/css/bootstrap.min.css";
@@ -17,9 +21,17 @@ Vue.config.productionTip = false;
paper.install(window);
window.toastr = require("toastr");
+
Vue.use(VueToastr2);
Vue.use(VTooltip);
Vue.use(Loading);
+Vue.use(VLazyImagePlugin);
+Vue.use(
+ new VueSocketIO({
+ debug: true,
+ connection: window.location.origin
+ })
+);
new Vue({
router,
diff --git a/client/src/mixins/shortcuts.js b/client/src/mixins/shortcuts.js
index 50a1ca99..21738508 100755
--- a/client/src/mixins/shortcuts.js
+++ b/client/src/mixins/shortcuts.js
@@ -3,7 +3,7 @@ import { mapMutations } from "vuex";
export default {
data() {
return {
- commands: [],
+ commands: []
};
},
methods: {
@@ -13,22 +13,22 @@ export default {
{
default: ["arrowup"],
function: this.moveUp,
- name: "Move Up Annotaitons",
+ name: "Move Up Annotations"
},
{
default: ["arrowdown"],
function: this.moveDown,
- name: "Move Down Annotations",
+ name: "Move Down Annotations"
},
{
default: ["arrowright"],
function: this.stepIn,
- name: "Expand Category",
+ name: "Expand Category"
},
{
default: ["arrowleft"],
function: this.stepOut,
- name: "Collapse Category",
+ name: "Collapse Category"
},
{
default: ["space"],
@@ -37,35 +37,44 @@ export default {
if (this.currentCategory) {
this.currentCategory.createAnnotation();
}
- },
+ }
},
{
- default: ["delete"],
+ default: ["backspace"],
name: "Delete Current Annotation",
function: () => {
if (this.currentAnnotation) {
- this.currentAnnotation.deleteAnnotation();
+ let currentKeypoint = this.currentAnnotation.currentKeypoint;
+ if (currentKeypoint) {
+ this.currentAnnotation.keypoints.deleteKeypoint(
+ currentKeypoint
+ );
+ this.currentAnnotation.tagRecomputeCounter++;
+ this.currentAnnotation.currentKeypoint = null;
+ } else {
+ this.currentAnnotation.deleteAnnotation();
+ }
}
- },
+ }
},
{
default: ["control", "z"],
name: "Undo Last Action",
- function: this.undo,
+ function: this.undo
},
{
default: ["s"],
name: "Select Tool",
function: () => {
this.activeTool = "Select";
- },
+ }
},
{
default: ["p"],
name: "Polygon Tool",
function: () => {
- if (this.$refs.polygon.isDisabled) wwthis.activeTool = "Polygon";
- },
+ if (!this.$refs.polygon.isDisabled) this.activeTool = "Polygon";
+ }
},
{
default: ["w"],
@@ -73,72 +82,79 @@ export default {
function: () => {
if (!this.$refs.magicwand.isDisabled)
this.activeTool = "Magic Wand";
- },
+ }
+ },
+ {
+ default: ["k"],
+ name: "Keypoints Tool",
+ function: () => {
+ if (!this.$refs.magicwand.isDisabled) this.activeTool = "Keypoints";
+ }
},
{
default: ["b"],
name: "Brush Tool",
function: () => {
- if (this.$refs.brush.isDisabled) this.activeTool = "Brush";
- },
+ if (!this.$refs.brush.isDisabled) this.activeTool = "Brush";
+ }
},
{
default: ["e"],
name: "Eraser Tool",
function: () => {
- if (this.$refs.eraser.isDisabled) this.activeTool = "Eraser";
- },
+ if (!this.$refs.eraser.isDisabled) this.activeTool = "Eraser";
+ }
},
{
default: ["c"],
name: "Center Image",
- function: this.fit,
+ function: this.fit
},
{
default: ["control", "s"],
name: "Save",
- function: this.save,
+ function: this.save
},
{
title: "Polygon Tool Shortcuts",
default: ["escape"],
name: "Remove Current Polygon",
- function: this.$refs.polygon.deletePolygon,
+ function: this.$refs.polygon.deletePolygon
},
{
title: "Eraser Tool Shortcuts",
default: ["["],
name: "Increase Radius",
- function: this.$refs.eraser.increaseRadius,
+ function: this.$refs.eraser.increaseRadius
},
{
default: ["]"],
name: "Decrease Radius",
- function: this.$refs.eraser.decreaseRadius,
+ function: this.$refs.eraser.decreaseRadius
},
{
title: "Brush Tool Shortcuts",
default: ["["],
name: "Increase Radius",
- function: this.$refs.brush.increaseRadius,
+ function: this.$refs.brush.increaseRadius
},
{
default: ["]"],
name: "Decrease Radius",
- function: this.$refs.brush.decreaseRadius,
+ function: this.$refs.brush.decreaseRadius
},
{
title: "Magic Tool Shortcuts",
default: ["shift", "click"],
name: "Subtract Selection",
- readonly: true,
- },
+ readonly: true
+ }
];
- },
+ }
},
mounted() {
if (this.$route.name === "annotate") {
this.commands = this.annotator();
}
- },
+ }
};
diff --git a/client/src/mixins/toolBar/tool.js b/client/src/mixins/toolBar/tool.js
index 6eadb4c8..f845ea09 100755
--- a/client/src/mixins/toolBar/tool.js
+++ b/client/src/mixins/toolBar/tool.js
@@ -38,7 +38,8 @@ export default {
if (this.isDisabled) return;
this.$emit("update", this.name);
- }
+ },
+ setPreferences() {}
},
computed: {
isActive() {
diff --git a/client/src/models/admin.js b/client/src/models/admin.js
new file mode 100644
index 00000000..ece4df92
--- /dev/null
+++ b/client/src/models/admin.js
@@ -0,0 +1,15 @@
+import axios from "axios";
+
+const baseURL = "/api/admin/";
+
+export default {
+ getUsers(limit) {
+ return axios.get(baseURL + `users?limit=${limit}`);
+ },
+ createUser(user) {
+ return axios.post(baseURL + "user/", { ...user });
+ },
+ deleteUser(username) {
+ return axios.delete(baseURL + `user/${username}`);
+ }
+};
diff --git a/client/src/models/annotations.js b/client/src/models/annotations.js
new file mode 100644
index 00000000..2fa9bbc4
--- /dev/null
+++ b/client/src/models/annotations.js
@@ -0,0 +1,12 @@
+import axios from "axios";
+
+const baseURL = "/api/annotation/";
+
+export default {
+ create(annotation) {
+ return axios.post(baseURL, annotation);
+ },
+ delete(id) {
+ return axios.delete(`${baseURL}${id}`);
+ }
+};
diff --git a/client/src/models/categories.js b/client/src/models/categories.js
new file mode 100644
index 00000000..9d8568b9
--- /dev/null
+++ b/client/src/models/categories.js
@@ -0,0 +1,16 @@
+import axios from "axios";
+
+const baseURL = "/api/category/";
+
+export default {
+ allData(params) {
+ return axios.get(baseURL + "data", {
+ params: {
+ ...params
+ }
+ });
+ },
+ create(create) {
+ return axios.post(baseURL, { ...create });
+ }
+};
diff --git a/client/src/models/datasets.js b/client/src/models/datasets.js
new file mode 100644
index 00000000..4cc06a7f
--- /dev/null
+++ b/client/src/models/datasets.js
@@ -0,0 +1,64 @@
+import axios from "axios";
+
+const baseURL = "/api/dataset";
+
+export default {
+ allData(params) {
+ return axios.get(`${baseURL}/data`, {
+ params: {
+ ...params
+ }
+ });
+ },
+ getData(id, params) {
+ return axios.get(`${baseURL}/${id}/data`, {
+ params: {
+ ...params
+ }
+ });
+ },
+ create(name, categories) {
+ return axios.post(`${baseURL}/?name=${name}`, {
+ categories: categories
+ });
+ },
+ generate(id, body) {
+ return axios.post(`${baseURL}/${id}/generate`, {
+ ...body
+ });
+ },
+ scan(id) {
+ return axios.get(`${baseURL}/${id}/scan`);
+ },
+ exportingCOCO(id, categories) {
+ return axios.get(`${baseURL}/${id}/export?categories=${categories}`);
+ },
+ getCoco(id) {
+ return axios.get(`${baseURL}/${id}/coco`);
+ },
+ uploadCoco(id, file) {
+ let form = new FormData();
+ form.append("coco", file);
+
+ return axios.post(`${baseURL}/${id}/coco`, form, {
+ headers: {
+ "Content-Type": "multipart/form-data"
+ }
+ });
+ },
+ export(id, format) {
+ return axios.get(`${baseURL}/${id}/${format}`);
+ },
+ getUsers(id) {
+ return axios.get(`${baseURL}/${id}/users`);
+ },
+ getStats(id) {
+ return axios.get(`${baseURL}/${id}/stats`);
+ },
+ getExports(id) {
+ return axios.get(`${baseURL}/${id}/exports`);
+ },
+ resetMetadata(id) {
+ return axios.get(`${baseURL}/${id}/reset/metadata`);
+ }
+};
diff --git a/client/src/models/exports.js b/client/src/models/exports.js
new file mode 100644
index 00000000..9f55fa6c
--- /dev/null
+++ b/client/src/models/exports.js
@@ -0,0 +1,20 @@
+import axios from "axios";
+
+const baseURL = "/api/export";
+
+export default {
+ download(id, dataset) {
+ axios({
+ url: `${baseURL}/${id}/download`,
+ method: "GET",
+ responseType: "blob"
+ }).then(response => {
+ const url = window.URL.createObjectURL(new Blob([response.data]));
+ const link = document.createElement("a");
+ link.href = url;
+ link.setAttribute("download", `${dataset}-${id}.json`);
+ document.body.appendChild(link);
+ link.click();
+ });
+ }
+};
diff --git a/client/src/models/tasks.js b/client/src/models/tasks.js
new file mode 100644
index 00000000..f7734237
--- /dev/null
+++ b/client/src/models/tasks.js
@@ -0,0 +1,15 @@
+import axios from "axios";
+
+const baseURL = "/api/tasks/";
+
+export default {
+ all() {
+ return axios.get(baseURL);
+ },
+ delete(id) {
+ return axios.delete(baseURL + id);
+ },
+ getLogs(id) {
+ return axios.get(baseURL + id + "/logs");
+ }
+};
diff --git a/client/src/models/undos.js b/client/src/models/undos.js
new file mode 100644
index 00000000..6af6f97f
--- /dev/null
+++ b/client/src/models/undos.js
@@ -0,0 +1,15 @@
+import axios from "axios";
+
+const baseURL = "/api/undo/";
+
+export default {
+ all(limit, instance) {
+ return axios.get(baseURL + `list/?limit=${limit}&type=${instance}`);
+ },
+ undo(id, instance) {
+ return axios.post(baseURL + `?id=${id}&instance=${instance}`);
+ },
+ delete(id, instance) {
+ return axios.delete(baseURL + `?id=${id}&instance=${instance}`);
+ }
+};
diff --git a/client/src/router.js b/client/src/router.js
index f110f1ae..1070a157 100755
--- a/client/src/router.js
+++ b/client/src/router.js
@@ -11,6 +11,7 @@ import Undo from "@/views/Undo";
import Dataset from "@/views/Dataset";
import Auth from "@/views/Auth";
import User from "@/views/User";
+import Tasks from "@/views/Tasks";
import PageNotFound from "@/views/PageNotFound";
Vue.use(Router);
@@ -67,6 +68,11 @@ export default new Router({
name: "admin",
component: AdminPanel
},
+ {
+ path: "/tasks",
+ name: "tasks",
+ component: Tasks
+ },
{ path: "*", component: PageNotFound }
]
});
diff --git a/client/src/store/index.js b/client/src/store/index.js
index fcd0ff3c..4fd23abe 100644
--- a/client/src/store/index.js
+++ b/client/src/store/index.js
@@ -13,9 +13,13 @@ export default new Vuex.Store({
},
state: {
process: [],
- undo: []
+ undo: [],
+ dataset: ""
},
mutations: {
+ setDataset(state, dataset) {
+ state.dataset = dataset;
+ },
addProcess(state, process) {
state.process.push(process);
},
diff --git a/client/src/store/info.js b/client/src/store/info.js
index b74c3436..12223adc 100644
--- a/client/src/store/info.js
+++ b/client/src/store/info.js
@@ -7,12 +7,16 @@ const state = {
loginEnabled: true,
version: "loading",
totalUsers: 1,
- name: "COCO Annotator"
+ name: "COCO Annotator",
+ socket: null
};
const getters = {};
const mutations = {
+ socket(state, connected) {
+ state.socket = connected;
+ },
increamentUserCount(state) {
state.totalUsers++;
},
diff --git a/client/src/store/user.js b/client/src/store/user.js
index f0484c1f..ef5d7242 100644
--- a/client/src/store/user.js
+++ b/client/src/store/user.js
@@ -17,6 +17,9 @@ const getters = {
if (!state.user) return false;
if (!state.user.anonymous) return true;
return state.user.anonymous;
+ },
+ user(state) {
+ return state.user;
}
};
diff --git a/client/src/views/AdminPanel.vue b/client/src/views/AdminPanel.vue
index 588358e9..7b0e7978 100644
--- a/client/src/views/AdminPanel.vue
+++ b/client/src/views/AdminPanel.vue
@@ -1,7 +1,10 @@
-
+
Users
@@ -9,9 +12,22 @@
-
-
Create User
-
Refresh
+
+
+ Create User
+
+
+ Refresh
+
@@ -20,7 +36,10 @@
Limit
-
+
50
100
500
@@ -37,7 +56,9 @@
Name
Admin
- Delete
+
+ Delete
+
@@ -50,7 +71,12 @@
-
+
+
+
@@ -63,33 +89,74 @@