From 04b3778b562849302d9b6408e85373c19ceb8494 Mon Sep 17 00:00:00 2001 From: Yuto Jumonji Date: Wed, 9 Oct 2019 16:15:32 +0900 Subject: [PATCH 1/4] feature: Add copy/paste buttons --- front/app/labeling_tool/annotation.js | 28 ++++++++++++++++ front/app/labeling_tool/clipboard.jsx | 47 +++++++++++++++++++++++++++ front/app/labeling_tool/controls.jsx | 15 +++++++-- 3 files changed, 87 insertions(+), 3 deletions(-) create mode 100644 front/app/labeling_tool/clipboard.jsx diff --git a/front/app/labeling_tool/annotation.js b/front/app/labeling_tool/annotation.js index 026445ae..24f67cfd 100644 --- a/front/app/labeling_tool/annotation.js +++ b/front/app/labeling_tool/annotation.js @@ -244,6 +244,7 @@ class Annotation extends React.Component { } return null; } + // methods to history createHistory(label) { this._history.createHistory(label, 'change'); } @@ -267,6 +268,33 @@ class Annotation extends React.Component { removeFromHistory(id) { this.remove(id, false); } + // methods to clipboard + copyLabels(isAll) { + let target = []; + if (isAll) { + target = Array.from(this.state.labels.values()); + } else if (this._targetLabel != null) { + target = [this._targetLabel]; + } + return target.map(label => label.toObject()); + } + pasteLabels(data) { + const labels = new Map(this.state.labels); + data.forEach(obj => { + let klass = this._klassSet.getByName(obj.name); + let bboxes = {}; + this._controls.getTools().forEach(tool => { + const id = tool.candidateId; + if (obj.content[id] != null) { + bboxes[id] = tool.createBBox(obj.content[id]); + } + }); + let label = new Label(this, this._nextId--, klass, bboxes); + labels.set(label.id, label); + }); + //this._history.addHistory(label, 'create'); + this.setState({ labels }); + } // private _removeAll() { diff --git a/front/app/labeling_tool/clipboard.jsx b/front/app/labeling_tool/clipboard.jsx new file mode 100644 index 00000000..27024bae --- /dev/null +++ b/front/app/labeling_tool/clipboard.jsx @@ -0,0 +1,47 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; + +import Grid from '@material-ui/core/Grid'; +import Button from '@material-ui/core/Button'; + +class Clipboard extends React.Component { + + constructor(props) { + super(props); + this.annotation = null; + this._controls = props.controls; + this.state = { + copy: null + }; + props.getRef(this); + } + init(annotation) { + this.annotation = annotation; + } + hasCopy() { + return this.state.copy != null; + } + copy(isAll) { + const copy = this.annotation.copyLabels(isAll); + if (copy.length === 0) { + return; + } + this.setState({ copy: copy }); + } + paste() { + const copy = this.state.copy; + this.annotation.pasteLabels(copy); + } + + render() { + return ( + + + + + + ); + } +} +export default Clipboard; + diff --git a/front/app/labeling_tool/controls.jsx b/front/app/labeling_tool/controls.jsx index 036d6c1b..5122ce02 100644 --- a/front/app/labeling_tool/controls.jsx +++ b/front/app/labeling_tool/controls.jsx @@ -14,6 +14,7 @@ import {NavigateNext, NavigateBefore} from '@material-ui/icons'; import KlassSet from 'automan/labeling_tool/klass_set'; import Annotation from 'automan/labeling_tool/annotation'; import History from 'automan/labeling_tool/history'; +import Clipboard from 'automan/labeling_tool/clipboard'; import ImageLabelTool from 'automan/labeling_tool/image_label_tool'; import PCDLabelTool from 'automan/labeling_tool/pcd_label_tool'; @@ -31,6 +32,9 @@ class Controls extends React.Component { getKlassSet = (tgt) => { this.klassSet = tgt; } history = null; getHistory = (tgt) => { this.history = tgt; } + clipboard = null; + getClipboard = (tgt) => { this.clipboard = tgt; } + // progress frameLength = 0; // tool status @@ -89,7 +93,9 @@ class Controls extends React.Component { return Promise.all([ this.annotation.init(this.klassSet, this.history), this.klassSet.init(), - this.history.init(this.annotation) + this.history.init(this.annotation), + this.clipboard.init(this.annotation) + ]); } resize() { @@ -457,9 +463,12 @@ class Controls extends React.Component { - - + Date: Tue, 15 Oct 2019 15:14:47 +0900 Subject: [PATCH 2/4] feature: support history to paste --- front/app/labeling_tool/annotation.js | 73 +++++++++++++++++++-------- front/app/labeling_tool/history.jsx | 40 ++++++++------- 2 files changed, 75 insertions(+), 38 deletions(-) diff --git a/front/app/labeling_tool/annotation.js b/front/app/labeling_tool/annotation.js index 24f67cfd..9257dbc6 100644 --- a/front/app/labeling_tool/annotation.js +++ b/front/app/labeling_tool/annotation.js @@ -153,7 +153,7 @@ class Annotation extends React.Component { const label = new Label(this, this._nextId--, klass, bbox); const labels = new Map(this.state.labels); labels.set(label.id, label); - this._history.addHistory(label, 'create'); + this._history.addHistory([label], 'create'); this.setState({ labels }); return label; } @@ -203,7 +203,7 @@ class Annotation extends React.Component { //label.tableItem.addClass('has-image-bbox'); } } - remove(id, setHistory=true) { + remove(id) { let label = this.getLabel(id); if (label == null) { let txt = 'Label remove error: Error selector "' + id + '"'; @@ -211,9 +211,8 @@ class Annotation extends React.Component { return; } - if (setHistory) { - this._history.addHistory(label, 'delete'); - } + this._history.addHistory([label], 'delete'); + this._controls.getTools().forEach(tool => { if (label.bbox[tool.candidateId] != null) { tool.disposeBBox(label.bbox[tool.candidateId]); @@ -246,27 +245,59 @@ class Annotation extends React.Component { } // methods to history createHistory(label) { - this._history.createHistory(label, 'change'); + this._history.createHistory([label], 'change'); } addHistory() { this._history.addHistory(null); } - createFromHistory(id, obj) { - let bboxes = {}; - this._controls.getTools().forEach(tool => { - const id = tool.candidateId; - if (obj.content[id] != null) { - bboxes[id] = tool.createBBox(obj.content[id]); - } - }); - let label = new Label(this, id, obj.klass, bboxes); + createFromHistory(objects) { const labels = new Map(this.state.labels); - labels.set(label.id, label); + let labelList = []; + for (let obj of objects) { + let bboxes = {}; + this._controls.getTools().forEach(tool => { + const id = tool.candidateId; + if (obj.content[id] != null) { + bboxes[id] = tool.createBBox(obj.content[id]); + } + }); + let label = new Label(this, obj.id, obj.klass, bboxes); + labels.set(label.id, label); + if (label.id >= 0) { + this._deleted = this._deleted.filter(id => id != label.id); + } + labelList.push(label); + } this.setState({ labels }); - return label; + return labelList; } - removeFromHistory(id) { - this.remove(id, false); + removeFromHistory(objects) { + const tools = this._controls.getTools(); + const tgt = this._targetLabel; + const labels = new Map(this.state.labels); + for (let obj of objects) { + let label = this.getLabel(obj.id); + + tools.forEach(tool => { + if (label.bbox[tool.candidateId] != null) { + tool.disposeBBox(label.bbox[tool.candidateId]); + } + }); + if (tgt != null && label.id === tgt.id) { + this._targetLabel = null; + tgt.setTarget(false); + this._controls.getTools().forEach(tool => { + tool.updateTarget(tgt, null); + }); + } + if (label.id >= 0) { + this._deleted.push(label.id); + } + + labels.delete(label.id); + label.dispose(); + } + this.setState({ labels }); } // methods to clipboard copyLabels(isAll) { @@ -280,6 +311,7 @@ class Annotation extends React.Component { } pasteLabels(data) { const labels = new Map(this.state.labels); + let pastedLabels = []; data.forEach(obj => { let klass = this._klassSet.getByName(obj.name); let bboxes = {}; @@ -291,8 +323,9 @@ class Annotation extends React.Component { }); let label = new Label(this, this._nextId--, klass, bboxes); labels.set(label.id, label); + pastedLabels.push(label); }); - //this._history.addHistory(label, 'create'); + this._history.addHistory(pastedLabels, 'create'); this.setState({ labels }); } diff --git a/front/app/labeling_tool/history.jsx b/front/app/labeling_tool/history.jsx index fccea68d..3ccf8155 100644 --- a/front/app/labeling_tool/history.jsx +++ b/front/app/labeling_tool/history.jsx @@ -33,17 +33,18 @@ class History extends React.Component { 'delete': 'create', }; createAntiHist(hist) { - const label = this.annotation.getLabel(hist.id); const ret = { type: this.ANTI_TYPES[hist.type], - id: hist.id }; if (hist.type == 'change') { - ret.obj = label.toHistory(); + ret.objects = hist.objects.map(obj => { + const label = this.annotation.getLabel(obj.id); + return label.toHistory(); + }); } else if (hist.type === 'create') { - ret.obj = hist.obj + ret.objects = hist.objects; } else if (hist.type === 'delete') { - ret.obj = hist.obj + ret.objects = hist.objects; } else { // error } @@ -51,14 +52,16 @@ class History extends React.Component { } undoHist(hist) { if (hist.type === 'change') { - const label = this.annotation.getLabel(hist.id); - label.fromHistory(hist.obj); - this._controls.selectLabel(label); + for (let obj of hist.objects) { + const label = this.annotation.getLabel(obj.id); + label.fromHistory(obj); + } + this._controls.selectLabel(hist.objects[0].id); } else if (hist.type === 'create') { - this.annotation.removeFromHistory(hist.id); + this.annotation.removeFromHistory(hist.objects); } else if (hist.type === 'delete') { - const label = this.annotation.createFromHistory(hist.id, hist.obj); - this._controls.selectLabel(label); + const labels = this.annotation.createFromHistory(hist.objects); + this._controls.selectLabel(labels[0]); } else { // error } @@ -99,24 +102,25 @@ class History extends React.Component { redoHistory: [] }); } - createHistory(label, type) { + createHistory(labels, type) { this.tmpHist = { type: type, - obj: label.toHistory(), - id: label.id + objects: labels.map(label => label.toHistory()) }; } - addHistory(label, type) { + addHistory(labels, type) { const undoHist = this.state.undoHistory.slice(); const redoHist = []; let hist; - if (label == null) { + if (labels == null) { hist = this.tmpHist; } else { + if (labels.length == 0) { + return; + } hist = { type: type, - obj: label.toHistory(), - id: label.id + objects: labels.map(label => label.toHistory()) }; } undoHist.push(hist); From 7f92ad75330f449edd67d312e536052a25832101 Mon Sep 17 00:00:00 2001 From: Yuto Jumonji Date: Tue, 15 Oct 2019 15:26:24 +0900 Subject: [PATCH 3/4] fix: check history when saving confirmation --- front/app/labeling_tool/annotation.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/front/app/labeling_tool/annotation.js b/front/app/labeling_tool/annotation.js index 9257dbc6..eb44420d 100644 --- a/front/app/labeling_tool/annotation.js +++ b/front/app/labeling_tool/annotation.js @@ -83,6 +83,10 @@ class Annotation extends React.Component { if (this._deleted.length > 0) { return true; } + if (!this._history.hasUndo()) { + // check by history + return false; + } let changedFlag = false; this.state.labels.forEach(label => { changedFlag = changedFlag || label.isChanged; From cc6d4540c0f14665a0337b89815c182e5ab0ceb8 Mon Sep 17 00:00:00 2001 From: Yuto Jumonji Date: Tue, 15 Oct 2019 15:36:22 +0900 Subject: [PATCH 4/4] feature: add copy/paste shortcuts --- front/app/labeling_tool/clipboard.jsx | 3 +++ front/app/labeling_tool/controls.jsx | 10 ++++++++++ 2 files changed, 13 insertions(+) diff --git a/front/app/labeling_tool/clipboard.jsx b/front/app/labeling_tool/clipboard.jsx index 27024bae..cf19482e 100644 --- a/front/app/labeling_tool/clipboard.jsx +++ b/front/app/labeling_tool/clipboard.jsx @@ -22,6 +22,9 @@ class Clipboard extends React.Component { return this.state.copy != null; } copy(isAll) { + if (isAll === null) { + isAll = this.annotation.getTarget() == null; + } const copy = this.annotation.copyLabels(isAll); if (copy.length === 0) { return; diff --git a/front/app/labeling_tool/controls.jsx b/front/app/labeling_tool/controls.jsx index 5122ce02..90bb9275 100644 --- a/front/app/labeling_tool/controls.jsx +++ b/front/app/labeling_tool/controls.jsx @@ -135,6 +135,16 @@ class Controls extends React.Component { this.history.undo(); } } + } else if (e.keyCode == 67) { + // C key + if (e.ctrlKey) { + this.clipboard.copy(null); + } + } else if (e.keyCode == 86) { + // V key + if (e.ctrlKey) { + this.clipboard.paste(); + } } else { this.getTool().handles.keydown(e); }