diff --git a/Dockerfile b/Dockerfile index 4dfb491..0bcfaec 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,8 +15,7 @@ COPY server server COPY .logo-ascii .logo-ascii # Build frontend and install backend dependencies -RUN npm run installApp && npm run buildApp && npm install \ - && rm -rf src frontend +RUN npm i && cd frontend/ && npm i && npm run build && rm -rf src frontend && cd .. EXPOSE 3000 diff --git a/frontend/package.json b/frontend/package.json index 71d416c..63664a0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "pixano-app-frontend", - "version": "0.4.7", + "version": "0.4.9", "description": "This is a Pixano app.", "scripts": { "copyindex": "shx cp src/index.html ../build", @@ -23,11 +23,11 @@ "webpack-cli": "^3.3.10" }, "dependencies": { - "@babel/core": "^7.7.7", - "@babel/plugin-transform-runtime": "^7.7.6", + "@babel/core": "^7.16.0", + "@babel/plugin-transform-runtime": "^7.16.0", "@babel/polyfill": "^7.7.0", - "@babel/preset-env": "^7.7.7", - "@babel/runtime": "^7.7.7", + "@babel/preset-env": "^7.16.0", + "@babel/runtime": "^7.16.0", "@material/mwc-button": "0.19.1", "@material/mwc-checkbox": "0.19.1", "@material/mwc-circular-progress-four-color": "0.19.1", @@ -49,13 +49,13 @@ "@material/mwc-tab-bar": "0.19.1", "@material/mwc-textarea": "0.19.1", "@material/mwc-textfield": "0.19.1", - "@pixano/ai": "0.5.17", - "@pixano/core": "0.5.17", - "@pixano/graphics-2d": "0.5.17", - "@pixano/graphics-3d": "0.5.17", + "@pixano/ai": "0.6.0", + "@pixano/core": "0.6.0", + "@pixano/graphics-2d": "0.6.0", + "@pixano/graphics-3d": "0.6.0", "@trystan2k/fleshy-jsoneditor": "3.0.0", "@webcomponents/webcomponentsjs": "^2.4.0", - "babel-loader": "^8.0.6", + "babel-loader": "^8.2.3", "copy-webpack-plugin": "^5.1.1", "css-loader": "^3.4.0", "file-loader": "^5.0.2", @@ -65,9 +65,9 @@ "material-design-icons": "^3.0.1", "node-sass": "^4.13.0", "pwa-helpers": "^0.9.1", - "redux": "^4.0.4", + "redux": "^4.1.2", "redux-devtools-extension": "^2.13.8", - "redux-thunk": "^2.3.0", + "redux-thunk": "^2.4.0", "redux-undo": "^1.0.0", "sass-loader": "^8.0.0", "source-map-loader": "^0.2.4", diff --git a/frontend/src/helpers/attribute-picker.js b/frontend/src/helpers/attribute-picker.js index bd46be9..33193c3 100644 --- a/frontend/src/helpers/attribute-picker.js +++ b/frontend/src/helpers/attribute-picker.js @@ -112,6 +112,14 @@ export class AttributePicker extends LitElement { } static get properties () { + /** + * showDetail: Boolean, rendering mode for the selected category (showing all attributes or only the category) + * shortcuts : Array of strings, contains the list of all applicable keyboard shortcuts + * schema: shema for this annotation (i.e. category and attributes available for each category in this annotation) + * value: {category, options }, contains the value of the current category and its options (i.e. attributes available for this category) + * numDone: Number, only used for keypoints-box + * numTotal: Number, only used for keypoints-box + */ return { showDetail: { type: Boolean }, shortcuts: { type: Array }, @@ -400,16 +408,6 @@ export class AttributePicker extends LitElement { ${this.renderSimple} `; } - // render(){ - // return html` - // ${this.shortcutsDialog} - // - // ${this.renderDetail} - // ${this.renderSimple} - //
keypoints faits ${this.numDone} / ${this.numTotal}
- // `; - // } - } customElements.define('attribute-picker', AttributePicker); diff --git a/frontend/src/plugins/keypoints-box.js b/frontend/src/plugins/keypoints-box.js index d6dbf2b..25aca4e 100644 --- a/frontend/src/plugins/keypoints-box.js +++ b/frontend/src/plugins/keypoints-box.js @@ -108,7 +108,7 @@ export class PluginKeypointsBox extends TemplatePluginInstance { freeBoxes.shift(); this.attributePicker.numDone = this.attributePicker.numTotal - freeBoxes.length; store.dispatch(createAnnotation(newAnnotation)); - this.element.shapes = freeBoxes.length ? [{...freeBoxes[0], color: this._colorFor(freeBoxes[0].categoryName) }] : []; + this.element.shapes = freeBoxes.length ? [{...freeBoxes[0], color: this._colorFor(freeBoxes[0].category) }] : []; } else { this.element.shapes = []; } @@ -138,7 +138,7 @@ export class PluginKeypointsBox extends TemplatePluginInstance { .filter((r) => !ids.includes(r.id)); if (freeBoxes.length) { - this.element.shapes = [{...freeBoxes[0], color: this._colorFor(freeBoxes[0].categoryName) }]; + this.element.shapes = [{...freeBoxes[0], color: this._colorFor(freeBoxes[0].category) }]; } } diff --git a/frontend/src/plugins/segmentation.js b/frontend/src/plugins/segmentation.js index 6c3b2cb..c6b5209 100644 --- a/frontend/src/plugins/segmentation.js +++ b/frontend/src/plugins/segmentation.js @@ -14,7 +14,8 @@ import { store, getState } from '../store'; import '../helpers/attribute-picker'; import { subtract, union } from '../my-icons'; import { setAnnotations } from '../actions/annotations'; -import { TemplatePlugin } from '../templates/template-plugin'; +import { TemplatePluginInstance } from '../templates/template-plugin-instance'; +import { commonJson } from '../helpers/utils'; const EditionMode = { ADD_TO_INSTANCE: 'add_to_instance', @@ -27,7 +28,7 @@ const EditionMode = { * Reads labels as: * { id: 0, mask: Base64 } */ -export class PluginSegmentation extends TemplatePlugin { +export class PluginSegmentation extends TemplatePluginInstance { static get properties() { return { @@ -39,16 +40,15 @@ export class PluginSegmentation extends TemplatePlugin { constructor() { super(); - this.mode = 'edit'; + this.mode = 'create'; this.maskVisuMode = 'SEMANTIC'; this.currentEditionMode = EditionMode.NEW_INSTANCE; - this.selectedIds = [0,0,0]; } get toolDrawer() { return html` @@ -87,7 +87,7 @@ export class PluginSegmentation extends TemplatePlugin { ` @@ -110,53 +110,118 @@ export class PluginSegmentation extends TemplatePlugin { this.element.targetClass = schema.category.find((c) => c.name === schema.default).idx; } - onUpdate() { - const frame = { mask: this.element.getMask()}; - store.dispatch(setAnnotations({annotations: frame})); - } - - onSelection(evt) { - this.selectedIds = evt.detail; - this.updateDisplayOfSelectedProperties(); - } - - updateDisplayOfSelectedProperties() { - if (this.selectedIds && this.selectedIds.length) { - this.attributePicker.setAttributesIdx(this.selectedIds[2]); - } else { - this.attributePicker.setAttributesIdx(); - } - } - - onAttributeChanged() { - const value = this.attributePicker.selectedCategory; - this.element.targetClass = value.idx; - if (this.selectedIds && this.selectedIds.length - && (this.selectedIds[0] != 0 || this.selectedIds[1] != 0 || this.selectedIds[2] != 0)) { - this.element.fillSelectionWithClass(value.idx); - this.onUpdate(); - } - } - - get propertyPanel() { - return html` - - ` - } - - refresh() { - if (!this.element) { - return; - } - if (!this.annotations.mask) { - this.element.setEmpty(); - return; - } - const mask = this.annotations.mask; - if (mask != this.element.getMask()) { - this.element.setMask(mask); - } - } + /** + * Invoked on instance selection in the canvas. + * @param {CustomEvent} evt + */ + onSelection(evt) { + this.selectedIds = evt.detail; + if (this.selectedIds) {//only one id at a time for segmentation + const annot = this.annotations.filter((a) => JSON.stringify(this.selectedIds)===(a.id));// search the corresponding id + const common = commonJson(annot); + this.attributePicker.setAttributes(common); + } else { + // if null, nothing is selected + this.selectedIds = []; + } + } + + /** + * Invoked when a new instance is updated (created = updated for segmentation) + * @param {CustomEvent} evt + */ + onUpdate(evt) { + const updatedIds = evt.detail; + let frame = this.annotations; + // 1) update the mask (always id 0) + let mask = frame.find((l) => l.id === 0); + if (!mask) { + mask = {id: 0, mask: this.element.getMask()};//if the mask already exists => just overwrite the previous mask + frame.push(mask);//otherwise(first time), create it + } else { + mask.mask = this.element.getMask(); + } + // 2) update annotation info when needed + let label = frame.find((l) => l.id === JSON.stringify(updatedIds));// search the corresponding id + if (label) {//id exists in the database, update information + // nothing to do for annotation infos, only the mask has changed + } else {// this is a new id + // create the new label + label = {...this.attributePicker.defaultValue}; + // store the stringified values + const value = this.attributePicker.value; + Object.keys(label).forEach((key) => { + label[key] = JSON.parse(JSON.stringify(value[key])); + }); + label.id = JSON.stringify(updatedIds); + frame.push(label) + } + // 3) store the new annotation structure + store.dispatch(setAnnotations({annotations: frame})); + // selectedId has also changed, update it + this.selectedIds = updatedIds; + } + + /** + * Invoked on attribute change from property panel. + */ + onAttributeChanged() { + if (!this.selectedIds.length) {//nothing is selected + // only set the category acordingly to the selected attribute + const category = this.attributePicker.selectedCategory; + this.element.targetClass = category.idx; + return; + } + let frame = this.annotations; + // 1) update the mask (always id 0) + // change category in element + const category = this.attributePicker.selectedCategory; + this.element.targetClass = category.idx; + this.element.fillSelectionWithClass(category.idx); + // get the new mask and store it + let mask = frame.find((l) => l.id === 0); + mask.mask = this.element.getMask();//just overwrite the previous mask + // 2) update annotation info from attributes + const value = this.attributePicker.value; + let label = frame.find((l) => l.id === JSON.stringify(this.selectedIds));// search the corresponding id + Object.keys(value).forEach((key) => { + label[key] = JSON.parse(JSON.stringify(value[key])); + }); + // category has changed => selectedId has also changed, update it + const updatedIds = this.element.selectedId; + label.id = JSON.stringify(updatedIds); + this.selectedIds = updatedIds; + // 3) store the new annotation structure + store.dispatch(setAnnotations({annotations: frame})); + } + + /** + * Invoked on instance removal + * @param {CustomEvent} evt + */ + onDelete(evt) { + const ids = evt.detail; + let frame = this.annotations; + // 1) update the mask (always id 0) + // get the new mask and store it + let mask = frame.find((l) => l.id === 0); + mask.mask = this.element.getMask();//just overwrite the previous mask + // 2) update annotation info (= delete corresponding id) + frame = frame.filter((l) => l.id !== JSON.stringify(ids)) + // 3) store the new annotation structure + store.dispatch(setAnnotations({annotations: frame})); + } + + refresh() {//get back annotation into element + console.log("refresh") + if (!this.element) { + return; + } + // 1) get back the mask into element + let mask = this.annotations.find((l) => l.id === 0); + if (!mask) this.element.setEmpty(); + else this.element.setMask(mask.mask); + } getEditionMode() { if (this.element) return this.element.editionMode; @@ -173,87 +238,13 @@ export class PluginSegmentation extends TemplatePlugin { maskVisuMode=${this.maskVisuMode} @update=${this.onUpdate} @selection=${this.onSelection} - @mode=${this.onModeChange}>`; + @delete=${this.onDelete} + @mode=${this.onModeChange}>`;//onCreate never really called for segmentation : the mask is updated } + collect() { + console.log("should not be called") + } + } customElements.define('plugin-segmentation', PluginSegmentation); - - -// Code to handle attributes for segments -// /** -// * Invoked on instance selection in the canvas. -// * @param {CustomEvent} evt -// */ -// onSelection(evt) { -// this.selectedIds = evt.detail; -// if (this.selectedIds) {//only one id at a time for segmentation -// const annot = this.annotations.filter((a) => JSON.stringify(this.selectedIds)===(a.id));// search the corresponding id -// const common = commonJson(annot); -// this.attributePicker.setAttributes(common); -// } else { -// // if null, nothing is selected -// this.selectedIds = []; -// } -// } - -// /** -// * Invoked when a new instance is updated (created = updated for segmentation) -// * @param {CustomEvent} evt -// */ -// onUpdate(evt) { -// // 1) update annotation info when needed -// const updatedIds = evt.detail; -// const label = this.annotations.find((l) => l.id === JSON.stringify(updatedIds));// search the corresponding id -// if (label) {//id exists in the database, update information -// // nothing to do for annotation infos, only the mask has changed -// } else {// this is a new id -// // create the new label -// let label = {...this.attributePicker.defaultValue}; -// // store the stringified values -// const value = this.attributePicker.value; -// Object.keys(label).forEach((key) => { -// label[key] = JSON.parse(JSON.stringify(value[key])); -// }); -// label.id = JSON.stringify(updatedIds); -// store.dispatch(createAnnotation(label)); -// } -// // 2) update the mask (always id 0) -// const curr = this.annotations.find((l) => l.id === 0); -// const im = this.element.getMask(); -// const fn = curr ? updateAnnotation : createAnnotation; -// store.dispatch(fn({ id: 0, mask: im })); -// this.selectedIds = updatedIds; -// } - -// /** -// * Invoked on attribute change from property panel. -// */ -// onAttributeChanged() { -// if (!this.selectedIds.length) {//nothing is selected -// // only set the category acordingly to the selected attribute -// const category = this.attributePicker.selectedCategory; -// this.element.targetClass = category.idx; -// return; -// } -// // 2) update the mask (always id 0) -// // change category in element -// const category = this.attributePicker.selectedCategory; -// this.element.targetClass = category.idx; -// this.element.fillSelectionWithClass(category.idx); -// // get the new mask and store it -// const im = this.element.getMask(); -// store.dispatch(updateAnnotation({ id: 0, mask: im })); -// // 1) update annotation info from attributes -// const value = this.attributePicker.value; -// const label = this.annotations.find((l) => l.id === JSON.stringify(this.selectedIds));// search the corresponding id -// Object.keys(value).forEach((key) => { -// label[key] = JSON.parse(JSON.stringify(value[key])); -// }); -// // category has changed => selectedId has also changed, update it -// const updatedIds = this.element.selectedId; -// label.id = JSON.stringify(updatedIds); -// this.selectedIds = updatedIds; -// // update store -// store.dispatch(updateAnnotation(label)); -// } diff --git a/frontend/src/plugins/smart-segmentation.js b/frontend/src/plugins/smart-segmentation.js index 81cc8a5..c212867 100644 --- a/frontend/src/plugins/smart-segmentation.js +++ b/frontend/src/plugins/smart-segmentation.js @@ -10,6 +10,7 @@ import '@material/mwc-icon-button'; import '@material/mwc-icon-button-toggle'; import '@material/mwc-icon'; import { PluginSegmentation } from './segmentation'; +import { getState } from '../store'; /** * Plugin segmentation. @@ -20,12 +21,8 @@ export class PluginSmartSegmentation extends PluginSegmentation { initDisplay() { super.initDisplay(); - const tasks = this.info.tasks; - const taskName = this.info.taskName; - const task = tasks.find((t) => t.name === taskName); - if (!task) { - return; - } + const taskName = getState('application').taskName; + const task = getState('application').tasks.find((t) => t.name === taskName); if (task.spec.settings && task.spec.settings.model) { this.element.model = task.spec.settings.model; } @@ -39,7 +36,6 @@ export class PluginSmartSegmentation extends PluginSegmentation { title="Smart create" @click="${() => this.mode = 'smart-create'}"> - ` } @@ -49,7 +45,8 @@ export class PluginSmartSegmentation extends PluginSegmentation { maskVisuMode=${this.maskVisuMode} @update=${this.onUpdate} @selection=${this.onSelection} - @mode=${this.onModeChange}>`; + @delete=${this.onDelete} + @mode=${this.onModeChange}>`;//onCreate never really called for segmentation : the mask is updated } } diff --git a/frontend/src/templates/template-plugin-instance.js b/frontend/src/templates/template-plugin-instance.js index 366d286..621e7d8 100644 --- a/frontend/src/templates/template-plugin-instance.js +++ b/frontend/src/templates/template-plugin-instance.js @@ -25,7 +25,7 @@ export class TemplatePluginInstance extends TemplatePlugin { constructor(){ super(); - this.mode = 'edit'; + this.mode = 'create'; this.selectedIds = []; } @@ -138,7 +138,7 @@ export class TemplatePluginInstance extends TemplatePlugin { get toolDrawer() { return html` diff --git a/frontend/src/views/app-dashboard-admin.js b/frontend/src/views/app-dashboard-admin.js index 1414d2d..03cae5a 100644 --- a/frontend/src/views/app-dashboard-admin.js +++ b/frontend/src/views/app-dashboard-admin.js @@ -467,7 +467,7 @@ class AppDashboardAdmin extends TemplatePage { /** * Display table row - * Status | Data Id | Annotator | Validator | State | Time | Launch + * Status | Data Id | Annotator | Validator | State | Time | Thumbnail | Launch */ listitem(item) { const v = this.statusMap.get(item.status); @@ -528,7 +528,10 @@ class AppDashboardAdmin extends TemplatePage {
Time
-
+
+ +
+
`; } diff --git a/frontend/src/views/app-user-manager.js b/frontend/src/views/app-user-manager.js index f14c258..0406d63 100644 --- a/frontend/src/views/app-user-manager.js +++ b/frontend/src/views/app-user-manager.js @@ -8,6 +8,7 @@ import { html, css } from 'lit-element'; import { connect } from 'pwa-helpers/connect-mixin.js'; import TemplatePage from '../templates/template-page'; import { store, getState } from '../store'; +import { getValue } from '../helpers/utils'; import { logout, signup, getUsers, deleteUser, @@ -90,10 +91,15 @@ class AppUserManager extends connect(store)(TemplatePage) { }); } - onSaveUser(user) { - store.dispatch(updateUser(user)); - this.enabledUsername = ''; - } + onPasswordChanged(e) { + this.passwordElement.value = getValue(e); + } + + onSaveUser(user) { + user.password = this.passwordElement.value; + store.dispatch(updateUser(user)); + this.enabledUsername = ''; + } onCancel() { this.enabledUsername = ''; @@ -227,32 +233,33 @@ class AppUserManager extends connect(store)(TemplatePage) { `; } - listitem(user) { - return html` -
-

${user.username}

-

- -

-
- user.role = this.dropdownValues['role'][e.detail.index]}> - ${this.dropdownValues['role'].map((v) => html`${v}`)} - -
-
- user.preferences.theme = this.dropdownValues['preferences.theme'][e.detail.index]}> - ${this.dropdownValues['preferences.theme'].map((v) => html`${v}`)} - -
- ${this.editionCell(user)} -
- `; - } + listitem(user) { + return html` +
+

${user.username}

+

+ +

+
+ user.role = this.dropdownValues['role'][e.detail.index]}> + ${this.dropdownValues['role'].map((v) => html`${v}`)} + +
+
+ user.preferences.theme = this.dropdownValues['preferences.theme'][e.detail.index]}> + ${this.dropdownValues['preferences.theme'].map((v) => html`${v}`)} + +
+ ${this.editionCell(user)} +
+ `; + } get userSection() { diff --git a/package.json b/package.json index aab2f46..f8ec583 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pixano-app", - "version": "0.4.7", + "version": "0.4.9", "description": "This is a Pixano app.", "keywords": [], "license": "CECILL-C", @@ -29,6 +29,7 @@ "object-sizeof": "^1.5.3", "path": "^0.12.7", "save": "^2.4.0", + "semver": "^7.3.5", "short-uuid": "^3.1.1", "tmp": "^0.1.0" } diff --git a/server/config/parser.js b/server/config/parser.js index ee70274..48f6fcd 100644 --- a/server/config/parser.js +++ b/server/config/parser.js @@ -45,7 +45,7 @@ async function parse(databasePath) { return { id: a.id, name: a.name, - category: a.categoryName, + category: a.category, geometry: a.geometry, options: { ...a.options, diff --git a/server/helpers/data-populator.js b/server/helpers/data-populator.js index 4d3bb57..2be8d46 100644 --- a/server/helpers/data-populator.js +++ b/server/helpers/data-populator.js @@ -69,7 +69,8 @@ async function populateSimple(db, mediaRelativePath, hostWorkspacePath, datasetI for await (const f of files) { const id = generateKey(); const url = workspaceToMount(hostWorkspacePath, f); - const value = { id, dataset_id: datasetId, type: dataType, path: url, children: '', thumbnail: await imageThumbnail(f, {responseType: 'base64'})}; + let value = { id, dataset_id: datasetId, type: dataType, path: url, children: ''} + if (dataType=='image') value.thumbnail = await imageThumbnail(f, {responseType: 'base64', height: 100}); await bm.add({ type: 'put', key: dbkeys.keyForData(datasetId, id), value: value}); bar1.increment(); } diff --git a/server/routes/users.js b/server/routes/users.js index c2e8bb1..91984d0 100644 --- a/server/routes/users.js +++ b/server/routes/users.js @@ -254,7 +254,7 @@ async function put_user(req, res) { const user = await db.get(dbkeys.keyForUser(username)); return user.password === password; } catch (err) { - console.log(username, password, 'does not exist.'); + console.log(username, 'does not exist.'); return false; } }