Skip to content

Commit 4fe33a1

Browse files
zhiltsov-maxChris Lee-Messer
authored andcommitted
Add a dataset export button for tasks (cvat-ai#834)
* Add dataset export button for tasks in dashboard * Fix downloading, shrink list of export formats * Add strict export format check * Add strict export format check * Change REST api paths * Move formats declarations to server,
1 parent 1c93b58 commit 4fe33a1

File tree

9 files changed

+216
-19
lines changed

9 files changed

+216
-19
lines changed

cvat-core/src/annotations.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,25 @@
225225
return result;
226226
}
227227

228+
async function exportDataset(session, format) {
229+
if (!(format instanceof String || typeof format === 'string')) {
230+
throw new ArgumentError(
231+
'Format must be a string',
232+
);
233+
}
234+
if (!(session instanceof Task)) {
235+
throw new ArgumentError(
236+
'A dataset can only be created from a task',
237+
);
238+
}
239+
240+
let result = null;
241+
result = await serverProxy.tasks
242+
.exportDataset(session.id, format);
243+
244+
return result;
245+
}
246+
228247
module.exports = {
229248
getAnnotations,
230249
putAnnotations,
@@ -238,5 +257,6 @@
238257
selectObject,
239258
uploadAnnotations,
240259
dumpAnnotations,
260+
exportDataset,
241261
};
242262
})();

cvat-core/src/api-implementation.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,11 @@
7070
return result.map((el) => new AnnotationFormat(el));
7171
};
7272

73+
cvat.server.datasetFormats.implementation = async () => {
74+
const result = await serverProxy.server.datasetFormats();
75+
return result;
76+
};
77+
7378
cvat.server.register.implementation = async (username, firstName, lastName,
7479
email, password1, password2) => {
7580
await serverProxy.server.register(username, firstName, lastName, email,

cvat-core/src/api.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,20 @@ function build() {
115115
.apiWrapper(cvat.server.formats);
116116
return result;
117117
},
118+
/**
119+
* Method returns available dataset export formats
120+
* @method exportFormats
121+
* @async
122+
* @memberof module:API.cvat.server
123+
* @returns {module:String[]}
124+
* @throws {module:API.cvat.exceptions.PluginError}
125+
* @throws {module:API.cvat.exceptions.ServerError}
126+
*/
127+
async datasetFormats() {
128+
const result = await PluginRegistry
129+
.apiWrapper(cvat.server.datasetFormats);
130+
return result;
131+
},
118132
/**
119133
* Method allows to register on a server
120134
* @method register

cvat-core/src/server-proxy.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,22 @@
101101
return response.data;
102102
}
103103

104+
async function datasetFormats() {
105+
const { backendAPI } = config;
106+
107+
let response = null;
108+
try {
109+
response = await Axios.get(`${backendAPI}/server/dataset/formats`, {
110+
proxy: config.proxy,
111+
});
112+
response = JSON.parse(response.data);
113+
} catch (errorData) {
114+
throw generateError(errorData, 'Could not get export formats from the server');
115+
}
116+
117+
return response;
118+
}
119+
104120
async function register(username, firstName, lastName, email, password1, password2) {
105121
let response = null;
106122
try {
@@ -234,6 +250,35 @@
234250
}
235251
}
236252

253+
async function exportDataset(id, format) {
254+
const { backendAPI } = config;
255+
let url = `${backendAPI}/tasks/${id}/dataset?format=${format}`;
256+
257+
return new Promise((resolve, reject) => {
258+
async function request() {
259+
try {
260+
const response = await Axios
261+
.get(`${url}`, {
262+
proxy: config.proxy,
263+
});
264+
if (response.status === 202) {
265+
setTimeout(request, 3000);
266+
} else {
267+
url = `${url}&action=download`;
268+
resolve(url);
269+
}
270+
} catch (errorData) {
271+
reject(generateError(
272+
errorData,
273+
`Failed to export the task ${id} as a dataset`,
274+
));
275+
}
276+
}
277+
278+
setTimeout(request);
279+
});
280+
}
281+
237282
async function createTask(taskData, files, onUpdate) {
238283
const { backendAPI } = config;
239284

@@ -566,6 +611,7 @@
566611
about,
567612
share,
568613
formats,
614+
datasetFormats,
569615
exception,
570616
login,
571617
logout,
@@ -582,6 +628,7 @@
582628
saveTask,
583629
createTask,
584630
deleteTask,
631+
exportDataset,
585632
}),
586633
writable: false,
587634
},

cvat-core/src/session.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,12 @@
100100
objectStates, reset);
101101
return result;
102102
},
103+
104+
async exportDataset(format) {
105+
const result = await PluginRegistry
106+
.apiWrapper.call(this, prototype.annotations.exportDataset, format);
107+
return result;
108+
},
103109
},
104110
writable: true,
105111
}),
@@ -367,6 +373,19 @@
367373
* @instance
368374
* @async
369375
*/
376+
/**
377+
* Export as a dataset.
378+
* Method builds a dataset in the specified format.
379+
* @method exportDataset
380+
* @memberof Session.annotations
381+
* @param {module:String} format - a format
382+
* @returns {string} An URL to the dataset file
383+
* @throws {module:API.cvat.exceptions.PluginError}
384+
* @throws {module:API.cvat.exceptions.ServerError}
385+
* @throws {module:API.cvat.exceptions.ArgumentError}
386+
* @instance
387+
* @async
388+
*/
370389

371390

372391
/**
@@ -1132,6 +1151,8 @@
11321151
statistics: Object.getPrototypeOf(this).annotations.statistics.bind(this),
11331152
hasUnsavedChanges: Object.getPrototypeOf(this)
11341153
.annotations.hasUnsavedChanges.bind(this),
1154+
exportDataset: Object.getPrototypeOf(this)
1155+
.annotations.exportDataset.bind(this),
11351156
};
11361157

11371158
this.frames = {
@@ -1195,6 +1216,7 @@
11951216
annotationsStatistics,
11961217
uploadAnnotations,
11971218
dumpAnnotations,
1219+
exportDataset,
11981220
} = require('./annotations');
11991221

12001222
buildDublicatedAPI(Job.prototype);
@@ -1457,4 +1479,9 @@
14571479
const result = await dumpAnnotations(this, name, dumper);
14581480
return result;
14591481
};
1482+
1483+
Task.prototype.annotations.exportDataset.implementation = async function (format) {
1484+
const result = await exportDataset(this, format);
1485+
return result;
1486+
};
14601487
})();

cvat/apps/dashboard/static/dashboard/js/dashboard.js

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@
1313
*/
1414

1515
class TaskView {
16-
constructor(task, annotationFormats) {
16+
constructor(task, annotationFormats, exportFormats) {
1717
this.init(task);
1818
this._annotationFormats = annotationFormats;
19+
this._exportFormats = exportFormats;
1920

2021
this._UI = null;
2122
}
@@ -109,6 +110,28 @@ class TaskView {
109110
}
110111
}
111112

113+
async _exportDataset(button, formatName) {
114+
button.disabled = true;
115+
try {
116+
const format = this._exportFormats.find((x) => {
117+
return x.name == formatName;
118+
});
119+
if (!format) {
120+
throw `Unknown dataset export format '${formatName}'`;
121+
}
122+
const url = await this._task.annotations.exportDataset(format.tag);
123+
const tempElem = document.createElement('a');
124+
tempElem.href = `${url}`;
125+
document.body.appendChild(tempElem);
126+
tempElem.click();
127+
tempElem.remove();
128+
} catch (error) {
129+
showMessage(error.message);
130+
} finally {
131+
button.disabled = false;
132+
}
133+
}
134+
112135
init(task) {
113136
this._task = task;
114137
}
@@ -169,6 +192,22 @@ class TaskView {
169192
downloadButton.appendTo(buttonsContainer);
170193
uploadButton.appendTo(buttonsContainer);
171194

195+
const exportButton = $('<select class="regular dashboardButtonUI"'
196+
+ 'style="text-align-last: center;"> Export as Dataset </select>');
197+
$('<option selected disabled> Export as Dataset </option>').appendTo(exportButton);
198+
for (const format of this._exportFormats) {
199+
const item = $(`<option>${format.name}</li>`);
200+
if (format.is_default) {
201+
item.addClass('bold');
202+
}
203+
item.appendTo(exportButton);
204+
}
205+
exportButton.on('change', (e) => {
206+
this._exportDataset(e.target, e.target.value);
207+
exportButton.prop('value', 'Export as Dataset');
208+
});
209+
exportButton.appendTo(buttonsContainer)
210+
172211
$('<button class="regular dashboardButtonUI"> Update Task </button>').on('click', () => {
173212
this._update();
174213
}).appendTo(buttonsContainer);
@@ -207,14 +246,15 @@ class TaskView {
207246

208247

209248
class DashboardView {
210-
constructor(metaData, taskData, annotationFormats) {
249+
constructor(metaData, taskData, annotationFormats, exportFormats) {
211250
this._dashboardList = taskData.results;
212251
this._maxUploadSize = metaData.max_upload_size;
213252
this._maxUploadCount = metaData.max_upload_count;
214253
this._baseURL = metaData.base_url;
215254
this._sharePath = metaData.share_path;
216255
this._params = {};
217256
this._annotationFormats = annotationFormats;
257+
this._exportFormats = exportFormats;
218258

219259
this._setupList();
220260
this._setupTaskSearch();
@@ -273,7 +313,8 @@ class DashboardView {
273313
}));
274314

275315
for (const task of tasks) {
276-
const taskView = new TaskView(task, this._annotationFormats);
316+
const taskView = new TaskView(task,
317+
this._annotationFormats, this._exportFormats);
277318
dashboardList.append(taskView.render(baseURL));
278319
}
279320

@@ -735,9 +776,11 @@ window.addEventListener('DOMContentLoaded', () => {
735776
$.get('/dashboard/meta'),
736777
$.get(`/api/v1/tasks${window.location.search}`),
737778
window.cvat.server.formats(),
738-
).then((metaData, taskData, annotationFormats) => {
779+
window.cvat.server.datasetFormats(),
780+
).then((metaData, taskData, annotationFormats, exportFormats) => {
739781
try {
740-
new DashboardView(metaData[0], taskData[0], annotationFormats);
782+
new DashboardView(metaData[0], taskData[0],
783+
annotationFormats, exportFormats);
741784
} catch (exception) {
742785
$('#content').empty();
743786
const message = `Can not build CVAT dashboard. Exception: ${exception}.`;

cvat/apps/dataset_manager/task.py

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ def log_exception(logger=None, exc_info=True):
3636
def get_export_cache_dir(db_task):
3737
return osp.join(db_task.get_task_dirname(), 'export_cache')
3838

39+
EXPORT_FORMAT_DATUMARO_PROJECT = "datumaro_project"
40+
41+
3942
class TaskProject:
4043
@staticmethod
4144
def _get_datumaro_project_dir(db_task):
@@ -211,9 +214,7 @@ def save(self, save_dir=None, save_images=False):
211214
def export(self, dst_format, save_dir, save_images=False, server_url=None):
212215
if self._dataset is None:
213216
self._init_dataset()
214-
if dst_format == DEFAULT_FORMAT:
215-
self._dataset.save(save_dir=save_dir, save_images=save_images)
216-
elif dst_format == DEFAULT_FORMAT_REMOTE:
217+
if dst_format == EXPORT_FORMAT_DATUMARO_PROJECT:
217218
self._remote_export(save_dir=save_dir, server_url=server_url)
218219
else:
219220
self._dataset.export(output_format=dst_format,
@@ -291,8 +292,7 @@ def _remote_export(self, save_dir, server_url=None):
291292
])
292293

293294

294-
DEFAULT_FORMAT = "datumaro_project"
295-
DEFAULT_FORMAT_REMOTE = "datumaro_project_remote"
295+
DEFAULT_FORMAT = EXPORT_FORMAT_DATUMARO_PROJECT
296296
DEFAULT_CACHE_TTL = timedelta(hours=10)
297297
CACHE_TTL = DEFAULT_CACHE_TTL
298298

@@ -348,4 +348,36 @@ def clear_export_cache(task_id, file_path, file_ctime):
348348
.format(file_path))
349349
except Exception:
350350
log_exception(slogger.task[task_id])
351-
raise
351+
raise
352+
353+
354+
EXPORT_FORMATS = [
355+
{
356+
'name': 'Datumaro',
357+
'tag': EXPORT_FORMAT_DATUMARO_PROJECT,
358+
'is_default': True,
359+
},
360+
{
361+
'name': 'PASCAL VOC 2012',
362+
'tag': 'voc',
363+
'is_default': False,
364+
},
365+
{
366+
'name': 'MS COCO',
367+
'tag': 'coco',
368+
'is_default': False,
369+
}
370+
]
371+
372+
def get_export_formats():
373+
from datumaro.components import converters
374+
375+
available_formats = set(name for name, _ in converters.items)
376+
available_formats.add(EXPORT_FORMAT_DATUMARO_PROJECT)
377+
378+
public_formats = []
379+
for fmt in EXPORT_FORMATS:
380+
if fmt['tag'] in available_formats:
381+
public_formats.append(fmt)
382+
383+
return public_formats

cvat/apps/engine/static/engine/js/cvat-core.min.js

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)