diff --git a/app/api/annotator.py b/app/api/annotator.py
index e95278ad..53071495 100644
--- a/app/api/annotator.py
+++ b/app/api/annotator.py
@@ -46,7 +46,11 @@ def post(self):
if db_category is None:
continue
- db_category.update(set__color=category.get('color'))
+ db_category.update(
+ set__color=category.get('color'),
+ set__keypoint_edges=category.get('keypoint_edges', []),
+ set__keypoint_labels=category.get('keypoint_labels', [])
+ )
# Iterate every annotation from the data annotations
for annotation in category.get('annotations', []):
@@ -63,6 +67,7 @@ def post(self):
# Update annotation in database
db_annotation.update(
+ set__keypoints=annotation.get('keypoints', []),
set__metadata=annotation.get('metadata'),
set__color=annotation.get('color')
)
diff --git a/app/models.py b/app/models.py
index 07dab566..31566c72 100644
--- a/app/models.py
+++ b/app/models.py
@@ -221,6 +221,8 @@ class AnnotationModel(db.DynamicDocument):
height = db.IntField()
color = db.StringField()
+
+ keypoints = db.ListField(default=[])
metadata = db.DictField(default={})
paper_object = db.ListField(default=[])
@@ -311,6 +313,10 @@ class CategoryModel(db.DynamicDocument):
deleted = db.BooleanField(default=False)
deleted_date = db.DateTimeField()
+ keypoint_edges = db.ListField(default=[])
+ keypoint_labels = db.ListField(default=[])
+
+
@classmethod
def bulk_create(cls, categories):
@@ -462,6 +468,7 @@ def api_json(self):
"name": self.name
}
+
class CocoImportModel(db.DynamicDocument):
id = db.SequenceField(primary_key=True)
creator = db.StringField(required=True)
diff --git a/app/util/coco_util.py b/app/util/coco_util.py
index 1ceec93c..f71ca6e4 100644
--- a/app/util/coco_util.py
+++ b/app/util/coco_util.py
@@ -114,24 +114,40 @@ def get_image_coco(image):
annotations = []
for category in bulk_categories:
-
+ category = category[1]
category_annotations = AnnotationModel.objects(
- deleted=False, category_id=category[1].id, image_id=image.get('id')
+ deleted=False, category_id=category.id, image_id=image.get('id')
).exclude('paper_object', 'deleted_date').all()
-
+
if len(category_annotations) == 0:
continue
+
+ has_keypoints = len(category.keypoint_labels) > 0
for annotation in category_annotations:
annotation = fix_ids(annotation)
- if len(annotation.get('segmentation')) != 0:
+ if len(annotation.get('segmentation', [])) != 0 or \
+ len(annotation.get('keypoints', [])) != 0:
del annotation['deleted']
- del annotation['paper_object']
+
+ if not has_keypoints:
+ del annotation['keypoints']
+ else:
+ arr = np.array(annotation.get('keypoints', []))
+ arr = arr[2::3]
+ annotation['num_keypoints'] = len(arr[arr > 0])
+
annotations.append(annotation)
- category = fix_ids(category[1])
+ category = fix_ids(category)
del category['deleted']
+ if has_keypoints:
+ category['keypoints'] = category.pop('keypoint_labels')
+ category['skeleton'] = category.pop('keypoint_edges')
+ else:
+ del category['keypoint_edges']
+ del category['keypoint_labels']
categories.append(category)
del image['deleted']
@@ -169,7 +185,15 @@ def get_dataset_coco(dataset):
for category in categories:
category = fix_ids(category[1])
+
del category['deleted']
+ if len(category.keypoint_labels) > 0:
+ category['keypoints'] = category.pop('keypoint_labels')
+ category['skeleton'] = category.pop('keypoint_edges')
+ else:
+ del category['keypoint_edges']
+ del category['keypoint_labels']
+
coco.get('categories').append(category)
for image in images:
@@ -180,8 +204,19 @@ def get_dataset_coco(dataset):
annotations = fix_ids(annotations.all())
for annotation in annotations:
- if len(annotation.get('segmentation', [])) != 0:
+
+ has_keypoints = len(annotation.get('keypoints', [])) > 0
+ has_segmentation = len(annotation.get('segmentation', [])) > 0
+
+ if has_keypoints or has_keypoints:
del annotation['deleted']
+
+ if not has_keypoints:
+ del annotation['keypoints']
+ else:
+ arr = np.array(annotation.get('keypoints', []))
+ arr = arr[2::3]
+ annotation['num_keypoints'] = len(arr[arr > 0])
coco.get('annotations').append(annotation)
image = fix_ids(image)
@@ -192,11 +227,4 @@ def get_dataset_coco(dataset):
def _fit(value, max_value, min_value):
-
- if value > max_value:
- return max_value
-
- if value < min_value:
- return min_value
-
- return value
+ return max(min(value, max_value), min_value)
diff --git a/client/src/components/PanelInputDropdown.vue b/client/src/components/PanelInputDropdown.vue
new file mode 100755
index 00000000..92927db2
--- /dev/null
+++ b/client/src/components/PanelInputDropdown.vue
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
diff --git a/client/src/components/PanelText.vue b/client/src/components/PanelText.vue
new file mode 100755
index 00000000..8b8a25d3
--- /dev/null
+++ b/client/src/components/PanelText.vue
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
diff --git a/client/src/components/annotator/Annotation.vue b/client/src/components/annotator/Annotation.vue
index 1393671d..009efb0b 100755
--- a/client/src/components/annotator/Annotation.vue
+++ b/client/src/components/annotator/Annotation.vue
@@ -1,10 +1,10 @@
@@ -52,6 +52,65 @@
/>
+
+
{
+ if (!this.$parent.isCurrent) return;
+ 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.
* @param {paper.CompoundPath} compound compound to unite current annotation path with
@@ -351,6 +523,7 @@ export default {
this.compoundPath.remove();
this.compoundPath = newCompound;
+ this.keypoints.bringToFront();
if (simplify) this.simplifyPath();
},
@@ -368,6 +541,7 @@ export default {
this.compoundPath.remove();
this.compoundPath = newCompound;
+ this.keypoints.bringToFront();
if (simplify) this.simplifyPath();
},
@@ -381,9 +555,12 @@ export default {
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 l = Math.round(this.compoundPath.fillColor.lightness * 50);
let s = Math.round(this.compoundPath.fillColor.saturation * 100);
- this.compoundPath.strokeColor = "hsl(" + h + "," + s + "%," + l + "%)";
+
+ let hsl = "hsl(" + h + "," + s + "%," + l + "%)";
+ this.compoundPath.strokeColor = hsl;
+ this.keypoints.color = hsl;
},
export() {
if (this.compoundPath == null) this.createCompoundPath();
@@ -402,6 +579,15 @@ export default {
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;
@@ -432,6 +618,7 @@ export default {
if (this.compoundPath == null) return;
this.compoundPath.visible = newVisible;
+ this.keypoints.visible = newVisible;
},
compoundPath() {
if (this.compoundPath == null) return;
@@ -439,7 +626,10 @@ export default {
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();
@@ -447,12 +637,29 @@ export default {
isCurrent() {
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) {
+ newEdges.forEach(e => this.keypoints.addEdge(e));
}
},
computed: {
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;
@@ -473,6 +680,51 @@ 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);
+ },
+ hsl() {
+ if (this.compoundPath == null) return [0, 0, 0];
+ let h = Math.round(this.compoundPath.fillColor.hue);
+ let l = Math.round(this.compoundPath.fillColor.lightness * 50);
+ let s = Math.round(this.compoundPath.fillColor.saturation * 100);
+
+ return [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: {
@@ -496,6 +748,9 @@ export default {
},
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 cf39da10..39921ff6 100755
--- a/client/src/components/annotator/Category.vue
+++ b/client/src/components/annotator/Category.vue
@@ -69,10 +69,14 @@
@click="onAnnotationClick(listIndex)"
:opacity="opacity"
:index="listIndex"
+ :keypoint-edges="keypoint.edges"
+ :keypoint-labels="keypoint.labels"
ref="annotation"
:hover="hover.annotation"
+ :active-tool="activeTool"
@deleted="annotationDeleted"
/>
+
+
+
+
+
+
+
@@ -124,10 +139,11 @@ import paper from "paper";
import axios from "axios";
import Annotation from "@/components/annotator/Annotation";
+import TagsInput from "@/components/TagsInput";
export default {
name: "Category",
- components: { Annotation },
+ components: { Annotation, TagsInput },
props: {
category: {
type: Object,
@@ -160,12 +176,20 @@ export default {
simplify: {
type: Number,
default: 1
+ },
+ activeTool: {
+ type: String,
+ required: true
}
},
data: function() {
return {
group: null,
color: this.category.color,
+ keypoint: {
+ labels: this.category.keypoint_labels,
+ edges: this.category.keypoint_edges
+ },
selectedAnnotation: -1,
showAnnotations: false,
isVisible: false,
@@ -237,7 +261,9 @@ export default {
visualize: this.isVisible,
color: this.color,
metadata: [],
- annotations: []
+ annotations: [],
+ keypoint_labels: this.keypoint.labels,
+ keypoint_edges: this.keypoint.edges
};
if (refs.hasOwnProperty("annotation")) {
@@ -248,6 +274,27 @@ export default {
return categoryData;
},
+
+ addKeypointEdge(edge) {
+ this.keypoint.edges.push(edge);
+ },
+ removeKeypointEdge(edge) {
+ let index = this.keypoint.edges.findIndex(e => {
+ let i1 = Math.min(edge[0], edge[1]) == Math.min(e[0], e[1]);
+ let i2 = Math.max(edge[0], edge[1]) == Math.max(e[0], e[1]);
+
+ return i1 && i2;
+ });
+
+ if (index !== -1) {
+ let edge = this.keypoint.edges[index];
+ this.keypoint.edges.splice(index, 1);
+ let annotations = this.$refs.annotation;
+ if (annotations) {
+ annotations.forEach(a => a.keypoints.removeLine(edge));
+ }
+ }
+ },
/**
* Event handler for visibility button
*/
@@ -325,20 +372,24 @@ export default {
setColor() {
if (!this.isVisible) return;
+ let annotations = this.$refs.annotation;
if (this.showAnnotations) {
- let annotations = this.$refs.annotation;
- if (annotations) {
- annotations.forEach(annotation => {
- annotation.setColor();
- });
- }
+ if (annotations) annotations.forEach(a => a.setColor());
} else {
if (this.group != null) {
this.group.fillColor = this.color;
let h = Math.round(this.group.fillColor.hue);
- let l = Math.round((this.group.fillColor.lightness - 0.2) * 100);
+ let l = Math.round(this.group.fillColor.lightness * 50);
let s = Math.round(this.group.fillColor.saturation * 100);
- this.group.strokeColor = "hsl(" + h + "," + s + "%," + l + "%)";
+ let hsl = "hsl(" + h + "," + s + "%," + l + "%)";
+ this.group.strokeColor = hsl;
+
+ if (annotations) {
+ annotations.forEach(a => {
+ a.keypoints.color = hsl;
+ a.keypoints.bringToFront();
+ });
+ }
}
}
},
@@ -392,6 +443,9 @@ export default {
isVisible(newVisible) {
if (this.group == null) return;
this.group.visible = newVisible;
+ let annotations = this.$refs.annotation;
+ if (annotations)
+ annotations.forEach(a => (a.keypoints.visible = newVisible));
this.setColor();
},
showAnnotations(showing) {
diff --git a/client/src/components/annotator/panels/KeypointPanel.vue b/client/src/components/annotator/panels/KeypointPanel.vue
new file mode 100755
index 00000000..516488c1
--- /dev/null
+++ b/client/src/components/annotator/panels/KeypointPanel.vue
@@ -0,0 +1,60 @@
+
+
+
+
+
diff --git a/client/src/components/annotator/tools/BrushTool.vue b/client/src/components/annotator/tools/BrushTool.vue
index 0a53d320..28a054fe 100755
--- a/client/src/components/annotator/tools/BrushTool.vue
+++ b/client/src/components/annotator/tools/BrushTool.vue
@@ -118,7 +118,7 @@ export default {
if (active) {
this.tool.activate();
- localStorage.setItem("editorTool", this.name)
+ localStorage.setItem("editorTool", this.name);
}
},
/**
diff --git a/client/src/components/annotator/tools/EraserTool.vue b/client/src/components/annotator/tools/EraserTool.vue
index c4d1168a..8be6591b 100755
--- a/client/src/components/annotator/tools/EraserTool.vue
+++ b/client/src/components/annotator/tools/EraserTool.vue
@@ -116,7 +116,7 @@ export default {
if (active) {
this.tool.activate();
- localStorage.setItem("editorTool", this.name)
+ 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 94ab31e4..eb30b3d2 100755
--- a/client/src/components/annotator/tools/MagicWandTool.vue
+++ b/client/src/components/annotator/tools/MagicWandTool.vue
@@ -36,7 +36,7 @@ export default {
isActive(active) {
if (active) {
this.tool.activate();
- localStorage.setItem("editorTool", this.name)
+ localStorage.setItem("editorTool", this.name);
}
}
},
diff --git a/client/src/components/annotator/tools/PolygonTool.vue b/client/src/components/annotator/tools/PolygonTool.vue
index 5f7f701e..e9afa985 100755
--- a/client/src/components/annotator/tools/PolygonTool.vue
+++ b/client/src/components/annotator/tools/PolygonTool.vue
@@ -224,7 +224,7 @@ export default {
isActive(active) {
if (active) {
this.tool.activate();
- localStorage.setItem("editorTool", this.name)
+ localStorage.setItem("editorTool", this.name);
}
},
/**
diff --git a/client/src/components/annotator/tools/SelectTool.vue b/client/src/components/annotator/tools/SelectTool.vue
index d426eacb..5f82d69f 100755
--- a/client/src/components/annotator/tools/SelectTool.vue
+++ b/client/src/components/annotator/tools/SelectTool.vue
@@ -46,14 +46,7 @@ export default {
fill: false,
tolerance: 5,
match: hit => {
- if (this.point == null) return true;
- if (
- hit.item instanceof paper.Path ||
- hit.item instanceof paper.CompoundPath
- ) {
- return !hit.item.hasOwnProperty("indicator");
- }
- return true;
+ return !hit.item.hasOwnProperty("indicator");
}
}
};
@@ -177,6 +170,8 @@ export default {
if (this.point != null) {
this.edit.canMove = this.point.contains(event.point);
+ } else {
+ this.edit.canMove = false;
}
},
createPoint(point) {
@@ -190,8 +185,7 @@ export default {
this.point.indicator = true;
},
onMouseDrag(event) {
- if (this.segment) {
- if (!this.edit.canMove) return;
+ if (this.segment && this.edit.canMove) {
this.createPoint(event.point);
this.segment.point = event.point;
}
diff --git a/client/src/components/cards/ImageCard.vue b/client/src/components/cards/ImageCard.vue
index 3777b504..905017e6 100755
--- a/client/src/components/cards/ImageCard.vue
+++ b/client/src/components/cards/ImageCard.vue
@@ -96,7 +96,7 @@ export default {
return {
hover: false,
showAnnotations: true,
- loaderUrl: require("@/assets/loader.gif"),
+ loaderUrl: require("@/assets/loader.gif")
};
},
methods: {
diff --git a/client/src/components/tasks/Task.vue b/client/src/components/tasks/Task.vue
index ff8a0244..73ea12ed 100644
--- a/client/src/components/tasks/Task.vue
+++ b/client/src/components/tasks/Task.vue
@@ -100,7 +100,7 @@ export default {
showLogs: "getLogs",
completed() {
if (this.showLogs) {
- this.getLogs()
+ this.getLogs();
}
}
},
diff --git a/client/src/libs/keypoints.js b/client/src/libs/keypoints.js
new file mode 100644
index 00000000..c347c93f
--- /dev/null
+++ b/client/src/libs/keypoints.js
@@ -0,0 +1,435 @@
+import paper from "paper";
+
+export class Keypoints extends paper.Group {
+ constructor(edges, args) {
+ super();
+ args = args || {};
+
+ this._edges = {};
+
+ this._lines = {};
+ this._labelled = {};
+ this._keypoints = [];
+
+ 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();
+ 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));
+ }
+
+ get color() {
+ return this._color;
+ }
+
+ set lineWidth(val) {
+ this._lineWidth = val;
+ this.strokeWidth = val;
+ this._keypoints.forEach(k => (k.path.storkeWidth = 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.strokeWidth;
+ line.indicator = true;
+
+ if (firstKeypoint.path.isBelow(secondKeypoint.path)) {
+ line.insertBelow(firstKeypoint.path);
+ } else {
+ line.insertBelow(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 storkeWidth = 1;
+ if (this.path !== null) {
+ storkeWidth = this.path.strokeWidth;
+ 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 = this.color;
+ this.path.strokeWidth = storkeWidth;
+ 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/mixins/shortcuts.js b/client/src/mixins/shortcuts.js
index 6d6b63aa..e5bb4091 100755
--- a/client/src/mixins/shortcuts.js
+++ b/client/src/mixins/shortcuts.js
@@ -44,7 +44,16 @@ export default {
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();
+ }
}
}
},
@@ -75,6 +84,13 @@ export default {
this.activeTool = "Magic Wand";
}
},
+ {
+ default: ["k"],
+ name: "Keypoints Tool",
+ function: () => {
+ if (!this.$refs.magicwand.isDisabled) this.activeTool = "Keypoints";
+ }
+ },
{
default: ["b"],
name: "Brush Tool",
diff --git a/client/src/views/Annotator.vue b/client/src/views/Annotator.vue
index 3b2c892a..120f5555 100755
--- a/client/src/views/Annotator.vue
+++ b/client/src/views/Annotator.vue
@@ -39,6 +39,12 @@
@setcursor="setCursor"
ref="eraser"
/>
+
+
@@ -115,6 +121,7 @@
:index="index"
@click="onCategoryClick"
:current="current"
+ :active-tool="activeTool"
ref="category"
/>
@@ -154,6 +161,13 @@
+
+
+
+
@@ -186,6 +200,7 @@ import SelectTool from "@/components/annotator/tools/SelectTool";
import MagicWandTool from "@/components/annotator/tools/MagicWandTool";
import EraserTool from "@/components/annotator/tools/EraserTool";
import BrushTool from "@/components/annotator/tools/BrushTool";
+import KeypointTool from "@/components/annotator/tools/KeypointTool";
import CopyAnnotationsButton from "@/components/annotator/tools/CopyAnnotationsButton";
import CenterButton from "@/components/annotator/tools/CenterButton";
@@ -203,6 +218,7 @@ import SelectPanel from "@/components/annotator/panels/SelectPanel";
import MagicWandPanel from "@/components/annotator/panels/MagicWandPanel";
import BrushPanel from "@/components/annotator/panels/BrushPanel";
import EraserPanel from "@/components/annotator/panels/EraserPanel";
+import KeypointPanel from "@/components/annotator/panels/KeypointPanel";
import { mapMutations } from "vuex";
@@ -219,6 +235,7 @@ export default {
MagicWandTool,
EraserTool,
BrushTool,
+ KeypointTool,
DownloadButton,
SaveButton,
SettingsButton,
@@ -231,7 +248,8 @@ export default {
ModeButton,
UndoButton,
HideAllButton,
- ShowAllButton
+ ShowAllButton,
+ KeypointPanel
},
mixins: [toastrs, shortcuts],
props: {
@@ -538,7 +556,7 @@ export default {
},
selectLastEditorTool() {
- this.activeTool = localStorage.getItem("editorTool") || "Select"
+ this.activeTool = localStorage.getItem("editorTool") || "Select";
},
setCursor(newCursor) {
diff --git a/client/src/views/Dataset.vue b/client/src/views/Dataset.vue
index 8e6711c0..0e49df9e 100755
--- a/client/src/views/Dataset.vue
+++ b/client/src/views/Dataset.vue
@@ -240,7 +240,7 @@ import PanelToggle from "@/components/PanelToggle";
import JQuery from "jquery";
import { mapMutations } from "vuex";
-let $ = JQuery
+let $ = JQuery;
export default {
name: "Dataset",
@@ -374,8 +374,8 @@ export default {
this.$router.push({ path: "/tasks", query: { id: this.importing.id } });
return;
}
-
- $('#cocoUpload').modal('show');
+
+ $("#cocoUpload").modal("show");
},
importCOCO() {
let uploaded = document.getElementById("coco");
@@ -426,7 +426,6 @@ export default {
},
sockets: {
taskProgress(data) {
-
if (data.id === this.scan.id) {
this.scan.progress = data.progress;
}