diff --git a/Dockerfile b/Dockerfile index 0bcfaec..77b826b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,9 @@ -FROM ubuntu:18.04 +FROM ubuntu:20.04 # Install Node.js -RUN apt update && apt install -y --reinstall ca-certificates curl build-essential +RUN apt-get update && apt-get install -y --reinstall ca-certificates curl build-essential RUN curl --silent --location https://deb.nodesource.com/setup_12.x | bash - -RUN apt install -y nodejs && apt install -y python-requests +RUN apt-get install -y nodejs RUN npm install -g npm@6.10.0 # Copy files for the frontend @@ -15,10 +15,14 @@ COPY server server COPY .logo-ascii .logo-ascii # Build frontend and install backend dependencies -RUN npm i && cd frontend/ && npm i && npm run build && rm -rf src frontend && cd .. +RUN npm deps && npm run build && rm -rf frontend EXPOSE 3000 +# default files and folders (usefull when no volume can be mounted with this image) +RUN mkdir -p /data + + # ENTRYPOINT ["node", "server/server.js"] RUN echo 'cat .logo-ascii && node server/server.js "$@"' > entrypoint.sh ENTRYPOINT ["sh", "entrypoint.sh" ] diff --git a/Dockerfile-local b/Dockerfile-local index 03c6508..c35b71c 100644 --- a/Dockerfile-local +++ b/Dockerfile-local @@ -1,10 +1,10 @@ -# Build Docker image -FROM ubuntu:18.04 +FROM ubuntu:20.04 # Install Node.js RUN apt-get update && apt-get install -y --reinstall ca-certificates curl build-essential -RUN curl --silent --location https://deb.nodesource.com/setup_10.x | bash - -RUN apt install -y nodejs && apt install -y python-requests +RUN curl --silent --location https://deb.nodesource.com/setup_12.x | bash - +RUN apt-get install -y nodejs +RUN npm install -g npm@6.10.0 # Copy bundled frontend COPY build build @@ -18,6 +18,10 @@ COPY .logo-ascii .logo-ascii RUN npm install EXPOSE 3000 +# default files and folders (usefull when no volume can be mounted with this image) +RUN mkdir -p /data + + # ENTRYPOINT ["node", "server/server.js"] RUN echo 'cat .logo-ascii && node server/server.js "$@"' > entrypoint.sh ENTRYPOINT ["sh", "entrypoint.sh" ] diff --git a/README.md b/README.md index 94bc02d..fb4c670 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,52 @@ # Pixano +[![License](https://img.shields.io/badge/license-CeCILL--C-blue.svg)](LICENSE) [![Live Demo](https://img.shields.io/badge/demo-online-green.svg)](http://pixano.cea.fr/smart-annotation/) [![License](https://img.shields.io/docker/pulls/pixano/pixano-app.svg)](Docker) + +## What is PIXANO ? +[Pixano](https://pixano.cea.fr/) is a web-based smart-annotation tool for computer vision applications. The modules are driven by artificial intelligence, which assists the human user with annotation tasks and accelerate the annotation process. Try some of our features [online](https://pixano.github.io/demo/demo.html)! + +[![pixano.gif](documentation/pixano.gif)](https://www.youtube.com/watch?v=z5T2HhnugJo) + Pixano App =============== -[![License](https://img.shields.io/badge/license-CeCILL--C-blue.svg)](LICENSE) [![Live Demo](https://img.shields.io/badge/demo-online-green.svg)](http://pixano.cea.fr/smart-annotation/) [![License](https://img.shields.io/docker/pulls/pixano/pixano-app.svg)](Docker) -Pixano App is a web-based annotation tool. It relies on web components dedicated to annotation [pixano-elements](https://github.com/pixano/pixano-elements). This document explains how to run it. +## Table of Contents +* [Installation](#1-installation) + - [Using Docker Image](#using-docker-image) + - [From source (for developers)](#install-from-source-for-developers) +* [Usage](#2-usage) + - [Configure your first annotation project](#configure-your-first-annotation-project) + - [Start annotating](#start-annotating) + - [Export your annotations](#export-your-annotations) + - [Standalone vs distributed usage](#standalone-vs-distributed-usage) +* [Advanced usage](#3-advanced-usage) + - [Import predictions](#import-predictions) + - [Import/Export annotation format](#importexport-annotation-format) + - [Build docker from sources](#build-docker-from-sources) +* [Contributing](#4-contributing) + - [Pixano architecture: Pixano-app and Pixano-elements](#pixano-architecture-pixano-app-and-pixano-elements) + - [Some documentation to get started](#some-documentation-to-get-started) -## 1. Installation & Setup +## 1. Installation -### 1.a) Using Docker Image [recommended] +### Using Docker Image The easiest way to get up-and-running is to install [Docker](https://www.docker.com/). Then, you should be able to download and run the pre-built image using the docker command line tool. Find out more about the `pixano` image on its [Docker Hub](https://hub.docker.com/r/pixano/pixano-app/) page. -Here's the simplest way you can run the Pixano application using docker, assuming you're familiar with using -v argument to mount folders: +Here's the simplest way you can run the Pixano application using docker: ```bash sudo docker run -it --rm -v "$PWD":/data -p 3000:3000 pixano/pixano-app ``` -The path where you run this command must contain your folder of images. +The path where you run this command must contain the data you want to annotate. + +*NB: This path is defined as your workspace.* + +#### Optional: create an alias +In practice, we suggest you setup an alias called `pixano` to automatically expose the folder containing your specified image, so the script can read it and store results where you can access them. This is how you can do it in your terminal console on OSX or Linux: -[Optional] In practice, we suggest you setup an alias called `pixano` to automatically expose the folder containing your specified image, so the script can read it and store results where you can access them. This is how you can do it in your terminal console on OSX or Linux: ```bash # Setup the alias. Put this in your .bashrc file so it's available at startup. # Note that the --network host works only on Linux, use explicit port mapping for Windows and Mac @@ -33,71 +58,31 @@ pixano ./data-test --port 3001 # pixano ``` -You’ll see something similar to this. - -```bash - ┌────────────────────────────────────────────────────────────────────────┐ - │ │ - │ Serving /path/to/your/workspace │ - │ │ - │ - Local: http://localhost:3000 │ - │ - On Your Network: http://xxx.xxx.x.xx:3000 │ - │ │ - └────────────────────────────────────────────────────────────────────────┘ -``` - -Open your browser and hit `localhost:3000`. You should see the login page of the application. -![pixano-elements](./frontend/images/login.png) +### Install from source (for developers) -First authentification is: `username: admin` `password: admin`. +#### Install global dependencies -You can then create your annotation project in the `Tasks` tab or update your login in the `Users` tab. +- NodeJS (>=12) + To install on ubuntu: -If your `data-test` folder has the following structure: -``` -data-test -│ -└───images - │ xxx.jpg - │ yyy.jpg - │ - │ ... +```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_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 fill the task configuration as follows, which will create as many annotation jobs as there are images in your `image` folder: -![task-creation](./frontend/images/task-creation.png) - -*Update 2020.12.04: Make sure your image extensions are either `png` or `jpg`. - -*Update 2021.03.05: Videos are not directly handled: extract the video frames beforehand. For every plugin taking a sequence as input (e.g. `sequence-rectangle`, `sequence-polygon`, `tracking`, etc), each subfolder containing images will be considered as a sequence. - -### 1.b) Manual Installation [developers] - -#### Global dependencies + 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. -- 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_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 +#### Install application dependencies ```bash -# Install backend dependencies -npm i -# Install frontend dependencies -cd frontend/ -npm i +npm run deps ``` ##### Using a local pixano-element @@ -119,7 +104,14 @@ npm run build #### Run the application - In the command prompt, type in `node server/server.js /path/to/workspace/` from the root folder and hit enter. You’ll see something similar to this. +In the command prompt, type in `node server/server.js /path/to/your/workspace` from the root folder and hit enter. + +*NB: Make sure when typing this command that the workspace (`/path/to/your/workspace`) contains all of the data you want to use.* + + +## 2. Usage + +After running Pixano-App, you’ll see something similar to this: ```bash ┌────────────────────────────────────────────────────────────────────────┐ @@ -132,35 +124,52 @@ npm run build └────────────────────────────────────────────────────────────────────────┘ ``` -E.g: `node server/server.js ./data-test/`. +Open your browser and hit _http://localhost:3000_. You should see the login page of the application. -Open your browser and hit `localhost:3000`. You should see the login page of the application. First authentification is: `username: admin` `password: admin`. You can then create your annotation project in the `Tasks` tab or update your login in the `Users` tab. +![pixano-elements](./documentation/images/page-login.png) +First authentication is: `username: admin` `password: admin`. -*NB: When creating an annotation task, you will refer to the folder containing the images you want to annotate by a relative path from the `workspace` folder. Make sure when typing `node server/server.js /path/to/workspace/` that it contains all of the data you want to use.* +### Configure your first annotation project +Before annotating, configure your project by following our [admin's guide](./admin-guide.md). You will be able to: -## 2. Contributing +- define your datasets +- define your desired annotation tasks +- define your users and their role (annotators, validators, administrators) -If you want to edit the application to your liking, fork this repository. +### Start annotating -- To get familiar with how the app is built from Web Components, read the [LitElement](https://lit-element.polymer-project.org/) documentation. -- To get familiar with how the data is managed in the client, read the [redux](https://redux.js.org/introduction/getting-started) documentation. -- To better understand the Pixano server API, read its [documentation](documentation/rest-api.md) +Once a task is defined, you (or your annotators) will be able to annotate your dataset. See our [annotator's guide](./annotator-guide.md) for your first steps. +Our [plugins' guide](./plugins-guide.md) will help you in the use of your current task's specific plugin. -### Build docker from sources +### Export your annotations -To create a docker image of the application, build the application (step 1.b) and then run: -```bash -# You can change `pixano` by your choosen image name -sudo docker build -t pixano/pixano-app:my-tag . -# You can use the local Dockerfile if the build folder already exists -sudo docker build -t pixano/pixano-app:my-tag -f Dockerfile-local . -``` +Get your annotations and use them for any external application easily: + +- as an admin, go to the tasks tab +- press the "EXPORT TO FILES" button +- you will find the exported annotations in the root of your workspace (find more information on annotation format [bellow](#importexport-annotation-format)) + +### Standalone vs distributed usage + +Pixano-app can be used standalone on a single machine. In this case, the "admin" can also directly annotate and validate his datasets. See our [admin's guide](./admin-guide.md) for more details. + +Pixano-app is also developed to enable a distributed work: + +- install Pixano-app on a server and open its ip and port to your annotators inside your network +- define your datasets, tasks and users (See [admin's guide](./admin-guide.md)). The tasks will be automatically distributed between the annotators. +- each annotator can start working immediately from his computer without installing anything by connecting to _http://xxx.xxx.x.xx:3000_ -## 3. Import existing annotations / predictions -Create an `annotation` folder as such: +## 3. Advanced usage + +### Import predictions + +If you want to analyze predictions from your last detector or use these predictions as a pre-annotation, you can import these predictions as existing annotations by using our [annotation format](#importexport-annotation-format). + +### Import/Export annotation format + ``` data-test │ @@ -174,4 +183,45 @@ data-test │ xxx.json └─── yyy.json ``` -The `task1.json` file contains global task settings (task type, task categories, image folder, etc) and its correspoding `task1` folder contains an annotation file for each image. To prepare those files check the [import documentation](documentation/import-annotations.md). +The `task1.json` file contains global task settings (task type, task categories, image folder, etc) and its corresponding `task1` folder contains an annotation file for each image. To prepare those files check the [import documentation](documentation/import-annotations.md). + +### Build docker from sources + +To create a docker image of the application, build the application (step 1.b) and then run: +```bash +# You can change `pixano` by your choosen image name +sudo docker build -t pixano/pixano-app:my-tag . +# You can use the local Dockerfile if the build folder already exists +sudo docker build -t pixano/pixano-app:my-tag -f Dockerfile-local . +``` + + +## 4. Contributing + +If you tested Pixano and identified some issues or think some useful features are missing, please open an [issue](https://github.com/pixano/pixano-app/issues). + +If you want to edit the application to your liking, fork this repository! + +If you want to contribute more actively to the project, feel free to write your patches or new features and make a pull request! + +### Pixano architecture: Pixano-app and Pixano-elements + +![pixano.gif](documentation/Pixano-app_elements_det.png) + +**Pixano-app** is a monorepo built on top of web components dedicated to annotation (developed in a separate repo: [pixano-elements](https://github.com/pixano/pixano-elements)): + +- the backend manages the data (datasets to be annotated), the tasks (tasks to be performed by annotators) and the users (annotators, validators, admin) +- the frontend implements the web views and calls the elements through plugins +- backend and frontend communicate via a REST api + +[**Pixano-elements**](https://github.com/pixano/pixano-elements) provides a wide set of smart and re-usable web components to build highly customizable image and video annotation tools: 2D and 3D bounding boxes, polygons, segmentation masks, customizable labels, label temporal propagation, etc. **Pixano-app** relies on these web components. + +### Some documentation to get started +- General documentation: + - To get familiar with how the app is built from Web Components, read the [LitElement](https://lit-element.polymer-project.org/) documentation. + - To get familiar with how the data is managed in the client, read the [redux](https://redux.js.org/introduction/getting-started) documentation. +- Pixano's developers documentation + - To better understand the Pixano server API, read its [documentation](documentation/rest-api.md) + - To get familiar with Pixano's elements, take a look at its [dedicated repository](https://github.com/pixano/pixano-elements) and [modules documentation](https://pixano.github.io/docs/) + + diff --git a/admin-guide.md b/admin-guide.md new file mode 100644 index 0000000..c649a00 --- /dev/null +++ b/admin-guide.md @@ -0,0 +1,112 @@ +# Pixano + +A Guide for Pixano App Configuration and Administration +=============== + +This guide will help you to configure your annotation project. + +*The screenshots on this page are all generated using the "data-test" test data set. You can get the exact same results by running Pixano-app with the following command:* + +```bash +node server/server.js ./data-test +``` + +## Table of Contents +* [Login and first step](#login-and-first-step) +* [Create new tasks and load linked datasets](#create-new-tasks-and-load-linked-datasets) + - [Task definition](#a-task-is-defined-by) + - [Your datasets](#your-datasets) + - [About data format](#about-data-format) +* [Create new users](#create-new-users) +* [Dashboard: View / Annotate / Validate](#dashboard-view-annotate-validate) + +----------------------- + +## Login and first step + +![pixano-elements](./documentation/images/page-login.png) + +First authentication is: `username: admin` `password: admin`. + +After login in as admin, Pixano's dashboard will be displayed. + +![pixano-elements](./documentation/images/page-dashboard.png) + +You can then create your annotation project in the [`Tasks` tab](#create-new-tasks) and update your login or create new users in the [`Users` tab](#create-new-users). + +## Create new tasks and load linked datasets + +![pixano-elements](./documentation/images/page-task-manager.png) + +Create your first task by clicking on _NEW TASK_. + + +![pixano-elements](./documentation/images/page-task-manager-rectangle.png) + +You can now fill the task configuration as follows. When clicking on _CREATE TASK_ Pixano will load all the images in the `Data folder` and create a task with the name `Task name`. For each image, an annotation job will be created and served to the annotators. + +*NB: Your `images` folder refers to the folder containing the images you want to annotate by a relative path to the `workspace` folder (in this example, the workspace is ./data-test).* + +You can define as many tasks as needed. They can refer to different datasets or not. For each defined task, you can: + +- delete them one by one by clicking on _REMOVE TASK_ +- modify their attributes and saving your changes by clicking on _SAVE TASK_ + + + + +#### A task is defined by: +- `Task name`: the name you want to give to this task +- `Data folder`: the folder containing the data to be annotated in this task (relative path to the `workspace` folder) +- `Plugin`: the plugin/functionality to be used for this task (choose it from the list). For example, if you want to annotate vehicles by rectangles, choose `rectangle`. Get more information on plugins in our [plugins' guide](./plugins-guide.md). +- Left panel: a form that gives the annotation attributes: for each class defined, you can choose a color and properties of various formats. Define as many classes you need for your task. A default form is provided that gives you all the options available for the selected plugin. +- Right panel: only used in the case of a smart plugin. Place here the path of the model to be used (or use the default model) + + +#### Your datasets + +Each annotation task refers to a defined dataset. The same dataset can be used for multiple tasks. + +A dataset is a folder containing images (directly or inside a hierarchy of folders). + +For example, your `data-test` folder could have the following structure: +``` +data-test +│ +└───images + │ xxx.jpg + │ yyy.jpg + │ + │ ... +``` + +#### About data format: + +- Make sure your image extensions are either `png` or `jpg`. +- Videos are not directly handled: extract the video frames beforehand. For every plugin taking a sequence as input (e.g. `sequence-rectangle`, `sequence-polygon`, `tracking`, etc), each subfolder containing images will be considered as a sequence. + + +## Create new users + +![pixano-elements](./documentation/images/page-user-manager.png) + +Define your users and their role: administrator (Admin) or annotator (User); and clic on _ADD_. + + + +## Dashboard: View / Annotate / Validate + +Once you defined some tasks, your dashboard will show up similar to this: + +![pixano-elements](./documentation/images/page-dashboard-task1.png) + +You can select a task to be or being processed (and its linked dataset to be viewed) through the drop list _Task_. You are now able to navigate in your dataset and view its images and see for each of them its status (annotated, validated, etc). The filtering bar enables you to search for a subpart of your dataset. + +As an admin: + +- using the _START ANNOTATING_ button, the annotation work can begin following the previously defined tasks. +- using the _START VALIDATING_ button, you will be able to validate the data that has been annotated so long. + +If you defined users, these users will be able to connect to Pixano and start annotating (see our [annotator's guide](./annotator-guide.md)). +For now, only an admin is allowed to validate the annotations. + diff --git a/annotator-guide.md b/annotator-guide.md new file mode 100644 index 0000000..d46ad00 --- /dev/null +++ b/annotator-guide.md @@ -0,0 +1,56 @@ +# Pixano + +A Guide for Annotators +=============== + +This guide will help you to start annotating. + +## Table of Contents +* [Login and first step](#login-and-first-step) +* [Start annotating](#start-annotating) + - [Task definition](#a-task-is-defined-by) + - [Your datasets](#your-datasets) + - [About data format](#about-data-format) + + +----------------------- + +## Login and first step + +Open your browser and hit `http://xxx.xxx.x.xx:3000` if Pixano is on the network or `localhost:3000` on a local server. You should see the login page of the application. + +![pixano-elements](./documentation/images/page-login.png) + +After login in with your annotator account, Pixano's annotator dashboard will be displayed. + +![pixano-elements](./documentation/images/page-annotator-dashboard.png) + +You can now choose in the `Task` drop list the task on which you would like to start working. + +## Start annotating + +Jobs corresponding to the chosen task are coming one at a time, you should see something similar to this: +![pixano-elements](./documentation/images/page-annotate-rectangle.png) + +This view is divided into several zones: + +- the left panel shows icons representing each tool you can use for the present task +- the middle panel shows the current image to annotate. Here are some actions you will be able to do in order to do and facilitate your work: + - you can zoom in or out using the scroll of your mouse + - the keys `m` and `p` can be used to darken or brighten the image + - left clic to use the selected tool + - see all available shortcuts in our [plugins' guide](./plugins-guide.md). +- the right panel shows the annotation attributes linked to your current task: + - choose here the class of the object you want to annotate + - fill the properties/labels if some have been defined. See an example [here](https://pixano.cea.fr/customizable-labels/). +- the upper panel gives you: + - some information on the current job: plugin being used (in this example _rectangle_), current image. + - the ability to revert or redo your last action + - the save button enables you to save your current annotation without submitting it. If you go out of Pixano and come back later, you will find your work untouched + - the _SUBMIT_ button: clic here when you think you did the work for this job. Your annotation will be saved and sent to the server for validation. Your next job will show up immediately. + +![pixano-elements](./documentation/images/page-annotate-rectangle-params.png) + + + + diff --git a/data-test/video/01.png b/data-test/video/01.png new file mode 100644 index 0000000..09fe67a Binary files /dev/null and b/data-test/video/01.png differ diff --git a/data-test/video/02.png b/data-test/video/02.png new file mode 100644 index 0000000..d426cc4 Binary files /dev/null and b/data-test/video/02.png differ diff --git a/data-test/video/03.png b/data-test/video/03.png new file mode 100644 index 0000000..baa8cd8 Binary files /dev/null and b/data-test/video/03.png differ diff --git a/data-test/video/04.png b/data-test/video/04.png new file mode 100644 index 0000000..e2b3898 Binary files /dev/null and b/data-test/video/04.png differ diff --git a/data-test/video/05.png b/data-test/video/05.png new file mode 100644 index 0000000..762d9c5 Binary files /dev/null and b/data-test/video/05.png differ diff --git a/data-test/video/06.png b/data-test/video/06.png new file mode 100644 index 0000000..485985c Binary files /dev/null and b/data-test/video/06.png differ diff --git a/data-test/video/07.png b/data-test/video/07.png new file mode 100644 index 0000000..d90aeae Binary files /dev/null and b/data-test/video/07.png differ diff --git a/data-test/video/08.png b/data-test/video/08.png new file mode 100644 index 0000000..3b1ac51 Binary files /dev/null and b/data-test/video/08.png differ diff --git a/data-test/video/09.png b/data-test/video/09.png new file mode 100644 index 0000000..bd55094 Binary files /dev/null and b/data-test/video/09.png differ diff --git a/data-test/video/10.png b/data-test/video/10.png new file mode 100644 index 0000000..9599a8c Binary files /dev/null and b/data-test/video/10.png differ diff --git a/documentation/Pixano-app_elements_det.png b/documentation/Pixano-app_elements_det.png new file mode 100644 index 0000000..b28dd8b Binary files /dev/null and b/documentation/Pixano-app_elements_det.png differ diff --git a/documentation/gifs/cuboid.gif b/documentation/gifs/cuboid.gif new file mode 100644 index 0000000..bea5930 Binary files /dev/null and b/documentation/gifs/cuboid.gif differ diff --git a/documentation/gifs/smart-rectangle.gif b/documentation/gifs/smart-rectangle.gif new file mode 100644 index 0000000..85dbf12 Binary files /dev/null and b/documentation/gifs/smart-rectangle.gif differ diff --git a/documentation/gifs/smart-segmentation.gif b/documentation/gifs/smart-segmentation.gif new file mode 100644 index 0000000..0e5ea69 Binary files /dev/null and b/documentation/gifs/smart-segmentation.gif differ diff --git a/documentation/images/page-annotate-rectangle-done.png b/documentation/images/page-annotate-rectangle-done.png new file mode 100644 index 0000000..f3a4695 Binary files /dev/null and b/documentation/images/page-annotate-rectangle-done.png differ diff --git a/documentation/images/page-annotate-rectangle-params.png b/documentation/images/page-annotate-rectangle-params.png new file mode 100644 index 0000000..a2dc4bb Binary files /dev/null and b/documentation/images/page-annotate-rectangle-params.png differ diff --git a/documentation/images/page-annotate-rectangle.png b/documentation/images/page-annotate-rectangle.png new file mode 100644 index 0000000..4505b32 Binary files /dev/null and b/documentation/images/page-annotate-rectangle.png differ diff --git a/documentation/images/page-annotator-dashboard.png b/documentation/images/page-annotator-dashboard.png new file mode 100644 index 0000000..7436b2b Binary files /dev/null and b/documentation/images/page-annotator-dashboard.png differ diff --git a/documentation/images/page-dashboard-task1.png b/documentation/images/page-dashboard-task1.png new file mode 100644 index 0000000..3378eaf Binary files /dev/null and b/documentation/images/page-dashboard-task1.png differ diff --git a/documentation/images/page-dashboard.png b/documentation/images/page-dashboard.png new file mode 100644 index 0000000..6c2f79a Binary files /dev/null and b/documentation/images/page-dashboard.png differ diff --git a/frontend/images/login.png b/documentation/images/page-login-admin.png similarity index 100% rename from frontend/images/login.png rename to documentation/images/page-login-admin.png diff --git a/documentation/images/page-login.png b/documentation/images/page-login.png new file mode 100644 index 0000000..08c7ce4 Binary files /dev/null and b/documentation/images/page-login.png differ diff --git a/documentation/images/page-task-manager-rectangle-tag.png b/documentation/images/page-task-manager-rectangle-tag.png new file mode 100644 index 0000000..37d483c Binary files /dev/null and b/documentation/images/page-task-manager-rectangle-tag.png differ diff --git a/documentation/images/page-task-manager-rectangle.png b/documentation/images/page-task-manager-rectangle.png new file mode 100644 index 0000000..d2c905b Binary files /dev/null and b/documentation/images/page-task-manager-rectangle.png differ diff --git a/documentation/images/page-task-manager-smart-segmentation.png b/documentation/images/page-task-manager-smart-segmentation.png new file mode 100644 index 0000000..7bda7f9 Binary files /dev/null and b/documentation/images/page-task-manager-smart-segmentation.png differ diff --git a/documentation/images/page-task-manager.png b/documentation/images/page-task-manager.png new file mode 100644 index 0000000..58d1b63 Binary files /dev/null and b/documentation/images/page-task-manager.png differ diff --git a/documentation/images/page-user-manager.png b/documentation/images/page-user-manager.png new file mode 100644 index 0000000..7c2a1f8 Binary files /dev/null and b/documentation/images/page-user-manager.png differ diff --git a/documentation/pixano.gif b/documentation/pixano.gif new file mode 100644 index 0000000..55d30f0 Binary files /dev/null and b/documentation/pixano.gif differ diff --git a/frontend/images/task-creation.png b/frontend/images/task-creation.png deleted file mode 100644 index 5615074..0000000 Binary files a/frontend/images/task-creation.png and /dev/null differ diff --git a/frontend/package.json b/frontend/package.json index 63664a0..d823233 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "pixano-app-frontend", - "version": "0.4.9", + "version": "0.5.0", "description": "This is a Pixano app.", "scripts": { "copyindex": "shx cp src/index.html ../build", @@ -49,10 +49,10 @@ "@material/mwc-tab-bar": "0.19.1", "@material/mwc-textarea": "0.19.1", "@material/mwc-textfield": "0.19.1", - "@pixano/ai": "0.6.0", - "@pixano/core": "0.6.0", - "@pixano/graphics-2d": "0.6.0", - "@pixano/graphics-3d": "0.6.0", + "@pixano/ai": "0.6.1", + "@pixano/core": "0.6.1", + "@pixano/graphics-2d": "0.6.1", + "@pixano/graphics-3d": "0.6.1", "@trystan2k/fleshy-jsoneditor": "3.0.0", "@webcomponents/webcomponentsjs": "^2.4.0", "babel-loader": "^8.2.3", diff --git a/frontend/src/actions/application.js b/frontend/src/actions/application.js index b8cf3c5..20514ac 100644 --- a/frontend/src/actions/application.js +++ b/frontend/src/actions/application.js @@ -135,6 +135,8 @@ export const fetchRangeResults = (page, pageSize) => (dispatch, getState) => { } return GET(url, dispatch).then((data) => { return Promise.resolve(data); + }).catch((error) => { + return Promise.reject(error); }); } else { return Promise.reject('Program error'); diff --git a/frontend/src/helpers/attribute-picker.js b/frontend/src/helpers/attribute-picker.js index 33193c3..eadeaf2 100644 --- a/frontend/src/helpers/attribute-picker.js +++ b/frontend/src/helpers/attribute-picker.js @@ -11,16 +11,8 @@ import '@material/mwc-checkbox'; import '@material/mwc-formfield'; import '@material/mwc-select'; import '@material/mwc-list/mwc-list-item'; +import { getValue } from '../helpers/utils'; -const default_schema = { - category: [ - {name: 'car', color: "#eca0a0"}, - {name: 'moon', color: "#eca0a0", properties: [ - {name: 'size', type: 'dropdown', enum: ['little', 'big'], default: 'little'} - ]} - ], - default: 'car' -}; // TODO: move to pixano-elements export class AttributePicker extends LitElement { @@ -139,7 +131,7 @@ export class AttributePicker extends LitElement { if (!category && schema.category.length) { category = schema.category[0]; } - if (category.properties) { + if (category && category.properties) { const d = {}; category.properties.forEach((p) => { d[p.name] = p.default; @@ -186,10 +178,10 @@ export class AttributePicker extends LitElement { ]; this.showDetail = false; this.mem = ''; - this.schema = default_schema; - const options = this.getDefaultAttributesForCategory(default_schema, default_schema.default); - this.value = {category: default_schema.default, options }; - this.mem = ''; + this.schema = {}; + this.schema.category = []; + const options = {}; + this.value = {category: '', options }; this.onKeyDown = this.onKeyDown.bind(this); this.onKeyUp = this.onKeyUp.bind(this); } @@ -320,7 +312,6 @@ export class AttributePicker extends LitElement { htmlProp(prop) { if (prop.type === 'dropdown') { // list of attribute choices - return html` { const idx = e.detail.index; @@ -335,30 +326,33 @@ export class AttributePicker extends LitElement { ` } else if (prop.type === 'checkbox') { - const checked = this.value.options[prop.name]; + const checked = JSON.parse(JSON.parse(JSON.stringify(this.value.options[prop.name]).toLowerCase()));// if the initial value was a string like "false" or "False", we want it to be interpreted as a boolean return html` { - if (checked != evt.path[0].checked) { + const path = evt.composedPath(); + const input = path[0]; + if (checked != input.checked) { this.value.options[prop.name] = !checked; - this.value = {...this.value}; + this.value = {...this.value}; this._notifyUpdate(); } } }> ` - } else if (prop.type === 'textfield') { - const checked = this.value.options[prop.name]; - return html` - - { console.log("change textfield"); } + } else if (prop.type === 'textfield') { + const textval = this.value.options[prop.name]; + return html` + { + this.value.options[prop.name] = getValue(evt); + this.value = {...this.value}; + this._notifyUpdate(); + } }> - - ` - } + ` + } return html``; } diff --git a/frontend/src/helpers/data-loader.js b/frontend/src/helpers/data-loader.js deleted file mode 100644 index ffe366f..0000000 --- a/frontend/src/helpers/data-loader.js +++ /dev/null @@ -1,326 +0,0 @@ -/** - * @copyright CEA-LIST/DIASI/SIALV/LVA (2019) - * @author CEA-LIST/DIASI/SIALV/LVA - * @license CECILL-C -*/ - -/** - * Local video cache. - */ -export class VideoCache { - - constructor() { - this.frames = []; - this._numFrames = -1; - this._sourceId = ''; - this._frameIndex = -1; - } - - get sourceId() { - return this._sourceId; - } - - set sourceId(value) { - this._sourceId = value; - } - - get frameIndex() { - return this._frameIndex; - } - - set frameIndex(frameIndex) { - this._frameIndex = frameIndex; - } - - getNextIdxToLoad(start) { - const idx = this.frames.slice(start + 1).findIndex((f) => f.data == null); - if (idx !== -1) { - return start + 1 + idx; - } else { - return -1; - } - } - - /** - * Set the total number of frames. - * @param value total number of frames - */ - setNumFrames(value) { - this._numFrames = value; - } - - /** - * Get the total number of frames. - */ - get numFrames() { - return this._numFrames; - } - - /** - * Get the number of currently loaded frames in the cache. - */ - getNumLoadedFrames() { - return this.frames.filter((f) => f.data != null).length; - } - - getLoadedBetween(a, b) { - return this.frames.slice(a, b).filter((f) => f.data != null).length; - } - - getMaxLoaded() { - const revArray = this.frames.slice().reverse(); - const lastIdx = revArray.findIndex((f) => f.data != null); - return this.frames.length - lastIdx; - } - - /** - * Chech if the frames are completly loaded in the cache. - */ - isFullyLoaded() { - return ((this.numFrames === this.frames.length) && this.frames.length > 0); - } - - isLoadedByTimestamp(timestamp) { - const index = this.frames.findIndex((f) => f.timestamp === timestamp); - return index !== -1 && this.frames[index].data != null; - } - - isLoadedByIndex(idx) { - return this.frames[idx] && this.frames[idx].data != null; - } - - /** - * Get image from the cache by id - * @param id frame id in the cache - */ - getFrameByIndex(idx) { - return (this.frames[idx] && this.frames[idx].data != null) ? this.frames[idx].data : null; - } - - /** - * Get image from the cache by timestamp - * @param id frame id in the cache - */ - getFrameByTimestamp(timestamp) { - const index = this.frames.findIndex((f) => f.timestamp === timestamp); - if (index !== -1) { - return this.frames[index].data; - } - return new Image(); - } - - /** - * Set image from the cache by id - * @param id frame id in the cache - */ - setCacheByTimestamp(frame) { - const index = this.frames.findIndex((f) => f.timestamp === frame.timestamp); - this.frames[index] = frame; - } - - /** - * Get the timestamp for the frame at index. - * @param id frame index - */ - toTimestamp(id) { - return this.frames[id] ? this.frames[id].timestamp : -1; - } - - /** - * Get the timestamp for the frame at index. - * @param id frame index - */ - getFrameIndex(timestamp) { - return this.frames.findIndex((f) => f.timestamp === timestamp); - } - - /** - * Add new frame to the cache. - * @param f frame to add - */ - add(f) { - this.frames.push(f); - } - - /** - * Clear cache. - */ - clear() { - this.frames = []; - this._numFrames = 0; - } - - clearData() { - this.frames.forEach((f) => f.data = null); - } -} - -////// Data loaders - -function readImage(dataUrl) { - return new Promise((resolve) => { - const image = new Image(); - image.onload = () => { - resolve(image); - }; - image.src = dataUrl; - }); -} - -function readPcl(path) { - return new Promise((resolve) => { - fetch(path).then((response) => { - return response.ok ? response.arrayBuffer() : Promise.reject(response.status); - }).then((points) => { - resolve(new Float32Array(points)); - }); - }); -} - -function read(path) { - if (!path) { - console.warn('Path undefined', path); - } - if (path.endsWith('bin')) { - return readPcl(path); - } else if (path.match(/\.(jpeg|jpg|gif|png)$/) != null) { - return readImage(path); - } -} - -function dynamicRead(path) { - if (typeof path === 'string') { - return read(path); - } else if (Array.isArray(path)) { - return Promise.all(path.map(read)); - } -} - -/** - * Handle loading of view(s) - */ -export class Loader extends EventTarget { - - load(path) { - return dynamicRead(path); - } -} - -/** - * Handle loading of a sequence - * of files. - */ -export class SequenceLoader extends EventTarget { - - constructor() { - super(); - this.bufferSize = 2500; // number of frames max - this.loadedFrameNumber = 0; - this.isLoading = false; - this.loadStop = false; - this.cache = null; - this.frames = []; - this._eventAbortCompleted = new Event('cancel_completed'); - } - - /** - * Load metadata - * @param { [] } frames - */ - init(frames) { - // fill cache with empty timestamped images to make sure that - // the timestamps are in order - this.cache = new VideoCache; - for (const source of frames) { - this.cache.add({ timestamp: source.timestamp, data: null }); - } - this.cache.setNumFrames(frames.length); - this.frames = frames; - this.frames.sort((a, b) => { - return a.timestamp - b.timestamp; - }); - return Promise.resolve(frames.length); - } - - read(path) { - return dynamicRead(path); - } - - /** - * Peek frame at index. - * @param {number} idx - */ - peekFrame(idx) { - const requestedFrame = this.cache.getFrameByIndex(idx); - if (requestedFrame == null) { - return this.abortLoading().then(() => { - this.cache.clearData(); - return this.load(idx); - }); - // if frame not loaded, abort current load and start from there - // this.videoLoader.setFrameIndex(frameIndex); - } else { - return Promise.resolve(requestedFrame); - } - } - - /** - * Cancel image requests by emptying their src - */ - abortLoading() { - if (!this.isLoading) { - return Promise.resolve(); - } else { - this.loadStop = true; - const self = this; - return new Promise((resolve, reject) => { - self.addEventListener('cancel_completed', () => { - self.isLoading = false; - resolve(); - }) - }); - } - } - - /** - * Launch load of images. - * Resolve first frame as soon as loaded - * @param idx first frame index to load - */ - load(idx, startBufferIdx) { - startBufferIdx = startBufferIdx || idx; - const self = this; - if (!this.frames[idx]) { - return Promise.resolve(); - } - const timestamp = this.frames[idx] ? this.frames[idx].timestamp : 0; - const path = this.frames[idx].path; - const next = idx + 1; - this.isLoading = true; - const maxi = Math.min(this.frames.length - 1, startBufferIdx + this.bufferSize - 1) - if (this.loadStop) { - this.dispatchEvent(this._eventAbortCompleted) - this.loadStop = false; - this.isLoading = false; - } else { - return new Promise((resolve) => { - return this.read(path).then((data) => { - self.cache.setCacheByTimestamp({ timestamp, data }); - this.dispatchEvent(new CustomEvent('loaded_frame_index', {detail: idx})); - if (idx === startBufferIdx) { - resolve(data); - } - if (next <= maxi){ - self.load(next, startBufferIdx); - } else { - this.isLoading = false; - if (this.loadStop) { - this.dispatchEvent(this._eventAbortCompleted) - this.loadStop = false; - } - } - }); - }); - } - } -} diff --git a/frontend/src/helpers/pop-up.js b/frontend/src/helpers/pop-up.js index 9b93976..1fb917d 100644 --- a/frontend/src/helpers/pop-up.js +++ b/frontend/src/helpers/pop-up.js @@ -27,8 +27,7 @@ export class PopUp extends LitElement { updated(changedProperties) { if (changedProperties.has('message') && this.message) { - console.log('this.message changed', this.message) - this.messageElement.innerHTML = this.message.replace(/\n/g, '
'); + this.messageElement.innerHTML = this.message.toString().replace(/\n/g, '
'); } } diff --git a/frontend/src/plugins/classification.js b/frontend/src/plugins/classification.js new file mode 100644 index 0000000..22af5b1 --- /dev/null +++ b/frontend/src/plugins/classification.js @@ -0,0 +1,54 @@ +/** + * @copyright CEA-LIST/DIASI/SIALV/LVA (2021) + * @author CEA-LIST/DIASI/SIALV/LVA + * @license CECILL-C +*/ + +import { html } from 'lit-element'; +import '@pixano/graphics-2d/lib/pxn-classification'; +import { TemplatePlugin } from '../templates/template-plugin'; +import '../helpers/attribute-picker'; +import { store } from '../store'; +import { setAnnotations } from '../actions/annotations'; + + +export class PluginClassification extends TemplatePlugin { + + /** + * If an ES6 class contains a static getter property but the class itself + * is un-used, it will correctly be removed from the bundle. + */ + static dataType() { + return 'image'; + } + + refresh() { + if (!this.element) return; + if (this.annotations.length===0) this.attributePicker.setAttributes(this.attributePicker.defaultValue);// initialize to default + else { + this.element.annotations = JSON.parse(JSON.stringify(this.annotations)); + this.attributePicker.setAttributes(this.element.annotations[0]); + } + } + + /** + * Invoked on attribute change from + * property panel. + */ + onAttributeChanged() { + const value = this.attributePicker.value; + store.dispatch(setAnnotations({annotations: [value]}));//Save current state to redux database (to keep history) + } + + /** + * Implement property panel content, details always visible + */ + get propertyPanel() { + return html`` + } + + get editor() { + return html``; + } +} +customElements.define('plugin-classification', PluginClassification); diff --git a/frontend/src/plugins/index.js b/frontend/src/plugins/index.js index e96167a..671b534 100644 --- a/frontend/src/plugins/index.js +++ b/frontend/src/plugins/index.js @@ -7,12 +7,14 @@ * @license CECILL-C */ -// import {PluginRectangle} from '../plugins/rectangle'; +import '../helpers/attribute-picker'; + /** * List of all plugin names */ export const pluginsList = [ + 'classification', 'rectangle', 'polygon', 'cuboid', @@ -61,58 +63,71 @@ export const defaultLabelValues = (pluginName) => { switch(pluginName) { case 'sequence-segmentation': case 'smart-segmentation': - case 'segmentation': { - return { - category: [ - {name: 'car', color: "green", idx: 1, instance: true}, - {name: 'person', color: "#eca0a0", idx: 2, instance: true}, - {name: 'road', color: "blue", idx: 3, instance: false} - ], - default: 'person' - } - } - case 'smart-rectangle': { + case 'segmentation': return { - category: [ - {name: 'car', color: "green"}, - {name: 'person', color: "#eca0a0"}, - ], - default: 'person' - } - } + category: [ + { name: 'class1', color: "blue", idx: 1, instance: true, properties: [] }, + { name: 'class2', color: "#eca0a0", idx: 2, instance: false, properties: [] }, + { + name: 'class3', color: "green", idx: 3, instance: true, properties: [ + { name: 'checkbox example', type: 'checkbox', default: false }, + { name: 'dropdown example', type: 'dropdown', enum: ['something', 'something else', 'anything else'], default: 'something' }, + { name: 'textfield example', type: 'textfield', default: 'some text' } + ] + } + ], + default: 'class1' + }; + case 'smart-tracking': - case 'tracking': { + case 'tracking': return { - category: [ - {name: 'car', color: "green", properties: []}, - {name: 'person', color: "#eca0a0", properties: [ - {name: 'posture', type: 'dropdown', enum: ['standing', 'bending', 'sitting', 'lying'], - persistent: false, default: 'standing'} - ]}, - ], - default: 'person' - }; - } + category: [ + { name: 'class1', color: "blue", properties: [] }, + { name: 'class2', color: "#eca0a0", properties: [] }, + { + name: 'class3', color: "green", properties: [ + { name: 'checkbox example', type: 'checkbox', default: false, persistent: true }, + { name: 'dropdown example', type: 'dropdown', enum: ['something', 'something else', 'anything else'], default: 'something', persistent: false }, + { name: 'textfield example', type: 'textfield', default: 'some text' } + ] + } + ], + default: 'class1' + }; + + case 'classification': + return { + category: [ + { + name: 'classification', color: "black", properties: [ + { name: 'checkbox example', type: 'checkbox', default: false }, + { name: 'dropdown example', type: 'dropdown', enum: ['something', 'something else', 'anything else'], default: 'something' }, + { name: 'textfield example', type: 'textfield', default: 'some text' } + ] + } + ], + default: 'classification' + }; + case 'sequence-rectangle': + case 'smart-rectangle': case 'rectangle': - case 'sequence-rectangle': - default: { + default: return { - category: [ - { - name: 'car', color: "green", properties: [ - {name: 'isBlue', type: 'checkbox', default: false} - ] - }, - { - name: 'person', color: "#eca0a0", properties: [ - {name: 'size', type: 'dropdown', enum: ['little', 'big'], default: 'little'} - ] - } - ], - default: 'person' - } - } + category: [ + { name: 'class1', color: "blue", properties: [] }, + { name: 'class2', color: "#eca0a0", properties: [] }, + { + name: 'class3', color: "green", properties: [ + { name: 'checkbox example', type: 'checkbox', default: false }, + { name: 'dropdown example', type: 'dropdown', enum: ['something', 'something else', 'anything else'], default: 'something' }, + { name: 'textfield example', type: 'textfield', default: 'some text' } + ] + } + ], + default: 'class1' + }; } } diff --git a/frontend/src/plugins/segmentation.js b/frontend/src/plugins/segmentation.js index c6b5209..3aec0d1 100644 --- a/frontend/src/plugins/segmentation.js +++ b/frontend/src/plugins/segmentation.js @@ -213,7 +213,6 @@ export class PluginSegmentation extends TemplatePluginInstance { } refresh() {//get back annotation into element - console.log("refresh") if (!this.element) { return; } diff --git a/frontend/src/views/app-dashboard-admin.js b/frontend/src/views/app-dashboard-admin.js index 03cae5a..f9fe7b7 100644 --- a/frontend/src/views/app-dashboard-admin.js +++ b/frontend/src/views/app-dashboard-admin.js @@ -83,19 +83,19 @@ class AppDashboardAdmin extends TemplatePage { onActivate() { this.stateChanged(getState()); - this.refreshGrid(); + if (this.table) this.refreshGrid();//don't refresh if no task has been created for now } /** * Refresh the grid from the database * state. */ - refreshGrid() { + async refreshGrid() { this.table.items.forEach((e) => e.selected = false); this.tableCheckbox.checked = false; this.nbSelectedJobs = 0; this.table.layout(); - this.getResults().then((res) => { + await this.getResults().then((res) => { this.items = res; }); } @@ -109,13 +109,13 @@ class AppDashboardAdmin extends TemplatePage { * @param {String} key * @param {String} value */ - updateFilter(key, value) { + async updateFilter(key, value) { const oldFilters = getState('application').filters if (oldFilters[key] !== value){ const newFilters = {...oldFilters, [key]: value}; store.dispatch(updateFilters(newFilters)); - this.refreshGrid(); - } + await this.refreshGrid(); + } } /** diff --git a/frontend/src/views/app-project-manager.js b/frontend/src/views/app-project-manager.js index e90dde5..0150694 100644 --- a/frontend/src/views/app-project-manager.js +++ b/frontend/src/views/app-project-manager.js @@ -24,6 +24,7 @@ import '@trystan2k/fleshy-jsoneditor/fleshy-jsoneditor.js'; import { defaultLabelValues, pluginsList, getDataType, defaultSettings } from '../plugins/index'; import { + updateTaskName, snapshotProject, exportTasks, importTasks, @@ -162,31 +163,23 @@ class AppProjectManager extends connect(store)(TemplatePage) { }) } - /** - * Fired when task tab changes. - * @param {*} e - */ - onTabChanged(e) { - const idx = e.detail.index; - this.taskIdx = idx; - } + /** + * Fired when task tab changes. + * @param {*} e + */ + onTabChanged(e) { + if (this.taskIdx !== e.detail.index) { + if (this.creatingTask) this.onCancelTask();// if tab change after a "new task" without hitting "create Task" + this.taskIdx = e.detail.index; + store.dispatch(updateTaskName(this.tasks[this.taskIdx].name)); + } + } /** * Create the new configured task. */ onCreateTask() { - let task = {...this.tasks[this.taskIdx]}; - const reg = new RegExp('[^=a-zA-Z0-9-_]+',); - const isWrongContained = reg.test(task.name); - if (!task.name || isWrongContained) { - window.alert("Enter a correct task name"); - return; - } - task.spec.label_schema = this.taskSettings.json; - task.spec.settings = this.pluginSettings.json; - store.dispatch(postTask(task)).then(() => { - this.creatingTask = false; - }); + this.SaveOrCreateTask() } /** @@ -196,24 +189,38 @@ class AppProjectManager extends connect(store)(TemplatePage) { this.tasks.pop(); this.tasks = [...this.tasks]; this.taskIdx = this.tasks.length - 1; + this.creatingTask = false; } /** - * Save currently displayed task. + * Save currently displayed task. (for now : only task details can be changed, the name, dataset and annotation type have to remain the same) */ onSaveTask() { - let task = {...this.tasks[this.taskIdx]}; - if (!task.name) { - window.alert("Enter a task name"); - return; - } - task.spec.label_schema = this.taskSettings.json; - task.spec.settings = this.pluginSettings.json; - store.dispatch(putTask(task)).then(() => { - this.creatingTask = false; - }); + this.SaveOrCreateTask(); } + /** + * Save a modified task or create the new configured task. + */ + SaveOrCreateTask() { + let task = { ...this.tasks[this.taskIdx] }; + const reg = new RegExp('[^=a-zA-Z0-9-_]+',); + const isWrongContained = reg.test(task.name); + if (!task.name || isWrongContained) { + this.errorPopup("Enter a correct task name"); + return; + } + task.spec.label_schema = this.taskSettings.json; + task.spec.settings = this.pluginSettings.json; + const fn = this.creatingTask ? postTask : putTask; + store.dispatch(fn(task)).then(() => { + this.creatingTask = false; + }).catch((error) => { + this.errorPopup(error.message); + this.onCancelTask(); + }); + } + static get styles() { return [super.styles, css` .section { diff --git a/package.json b/package.json index f8ec583..06278f5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pixano-app", - "version": "0.4.9", + "version": "0.5.0", "description": "This is a Pixano app.", "keywords": [], "license": "CECILL-C", @@ -8,6 +8,11 @@ "node": ">=12.0.0", "npm": ">=6.0.0" }, + "scripts": { + "deps": "rm -f package-lock.json && npm i && cd frontend && rm -f package-lock.json && npm i", + "build": "cd frontend && npm run build", + "installLocalElements": "cd frontend && npm run installLocalElements" + }, "bin": "./server/serve.js", "dependencies": { "@material/image-list": "^13.0.0", diff --git a/plugins-guide.md b/plugins-guide.md new file mode 100644 index 0000000..3beaff6 --- /dev/null +++ b/plugins-guide.md @@ -0,0 +1,215 @@ +# Pixano + + + + + +A Guide for Pixano's plugin +=============== + +## List of available plugins +* 2D plugins + * [classification](#classification) + * [keypoints](#keypoints) + * [rectangle](#rectangle) + * [polygon](#polygon) + * [segmentation](#segmentation) +* 3D plugins + * [cuboid](#cuboid) +* [Sequences and tracking](sequences-and-tracking) +* Smart plugins + * [smart-rectangle](#smart-rectangle) + * [smart-segmentation](#smart-segmentation) + * [smart-tracking](#smart-tracking) + + +# 2D plugins +#### Generic buttons and shortcuts (available for all 2D plugins) +| Button | Key Shortcut | Description | +|:------:|:------------:|-------------| +| ![navigation](https://raw.githubusercontent.com/google/material-design-icons/master/png/maps/navigation/materialicons/24dp/1x/baseline_navigation_black_24dp.png) | | `Select/Edit shape or instance` |) | | `Select/Edit shape or instance` | +| ![add_circle_outline](https://raw.githubusercontent.com/google/material-design-icons/master/png/content/add_circle_outline/materialicons/24dp/1x/baseline_add_circle_outline_black_24dp.png) | | `Create a new object` | +| ![tonality](https://raw.githubusercontent.com/google/material-design-icons/master/png/image/tonality/materialicons/24dp/1x/baseline_tonality_black_24dp.png) | | `Hide/Show labels` | +| | `Tab` | `Loop through the scene shapes/instances` | +| | `Escape` | `Unselect shapes or instance` | +| | `Delete` | `Delete selected shapes or instance` | +| | `m` | `Darken image` | +| | `p` | `Brighten image` | +| | `Ctrl+C` | `Copy in clipboard currently selected shapes/instance` | +| | `Ctrl+V` | `Create new shapes/instance (with new ids) from the clipboard content` | +| | `Ctrl+Space` | `Toggle labels (hide / show)` | + + +## Classification +#### When should you use this plugin? +For classification purpose: each image has one or some properties you want to annotate. + + + + + +## Keypoints + +#### When should you use this plugin? +If you need to annotate structured or articulated objects like skeletons, this plugin will enable you to: + +- define the structure of the objects to annotate: + - vertexes: for instance for a skeleton you could have a structure like: head, left hand, right hand, etc. + - edges: the relationship between vertexes, in our skeleton example: the head is linked to the left hand and to he right hand, etc +- annotate as many instances of this object an image can contain. +#### Demo / video + +An interactive demonstration is available for this plugin [here](https://pixano.cea.fr/keypoints/). +#### Specific buttons and shortcuts +| Button | Key Shortcut | Description | +|:------:|:------------:|-------------| +| ![swap_horiz](https://raw.githubusercontent.com/google/material-design-icons/master/png/action/swap_horiz/materialicons/24dp/1x/baseline_swap_horiz_black_24dp.png) | `c` | `Swap nodes` | +| | `h` | `AllVisible` | + + +## Rectangle + +#### When should you use this plugin? +This plugin is used to annotate thanks to classical bounding boxes. +#### Demo / video + +An interactive demonstration is available for this plugin [here](https://pixano.cea.fr/bounding-box/). + + +## Polygon + +#### When should you use this plugin? +If you want to annotate objects by outlining them or by using more edges then a simple rectangle, this plugin enables you to draw polygons of as many edges as needed. If your goal is to draw outlines with pixel wise precision, have also a look at the [segmentation](#segmentation) plugin. +#### Demo / video + +An interactive demonstration is available for this plugin [here](https://pixano.cea.fr/polygon/). +#### Specific buttons and shortcuts +| Button | Key Shortcut | Description | +|:------:|:------------:|-------------| +| ![call_merge](https://raw.githubusercontent.com/google/material-design-icons/master/png/communication/call_merge/materialicons/24dp/1x/baseline_call_merge_black_24dp.png) | | `Group polygons` | +| ![call_split](https://raw.githubusercontent.com/google/material-design-icons/master/png/communication/call_split/materialicons/24dp/1x/baseline_call_split_black_24dp.png) | | `Split polygon` | +| ![timeline](https://raw.githubusercontent.com/google/material-design-icons/master/png/action/timeline/materialicons/24dp/1x/baseline_timeline_black_24dp.png) | | `Polyline/Polygon` | + + +## Segmentation + +#### When should you use this plugin? +Whatever type of segmentation you need: instance, semantic or panoptic segmentation; this plugin enables you to annotate pixel wise masks using different tools: + +- the polygon tool enables you to outline objects in a classical way +- the scalable brush tool enables you to draw on the image with high precision +- you can use the Union/Subtract option to complete any selected object you draw previously +- you can sanitize your masks by filtering it i.e. removing all too small isolated blobs +- different view modes are available: + - you can adapt the opacity of your annotation + - you can switch between the instance (see all instances separately) and semantic (one color per class, no matter if there are more than one instances in the class) view. +#### Demo / video + +An interactive demonstration is available for this plugin [here](https://pixano.cea.fr/pixelwise/). +#### Specific buttons and shortcuts +| Button | Key Shortcut | Description | +|:------:|:------------:|-------------| +| ![brush](https://raw.githubusercontent.com/google/material-design-icons/master/png/image/brush/materialicons/24dp/1x/baseline_brush_black_24dp.png) | | `Add instance (Brush style)` | +| ${union} | `Shift` | `Add to the selected instance` | +| ${subtract} | `Ctrl` | `Remove from the selected instance` | +| ![lock](https://raw.githubusercontent.com/google/material-design-icons/master/png/action/lock/materialicons/24dp/1x/baseline_lock_black_24dp.png) | | `Lock instances on click` | +| ![tonality](https://raw.githubusercontent.com/google/material-design-icons/master/png/image/tonality/materialicons/24dp/1x/baseline_tonality_black_24dp.png) | | `Switch opacity` | +| ![filter_center_focus](https://raw.githubusercontent.com/google/material-design-icons/master/png/image/filter_center_focus/materialicons/24dp/1x/baseline_filter_center_focus_black_24dp.png) | | `Filter isolated` | +| ![face](https://raw.githubusercontent.com/google/material-design-icons/master/png/action/face/materialicons/24dp/1x/baseline_face_black_24dp.png) | | `Switch instance/semantic` | +| ${increase} | `+` | `Brush size increase` | +| ${decrease} | `-` | `Brush size decrease` | + + +# 3D plugins +## Cuboid +![cuboid.gif](documentation/gifs/cuboid.gif) +#### When should you use this plugin? +If your input is a point cloud, this plugin will enable you to draw cuboids around your 3D objects. During your work, you will be able to navigate inside your point cloud, to zoom in or out, and modify your cuboids as many times as needed. +*NB: For now, only the pcl point cloud format is available.* +#### Demo / video + +An interactive demonstration is available for this plugin [here](https://pixano.cea.fr/3d-bounding-box/). + +#### Buttons and shortcuts +| Button | Key Shortcut | Description | +|:------:|:------------:|-------------| +| ![3d_rotation](https://raw.githubusercontent.com/google/material-design-icons/master/png/action/3d_rotation/materialicons/24dp/1x/baseline_3d_rotation_black_24dp.png) | | `rotate` | +| ![swap_horiz](https://raw.githubusercontent.com/google/material-design-icons/master/png/action/swap_horiz/materialicons/24dp/1x/baseline_swap_horiz_black_24dp.png) | | `swap` | +| ${camera} | | `toggleView` | +| | `n` | `Switch to create mode` | +| | `Escape` | `Unselect shapes` | +| | `Delete` | `Delete selected shapes` | +| | `Ctrl+C` | `Copy in clipboard currently selected cuboid` | +| | `Ctrl+V` | `Create new cuboid (with new id) from the clipboard content` | +| | `+` | `Scale up size of points in pointcloud` | +| | `-` | `Scale down size of points in pointcloud` | + + + + +# Sequences and tracking +## Sequences +In Pixano, a sequence is an ordered collection of 2D or 3D images stored in a folder. +*NB: For now, Pixano does not manage video formats. If you want to annotate a video, you should first extract its frames into a folder, and then load this folder as a sequence. Tools like ffmpeg do that very easily.* + +The sequence-* plugins are extensions of 2D and 3D plugins and are available for all of them. + + +--> + + +## Tracking +#### When should you use this plugin? +Annotating image or point cloud sequences frame by frame is extremely time-consuming. Increase productivity by annotating sparingly on key frames. Extrapolate from a key frame or interpolate between two key frames. In Pixano, the tracking plugin is a special case of a sequence plugin enabling interpolation between frames to accelerate your work. + *NB: For now, the tracking plugin is an extension of sequence-rectangle. Extending the other sequence plugins is an ongoing work.* + + +--> + + + + +# Smart plugins + +## Smart-rectangle +![smart-rectangle.gif](documentation/gifs/smart-rectangle.gif) +#### When should you use this plugin? +This plugin is used for the same purpose then the [rectangle](#rectangle) plugin, but offers the possibility of annotating objects in one click: adjust approximately the size of the tool to the size of the objects you want to annotate, and click on each of them. +#### Demo / video + +An interactive demonstration is available for this plugin [here](https://pixano.cea.fr/smart-annotation/). +#### Specific buttons and shortcuts +| Button | Key Shortcut | Description | +|:------:|:------------:|-------------| +| ![flare](https://raw.githubusercontent.com/google/material-design-icons/master/png/image/flare/materialicons/24dp/1x/baseline_flare_black_24dp.png) | | `Smart mode` | +| ![call_split](https://raw.githubusercontent.com/google/material-design-icons/master/png/communication/call_split/materialicons/24dp/1x/baseline_call_split_black_24dp.png) | | `Split polygon` | +| ![timeline](https://raw.githubusercontent.com/google/material-design-icons/master/png/action/timeline/materialicons/24dp/1x/baseline_timeline_black_24dp.png) | | `Polyline/Polygon` | +| ${increase} | `+` | `ROI increase` | +| ${decrease} | `-` | `ROI decrease` | + + +## Smart-segmentation +![smart-segmentation.gif](documentation/gifs/smart-segmentation.gif) +#### When should you use this plugin? +This plugin is used for the same purpose then the [segmentation](#segmentation) plugin, but offers the possibility of annotating objects in one click-and-move: create a bounding box approximately around the object you want to annotate, Pixano will automatically infer a mask of this object. +#### Demo / video + +An interactive demonstration is available for this plugin [here](https://pixano.cea.fr/smart-segmentation/). +#### Specific buttons and shortcuts +| Button | Shortcut | behavior | +|:------:|:------------:|-------------| +| Same | as | [segmentation](#segmentation) | +| ![add_circle_outline](https://raw.githubusercontent.com/google/material-design-icons/master/png/content/add_circle_outline/materialicons/24dp/1x/baseline_add_circle_outline_black_24dp.png) | | `Smart create` | + + +## Smart-tracking + +#### When should you use this plugin? +This plugin is used for the same purpose then the [tracking](#tracking) plugin, but instead of interpolating between key frames, this plugin enables to track a selected object dynamically: create a clean bounding box around the object you want to annotate, press the `t` key, Pixano will automatically infer the bounding box of the next frame. If you selected _Continuous tracking_, Pixano will continue to track one frame after the other until you press `Escape`. + +--> + + + + + diff --git a/server/routes/datasets.js b/server/routes/datasets.js index cf73b38..b517324 100644 --- a/server/routes/datasets.js +++ b/server/routes/datasets.js @@ -201,6 +201,7 @@ const getDataDetails = async (dataset_id, data_id, relative = false) => { * @param {String} workspace */ async function getOrcreateDataset(dataset, workspace) { + dataset.path = path.normalize(dataset.path+'/');//normalize path in order to not duplicate datasets because of a typo error const existingDataset = await getDatasetFromPath(db, dataset.path, dataset.data_type); if (!existingDataset) { const newDataset = { diff --git a/server/routes/tasks.js b/server/routes/tasks.js index a86e799..db6cab4 100644 --- a/server/routes/tasks.js +++ b/server/routes/tasks.js @@ -85,6 +85,7 @@ async function post_tasks(req, res) { * * @apiErrorExample Error-Response: * HTTP/1.1 400 Import error + * HTTP/1.1 401 Unauthorized */ async function import_tasks(req, res) { checkAdmin(req, async () => { @@ -311,7 +312,7 @@ async function export_tasks(req, res) { } /** - * @api {put} /tasks/:task_name Update task details + * @api {put} /tasks/:task_name Update task details (for now : only task details can be changed, the name, dataset and annotation type have to remain the same) * @apiName PutTask * @apiGroup Tasks * @apiPermission admin diff --git a/server/routes/users.js b/server/routes/users.js index 91984d0..c8d94f6 100644 --- a/server/routes/users.js +++ b/server/routes/users.js @@ -254,7 +254,6 @@ async function put_user(req, res) { const user = await db.get(dbkeys.keyForUser(username)); return user.password === password; } catch (err) { - console.log(username, 'does not exist.'); return false; } }