diff --git a/.dockerignore b/.dockerignore index bd64114..541410d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1 +1,3 @@ -*.tar \ No newline at end of file +*.tar +node_modules +frontend/node_modules \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index ae0d783..4dfb491 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,26 +2,21 @@ FROM ubuntu:18.04 # Install Node.js RUN apt update && apt install -y --reinstall ca-certificates curl build-essential -RUN curl --silent --location https://deb.nodesource.com/setup_10.x | bash - +RUN curl --silent --location https://deb.nodesource.com/setup_12.x | bash - RUN apt install -y nodejs && apt install -y python-requests +RUN npm install -g npm@6.10.0 -COPY package.json package.json - -COPY webpack.config.js webpack.config.js - -COPY src src - -COPY images images +# Copy files for the frontend +COPY frontend frontend +# Copy files for the backend +COPY package.json package.json COPY server server - COPY .logo-ascii .logo-ascii -RUN npm install -g npm@7.23.0 && npm i \ - && npm run build \ - && rm -rf src node_modules images \ - && npm i -D \ - && rm -rf package-lock.json webpack.config.js +# Build frontend and install backend dependencies +RUN npm run installApp && npm run buildApp && npm install \ + && rm -rf src frontend EXPOSE 3000 diff --git a/Dockerfile-local b/Dockerfile-local index c02aeba..997473f 100644 --- a/Dockerfile-local +++ b/Dockerfile-local @@ -1,5 +1,4 @@ # Build Docker image - FROM ubuntu:18.04 # Install Node.js @@ -7,20 +6,16 @@ RUN apt-get update && apt-get install -y --reinstall ca-certificates curl build- RUN curl --silent --location https://deb.nodesource.com/setup_10.x | bash - RUN apt install -y nodejs && apt install -y python-requests -COPY package.json package.json - -COPY webpack.config.js webpack.config.js - +# Copy bundled frontend COPY build build -COPY images images - +# Copy files for the backend +COPY package.json package.json COPY server server - COPY .logo-ascii .logo-ascii -RUN npm install -g npm@7.23.0 && npm i -D - +# Install backend dependencies +RUN npm install EXPOSE 3000 # ENTRYPOINT ["node", "server/server.js"] diff --git a/README.md b/README.md index 737383a..ac963f4 100644 --- a/README.md +++ b/README.md @@ -75,37 +75,35 @@ You can fill the task configuration as follows, which will create as many annota #### Global dependencies -- NodeJS (>=10) +- NodeJS (>=12) To install on ubuntu: ```bash # Make sure you have curl installed sudo apt install curl # Then download and execute the Node.js 10.x installer - curl -sL https://deb.nodesource.com/setup_10.x | sudo -E bash - + curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash - # Once the installer is done doing its thing, you will need to install (or upgrade) Node.js sudo apt install nodejs # Make sure the version is now correct nodejs --version + npm install -g npm@6.10.0 ``` You can read this nice [introduction](https://codeburst.io/the-only-nodejs-introduction-youll-ever-need-d969a47ef219) to NodeJS in case you're curious on how it works: #### Application dependencies ```bash -# Install application dependencies +# Install application backend and frontend dependencies npm i +npm run installApp ``` ##### Using a local pixano-element If you want to use custom `pixano-element` modules from local path instead of the NPM registry, link them as explained below: ```bash -npm run cleanPixanoElements # Install application dependencies and local pixano-elements -npm run installLocalPixanoElement --pixano-elements-address=../pixano-elements --element=core -npm run installLocalPixanoElement --pixano-elements-address=../pixano-elements --element=ai -npm run installLocalPixanoElement --pixano-elements-address=../pixano-elements --element=graphics-2d -npm run installLocalPixanoElement --pixano-elements-address=../pixano-elements --element=graphics-3d +cd frontend/ && npm run installLocalElements --path=../../pixano-elements && cd ../ ``` *NB: Make sure you have the git repository of pixano-elements next to the pixano-app folder and that you have followed the pixano-elements build instructions before running the above commands.* @@ -114,7 +112,7 @@ npm run installLocalPixanoElement --pixano-elements-address=../pixano-elements - ```bash # Bundle the application using Webpack # This will create a build folder containing all the sources to be served -npm run build +npm run buildApp ``` #### Run the application diff --git a/images/favicon.ico b/frontend/images/favicon.ico similarity index 100% rename from images/favicon.ico rename to frontend/images/favicon.ico diff --git a/images/login.png b/frontend/images/login.png similarity index 100% rename from images/login.png rename to frontend/images/login.png diff --git a/images/pixano-logo-grad.svg b/frontend/images/pixano-logo-grad.svg similarity index 100% rename from images/pixano-logo-grad.svg rename to frontend/images/pixano-logo-grad.svg diff --git a/images/pixano-mono-grad.svg b/frontend/images/pixano-mono-grad.svg similarity index 100% rename from images/pixano-mono-grad.svg rename to frontend/images/pixano-mono-grad.svg diff --git a/images/pixano_logo.png b/frontend/images/pixano_logo.png similarity index 100% rename from images/pixano_logo.png rename to frontend/images/pixano_logo.png diff --git a/images/task-creation.png b/frontend/images/task-creation.png similarity index 100% rename from images/task-creation.png rename to frontend/images/task-creation.png diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..d42395f --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,80 @@ +{ + "name": "pixano-app-frontend", + "version": "0.4.7", + "description": "This is a Pixano app.", + "scripts": { + "copyindex": "shx cp src/index.html build", + "copyimage": "shx cp -r images build/", + "copywc": "shx cp node_modules/@webcomponents/webcomponentsjs/webcomponents-* build/", + "build": "shx rm -rf build && shx mkdir build && npm run copyindex && npm run copyimage && npm run copywc && webpack --config webpack.config.js", + "clean": "shx rm -rf node_modules", + "installLocalElements": "shx rm -rf node_modules/@pixano ; shx rm -rf package-lock.json ; npm i ${npm_config_path}/packages/core ; npm i ${npm_config_path}/packages/ai ; npm i ${npm_config_path}/packages/graphics-2d; npm i ${npm_config_path}/packages/graphics-3d" + }, + "keywords": [], + "license": "CECILL-C", + "engines": { + "node": ">=10.0.0", + "npm": ">=6.0.0" + }, + "bin": "./server/serve.js", + "devDependencies": { + "shx": "^0.3.3", + "webpack": "^4.41.5", + "webpack-cli": "^3.3.10" + }, + "dependencies": { + "@babel/core": "^7.7.7", + "@babel/plugin-transform-runtime": "^7.7.6", + "@babel/polyfill": "^7.7.0", + "@babel/preset-env": "^7.7.7", + "@babel/runtime": "^7.7.7", + "@material/mwc-button": "0.19.1", + "@material/mwc-checkbox": "0.19.1", + "@material/mwc-circular-progress-four-color": "0.19.1", + "@material/mwc-dialog": "0.19.1", + "@material/mwc-fab": "0.19.1", + "@material/mwc-formfield": "0.19.1", + "@material/mwc-icon": "0.19.1", + "@material/mwc-icon-button": "0.19.1", + "@material/mwc-icon-button-toggle": "0.19.1", + "@material/mwc-linear-progress": "0.19.1", + "@material/mwc-list": "0.19.1", + "@material/mwc-menu": "0.19.1", + "@material/mwc-radio": "0.19.1", + "@material/mwc-select": "0.19.1", + "@material/mwc-slider": "0.19.1", + "@material/mwc-snackbar": "0.19.1", + "@material/mwc-switch": "0.19.1", + "@material/mwc-tab": "0.19.1", + "@material/mwc-tab-bar": "0.19.1", + "@material/mwc-textarea": "0.19.1", + "@material/mwc-textfield": "0.19.1", + "@pixano/ai": "0.5.16", + "@pixano/core": "0.5.16", + "@pixano/graphics-2d": "0.5.16", + "@pixano/graphics-3d": "0.5.16", + "@trystan2k/fleshy-jsoneditor": "3.0.0", + "@webcomponents/webcomponentsjs": "^2.4.0", + "babel-loader": "^8.0.6", + "copy-webpack-plugin": "^5.1.1", + "css-loader": "^3.4.0", + "file-loader": "^5.0.2", + "html-webpack-plugin": "^3.2.0", + "lit-element": "2.4.0", + "lit-redux-router": "^0.10.0", + "material-design-icons": "^3.0.1", + "node-sass": "^4.13.0", + "pwa-helpers": "^0.9.1", + "redux": "^4.0.4", + "redux-devtools-extension": "^2.13.8", + "redux-thunk": "^2.3.0", + "redux-undo": "^1.0.0", + "sass-loader": "^8.0.0", + "source-map-loader": "^0.2.4", + "string-replace-loader": "^2.2.0", + "style-loader": "^1.0.2", + "typeface-roboto": "0.0.75", + "url-loader": "^3.0.0", + "web-animations-js": "^2.3.2" + } +} diff --git a/src/actions/annotations.js b/frontend/src/actions/annotations.js similarity index 100% rename from src/actions/annotations.js rename to frontend/src/actions/annotations.js diff --git a/src/actions/application.js b/frontend/src/actions/application.js similarity index 100% rename from src/actions/application.js rename to frontend/src/actions/application.js diff --git a/src/actions/media.js b/frontend/src/actions/media.js similarity index 100% rename from src/actions/media.js rename to frontend/src/actions/media.js diff --git a/src/actions/requests.js b/frontend/src/actions/requests.js similarity index 100% rename from src/actions/requests.js rename to frontend/src/actions/requests.js diff --git a/src/actions/user.js b/frontend/src/actions/user.js similarity index 100% rename from src/actions/user.js rename to frontend/src/actions/user.js diff --git a/src/app.js b/frontend/src/app.js similarity index 100% rename from src/app.js rename to frontend/src/app.js diff --git a/src/helpers/attribute-picker.js b/frontend/src/helpers/attribute-picker.js similarity index 100% rename from src/helpers/attribute-picker.js rename to frontend/src/helpers/attribute-picker.js diff --git a/src/helpers/data-loader.js b/frontend/src/helpers/data-loader.js similarity index 100% rename from src/helpers/data-loader.js rename to frontend/src/helpers/data-loader.js diff --git a/src/helpers/pop-up.js b/frontend/src/helpers/pop-up.js similarity index 100% rename from src/helpers/pop-up.js rename to frontend/src/helpers/pop-up.js diff --git a/src/helpers/utils.js b/frontend/src/helpers/utils.js similarity index 100% rename from src/helpers/utils.js rename to frontend/src/helpers/utils.js diff --git a/src/index.html b/frontend/src/index.html similarity index 100% rename from src/index.html rename to frontend/src/index.html diff --git a/src/models/mixins/sequence-mixin.js b/frontend/src/models/mixins/sequence-mixin.js similarity index 100% rename from src/models/mixins/sequence-mixin.js rename to frontend/src/models/mixins/sequence-mixin.js diff --git a/src/models/plugin-style.js b/frontend/src/models/plugin-style.js similarity index 100% rename from src/models/plugin-style.js rename to frontend/src/models/plugin-style.js diff --git a/src/models/plugins.js b/frontend/src/models/plugins.js similarity index 100% rename from src/models/plugins.js rename to frontend/src/models/plugins.js diff --git a/src/models/template-page.js b/frontend/src/models/template-page.js similarity index 100% rename from src/models/template-page.js rename to frontend/src/models/template-page.js diff --git a/src/models/template-plugin-instance.js b/frontend/src/models/template-plugin-instance.js similarity index 100% rename from src/models/template-plugin-instance.js rename to frontend/src/models/template-plugin-instance.js diff --git a/src/models/template-plugin.js b/frontend/src/models/template-plugin.js similarity index 100% rename from src/models/template-plugin.js rename to frontend/src/models/template-plugin.js diff --git a/src/my-icons.js b/frontend/src/my-icons.js similarity index 100% rename from src/my-icons.js rename to frontend/src/my-icons.js diff --git a/src/plugins/cuboid.js b/frontend/src/plugins/cuboid.js similarity index 100% rename from src/plugins/cuboid.js rename to frontend/src/plugins/cuboid.js diff --git a/frontend/src/plugins/keypoints-box.js b/frontend/src/plugins/keypoints-box.js new file mode 100644 index 0000000..18da9b6 --- /dev/null +++ b/frontend/src/plugins/keypoints-box.js @@ -0,0 +1,202 @@ +/** + * @copyright CEA-LIST/DIASI/SIALV/LVA (2019) + * @author CEA-LIST/DIASI/SIALV/LVA + * @license CECILL-C +*/ + +import { html } from 'lit-element'; +import { settings } from '@pixano/graphics-2d/lib/pxn-graph'; +import '@material/mwc-icon-button'; +import { colorNames, shuffle } from '@pixano/core/lib/utils' +import { TemplatePluginInstance } from '../models/template-plugin-instance'; +import { store } from '../store'; +import { updateAnnotation, createAnnotation, setAnnotations } from '../actions/annotations'; + +export class PluginKeypointsBox extends TemplatePluginInstance { + + constructor() { + super(); + this.viewMode = 'single_to_annotate'; // or all; + this.colors = shuffle(Object.keys(colorNames)); // list of color names + // window.addEventListener('keydown', (evt) => { + // const lowerKey = evt.key.toLowerCase(); + // if (lowerKey === 'c') { + // this.viewMode = this.viewMode == 'all' ? 'single_to_annotate' : 'all'; + // this.refresh(); + // } + // }); + } + + updated(changedProperties) { + if (changedProperties.has('mode')) { + this.viewMode = this.mode == 'create' ? 'single_to_annotate' : 'all'; + this.refresh(); + } + } + + firstUpdated() { + super.firstUpdated(); + settings.edges = [[0,1], [1,2]]; + settings.colorFillType = "order"; + settings.orderedColors =[ + 0xffff00, 0x008000, 0xff0000 + ]; + // To edit skeleton structure: + // this.element.graphType = { + // names: ['center'] + // } + } + + newData() { + super.newData(); + // reset global counter + const ids = this.annotations + .filter((a) => a.geometry.type == "graph") + .map((kpt) => kpt.id); + const boxes = this.annotations + .filter((a) => a.geometry.type == "rectangle") + const doneBoxes = boxes.filter((r) => ids.includes(r.id)); + this.attributePicker.numDone = doneBoxes.length; + this.attributePicker.numTotal = boxes.length; + + } + + refresh() { + if (!this.element) { + return; + } + // need to make immutable variable as not to change directly + // the redux store + if (this.viewMode == 'single_to_annotate') { + this.showNextFreeBox(); + } else { + this.showAll(); + } + + } + + /** + * Invoked on shape change + * @param {CustomEvent} evt + */ + onUpdate() { + const shapes = [...this.element.shapes].map((s) => { + const s2 = JSON.parse(JSON.stringify(s)); + delete s2.color; + return s2; + }); + store.dispatch(setAnnotations(shapes)); + } + + /** + * Invoked on instance removal + * @param {CustomEvent} evt + */ + onDelete(evt) { + const shapes = [...this.element.shapes].map((s) => { + const s2 = JSON.parse(JSON.stringify(s)); + delete s2.color; + return s2; + }); + store.dispatch(setAnnotations(shapes)); + this.attributePicker.numDone -= evt.detail.length; + } + + onCreate(evt) { + if (this.viewMode == 'all') { + this.element.shapes.delete(evt.detail); + return; + } + const ids = this.annotations + .filter((a) => a.geometry.type == "graph") + .map((kpt) => kpt.id); + const freeBoxes = this.annotations + .filter((a) => a.geometry.type == "rectangle") + .filter((r) => !ids.includes(r.id)); + + if (freeBoxes.length) { + evt.detail.id = freeBoxes[0].id; + let { color, ...newAnnotation } = { + ...JSON.parse(JSON.stringify(evt.detail)), + ...this.attributePicker.defaultValue + }; + if (this.isSequence) { + newAnnotation.timestamp = this.targetFrameIdx; + } + 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].category) }] : []; + } else { + this.element.shapes = []; + } + } + + showAll() { + const annotations = JSON.parse(JSON.stringify(this.annotations)); + const rectangles = annotations.filter((a) => a.geometry.type == "rectangle"); + const keypoints = annotations.filter((a) => a.geometry.type == "graph"); + + rectangles.forEach((rect, idx) => { + rect.color = this.colors[idx%this.colors.length]; + const kpt = keypoints.find((k) => k.id == rect.id); + if (kpt) { + kpt.color = rect.color; + } + }) + this.element.shapes = [...keypoints, ...rectangles]; + } + + showNextFreeBox() { + const ids = this.annotations + .filter((a) => a.geometry.type == "graph") + .map((kpt) => kpt.id); + const freeBoxes = this.annotations + .filter((a) => a.geometry.type == "rectangle") + .filter((r) => !ids.includes(r.id)); + if (freeBoxes.length) { + + this.element.shapes = [{...freeBoxes[0], color: this._colorFor(freeBoxes[0].category) }]; + } + } + + allVisible() { + const selectedLabels = this.element.selectedShapes; + if (selectedLabels.length === 1) { + selectedLabels[0].geometry.visibles = selectedLabels[0].geometry.visibles.map(() => true); + store.dispatch(updateAnnotation( + { + ...JSON.parse(JSON.stringify(selectedLabels[0])) + })); + } + } + + swap() { + const selectedLabels = this.element.selectedShapes; + if (selectedLabels.length === 1) { + const vs = selectedLabels[0].geometry.vertices; + selectedLabels[0].geometry.vertices = [vs[0], vs[1], vs[4], vs[5], vs[2], vs[3]]; + store.dispatch(updateAnnotation( + { + ...JSON.parse(JSON.stringify(selectedLabels[0])) + })); + } + } + + get toolDrawer() { + return html` + ${super.toolDrawer} + ` + } + + get editor() { + return html``; + } +} +customElements.define('plugin-keypoints-box', PluginKeypointsBox); diff --git a/src/plugins/keypoints.js b/frontend/src/plugins/keypoints.js similarity index 100% rename from src/plugins/keypoints.js rename to frontend/src/plugins/keypoints.js diff --git a/src/plugins/polygon.js b/frontend/src/plugins/polygon.js similarity index 100% rename from src/plugins/polygon.js rename to frontend/src/plugins/polygon.js diff --git a/src/plugins/rectangle.js b/frontend/src/plugins/rectangle.js similarity index 100% rename from src/plugins/rectangle.js rename to frontend/src/plugins/rectangle.js diff --git a/src/plugins/segmentation.js b/frontend/src/plugins/segmentation.js similarity index 100% rename from src/plugins/segmentation.js rename to frontend/src/plugins/segmentation.js diff --git a/src/plugins/sequence-cuboid.js b/frontend/src/plugins/sequence-cuboid.js similarity index 100% rename from src/plugins/sequence-cuboid.js rename to frontend/src/plugins/sequence-cuboid.js diff --git a/src/plugins/sequence-keypoints.js b/frontend/src/plugins/sequence-keypoints.js similarity index 100% rename from src/plugins/sequence-keypoints.js rename to frontend/src/plugins/sequence-keypoints.js diff --git a/frontend/src/plugins/sequence-point-rectangle.js b/frontend/src/plugins/sequence-point-rectangle.js new file mode 100644 index 0000000..8683ba2 --- /dev/null +++ b/frontend/src/plugins/sequence-point-rectangle.js @@ -0,0 +1,136 @@ +/** + * @copyright CEA-LIST/DIASI/SIALV/LVA (2019) + * @author CEA-LIST/DIASI/SIALV/LVA + * @license CECILL-C +*/ + +import { html } from 'lit-element'; +import { PluginRectangle } from './rectangle'; +import { sequence } from '../models/mixins/sequence-mixin'; +import { store } from '../store'; +import { updateAnnotation } from '../actions/annotations'; + +export class PluginSequencePointRectangle extends sequence(PluginRectangle) { + + constructor(){ + super(); + this.targetAttribute = 'posture'; + window.addEventListener('keydown', (evt) => { + if (evt.key === 'x') { + this.mode = this.mode === 'point' ? 'edit' : 'point'; + } + if (evt.code == "q") { + // select occlusion + const category = this.attributePicker.schema.category[0].properties.find((c) => c.name === "occlusion"); + if (category) { + this.targetAttribute = category.name; + } + } + if (evt.code == "s") { + // select occlusion + const category = this.attributePicker.schema.category[0].properties.find((c) => c.name === "truncation"); + if (category) { + this.targetAttribute = category.name; + } + } + if (evt.code == "q") { + // select occlusion + const category = this.attributePicker.schema.category[0].properties.find((c) => c.name === "posture"); + if (category) { + this.targetAttribute = category.name; + } + } + if (!isNaN(evt.key)) { + const attr = this.attributePicker.schema.category[0].properties.find((c) => c.name == this.targetAttribute); + if (attr == undefined) { return; } + const num = Math.round(evt.key); + [...this.element.targetShapes].forEach((s) => { + if (num < attr.enum.length) { + s.options[this.targetAttribute] = attr.enum[num]; + store.dispatch(updateAnnotation(s)); + } + }); + this.updateDisplayOfSelectedProperties(); + } + }); + } + + get annotations() { + const annotations = super.annotations; + return annotations.filter((a) => a.detection != true); + } + + onPoint(evt) { + const p = evt.detail; + const predictions = super.annotations.filter((a) => a.detection); + if (!predictions.length) { + console.log('no predictions.') + return; + } + predictions.sort((a, b) => { + const ax0 = a.geometry.vertices[0]; + const ay0 = a.geometry.vertices[1]; + const acx = 0.5 * (ax0 + a.geometry.vertices[2]); + const acy = 0.5 * (ay0 + a.geometry.vertices[3]); + + const bx0 = b.geometry.vertices[0]; + const by0 = b.geometry.vertices[1]; + const bcx = 0.5 * (bx0 + b.geometry.vertices[2]); + const bcy = 0.5 * (by0 + b.geometry.vertices[3]); + + const da = (acx - p.x) * (acx - p.x) + (acy - p.y) * (acy - p.y); + const db = (bcx - p.x) * (bcx - p.x) + (bcy - p.y) * (bcy - p.y); + + return da - db; + }); + if (predictions[0] && this.isInside(predictions[0].geometry.vertices, p)) { + const containedBox = JSON.parse(JSON.stringify(predictions[0])); + delete containedBox.detection; + store.dispatch(updateAnnotation(containedBox)); + this.refresh(); + } + } + + get editor() { + return html``; + } + + get toolDrawer() { + return html` + + + + + + + ` + } + + isInside(box, p) { + return ( + box[0] <= p.x && + p.x <= box[2] && + box[1] <= p.y && + p.y <= box[3] + ); + } + +} + +customElements.define('plugin-sequence-point-rectangle', PluginSequencePointRectangle); \ No newline at end of file diff --git a/src/plugins/sequence-polygon.js b/frontend/src/plugins/sequence-polygon.js similarity index 100% rename from src/plugins/sequence-polygon.js rename to frontend/src/plugins/sequence-polygon.js diff --git a/src/plugins/sequence-rectangle.js b/frontend/src/plugins/sequence-rectangle.js similarity index 100% rename from src/plugins/sequence-rectangle.js rename to frontend/src/plugins/sequence-rectangle.js diff --git a/src/plugins/sequence-segmentation.js b/frontend/src/plugins/sequence-segmentation.js similarity index 100% rename from src/plugins/sequence-segmentation.js rename to frontend/src/plugins/sequence-segmentation.js diff --git a/src/plugins/smart-rectangle.js b/frontend/src/plugins/smart-rectangle.js similarity index 100% rename from src/plugins/smart-rectangle.js rename to frontend/src/plugins/smart-rectangle.js diff --git a/src/plugins/smart-segmentation.js b/frontend/src/plugins/smart-segmentation.js similarity index 100% rename from src/plugins/smart-segmentation.js rename to frontend/src/plugins/smart-segmentation.js diff --git a/frontend/src/plugins/tracking-point.js b/frontend/src/plugins/tracking-point.js new file mode 100644 index 0000000..64f2b0f --- /dev/null +++ b/frontend/src/plugins/tracking-point.js @@ -0,0 +1,199 @@ +/** + * @copyright CEA-LIST/DIASI/SIALV/LVA (2019) + * @author CEA-LIST/DIASI/SIALV/LVA + * @license CECILL-C +*/ + +import { html } from 'lit-element'; +import '@pixano/graphics-2d'; +import { sequence } from '../models/mixins/sequence-mixin'; +import { store, getStoreState } from '../store'; +import { TemplatePlugin } from "../models/template-plugin"; +import { updateAnnotation, setAnnotations, detections } from '../actions/annotations'; + +export class PluginTrackingPoint extends sequence(TemplatePlugin) { + + static get properties() { + const s = super.properties || {}; + return { + ...s, + selectedTracks: { type: Object }, + tracks: { type: Object } + }; + } + + /** + * Handle new media to display + */ + newData() { + const mediaInfo = getStoreState('media').info; + if (!mediaInfo.children) { + return; + } + const paths = mediaInfo.children.map((c) => c.path); + this.element.input = paths; + const onLoad = () => { + // refresh annoations on media loaded + this.refresh(); + this.element.removeEventListener('load', onLoad); + }; + this.element.addEventListener('load', onLoad); + } + + constructor() { + super(); + this.targetAttribute = 'posture'; + window.addEventListener('keydown', (evt) => { + if (evt.key === 'x') { + this.element.mode = this.element.mode === 'point' ? 'edit' : 'point'; + } + if (evt.key == "q") { + // select occlusion + const category = this.element.categories[0].properties.find((c) => c.name === "occlusion"); + if (category) { + this.targetAttribute = category.name; + } + } + if (evt.key == "d") { + // select truncation + const category = this.element.categories[0].properties.find((c) => c.name === "truncation"); + if (category) { + this.targetAttribute = category.name; + } + } + if (evt.key == "f") { + // select occlusion + const category = this.element.categories[0].properties.find((c) => c.name === "posture"); + if (category) { + this.targetAttribute = category.name; + } + } + if (!isNaN(evt.key)) { + const attr = this.element.categories[0].properties.find((c) => c.name == this.targetAttribute); + if (attr == undefined) { return; } + const num = Math.round(evt.key); + [...this.element.selectedTracks].forEach((t) => { + t.keyShapes[this.element.timestamp].labels[this.targetAttribute] = attr.enum[num]; + }); + this.element.requestUpdate(); + store.dispatch(setAnnotations({detections: getStoreState('annotations').detections, tracks: this.tracks})); + } + }); + } + + /** + * Recompute labels to be displayed from + * the redux store + */ + initDisplay() { + const tasks = getStoreState('application').tasks; + const taskName = getStoreState('application').taskName; + const task = tasks.find((t) => t.name === taskName); + if (this.element && task.spec.label_schema) { + this.element.categories = task.spec.label_schema.category; + } + } + + refresh() { + if (!this.element) { + return; + } + this.tracks = this.annotations || {}; + } + + onUpdate() { + store.dispatch(setAnnotations({detections: getStoreState('annotations').detections, tracks: this.tracks})); + } + + /** + * Getter of redux store annotations filtered by current timestamp. + * Should be overloaded because tracking annotation are not indexed + * by timestamp at first and do not need to be filtred by timestamp. + */ + get annotations() { + return JSON.parse(JSON.stringify(getStoreState('annotations'))).tracks; + } + + get detections() { + const detections = getStoreState('annotations').detections; + if (detections) { + return detections.filter((l) => l.timestamp === this.targetFrameIdx); // && l.detection); + } else { + return []; + } + + } + + onPoint(evt) { + console.log('on point', this.detections) + const p = evt.detail; + const predictions = this.detections.filter((d) => isInside(d.geometry.vertices, p)); + if (!predictions.length) { + console.log('no predictions.') + return; + } + predictions.sort((a, b) => { + const ax0 = a.geometry.vertices[0]; + const ay0 = a.geometry.vertices[1]; + const acx = 0.5 * (ax0 + a.geometry.vertices[2]); + const acy = 0.5 * (ay0 + a.geometry.vertices[3]); + + const bx0 = b.geometry.vertices[0]; + const by0 = b.geometry.vertices[1]; + const bcx = 0.5 * (bx0 + b.geometry.vertices[2]); + const bcy = 0.5 * (by0 + b.geometry.vertices[3]); + + const da = (acx - p.x) * (acx - p.x) + (acy - p.y) * (acy - p.y); + const db = (bcx - p.x) * (bcx - p.x) + (bcy - p.y) * (bcy - p.y); + + return da - db; + }); + if (predictions[0]) { + const containedBox = predictions[0]; + console.log('Got prediction.') + // remove detection field in detection for info that its been used + // it is useless in the annotation object + delete containedBox.detection; + // if there is a selected track, add keyshape + // else create a new track + if (this.element.selectedTracks.size) { + const currentTrack = this.element.selectedTracks.values().next().value; + this.element.addNewKeyShapes([ + { + ...JSON.parse(JSON.stringify(containedBox)), + id: currentTrack.id + } + ]); + } else { + this.element.newTrack({ + detail: JSON.parse(JSON.stringify(containedBox)) + }); + } + } else { + console.log('no predictions.') + } + } + + render() { + return html` {}} + @update-tracks=${this.onUpdate} + @delete-track=${this.onUpdate}>`; + } + +} + +customElements.define('plugin-tracking-point', PluginTrackingPoint); + + +export function isInside(box, p) { + return ( + box[0] <= p.x && + p.x <= box[2] && + box[1] <= p.y && + p.y <= box[3] + ); +} \ No newline at end of file diff --git a/src/plugins/tracking.js b/frontend/src/plugins/tracking.js similarity index 100% rename from src/plugins/tracking.js rename to frontend/src/plugins/tracking.js diff --git a/src/reducers/annotations.js b/frontend/src/reducers/annotations.js similarity index 100% rename from src/reducers/annotations.js rename to frontend/src/reducers/annotations.js diff --git a/src/reducers/application.js b/frontend/src/reducers/application.js similarity index 100% rename from src/reducers/application.js rename to frontend/src/reducers/application.js diff --git a/src/reducers/media.js b/frontend/src/reducers/media.js similarity index 100% rename from src/reducers/media.js rename to frontend/src/reducers/media.js diff --git a/src/reducers/reducer.js b/frontend/src/reducers/reducer.js similarity index 100% rename from src/reducers/reducer.js rename to frontend/src/reducers/reducer.js diff --git a/src/reducers/user.js b/frontend/src/reducers/user.js similarity index 100% rename from src/reducers/user.js rename to frontend/src/reducers/user.js diff --git a/src/store.js b/frontend/src/store.js similarity index 100% rename from src/store.js rename to frontend/src/store.js diff --git a/src/views/app-404.js b/frontend/src/views/app-404.js similarity index 100% rename from src/views/app-404.js rename to frontend/src/views/app-404.js diff --git a/src/views/app-dashboard-admin.js b/frontend/src/views/app-dashboard-admin.js similarity index 100% rename from src/views/app-dashboard-admin.js rename to frontend/src/views/app-dashboard-admin.js diff --git a/src/views/app-dashboard-user.js b/frontend/src/views/app-dashboard-user.js similarity index 100% rename from src/views/app-dashboard-user.js rename to frontend/src/views/app-dashboard-user.js diff --git a/src/views/app-explore.js b/frontend/src/views/app-explore.js similarity index 100% rename from src/views/app-explore.js rename to frontend/src/views/app-explore.js diff --git a/src/views/app-label.js b/frontend/src/views/app-label.js similarity index 100% rename from src/views/app-label.js rename to frontend/src/views/app-label.js diff --git a/src/views/app-login.js b/frontend/src/views/app-login.js similarity index 100% rename from src/views/app-login.js rename to frontend/src/views/app-login.js diff --git a/src/views/app-project-manager.js b/frontend/src/views/app-project-manager.js similarity index 100% rename from src/views/app-project-manager.js rename to frontend/src/views/app-project-manager.js diff --git a/src/views/app-user-manager.js b/frontend/src/views/app-user-manager.js similarity index 100% rename from src/views/app-user-manager.js rename to frontend/src/views/app-user-manager.js diff --git a/webpack.config.js b/frontend/webpack.config.js similarity index 100% rename from webpack.config.js rename to frontend/webpack.config.js diff --git a/package.json b/package.json index 6cb14c0..e91fdff 100644 --- a/package.json +++ b/package.json @@ -1,24 +1,19 @@ { "name": "pixano-app", - "version": "0.4.6", + "version": "0.4.7", "description": "This is a Pixano app.", "scripts": { - "copyindex": "shx cp src/index.html build", - "copyimage": "shx cp -r images build/", - "copywc": "shx cp node_modules/@webcomponents/webcomponentsjs/webcomponents-* build/", - "build": "shx rm -rf build && shx mkdir build && npm run copyindex && npm run copyimage && npm run copywc && webpack --config webpack.config.js", - "clean": "shx rm -rf node_modules", - "installLocalPixanoElement": "shx rm -rf node_modules/@pixano ; shx rm -rf package-lock.json ; npm i ${npm_config_elements_path}/packages/core ; npm i ${npm_config_elements_path}/packages/ai ; npm i ${npm_config_elements_path}/packages/graphics-2d; npm i ${npm_config_elements_path}/packages/graphics-3d" + "installApp": "npm --prefix frontend/ install", + "buildApp": "npm --prefix frontend/ run build && mv frontend/build/ build" }, "keywords": [], "license": "CECILL-C", "engines": { - "node": ">=10.0.0", + "node": ">=12.0.0", "npm": ">=6.0.0" }, "bin": "./server/serve.js", - "devDependencies": { - "shx": "^0.3.3", + "dependencies": { "archiver": "^3.1.1", "arg": "^4.1.3", "bcrypt": "5.0.0", @@ -29,7 +24,6 @@ "express": "^4.17.1", "fs": "0.0.1-security", "glob": "^7.1.6", - "google-protobuf": "^3.11.2", "jsonwebtoken": "^8.5.1", "level": "^6.0.1", "moment": "^2.24.0", @@ -39,62 +33,5 @@ "save": "^2.4.0", "short-uuid": "^3.1.1", "tmp": "^0.1.0" - }, - "dependencies": { - "@babel/core": "^7.7.7", - "@babel/plugin-transform-runtime": "^7.7.6", - "@babel/polyfill": "^7.7.0", - "@babel/preset-env": "^7.7.7", - "@babel/runtime": "^7.7.7", - "@material/mwc-button": "0.19.1", - "@material/mwc-checkbox": "0.19.1", - "@material/mwc-circular-progress-four-color": "0.19.1", - "@material/mwc-dialog": "0.19.1", - "@material/mwc-fab": "0.19.1", - "@material/mwc-formfield": "0.19.1", - "@material/mwc-icon": "0.19.1", - "@material/mwc-icon-button": "0.19.1", - "@material/mwc-icon-button-toggle": "0.19.1", - "@material/mwc-linear-progress": "0.19.1", - "@material/mwc-list": "0.19.1", - "@material/mwc-menu": "0.19.1", - "@material/mwc-radio": "0.19.1", - "@material/mwc-select": "0.19.1", - "@material/mwc-slider": "0.19.1", - "@material/mwc-snackbar": "0.19.1", - "@material/mwc-switch": "0.19.1", - "@material/mwc-tab": "0.19.1", - "@material/mwc-tab-bar": "0.19.1", - "@material/mwc-textarea": "0.19.1", - "@material/mwc-textfield": "0.19.1", - "@pixano/ai": "0.5.16", - "@pixano/core": "0.5.16", - "@pixano/graphics-2d": "0.5.16", - "@pixano/graphics-3d": "0.5.16", - "@trystan2k/fleshy-jsoneditor": "3.0.0", - "@webcomponents/webcomponentsjs": "^2.4.0", - "babel-loader": "^8.0.6", - "copy-webpack-plugin": "^5.1.1", - "css-loader": "^3.4.0", - "file-loader": "^5.0.2", - "html-webpack-plugin": "^3.2.0", - "lit-element": "2.4.0", - "lit-redux-router": "^0.10.0", - "material-design-icons": "^3.0.1", - "node-sass": "^4.13.0", - "pwa-helpers": "^0.9.1", - "redux": "^4.0.4", - "redux-devtools-extension": "^2.13.8", - "redux-thunk": "^2.3.0", - "redux-undo": "^1.0.0", - "sass-loader": "^8.0.0", - "source-map-loader": "^0.2.4", - "string-replace-loader": "^2.2.0", - "style-loader": "^1.0.2", - "typeface-roboto": "0.0.75", - "url-loader": "^3.0.0", - "web-animations-js": "^2.3.2", - "webpack": "^4.41.5", - "webpack-cli": "^3.3.10" } }