├── LICENSE ├── README.md ├── amt-question-form.html ├── craco.config.js ├── images-readme ├── coco-thumbnail.png ├── system-overall.png └── system-pages.png ├── package.json ├── src ├── App.tsx ├── api │ ├── CocoAPI │ │ ├── createCocoAnnotation.ts │ │ ├── getCocoHit.ts │ │ ├── insertPage.ts │ │ └── searchCocoAnnotation.ts │ └── index.ts ├── components │ ├── Header │ │ ├── Header.style.tsx │ │ ├── Header.tsx │ │ └── index.ts │ ├── Icon │ │ ├── Icon.handler.ts │ │ ├── Icon.style.ts │ │ ├── Icon.tsx │ │ ├── Icon.utils.ts │ │ ├── IconDataTransferType.ts │ │ └── index.ts │ ├── IconBox │ │ ├── IconBox.handler.ts │ │ ├── IconBox.style.ts │ │ ├── IconBox.tsx │ │ ├── IconBox.util.ts │ │ ├── images │ │ │ ├── LeftChevron.png │ │ │ └── RightChevron.png │ │ └── index.ts │ ├── ImageBoard │ │ ├── ImageBoard.handler.ts │ │ ├── ImageBoard.style.ts │ │ ├── ImageBoard.tsx │ │ └── index.ts │ ├── ImageContain │ │ ├── ImageContain.style.ts │ │ ├── ImageContain.tsx │ │ └── index.ts │ ├── MagnifierOnMouse │ │ ├── MagnifierOnMouse.handler.ts │ │ ├── MagnifierOnMouse.style.ts │ │ ├── MagnifierOnMouse.tsx │ │ └── index.ts │ ├── TextOnMouse │ │ ├── TextOnMouse.handler.ts │ │ ├── TextOnMouse.style.ts │ │ ├── TextOnMouse.tsx │ │ └── index.ts │ └── index.ts ├── constants │ ├── index.ts │ └── json │ │ ├── icon-categories.json │ │ ├── icons.json │ │ └── image-list.json ├── hooks │ ├── index.ts │ ├── useBeforeUnload.ts │ └── useIconAction.ts ├── index.css ├── index.tsx ├── models │ ├── CocoAnnotation.ts │ ├── CocoAnnotationPage.ts │ ├── CocoHIT.ts │ ├── Point.ts │ └── index.ts ├── pages │ ├── AnnotatorPage │ │ ├── AnnotatorPage.handler.ts │ │ ├── AnnotatorPage.style.ts │ │ ├── AnnotatorPage.tsx │ │ ├── AnnotatorPage.utils.ts │ │ └── index.ts │ └── index.ts ├── react-app-env.d.ts ├── stores │ ├── AppState │ │ ├── default.ts │ │ ├── index.ts │ │ └── type.ts │ ├── ImageBoardState │ │ ├── default.ts │ │ ├── index.ts │ │ └── type.ts │ ├── TextOnMouseState │ │ ├── default.ts │ │ ├── index.ts │ │ └── type.ts │ ├── UserState │ │ ├── atom.ts │ │ ├── default.ts │ │ ├── index.ts │ │ └── type.ts │ └── index.ts └── utils │ ├── index.ts │ └── throttle.ts ├── tsconfig.json ├── tsconfig.path.json └── yarn.lock /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022-present NAVER Corp. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # COCO Image Labelling Tool - Frontend (FE) ([ICCV'23 Paper](https://arxiv.org/abs/2303.17595)) 2 | 3 | [COCO](https://cocodataset.org) is a computer vision dataset with crowdsourced annotations. 4 | For every object of interest in each image, there is an instance-wise segmentation along with its class label, as well as image-wide description (caption). 5 | As detailed in the [COCO report](https://arxiv.org/abs/1405.0312), the tool has been carefully designed to make the crowdsourced annotation process efficient. 6 | The instance segmentation annotation procedure consists of several steps, including the curation of candidate images, class labelling, instance labelling, and instance segmentation. 7 | In this repository, we focus on the class labelling stage. 8 | The annotator is presented one image at a time and tags the present classes in the image. 9 | This "tagging" paradigm for class labelling is arguably one of the most popular approaches to put semantic labels on images. 10 | 11 | We open source the frontend (FE) modules for COCO class label annotation. Our FE is a reproduction of the original interface. 12 | This replicated annotation system has been used for the [**Neglected Free Lunch**](https://github.com/naver-ai/NeglectedFreeLunch) project, published as an [**ICCV'23 Paper**](https://arxiv.org/abs/2303.17595). 13 | 14 | > Warning: The full annotation system works only when the backend is set up, which we do not support. 15 | > However, the repository contains sufficient information for configuring the BE on your own. 16 | 17 | 18 | ## Example view of the FE interface 19 | 20 | Watch the videos below for an idea of how the interface works. 21 | For each page of the human intelligence task (HIT), the annotator is asked to drag and drop icons for categories that are present in the image. 22 | The worker then clicks the "Submit" button which will load the next page. 23 | 24 | 25 | coco-thumbnail.png 26 | 27 | 28 | (Youtube video demo - click to open) 29 | 30 | 31 | ## What are being recorded? 32 | 33 | For each image, we record the following data structure. The data collected are much richer than the COCO annotations themselves. For example, our FE collects the time series of annotators' interactions with the images on the FE page. It also contains information about the icon location on the image and various timestamps and durations for interacting with the annotation tool. 34 | 35 | ```json 36 | { 37 | "image_id": 459214, 38 | "originalImageHeight": 428, 39 | "originalImageWidth": 640, 40 | "imageHeight": 450, 41 | "imageWidth": 450, 42 | "timeSpent": 22283, 43 | "actionHistories": [ 44 | {"actionType": "add", 45 | "iconType": "pizza", 46 | "pointTo": {"x": 0.5839524517087668, "y": 0.5888888888888889}, 47 | "timeAt": 16686}, 48 | {"actionType": "add", 49 | "iconType": "cup", 50 | "pointTo": {"x": 0.5839524517087668, "y": 0.5888888888888889}, 51 | "timeAt": 16686} 52 | ], 53 | "categoryHistories": [ 54 | {"categoryIndex": 1, 55 | "categoryName": "Animal", 56 | "timeAt": 10815, 57 | "usingKeyboard": false}, 58 | {"categoryIndex": 10, 59 | "categoryName": "IndoorObjects", 60 | "timeAt": 19415, 61 | "usingKeyboard": false} 62 | ], 63 | "mouseTracking": [ 64 | {"timeAt": 15725, 65 | "x": 0.6790490341753344, 66 | "y": 0.8622222222222222}, 67 | {"timeAt": 17426, 68 | "x": 0.7176820208023774, 69 | "y": 0.6422222222222222} 70 | ], 71 | "worker_id": "00AA3B5E80" 72 | } 73 | ``` 74 | - `image_id`: COCO image identifier 75 | - `imageHeight`, `imageWidth`: Number of pixels in the FE page 76 | - `timeSpent`: Number of milliseconds spent on this page 77 | - `actionHistories`: Time series of actions related to positioning the class icons 78 | - `categoryHistories`: Time series of actions related to the superclass bar at the lower part of the page 79 | - `mouseTracking`: Trajectory of mouse cursor over the image region 80 | - `worker_id`: We STRONGLY SUGGEST to anonymise the workers AMT identifiers when utilising them in any form. 81 | 82 | ## Overall architecture 83 | 84 | 85 | 86 | #### Overall architecture for our COCO Annotation tool 87 | 88 | 1. [Amazon Mechanical Turk](https://www.mturk.com/) (AMT) provides the Human Intelligence Task (HIT) identifiers for the current HIT via url (`?hitDatasetName=ABCDEF&cocoHitId=abcdef012345`) 89 | 2. Through [API Gateway](https://aws.amazon.com/api-gateway/), the HIT identifiers are queried (`hitDatasetName` and `cocoHitId`). 90 | 3. The responsible [DynamoDB](https://aws.amazon.com/dynamodb/) (DDB) table returns the necessary information for building the frontend view (image url). 91 | 4. (and 5.) The AMT worker drags and drops the class icons on the corresponding object in the image. 92 | 6. (and 7.) The annotations are sent to the DDB tables (`CocoAnnotation` and `CocoAnnotationPages`). 93 | 94 | 95 | 96 | #### Detailed update schedules for annotations across pages 97 | 98 | - Each HIT consists of `N` pages of image selection tasks. 99 | - Opening the Amplify page triggers the recording of basic information about the entire HIT on the `CocoAnnotation` table. 100 | - Upon clicking on the `Submit` button on each page, the annotation data for the page are sent to the `CocoAnnotationPages` table. 101 | - The `CocoAnnotation` and `CocoAnnotationPages` are associated through the Annotation ID column. 102 | 103 | 104 | ## Building the frontend 105 | 106 | Run 107 | 108 | ```bash 109 | yarn install 110 | yarn start 111 | ``` 112 | 113 | We have hosted the web page with [AWS Amplify](https://aws.amazon.com/amplify/) that has supported a CI/CD with the current repository. 114 | 115 | ## More on the backend 116 | 117 | We do not support BE in this repository. 118 | If you wish to actually build the whole architecture, you will need to configure the BE resources by yourself. 119 | 120 | For your information, below is the list of BE resources we have used for the overall system. 121 | 122 | | Category | AWS Type | Resource Name | Description | 123 | | -------- | ----------- |----------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------| 124 | | Function | [Lambda](https://aws.amazon.com/lambda/) | `CocoAPI` | Functions for reading and writing on the DynamoDB Tables. | 125 | | Api | [API Gateway](https://aws.amazon.com/api-gateway/) | `CocoAPI` | Routing for the `CocoAPI` functions. | 126 | | Storage | [DynamoDB](https://aws.amazon.com/dynamodb/) | `CocoHIT` | DB for MTurk tasks (grouping of images into HITs). | 127 | | Storage | [DynamoDB](https://aws.amazon.com/dynamodb/) | `CocoAnnotation` | DB for annotations per HIT (=`N` pages of annotation tasks). It contains reference to `N` entries in the `CocoAnnotationPage` table. | 128 | | Storage | [DynamoDB](https://aws.amazon.com/dynamodb/) | `CocoAnnotationPage` | DB for annotations per page (single image). | 129 | 130 | Sufficient information for configuring your own BE is given at: 131 | 132 | - The interface for the API access from the FE to DynamoDB is available at [src/api/CocoAPI/*.ts](src/api/CocoAPI). 133 | - The required list of columns and corresponding types for DynamoDB tables are available at [src/models/*.ts](src/models). 134 | 135 | ## Amazon Mechanical Turk (AMT or MTurk) 136 | 137 | The above web page can be integrated into the ["Survey" tasks](https://blog.mturk.com/getting-started-with-surveys-on-mturk-e2eea524c73) supported by AMT. 138 | When workers choose to work on a "Survey" task, they enter a landing page designed by the HIT requesters. 139 | We use the HTML file [amt-question-form.html](amt-question-form.html) as the landing page. 140 | The page contains instructions as well as the url link to the Amplify page described above. 141 | The url is built automatically, given the requester-specified parameters: `toolLink`, `version`, `hitDatasetName`, and `cocoHitId`. 142 | They are defined by the requester in batch through a [CSV database](https://blog.mturk.com/using-csv-files-to-create-multiple-hits-in-the-requester-ui-22a25ec563dc). 143 | 144 | When annotations are completed, we use the [AMT API](https://docs.aws.amazon.com/AWSMechTurk/latest/AWSMturkAPI/Welcome.html) to read and match the workers' task submission status on the AMT server and the annotation data on our DDB tables. 145 | We assess the sanity of submitted work and make accept/reject decisions for the submissions through the [AMT API](https://docs.aws.amazon.com/AWSMechTurk/latest/AWSMturkAPI/Welcome.html). 146 | 147 | ## Acknowledgement 148 | 149 | * [Dante @1000ship](https://github.com/1000ship) is an amazing engineer who did most of the work in this repository. 150 | * [Seong Joon](https://seongjoonoh.com) was asking for more and more features in the meantime.. 151 | * This is the result of great discussions with the great HCI and AI researchers: 152 | * [Dongyoon](https://sites.google.com/site/dyhan0920/) 153 | * [Jean](https://jyskwon.github.io/) 154 | * [Junsuk](https://sites.google.com/site/junsukchoe/) 155 | * [John](https://johnr0.github.io/) 156 | * [Minsuk](https://minsukchang.com/) 157 | * [Sangdoo](https://sangdooyun.github.io/) 158 | * and funding from 159 | * [@Naver-AI](https://github.com/naver-ai) 160 | * [DGIST Intelligence Augmentation Group (DIAG)](https://diag.kr/). 161 | * We also thank the [COCO](https://cocodataset.org) authors, especially [Tsung-Yi Lin](https://tsungyilin.info/), for their great paper and personal communications. 162 | 163 | ## License 164 | ``` 165 | MIT license 166 | 167 | Copyright (c) 2022-present NAVER Corp. 168 | 169 | Permission is hereby granted, free of charge, to any person obtaining a copy 170 | of this software and associated documentation files (the "Software"), to deal 171 | in the Software without restriction, including without limitation the rights 172 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 173 | copies of the Software, and to permit persons to whom the Software is 174 | furnished to do so, subject to the following conditions: 175 | 176 | The above copyright notice and this permission notice shall be included in 177 | all copies or substantial portions of the Software. 178 | 179 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 180 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 181 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 182 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 183 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 184 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 185 | THE SOFTWARE. 186 | ``` 187 | 188 | ## Citing our work 189 | 190 | ``` 191 | @inproceedings{han2023iccv, 192 | title = {Neglected Free Lunch – Learning Image Classifiers Using Annotation Byproducts}, 193 | author = {Han, Dongyoon and Choe, Junsuk and Chun, Seonghyeok and Chung, John Joon Young and Chang, Minsuk and Yun, Sangdoo and Song, Jean Y. and Oh, Seong Joon}, 194 | booktitle = {International Conference on Computer Vision (ICCV)}, 195 | year = {2023} 196 | } 197 | ``` 198 | 199 | -------------------------------------------------------------------------------- /amt-question-form.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | 127 | 128 | 129 | 131 | 132 | 160 | 161 | 162 | 163 | 165 | 167 | 168 | 233 | -------------------------------------------------------------------------------- /craco.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const CracoAlias = require("craco-alias"); 3 | 4 | module.exports = { 5 | plugins: [ 6 | { 7 | plugin: CracoAlias, 8 | options: { 9 | source: "tsconfig", 10 | baseUrl: "./src", 11 | tsConfigPath: "./tsconfig.path.json" 12 | } 13 | } 14 | ] 15 | }; 16 | -------------------------------------------------------------------------------- /images-readme/coco-thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naver-ai/coco-annotation-tool/716fc4fb28fe2a2739c12e2a070a39ba135df272/images-readme/coco-thumbnail.png -------------------------------------------------------------------------------- /images-readme/system-overall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naver-ai/coco-annotation-tool/716fc4fb28fe2a2739c12e2a070a39ba135df272/images-readme/system-overall.png -------------------------------------------------------------------------------- /images-readme/system-pages.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naver-ai/coco-annotation-tool/716fc4fb28fe2a2739c12e2a070a39ba135df272/images-readme/system-pages.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hybrid-supervision-coco", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@aws-amplify/ui-react": "^1.2.23", 7 | "@craco/craco": "^6.3.0", 8 | "@testing-library/jest-dom": "^5.11.4", 9 | "@testing-library/react": "^11.1.0", 10 | "@testing-library/user-event": "^12.1.10", 11 | "aws-amplify": "^4.3.3", 12 | "aws-sdk": "^2.1013.0", 13 | "query-string": "^7.0.1", 14 | "react": "^17.0.2", 15 | "react-dom": "^17.0.2", 16 | "react-scripts": "4.0.3", 17 | "recoil": "^0.4.1", 18 | "styled-components": "^5.3.3", 19 | "styled-reset": "^4.3.4", 20 | "typescript": "^4.4.4", 21 | "web-vitals": "^1.0.1" 22 | }, 23 | "scripts": { 24 | "start": "craco start", 25 | "build": "craco build", 26 | "test": "craco test", 27 | "eject": "react-scripts eject", 28 | "clear": "rm -rf node_modules && rm yarn.lock" 29 | }, 30 | "eslintConfig": { 31 | "extends": [ 32 | "react-app", 33 | "react-app/jest" 34 | ] 35 | }, 36 | "browserslist": { 37 | "production": [ 38 | ">0.2%", 39 | "not dead", 40 | "not op_mini all" 41 | ], 42 | "development": [ 43 | "last 1 chrome version", 44 | "last 1 firefox version", 45 | "last 1 safari version" 46 | ] 47 | }, 48 | "devDependencies": { 49 | "@types/aws-sdk": "^2.7.0", 50 | "@types/node": "^16.11.4", 51 | "@types/query-string": "^6.3.0", 52 | "@types/react": "^17.0.32", 53 | "@types/react-dom": "^17.0.10", 54 | "@types/react-syntax-highlighter": "^13.5.2", 55 | "@types/recoil": "^0.0.9", 56 | "@types/styled-components": "^5.1.15", 57 | "@typescript-eslint/eslint-plugin": "^5.1.0", 58 | "@typescript-eslint/parser": "^5.1.0", 59 | "craco-alias": "^3.0.1", 60 | "eslint": "^7.11.0", 61 | "eslint-config-airbnb": "^18.2.1", 62 | "eslint-config-prettier": "^8.3.0", 63 | "eslint-plugin-import": "^2.25.2", 64 | "eslint-plugin-jsx-a11y": "^6.4.1", 65 | "eslint-plugin-prettier": "^4.0.0", 66 | "eslint-plugin-react": "^7.26.1", 67 | "eslint-plugin-react-hooks": "^4.2.0", 68 | "prettier": "^2.4.1" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | import { Header, MagnifierOnMouse, TextOnMouse } from "@components"; 5 | import { AnnotatorPage } from "@pages"; 6 | import { AppState, UserState } from "@stores"; 7 | import Amplify from "aws-amplify"; 8 | import { useEffect } from "react"; 9 | import { useRecoilValue } from "recoil"; 10 | import awsconfig from "./aws-exports"; // Not available in the public repository, for a good reason :) 11 | 12 | Amplify.configure(awsconfig); 13 | 14 | const App = () => { 15 | const app = useRecoilValue(AppState); 16 | const user = useRecoilValue(UserState); 17 | 18 | useEffect(() => { 19 | fetchUser(); 20 | }, []); 21 | 22 | return ( 23 | <> 24 | 25 | {app.version === "improved" && } 26 | {(user.isAdmin || app.page === "admin") && user.id?.length > 0 && ( 27 |
28 | )} 29 | {app.page === "annotator" ? ( 30 | 31 | ) : null} 32 | 33 | ); 34 | }; 35 | 36 | export default App; 37 | -------------------------------------------------------------------------------- /src/api/CocoAPI/createCocoAnnotation.ts: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | import API from "@aws-amplify/api"; 5 | import { CocoAnnotation, CocoHIT } from "@models"; 6 | 7 | export default async (cocoAnnotation: Partial) => { 8 | const { hitDatasetName, cocoHitID, ...annotation } = cocoAnnotation; 9 | 10 | const hit: CocoHIT = await API.post( 11 | "CocoAPI", 12 | `/api/coco/${hitDatasetName}/hits/${cocoHitID}/annotations`, 13 | { body: { ...annotation } } 14 | ); 15 | return hit; 16 | }; 17 | -------------------------------------------------------------------------------- /src/api/CocoAPI/getCocoHit.ts: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | import API from "@aws-amplify/api"; 5 | import { CocoHIT } from "@models"; 6 | 7 | export default async (hitDatasetName: string, cocoHitID: string) => { 8 | const hit: CocoHIT = await API.get( 9 | "CocoAPI", 10 | `/api/coco/${hitDatasetName}/hits/${cocoHitID}`, 11 | {} 12 | ); 13 | return hit; 14 | }; 15 | -------------------------------------------------------------------------------- /src/api/CocoAPI/insertPage.ts: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | import API from "@aws-amplify/api"; 5 | import { CocoAnnotationPage, CocoHIT } from "@models"; 6 | 7 | interface InsertPageParams { 8 | hitDatasetName: string; 9 | cocoAnnotationId: string; 10 | page: CocoAnnotationPage; 11 | isDone?: boolean; 12 | } 13 | 14 | export default async (params: InsertPageParams) => { 15 | const { hitDatasetName, cocoAnnotationId, page, isDone = false } = params; 16 | 17 | const endedAt = Date.now(); 18 | const hit: CocoHIT = await API.put( 19 | "CocoAPI", 20 | `/api/coco/${hitDatasetName}/annotations/${cocoAnnotationId}`, 21 | { body: { page, endedAt, isDone } } 22 | ); 23 | return hit; 24 | }; 25 | -------------------------------------------------------------------------------- /src/api/CocoAPI/searchCocoAnnotation.ts: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | import API from "@aws-amplify/api"; 5 | import { CocoAnnotation } from "@models"; 6 | 7 | interface CocoAnnotationWithPageCount extends CocoAnnotation { 8 | pageCount: number; 9 | } 10 | 11 | interface SearchCocoAnnotationsRequest { 12 | hitDatasetName: string; 13 | cocoHitID: string; 14 | workerID: string; 15 | } 16 | 17 | export default async (request: SearchCocoAnnotationsRequest) => { 18 | const { hitDatasetName, cocoHitID, workerID } = request; 19 | 20 | const hit: CocoAnnotationWithPageCount[] = await API.get( 21 | "CocoAPI", 22 | `/api/coco/${hitDatasetName}/hits/${cocoHitID}/annotations`, 23 | { 24 | queryStringParameters: { 25 | workerID, 26 | }, 27 | } 28 | ); 29 | return hit; 30 | }; 31 | -------------------------------------------------------------------------------- /src/api/index.ts: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | 5 | import createCocoAnnotation from "./CocoAPI/createCocoAnnotation"; 6 | import getCocoHit from "./CocoAPI/getCocoHit"; 7 | import insertPage from "./CocoAPI/insertPage"; 8 | import searchCocoAnnotation from "./CocoAPI/searchCocoAnnotation"; 9 | 10 | export { getCocoHit, createCocoAnnotation, insertPage, searchCocoAnnotation }; 11 | -------------------------------------------------------------------------------- /src/components/Header/Header.style.tsx: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | import styled from "styled-components"; 5 | 6 | export const HeaderContainer = styled.div` 7 | display: flex; 8 | justify-content: space-between; 9 | align-items: center; 10 | padding: 1rem; 11 | background-color: black; 12 | color: white; 13 | font-size: 18px; 14 | font-weight: bold; 15 | `; 16 | 17 | export const LoadingContainer = styled.div` 18 | display: flex; 19 | align-items: center; 20 | & > * { 21 | margin: 0px 4px; 22 | } 23 | margin: 0px -4px; 24 | `; 25 | 26 | export const LoadingCircle = styled.div` 27 | width: 16px; 28 | height: 16px; 29 | border-radius: 100%; 30 | border: 4px solid gray; 31 | border-top: 4px solid white; 32 | animation: spin 1s linear infinite; 33 | @keyframes spin { 34 | 0% { 35 | transform: rotate(0deg); 36 | } 37 | 100% { 38 | transform: rotate(360deg); 39 | } 40 | } 41 | `; 42 | 43 | export const HeaderGroup = styled.div` 44 | display: flex; 45 | flex-direction: row; 46 | align-items: center; 47 | & > * { 48 | margin: 0px 16px; 49 | } 50 | margin: 0px -16px; 51 | `; 52 | 53 | export const HeaderAnchor = styled.a` 54 | cursor: pointer; 55 | color: white; 56 | text-decoration: underline; 57 | &:hover { 58 | color: #ffc107; 59 | } 60 | `; 61 | 62 | export const HeaderText = styled.div` 63 | color: rgb(200, 200, 200); 64 | `; 65 | -------------------------------------------------------------------------------- /src/components/Header/Header.tsx: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | import { AmplifySignOut } from "@aws-amplify/ui-react"; 5 | import { AppState, UserState } from "@stores"; 6 | import { setQueryString } from "@utils"; 7 | import React from "react"; 8 | import { useRecoilState, useRecoilValue, useResetRecoilState } from "recoil"; 9 | import { HeaderAnchor, HeaderContainer, HeaderGroup } from "./Header.style"; 10 | 11 | const Header = () => { 12 | const [app, setApp] = useRecoilState(AppState); 13 | const user = useRecoilValue(UserState); 14 | const resetUser = useResetRecoilState(UserState); 15 | 16 | return ( 17 | 18 | 19 |
ID : {user.username}
20 |
21 | 22 | {user.isAdmin && ( 23 | <> 24 | 26 | setApp((app) => ({ ...app, debugMode: !app.debugMode })) 27 | } 28 | > 29 | {app.debugMode ? "Disable Debug Mode" : "Enable Debug Mode"} 30 | 31 | {app.page === "admin" ? ( 32 | 34 | setQueryString((qs) => ({ ...qs, page: undefined })) 35 | } 36 | > 37 | Go to Annotation Tool page 38 | 39 | ) : ( 40 | 42 | setQueryString((qs) => ({ ...qs, page: "admin" })) 43 | } 44 | > 45 | Go to Admin page 46 | 47 | )} 48 | 49 | )} 50 | 51 |
52 | ); 53 | }; 54 | 55 | export default Header; 56 | -------------------------------------------------------------------------------- /src/components/Header/index.ts: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | import Header from "./Header"; 5 | 6 | export default Header; 7 | -------------------------------------------------------------------------------- /src/components/Icon/Icon.handler.ts: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | import { IconDataTransferType } from "@components"; 5 | import { DRAG_ICON_OPACITY, DRAG_ICON_SIZE, IconType } from "@constants"; 6 | import useIconAction from "@hooks/useIconAction"; 7 | import { AppState, TextOnMouseState } from "@stores"; 8 | import React, { useCallback } from "react"; 9 | import { useRecoilValue, useSetRecoilState } from "recoil"; 10 | import { capitalize } from "./Icon.utils"; 11 | 12 | interface UseHandlerParams { 13 | iconType: IconType; 14 | } 15 | 16 | const useHandler = (params: UseHandlerParams) => { 17 | const { iconType } = params; 18 | const { deleteIcon } = useIconAction(); 19 | const app = useRecoilValue(AppState); 20 | const setTextOnMouseState = useSetRecoilState(TextOnMouseState); 21 | 22 | const onMouseEnter = useCallback(() => { 23 | setTextOnMouseState((state) => ({ 24 | ...state, 25 | text: capitalize(iconType), 26 | visible: true, 27 | })); 28 | document.body.style.cursor = "crosshair"; 29 | }, []); 30 | 31 | const onMouseLeave = useCallback(() => { 32 | setTextOnMouseState((state) => ({ 33 | ...state, 34 | text: "", 35 | visible: false, 36 | })); 37 | document.body.style.cursor = "unset"; 38 | }, []); 39 | 40 | const onDragStart = useCallback((e: React.DragEvent) => { 41 | e.dataTransfer.effectAllowed = "move"; 42 | 43 | const dragImage = new Image(DRAG_ICON_SIZE, DRAG_ICON_SIZE); 44 | dragImage.src = e.currentTarget.src; 45 | if (app.version === "improved") 46 | dragImage.style.opacity = `${DRAG_ICON_OPACITY}`; 47 | const dragContainer = document.createElement("div"); 48 | dragContainer.appendChild(dragImage); 49 | document.getElementById("backstage")?.appendChild(dragContainer); 50 | e.dataTransfer.setDragImage( 51 | dragContainer, 52 | DRAG_ICON_SIZE / 2, 53 | DRAG_ICON_SIZE / 2 54 | ); 55 | e.dataTransfer.setData( 56 | "application/json", 57 | JSON.stringify({ 58 | iconType, 59 | } as IconDataTransferType) 60 | ); 61 | }, []); 62 | 63 | const onDragEnd = useCallback(() => { 64 | setTextOnMouseState((state) => ({ 65 | ...state, 66 | text: "", 67 | visible: false, 68 | })); 69 | (document.getElementById("backstage") as HTMLDivElement).innerHTML = ""; 70 | }, []); 71 | 72 | const onClick = useCallback(() => { 73 | deleteIcon(iconType); 74 | setTextOnMouseState((state) => ({ 75 | ...state, 76 | text: "", 77 | visible: false, 78 | })); 79 | }, []); 80 | 81 | return { 82 | onMouseEnter, 83 | onMouseLeave, 84 | onDragStart, 85 | onDragEnd, 86 | onClick, 87 | }; 88 | }; 89 | 90 | export default useHandler; 91 | -------------------------------------------------------------------------------- /src/components/Icon/Icon.style.ts: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | import { ICON_SIZE } from "@constants"; 5 | import styled, { css } from "styled-components"; 6 | 7 | export const Container = styled.div` 8 | display: flex; 9 | flex-direction: column; 10 | align-items: center; 11 | justify-content: center; 12 | `; 13 | 14 | interface ImageProps { 15 | attached?: boolean; 16 | x?: number; 17 | y?: number; 18 | disabled?: boolean; 19 | } 20 | 21 | export const Image = styled.img` 22 | width: ${ICON_SIZE}px; 23 | height: ${ICON_SIZE}px; 24 | cursor: pointer; 25 | 26 | ${({ disabled }) => 27 | disabled 28 | ? css` 29 | opacity: 0.3; 30 | ` 31 | : css` 32 | &:hover { 33 | transform: scale(1.1); 34 | } 35 | `} 36 | 37 | ${({ disabled, attached = false, x = 0, y = 0 }) => 38 | attached && 39 | css` 40 | position: absolute; 41 | top: ${y}px; 42 | left: ${x}px; 43 | transform: translate(-50%, -50%) scale(0.5); 44 | 45 | ${!disabled && 46 | css` 47 | &:hover { 48 | transform: translate(-50%, -50%) scale(0.7); 49 | } 50 | `} 51 | `} 52 | `; 53 | 54 | export const Name = styled.span` 55 | font-size: 13px; 56 | font-weight: bold; 57 | color: #000; 58 | margin-top: 5px; 59 | text-align: center; 60 | `; 61 | -------------------------------------------------------------------------------- /src/components/Icon/Icon.tsx: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | import { IconRecord, IconType } from "@constants"; 5 | import React, { FC } from "react"; 6 | import { Container, Image, Name } from "./Icon.style"; 7 | import { capitalize } from "./Icon.utils"; 8 | import useHandler from "./Icon.handler"; 9 | 10 | interface IconProps { 11 | iconType: IconType; 12 | attached?: boolean; 13 | x?: number; 14 | y?: number; 15 | disabled?: boolean; 16 | displayName?: boolean; 17 | } 18 | 19 | const Icon: FC = (props) => { 20 | const { iconType, attached, x, y, disabled, displayName } = props; 21 | const { onMouseEnter, onMouseLeave, onDragStart, onDragEnd, onClick } = 22 | useHandler({ 23 | iconType, 24 | }); 25 | 26 | return ( 27 | 28 | {iconType} 42 | {displayName && {capitalize(iconType)}} 43 | 44 | ); 45 | }; 46 | 47 | export default Icon; 48 | -------------------------------------------------------------------------------- /src/components/Icon/Icon.utils.ts: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | export const capitalize = (str: string) => 5 | str.charAt(0).toUpperCase() + str.slice(1); 6 | -------------------------------------------------------------------------------- /src/components/Icon/IconDataTransferType.ts: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | import { IconType } from "@constants"; 5 | 6 | export interface IconDataTransferType { 7 | iconType: IconType; 8 | } 9 | -------------------------------------------------------------------------------- /src/components/Icon/index.ts: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | import { IconDataTransferType } from "./IconDataTransferType"; 5 | import Icon from "./Icon"; 6 | 7 | export type { IconDataTransferType }; 8 | export default Icon; 9 | -------------------------------------------------------------------------------- /src/components/IconBox/IconBox.handler.ts: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | import { IconCategoryNames, IconCategoryRecord } from "@constants"; 5 | import { ImageBoardState, TextOnMouseState } from "@stores"; 6 | import { useCallback, useMemo } from "react"; 7 | import { useRecoilState, useSetRecoilState } from "recoil"; 8 | 9 | const useHandler = () => { 10 | const [imageBoard, setImageBoard] = useRecoilState(ImageBoardState); 11 | const setTextOnMouse = useSetRecoilState(TextOnMouseState); 12 | 13 | const currentCategoryName = useMemo( 14 | () => IconCategoryNames[imageBoard.categoryIndex], 15 | [imageBoard.categoryIndex] 16 | ); 17 | 18 | const currentCategoryIcons = useMemo( 19 | () => IconCategoryRecord[currentCategoryName], 20 | [currentCategoryName] 21 | ); 22 | 23 | const hasPrevious = useMemo( 24 | () => imageBoard.categoryIndex > 0, 25 | [imageBoard.categoryIndex] 26 | ); 27 | 28 | const prevCategory = useCallback((usingKeyboard = false) => { 29 | if (typeof usingKeyboard !== "boolean") 30 | throw new Error("usingKeyboard must be a boolean"); 31 | setImageBoard((imageBoard) => 32 | imageBoard.categoryIndex > 0 33 | ? { 34 | ...imageBoard, 35 | categoryIndex: imageBoard.categoryIndex - 1, 36 | categoryHistories: [ 37 | ...imageBoard.categoryHistories, 38 | { 39 | categoryIndex: imageBoard.categoryIndex - 1, 40 | categoryName: IconCategoryNames[imageBoard.categoryIndex - 1], 41 | usingKeyboard, 42 | timeAt: Date.now() - imageBoard.startedAt, 43 | }, 44 | ], 45 | } 46 | : imageBoard 47 | ); 48 | setTextOnMouse((textOnMouse) => ({ ...textOnMouse, visible: false })); 49 | }, []); 50 | 51 | const hasNext = useMemo( 52 | () => imageBoard.categoryIndex < IconCategoryNames.length - 1, 53 | [imageBoard.categoryIndex] 54 | ); 55 | 56 | const nextCategory = useCallback((usingKeyboard = false) => { 57 | if (typeof usingKeyboard !== "boolean") 58 | throw new Error("usingKeyboard must be a boolean"); 59 | setImageBoard((imageBoard) => 60 | imageBoard.categoryIndex < IconCategoryNames.length - 1 61 | ? { 62 | ...imageBoard, 63 | categoryIndex: imageBoard.categoryIndex + 1, 64 | categoryHistories: [ 65 | ...imageBoard.categoryHistories, 66 | { 67 | categoryIndex: imageBoard.categoryIndex + 1, 68 | categoryName: IconCategoryNames[imageBoard.categoryIndex + 1], 69 | usingKeyboard, 70 | timeAt: Date.now() - imageBoard.startedAt, 71 | }, 72 | ], 73 | } 74 | : imageBoard 75 | ); 76 | setTextOnMouse((textOnMouse) => ({ ...textOnMouse, visible: false })); 77 | }, []); 78 | 79 | return { 80 | categoryIndex: imageBoard.categoryIndex, 81 | currentCategoryName, 82 | currentCategoryIcons, 83 | hasPrevious, 84 | prevCategory, 85 | hasNext, 86 | nextCategory, 87 | }; 88 | }; 89 | 90 | export default useHandler; 91 | -------------------------------------------------------------------------------- /src/components/IconBox/IconBox.style.ts: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | import { GAP_WITH_EACH_ICON } from "@constants"; 5 | import styled, { css } from "styled-components"; 6 | 7 | export const Container = styled.div` 8 | width: 100%; 9 | background-color: #eee; 10 | 11 | align-self: stretch; 12 | display: flex; 13 | flex-direction: column; 14 | justify-content: center; 15 | gap: 10px; 16 | 17 | & > * { 18 | &:first-child { 19 | margin-top: 16px; 20 | } 21 | margin-left: 16px; 22 | margin-right: 16px; 23 | &:last-child { 24 | margin-bottom: 16px; 25 | } 26 | } 27 | `; 28 | 29 | export const CategoryTitle = styled.div` 30 | max-width: fit-content; 31 | text-align: center; 32 | font-size: 24px; 33 | font-weight: bold; 34 | align-self: center; 35 | `; 36 | 37 | export const Category = styled.div` 38 | display: flex; 39 | flex-direction: row; 40 | justify-content: space-between; 41 | align-items: center; 42 | gap: 24px; 43 | `; 44 | export const CategoryIcons = styled.div` 45 | padding: 4px; 46 | flex-grow: 1; 47 | display: flex; 48 | flex-direction: row; 49 | align-items: flex-start; 50 | gap: ${GAP_WITH_EACH_ICON}px; 51 | `; 52 | 53 | interface ChevronButtonProps { 54 | disabled?: boolean; 55 | } 56 | 57 | export const ChevronButton = styled.button` 58 | background-color: transparent; 59 | border: none; 60 | outline: none; 61 | padding: 0; 62 | margin: 0; 63 | 64 | & > img { 65 | width: calc(179px / 6); 66 | height: calc(324px / 6); 67 | } 68 | 69 | ${({ disabled }) => 70 | disabled 71 | ? css` 72 | cursor: not-allowed; 73 | opacity: 0.1; 74 | ` 75 | : css` 76 | cursor: pointer; 77 | &:hover { 78 | transform: scale(1.1); 79 | } 80 | `} 81 | `; 82 | 83 | interface ProgressBarProps { 84 | progress: number; 85 | } 86 | 87 | export const ProgressBar = styled.div` 88 | position: relative; 89 | align-self: stretch; 90 | height: 24px; 91 | background-color: #fff; 92 | border-radius: 4px; 93 | 94 | display: flex; 95 | justify-content: flex-start; 96 | 97 | & > div { 98 | background-color: #00a8ff; 99 | ${({ progress }) => 100 | css` 101 | width: ${progress * 100}%; 102 | `} 103 | height: 100%; 104 | border-radius: 4px; 105 | transition: 0.3s ease-in-out width; 106 | } 107 | 108 | & > span { 109 | position: absolute; 110 | top: 50%; 111 | left: 50%; 112 | transform: translate(-50%, -50%); 113 | font-size: 12px; 114 | font-weight: bold; 115 | color: #000; 116 | } 117 | `; 118 | -------------------------------------------------------------------------------- /src/components/IconBox/IconBox.tsx: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | import { Icon } from "@components"; 5 | import { IconCategoryNames } from "@constants"; 6 | import { ImageBoardState } from "@stores"; 7 | import { useEffect } from "react"; 8 | import { useRecoilValue } from "recoil"; 9 | import useHandler from "./IconBox.handler"; 10 | import { 11 | Category, 12 | CategoryIcons, 13 | CategoryTitle, 14 | ChevronButton, 15 | Container, 16 | ProgressBar, 17 | } from "./IconBox.style"; 18 | import { camelcaseToAmpersand } from "./IconBox.util"; 19 | import LeftChevron from "./images/LeftChevron.png"; 20 | import RightChevron from "./images/RightChevron.png"; 21 | 22 | const IconBox = () => { 23 | const imageBoard = useRecoilValue(ImageBoardState); 24 | const { 25 | categoryIndex, 26 | currentCategoryName, 27 | currentCategoryIcons, 28 | hasPrevious, 29 | prevCategory, 30 | hasNext, 31 | nextCategory, 32 | } = useHandler(); 33 | const totalIndex = IconCategoryNames.length - 1; 34 | 35 | useEffect(() => { 36 | const keyDownHandler = (e: KeyboardEvent) => { 37 | if (e.keyCode === 37) { 38 | prevCategory(true); 39 | } else if (e.keyCode === 39) { 40 | nextCategory(true); 41 | } 42 | }; 43 | window.addEventListener("keydown", keyDownHandler); 44 | return () => window.removeEventListener("keydown", keyDownHandler); 45 | }, [prevCategory, nextCategory]); 46 | 47 | return ( 48 | 49 | {camelcaseToAmpersand(currentCategoryName)} 50 | 51 | prevCategory()}> 52 | Previous category 53 | 54 | 55 | {currentCategoryIcons.map((iconName) => ( 56 | 62 | ))} 63 | 64 | nextCategory()}> 65 | Next category 66 | 67 | 68 | 69 |
70 | 71 | {categoryIndex + 1} / {totalIndex + 1} 72 | 73 | 74 | 75 | ); 76 | }; 77 | 78 | export default IconBox; 79 | -------------------------------------------------------------------------------- /src/components/IconBox/IconBox.util.ts: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | export const camelcaseToAmpersand = (str: string) => 5 | str.replace(/([a-z])([A-Z])/g, "$1 & $2"); 6 | -------------------------------------------------------------------------------- /src/components/IconBox/images/LeftChevron.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naver-ai/coco-annotation-tool/716fc4fb28fe2a2739c12e2a070a39ba135df272/src/components/IconBox/images/LeftChevron.png -------------------------------------------------------------------------------- /src/components/IconBox/images/RightChevron.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naver-ai/coco-annotation-tool/716fc4fb28fe2a2739c12e2a070a39ba135df272/src/components/IconBox/images/RightChevron.png -------------------------------------------------------------------------------- /src/components/IconBox/index.ts: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | import IconBox from "./IconBox"; 5 | 6 | export default IconBox; 7 | -------------------------------------------------------------------------------- /src/components/ImageBoard/ImageBoard.handler.ts: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | import { IconDataTransferType } from "@components"; 5 | import useIconAction from "@hooks/useIconAction"; 6 | import { AppState, ImageBoardState, TextOnMouseState } from "@stores"; 7 | import { throttle } from "@utils"; 8 | import React, { SyntheticEvent, useCallback, useEffect, useRef } from "react"; 9 | import { useRecoilState, useRecoilValue, useResetRecoilState } from "recoil"; 10 | 11 | const useHandler = () => { 12 | const app = useRecoilValue(AppState); 13 | const [imageBoard, setImageBoard] = useRecoilState(ImageBoardState); 14 | const resetTextOnMouse = useResetRecoilState(TextOnMouseState); 15 | const { putIcon, deleteIcon } = useIconAction(); 16 | const imgRef = useRef(new Image()); 17 | 18 | useEffect(() => { 19 | if (app.imageUrl) imgRef.current.src = app.imageUrl; 20 | setImageBoard((imageBoard) => ({ 21 | ...imageBoard, 22 | imageLoading: true, 23 | })); 24 | imgRef.current.onload = () => { 25 | setImageBoard((imageBoard) => ({ 26 | ...imageBoard, 27 | imageLoading: false, 28 | })); 29 | }; 30 | }, [app.imageUrl]); 31 | 32 | const onDragEnter = (e: React.DragEvent) => { 33 | e.preventDefault(); 34 | e.stopPropagation(); 35 | e.currentTarget.style.border = "2px solid #333"; 36 | setImageBoard((imageBoard) => ({ ...imageBoard, isDragEnter: true })); 37 | }; 38 | 39 | const onDragLeave = (e: React.DragEvent) => { 40 | e.preventDefault(); 41 | e.stopPropagation(); 42 | e.currentTarget.style.border = ""; 43 | setImageBoard((imageBoard) => ({ ...imageBoard, isDragEnter: false })); 44 | }; 45 | 46 | const onDragOver = useCallback( 47 | throttle((e: React.DragEvent) => { 48 | e.preventDefault(); 49 | e.stopPropagation(); 50 | e.dataTransfer.dropEffect = "move"; 51 | 52 | const { offsetX, offsetY } = e.nativeEvent; 53 | const x = offsetX / imageBoard.imageWidth; 54 | const y = offsetY / imageBoard.imageHeight; 55 | setImageBoard((imageBoard) => ({ 56 | ...imageBoard, 57 | mousePoint: { x, y }, 58 | })); 59 | }, 1000 / 60), 60 | [imageBoard.imageWidth, imageBoard.imageHeight] 61 | ); 62 | 63 | const onDrop = (e: React.DragEvent) => { 64 | e.preventDefault(); 65 | e.stopPropagation(); 66 | e.currentTarget.style.border = ""; 67 | const { 68 | nativeEvent: { offsetX, offsetY }, 69 | dataTransfer, 70 | } = e; 71 | const { iconType } = JSON.parse( 72 | dataTransfer.getData("application/json") 73 | ) as IconDataTransferType; 74 | 75 | putIcon(iconType, { 76 | x: offsetX / imageBoard.imageWidth, 77 | y: offsetY / imageBoard.imageHeight, 78 | }); 79 | setImageBoard((imageBoard) => ({ ...imageBoard, isDragEnter: false })); 80 | }; 81 | 82 | const onDragOverToWindow = (e: DragEvent) => { 83 | e.preventDefault(); 84 | e.stopPropagation(); 85 | if (e.dataTransfer) e.dataTransfer.dropEffect = "move"; 86 | }; 87 | 88 | const onDropToWindow = (e: DragEvent) => { 89 | e.preventDefault(); 90 | e.stopPropagation(); 91 | const { dataTransfer } = e; 92 | if (!dataTransfer) return; 93 | const { iconType } = JSON.parse( 94 | dataTransfer.getData("application/json") 95 | ) as IconDataTransferType; 96 | deleteIcon(iconType); 97 | resetTextOnMouse(); 98 | }; 99 | 100 | const onLoad = (e: SyntheticEvent) => { 101 | const borderWidth = 2; 102 | const originalImageWidth = e.currentTarget.naturalWidth; 103 | const originalImageHeight = e.currentTarget.naturalHeight; 104 | const imageHeight = e.currentTarget.offsetHeight - borderWidth * 2; 105 | const imageWidth = e.currentTarget.offsetWidth - borderWidth * 2; 106 | 107 | setImageBoard((imageBoard) => ({ 108 | ...imageBoard, 109 | imageWidth, 110 | imageHeight, 111 | originalImageWidth, 112 | originalImageHeight, 113 | icons: {}, 114 | })); 115 | }; 116 | 117 | const onMouseMove = (e: React.MouseEvent) => { 118 | const { offsetX, offsetY } = e.nativeEvent; 119 | const x = offsetX / imageBoard.imageWidth; 120 | const y = offsetY / imageBoard.imageHeight; 121 | setImageBoard((imageBoard) => ({ 122 | ...imageBoard, 123 | mouseTracking: [ 124 | ...imageBoard.mouseTracking, 125 | { x, y, timeAt: Date.now() - imageBoard.startedAt }, 126 | ], 127 | mousePoint: { x, y }, 128 | })); 129 | }; 130 | 131 | return { 132 | onDragEnter, 133 | onDragLeave, 134 | onDragOver, 135 | onDrop, 136 | onDragOverToWindow, 137 | onDropToWindow, 138 | onLoad, 139 | onMouseMove, 140 | }; 141 | }; 142 | 143 | export default useHandler; 144 | -------------------------------------------------------------------------------- /src/components/ImageBoard/ImageBoard.style.ts: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | import { IMAGE_BOARD_HEIGHT } from "@constants"; 5 | import styled from "styled-components"; 6 | 7 | export const Container = styled.div` 8 | flex-shrink: 1; 9 | position: relative; 10 | min-width: 100px; 11 | max-width: 100%; 12 | height: ${IMAGE_BOARD_HEIGHT}px; 13 | `; 14 | 15 | export const Image = styled.img` 16 | border: 2px solid #ddd; 17 | box-sizing: content-box; 18 | user-select: none; 19 | display: block; 20 | height: 100%; 21 | max-width: 100%; 22 | max-height: ${IMAGE_BOARD_HEIGHT}px; 23 | `; 24 | 25 | export const Loading = styled.div` 26 | width: 24px; 27 | height: 24px; 28 | border-radius: 50%; 29 | border: 2px solid #ddd; 30 | border-top-color: #fff; 31 | animation: spin 1s linear infinite; 32 | justify-self: center; 33 | align-self: center; 34 | 35 | @keyframes spin { 36 | 0% { 37 | transform: rotate(0deg); 38 | } 39 | 100% { 40 | transform: rotate(360deg); 41 | } 42 | } 43 | `; 44 | -------------------------------------------------------------------------------- /src/components/ImageBoard/ImageBoard.tsx: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | import { Icon } from "@components"; 5 | import { IconType } from "@constants"; 6 | import { AppState, ImageBoardState } from "@stores"; 7 | import React, { useEffect } from "react"; 8 | import { useRecoilValue } from "recoil"; 9 | import { Loading, Container, Image } from "./ImageBoard.style"; 10 | import useHandler from "./ImageBoard.handler"; 11 | 12 | const ImageBoard = () => { 13 | const app = useRecoilValue(AppState); 14 | const imageBoard = useRecoilValue(ImageBoardState); 15 | const { 16 | onDragEnter, 17 | onDragLeave, 18 | onDragOver, 19 | onDrop, 20 | onDragOverToWindow, 21 | onDropToWindow, 22 | onLoad, 23 | onMouseMove, 24 | } = useHandler(); 25 | 26 | useEffect(() => { 27 | window.addEventListener("dragover", onDragOverToWindow); 28 | window.addEventListener("drop", onDropToWindow); 29 | return () => { 30 | window.removeEventListener("dragover", onDragOverToWindow); 31 | window.removeEventListener("drop", onDropToWindow); 32 | }; 33 | }, [onDragOverToWindow, onDropToWindow]); 34 | 35 | return ( 36 | 37 | {Object.getOwnPropertyNames(imageBoard.icons).map((iconType) => { 38 | const { x, y } = imageBoard.icons[iconType as IconType] ?? { 39 | x: 0, 40 | y: 0, 41 | }; 42 | return ( 43 | 50 | ); 51 | })} 52 | {imageBoard.imageLoading ? ( 53 | 54 | ) : ( 55 | 65 | )} 66 | 67 | ); 68 | }; 69 | 70 | export default ImageBoard; 71 | -------------------------------------------------------------------------------- /src/components/ImageBoard/index.ts: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | import ImageBoard from "./ImageBoard"; 5 | 6 | export default ImageBoard; 7 | -------------------------------------------------------------------------------- /src/components/ImageContain/ImageContain.style.ts: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | import { GAP_WITH_EACH_ICON, IMAGE_BOARD_HEIGHT } from "@constants"; 5 | import styled from "styled-components"; 6 | 7 | const PADDING = 12; 8 | const ICON_SIZE = 60; 9 | 10 | export const Container = styled.div` 11 | flex-shrink: 0; 12 | align-self: stretch; 13 | background-color: #eee; 14 | overflow-y: auto; 15 | height: ${IMAGE_BOARD_HEIGHT}px; 16 | 17 | & > * { 18 | margin-left: ${PADDING}px; 19 | margin-right: ${PADDING}px; 20 | } 21 | 22 | &:first-child { 23 | margin-top: ${PADDING}px; 24 | } 25 | 26 | &:last-child { 27 | margin-bottom: ${PADDING}px; 28 | } 29 | `; 30 | 31 | export const Title = styled.div` 32 | font-size: 16px; 33 | font-weight: bold; 34 | `; 35 | 36 | export const IconGrid = styled.div` 37 | margin: 8px; 38 | display: flex; 39 | flex-wrap: wrap; 40 | width: ${ICON_SIZE * 2 + GAP_WITH_EACH_ICON}px; 41 | gap: ${GAP_WITH_EACH_ICON}px; 42 | 43 | & > img { 44 | width: ${ICON_SIZE}px; 45 | height: ${ICON_SIZE}px; 46 | } 47 | `; 48 | -------------------------------------------------------------------------------- /src/components/ImageContain/ImageContain.tsx: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | import { IconRecord, IconType } from "@constants"; 5 | import { ImageBoardState } from "@stores"; 6 | import { capitalize } from "@utils"; 7 | import React from "react"; 8 | import { useRecoilValue } from "recoil"; 9 | import { Container, IconGrid, Title } from "./ImageContain.style"; 10 | 11 | const ImageContain = () => { 12 | const imageBoard = useRecoilValue(ImageBoardState); 13 | 14 | return ( 15 | 16 | Image contains 17 | 18 | {Object.getOwnPropertyNames(imageBoard.icons).map((iconType) => { 19 | return ( 20 | {capitalize(iconType)} 25 | ); 26 | })} 27 | 28 | 29 | ); 30 | }; 31 | 32 | export default ImageContain; 33 | -------------------------------------------------------------------------------- /src/components/ImageContain/index.ts: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | import ImageContain from "./ImageContain"; 5 | 6 | export default ImageContain; 7 | -------------------------------------------------------------------------------- /src/components/MagnifierOnMouse/MagnifierOnMouse.handler.ts: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | import { throttle } from "@utils"; 5 | import { useEffect, useState } from "react"; 6 | 7 | const useHandler = () => { 8 | const [position, setPosition] = useState({ x: 0, y: 0 }); 9 | 10 | useEffect(() => { 11 | const mouseMoveEvent = throttle( 12 | (e: MouseEvent) => setPosition({ x: e.clientX, y: e.clientY }), 13 | 1000 / 60, 14 | { trailing: true } 15 | ); 16 | 17 | window.addEventListener("mousemove", mouseMoveEvent); 18 | window.addEventListener("drag", mouseMoveEvent, false); 19 | return () => { 20 | window.removeEventListener("mousemove", mouseMoveEvent); 21 | window.removeEventListener("drag", mouseMoveEvent, false); 22 | }; 23 | }, []); 24 | 25 | return { position }; 26 | }; 27 | 28 | export default useHandler; 29 | -------------------------------------------------------------------------------- /src/components/MagnifierOnMouse/MagnifierOnMouse.style.ts: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | import { MAGNIFIER_RADIUS, MAGNIFIER_SCALE } from "@constants"; 5 | import styled from "styled-components"; 6 | 7 | interface ContainerProps { 8 | x: number; 9 | y: number; 10 | } 11 | 12 | export const Container = styled.div` 13 | position: fixed; 14 | top: ${(props) => props.y}px; 15 | left: ${(props) => props.x}px; 16 | z-index: 9999; 17 | 18 | user-select: none; 19 | pointer-events: none; 20 | 21 | width: ${MAGNIFIER_RADIUS * 2}px; 22 | height: ${MAGNIFIER_RADIUS * 2}px; 23 | background-color: #fff; 24 | border-radius: 50%; 25 | border: 1px solid #ddd; 26 | box-shadow: 0 4px 4px rgba(0, 0, 0, 0.1); 27 | overflow: hidden; 28 | 29 | transform: translate(-50%, calc(-100% - 20px)); 30 | `; 31 | 32 | export const Scaler = styled.div` 33 | position: absolute; 34 | width: 100%; 35 | height: 100%; 36 | overflow: hidden; 37 | transform: scale(${MAGNIFIER_SCALE}); 38 | transform-origin: center; 39 | `; 40 | 41 | export const Aim = styled.div` 42 | position: absolute; 43 | top: 50%; 44 | left: 50%; 45 | width: 4px; 46 | height: 4px; 47 | background-color: #ff0000; 48 | border: 0.5px solid #000; 49 | border-radius: 50%; 50 | transform: translate(-50%, -50%); 51 | `; 52 | -------------------------------------------------------------------------------- /src/components/MagnifierOnMouse/MagnifierOnMouse.tsx: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | import { MAGNIFIER_RADIUS } from "@constants"; 5 | import { AppState, ImageBoardState } from "@stores"; 6 | import React, { FC } from "react"; 7 | import { useRecoilValue } from "recoil"; 8 | import useHandler from "./MagnifierOnMouse.handler"; 9 | import { Aim, Container, Scaler } from "./MagnifierOnMouse.style"; 10 | 11 | const MagnifierOnMouse: FC = () => { 12 | const app = useRecoilValue(AppState); 13 | const imageBoard = useRecoilValue(ImageBoardState); 14 | const { position } = useHandler(); 15 | 16 | if (imageBoard.isDragEnter) 17 | return ( 18 | 19 | 20 | 36 | 37 | 38 | 39 | ); 40 | return <>; 41 | }; 42 | 43 | export default MagnifierOnMouse; 44 | -------------------------------------------------------------------------------- /src/components/MagnifierOnMouse/index.ts: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | import MagnifierOnMouse from "./MagnifierOnMouse"; 5 | 6 | export default MagnifierOnMouse; 7 | -------------------------------------------------------------------------------- /src/components/TextOnMouse/TextOnMouse.handler.ts: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | import { TextOnMouseState } from "@stores"; 5 | import { throttle } from "@utils"; 6 | import { useEffect, useState } from "react"; 7 | import { useRecoilState } from "recoil"; 8 | 9 | const useHandler = () => { 10 | const [textOnMouseState, setTextOnMouseState] = 11 | useRecoilState(TextOnMouseState); 12 | const { visible = false, text = "" } = textOnMouseState; 13 | const [position, setPosition] = useState({ x: 0, y: 0 }); 14 | 15 | useEffect(() => { 16 | const mouseMoveEvent = throttle( 17 | (e: MouseEvent) => { 18 | if (e.x > 0 && e.y > 0) { 19 | setPosition({ x: e.clientX, y: e.clientY }); 20 | } else { 21 | setTextOnMouseState((state) => ({ ...state, visible: false })); 22 | } 23 | }, 24 | 1000 / 60, 25 | { trailing: true } 26 | ); 27 | window.addEventListener("mousemove", mouseMoveEvent); 28 | window.addEventListener("drag", mouseMoveEvent, false); 29 | return () => { 30 | window.removeEventListener("mousemove", mouseMoveEvent); 31 | window.removeEventListener("drag", mouseMoveEvent, false); 32 | }; 33 | }, []); 34 | 35 | return { visible, position, text }; 36 | }; 37 | 38 | export default useHandler; 39 | -------------------------------------------------------------------------------- /src/components/TextOnMouse/TextOnMouse.style.ts: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | import styled from "styled-components"; 5 | 6 | interface ContainerProps { 7 | x: number; 8 | y: number; 9 | } 10 | 11 | export const Container = styled.div` 12 | position: fixed; 13 | top: ${(props) => props.y}px; 14 | left: ${(props) => props.x}px; 15 | z-index: 9999; 16 | 17 | user-select: none; 18 | pointer-events: none; 19 | 20 | padding: 4px; 21 | background-color: rgba(0, 0, 0, 0.6); 22 | color: white; 23 | font-weight: bold; 24 | border-radius: 4px; 25 | 26 | transform: translate(-50%, calc(50% + 4px)); 27 | `; 28 | -------------------------------------------------------------------------------- /src/components/TextOnMouse/TextOnMouse.tsx: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | import React, { FC } from "react"; 5 | import useHandler from "./TextOnMouse.handler"; 6 | import { Container } from "./TextOnMouse.style"; 7 | 8 | const TextOnMouse: FC = () => { 9 | const { visible, position, text } = useHandler(); 10 | 11 | if (visible) 12 | return ( 13 | 14 | {text} 15 | 16 | ); 17 | return <>; 18 | }; 19 | 20 | export default TextOnMouse; 21 | -------------------------------------------------------------------------------- /src/components/TextOnMouse/index.ts: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | import TextOnMouse from "./TextOnMouse"; 5 | 6 | export default TextOnMouse; 7 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | import Header from "./Header"; 5 | import Icon, { IconDataTransferType } from "./Icon"; 6 | import IconBox from "./IconBox"; 7 | import ImageBoard from "./ImageBoard"; 8 | import ImageContain from "./ImageContain"; 9 | import MagnifierOnMouse from "./MagnifierOnMouse"; 10 | import TextOnMouse from "./TextOnMouse"; 11 | 12 | export type { IconDataTransferType }; 13 | export { 14 | Icon, 15 | IconBox, 16 | TextOnMouse, 17 | ImageBoard, 18 | ImageContain, 19 | Header, 20 | MagnifierOnMouse, 21 | }; 22 | -------------------------------------------------------------------------------- /src/constants/index.ts: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | import IconData from "./json/icons.json"; 5 | import IconCategoryData from "./json/icon-categories.json"; 6 | import ImageList from "./json/image-list.json"; 7 | 8 | export type IconType = keyof typeof IconData; 9 | export const IconNames = Object.getOwnPropertyNames(IconData) as IconType[]; 10 | export const IconRecord = IconData as Record; 11 | 12 | export type IconCategoryType = keyof typeof IconCategoryData; 13 | export const IconCategoryNames = Object.getOwnPropertyNames( 14 | IconCategoryData 15 | ) as IconCategoryType[]; 16 | export const IconCategoryRecord = IconCategoryData as Record< 17 | IconCategoryType, 18 | IconType[] 19 | >; 20 | 21 | export const MAGNIFIER_SCALE = 1.5; 22 | export const MAGNIFIER_RADIUS = 80; 23 | export const DRAG_ICON_SIZE = 40; 24 | export const DRAG_ICON_OPACITY = 0.4; 25 | export const ICON_SIZE = 70; 26 | export const GAP_WITH_EACH_ICON = 12; 27 | export const IMAGE_BOARD_HEIGHT = 450; 28 | 29 | export const IMAGE_PATH = 30 | "https://hybridsupervision-coco.s3.us-east-2.amazonaws.com/train2014/"; 31 | export const ImageURLs = ImageList.map( 32 | (file_name) => `${IMAGE_PATH}${file_name}` 33 | ); 34 | 35 | export type AnnotationVersion = "baseline" | "improved"; 36 | -------------------------------------------------------------------------------- /src/constants/json/icon-categories.json: -------------------------------------------------------------------------------- 1 | { 2 | "PersonAccessory": [ 3 | "person", 4 | "backpack", 5 | "umbrella", 6 | "handbag", 7 | "tie", 8 | "suitcase" 9 | ], 10 | "Animal": [ 11 | "bird", 12 | "cat", 13 | "dog", 14 | "horse", 15 | "sheep", 16 | "cow", 17 | "elephant", 18 | "bear", 19 | "zebra", 20 | "giraffe" 21 | ], 22 | "Vehicle": [ 23 | "bicycle", 24 | "car", 25 | "motorcycle", 26 | "airplane", 27 | "bus", 28 | "train", 29 | "truck", 30 | "boat" 31 | ], 32 | "OutdoorObj": [ 33 | "traffic light", 34 | "fire hydrant", 35 | "stop sign", 36 | "parking meter", 37 | "bench" 38 | ], 39 | "Sports": [ 40 | "frisbee", 41 | "skis", 42 | "snowboard", 43 | "sports ball", 44 | "kite", 45 | "baseball bat", 46 | "baseball glove", 47 | "skateboard", 48 | "surfboard", 49 | "tennis racket" 50 | ], 51 | "Kitchenware": [ 52 | "bottle", 53 | "wine glass", 54 | "cup", 55 | "fork", 56 | "knife", 57 | "spoon", 58 | "bowl" 59 | ], 60 | "Food": [ 61 | "banana", 62 | "apple", 63 | "sandwich", 64 | "orange", 65 | "broccoli", 66 | "carrot", 67 | "hot dog", 68 | "pizza", 69 | "donut", 70 | "cake" 71 | ], 72 | "Furniture": [ 73 | "chair", 74 | "couch", 75 | "potted plant", 76 | "bed", 77 | "dining table", 78 | "toilet" 79 | ], 80 | "Appliance": ["microwave", "oven", "toaster", "sink", "refrigerator"], 81 | "Electronics": ["tv", "laptop", "mouse", "remote", "keyboard", "cell phone"], 82 | "IndoorObjects": [ 83 | "book", 84 | "clock", 85 | "vase", 86 | "scissors", 87 | "teddy bear", 88 | "hair drier", 89 | "toothbrush" 90 | ] 91 | } 92 | -------------------------------------------------------------------------------- /src/constants/json/icons.json: -------------------------------------------------------------------------------- 1 | { 2 | "person": "https://cocodataset.org/images/cocoicons/1.jpg", 3 | "backpack": "https://cocodataset.org/images/cocoicons/27.jpg", 4 | "umbrella": "https://cocodataset.org/images/cocoicons/28.jpg", 5 | "handbag": "https://cocodataset.org/images/cocoicons/31.jpg", 6 | "tie": "https://cocodataset.org/images/cocoicons/32.jpg", 7 | "suitcase": "https://cocodataset.org/images/cocoicons/33.jpg", 8 | "bicycle": "https://cocodataset.org/images/cocoicons/2.jpg", 9 | "car": "https://cocodataset.org/images/cocoicons/3.jpg", 10 | "motorcycle": "https://cocodataset.org/images/cocoicons/4.jpg", 11 | "airplane": "https://cocodataset.org/images/cocoicons/5.jpg", 12 | "bus": "https://cocodataset.org/images/cocoicons/6.jpg", 13 | "train": "https://cocodataset.org/images/cocoicons/7.jpg", 14 | "truck": "https://cocodataset.org/images/cocoicons/8.jpg", 15 | "boat": "https://cocodataset.org/images/cocoicons/9.jpg", 16 | "traffic light": "https://cocodataset.org/images/cocoicons/10.jpg", 17 | "fire hydrant": "https://cocodataset.org/images/cocoicons/11.jpg", 18 | "stop sign": "https://cocodataset.org/images/cocoicons/13.jpg", 19 | "parking meter": "https://cocodataset.org/images/cocoicons/14.jpg", 20 | "bench": "https://cocodataset.org/images/cocoicons/15.jpg", 21 | "bird": "https://cocodataset.org/images/cocoicons/16.jpg", 22 | "cat": "https://cocodataset.org/images/cocoicons/17.jpg", 23 | "dog": "https://cocodataset.org/images/cocoicons/18.jpg", 24 | "horse": "https://cocodataset.org/images/cocoicons/19.jpg", 25 | "sheep": "https://cocodataset.org/images/cocoicons/20.jpg", 26 | "cow": "https://cocodataset.org/images/cocoicons/21.jpg", 27 | "elephant": "https://cocodataset.org/images/cocoicons/22.jpg", 28 | "bear": "https://cocodataset.org/images/cocoicons/23.jpg", 29 | "zebra": "https://cocodataset.org/images/cocoicons/24.jpg", 30 | "giraffe": "https://cocodataset.org/images/cocoicons/25.jpg", 31 | "frisbee": "https://cocodataset.org/images/cocoicons/34.jpg", 32 | "skis": "https://cocodataset.org/images/cocoicons/35.jpg", 33 | "snowboard": "https://cocodataset.org/images/cocoicons/36.jpg", 34 | "sports ball": "https://cocodataset.org/images/cocoicons/37.jpg", 35 | "kite": "https://cocodataset.org/images/cocoicons/38.jpg", 36 | "baseball bat": "https://cocodataset.org/images/cocoicons/39.jpg", 37 | "baseball glove": "https://cocodataset.org/images/cocoicons/40.jpg", 38 | "skateboard": "https://cocodataset.org/images/cocoicons/41.jpg", 39 | "surfboard": "https://cocodataset.org/images/cocoicons/42.jpg", 40 | "tennis racket": "https://cocodataset.org/images/cocoicons/43.jpg", 41 | "bottle": "https://cocodataset.org/images/cocoicons/44.jpg", 42 | "wine glass": "https://cocodataset.org/images/cocoicons/46.jpg", 43 | "cup": "https://cocodataset.org/images/cocoicons/47.jpg", 44 | "fork": "https://cocodataset.org/images/cocoicons/48.jpg", 45 | "knife": "https://cocodataset.org/images/cocoicons/49.jpg", 46 | "spoon": "https://cocodataset.org/images/cocoicons/50.jpg", 47 | "bowl": "https://cocodataset.org/images/cocoicons/51.jpg", 48 | "banana": "https://cocodataset.org/images/cocoicons/52.jpg", 49 | "apple": "https://cocodataset.org/images/cocoicons/53.jpg", 50 | "sandwich": "https://cocodataset.org/images/cocoicons/54.jpg", 51 | "orange": "https://cocodataset.org/images/cocoicons/55.jpg", 52 | "broccoli": "https://cocodataset.org/images/cocoicons/56.jpg", 53 | "carrot": "https://cocodataset.org/images/cocoicons/57.jpg", 54 | "hot dog": "https://cocodataset.org/images/cocoicons/58.jpg", 55 | "pizza": "https://cocodataset.org/images/cocoicons/59.jpg", 56 | "donut": "https://cocodataset.org/images/cocoicons/60.jpg", 57 | "cake": "https://cocodataset.org/images/cocoicons/61.jpg", 58 | "chair": "https://cocodataset.org/images/cocoicons/62.jpg", 59 | "couch": "https://cocodataset.org/images/cocoicons/63.jpg", 60 | "potted plant": "https://cocodataset.org/images/cocoicons/64.jpg", 61 | "bed": "https://cocodataset.org/images/cocoicons/65.jpg", 62 | "dining table": "https://cocodataset.org/images/cocoicons/67.jpg", 63 | "toilet": "https://cocodataset.org/images/cocoicons/70.jpg", 64 | "tv": "https://cocodataset.org/images/cocoicons/72.jpg", 65 | "laptop": "https://cocodataset.org/images/cocoicons/73.jpg", 66 | "mouse": "https://cocodataset.org/images/cocoicons/74.jpg", 67 | "remote": "https://cocodataset.org/images/cocoicons/75.jpg", 68 | "keyboard": "https://cocodataset.org/images/cocoicons/76.jpg", 69 | "cell phone": "https://cocodataset.org/images/cocoicons/77.jpg", 70 | "microwave": "https://cocodataset.org/images/cocoicons/78.jpg", 71 | "oven": "https://cocodataset.org/images/cocoicons/79.jpg", 72 | "toaster": "https://cocodataset.org/images/cocoicons/80.jpg", 73 | "sink": "https://cocodataset.org/images/cocoicons/81.jpg", 74 | "refrigerator": "https://cocodataset.org/images/cocoicons/82.jpg", 75 | "book": "https://cocodataset.org/images/cocoicons/84.jpg", 76 | "clock": "https://cocodataset.org/images/cocoicons/85.jpg", 77 | "vase": "https://cocodataset.org/images/cocoicons/86.jpg", 78 | "scissors": "https://cocodataset.org/images/cocoicons/87.jpg", 79 | "teddy bear": "https://cocodataset.org/images/cocoicons/88.jpg", 80 | "hair drier": "https://cocodataset.org/images/cocoicons/89.jpg", 81 | "toothbrush": "https://cocodataset.org/images/cocoicons/90.jpg" 82 | } -------------------------------------------------------------------------------- /src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | import useBeforeUnload from "./useBeforeUnload"; 5 | import useIconAction from "./useIconAction"; 6 | 7 | export { useIconAction, useBeforeUnload }; 8 | -------------------------------------------------------------------------------- /src/hooks/useBeforeUnload.ts: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | import React, { useCallback, useEffect } from "react"; 5 | 6 | export default ( 7 | value?: boolean 8 | ): [boolean, React.Dispatch>] => { 9 | const [beforeUnload, setBeforeUnload] = React.useState(value ?? false); 10 | 11 | const beforeUnloadHandler = useCallback((e: BeforeUnloadEvent) => { 12 | e.preventDefault(); 13 | e.returnValue = 14 | "Now uploading your annotation data to database server. Are you sure leave here?"; 15 | return "Now uploading your annotation data to database server. Are you sure leave here?"; 16 | }, []); 17 | 18 | useEffect(() => { 19 | if (beforeUnload) { 20 | window.addEventListener("beforeunload", beforeUnloadHandler); 21 | } else { 22 | window.removeEventListener("beforeunload", beforeUnloadHandler); 23 | } 24 | }, [beforeUnload]); 25 | 26 | useEffect(() => { 27 | setBeforeUnload(value ?? false); 28 | }, [value]); 29 | 30 | return [beforeUnload, setBeforeUnload]; 31 | }; 32 | -------------------------------------------------------------------------------- /src/hooks/useIconAction.ts: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | import { IconType } from "@constants"; 5 | import { Point } from "@models"; 6 | import { ImageBoardState } from "@stores"; 7 | import { deleteKey } from "@utils"; 8 | import { useSetRecoilState } from "recoil"; 9 | 10 | const useIconAction = () => { 11 | const setImageBoard = useSetRecoilState(ImageBoardState); 12 | 13 | const putIcon = (iconType: IconType, droppedPoint: Point) => { 14 | setImageBoard((imageBoard) => { 15 | const previousPoint = imageBoard.icons[iconType] 16 | ? { 17 | x: imageBoard.icons[iconType]?.x ?? 0, 18 | y: imageBoard.icons[iconType]?.y ?? 0, 19 | } 20 | : undefined; 21 | 22 | return { 23 | ...imageBoard, 24 | icons: { 25 | ...imageBoard.icons, 26 | [iconType]: { 27 | ...imageBoard.icons[iconType], 28 | ...droppedPoint, 29 | }, 30 | }, 31 | actionHistories: [ 32 | ...imageBoard.actionHistories, 33 | { 34 | actionType: previousPoint ? "move" : "add", 35 | pointFrom: previousPoint, 36 | pointTo: droppedPoint, 37 | iconType, 38 | timeAt: new Date().getTime() - imageBoard.startedAt, 39 | }, 40 | ], 41 | }; 42 | }); 43 | }; 44 | 45 | const deleteIcon = (iconType: IconType) => { 46 | setImageBoard((imageBoard) => { 47 | if (!imageBoard.icons[iconType]) return imageBoard; 48 | const previousPoint = { 49 | x: imageBoard.icons[iconType]?.x ?? 0, 50 | y: imageBoard.icons[iconType]?.y ?? 0, 51 | }; 52 | return { 53 | ...imageBoard, 54 | icons: deleteKey(imageBoard.icons, iconType), 55 | actionHistories: [ 56 | ...imageBoard.actionHistories, 57 | { 58 | actionType: "remove", 59 | iconType, 60 | pointFrom: previousPoint, 61 | pointTo: undefined, 62 | timeAt: new Date().getTime() - imageBoard.startedAt, 63 | }, 64 | ], 65 | }; 66 | }); 67 | }; 68 | 69 | return { 70 | putIcon, 71 | deleteIcon, 72 | }; 73 | }; 74 | 75 | export default useIconAction; 76 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | 4 | 5 | body { 6 | margin: 0; 7 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 8 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 9 | sans-serif; 10 | -webkit-font-smoothing: antialiased; 11 | -moz-osx-font-smoothing: grayscale; 12 | } 13 | 14 | code { 15 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 16 | monospace; 17 | } 18 | 19 | #backstage { 20 | position: fixed; 21 | top: 0; 22 | left: 0; 23 | transform: translateY(-100%); 24 | } -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | import React from "react"; 5 | import ReactDOM from "react-dom"; 6 | import { RecoilRoot } from "recoil"; 7 | import App from "./App"; 8 | import "./index.css"; 9 | 10 | ReactDOM.render( 11 | 12 | 13 | 14 | 15 | , 16 | document.getElementById("root") 17 | ); 18 | -------------------------------------------------------------------------------- /src/models/CocoAnnotation.ts: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | import { CocoAnnotationPage } from "@models"; 5 | 6 | export interface CocoAnnotation { 7 | hitDatasetName?: string; 8 | cocoHitID?: string; 9 | id: string; 10 | annotatorID: string; 11 | workerID: string; 12 | hitID: string; 13 | assignmentID: string; 14 | pages?: CocoAnnotationPage[]; 15 | createdAt?: number; 16 | startedAt: number; 17 | isDone?: boolean; 18 | } 19 | -------------------------------------------------------------------------------- /src/models/CocoAnnotationPage.ts: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | import { Point } from "@models"; 5 | import { ActionHistory, CategoryHistory } from "@stores"; 6 | 7 | export interface CocoAnnotationPage { 8 | cocoAnnotationID?: string; 9 | id: string; 10 | pageno: number; 11 | startedAt: number; 12 | createdAt?: number; 13 | endedAt?: number; 14 | annotation: { 15 | imageURL: string; 16 | imageWidth: number; 17 | imageHeight: number; 18 | originalImageWidth: number; 19 | originalImageHeight: number; 20 | mouseTracking: Point[]; 21 | timeSpend: number; 22 | actionHistories: ActionHistory[]; 23 | categoryHistories: CategoryHistory[]; 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/models/CocoHIT.ts: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | export interface CocoHITPage { 5 | pageno: number; 6 | image: { 7 | id: string; 8 | url: string; 9 | }; 10 | } 11 | 12 | export interface CocoHIT { 13 | hitDatasetName: string; 14 | isUnique?: boolean; 15 | id: string; 16 | pages: CocoHITPage[]; 17 | } 18 | -------------------------------------------------------------------------------- /src/models/Point.ts: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | export interface Point { 5 | x: number; 6 | y: number; 7 | } 8 | -------------------------------------------------------------------------------- /src/models/index.ts: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | import { CocoAnnotation } from "./CocoAnnotation"; 5 | import { CocoAnnotationPage } from "./CocoAnnotationPage"; 6 | import { CocoHIT, CocoHITPage } from "./CocoHIT"; 7 | import { Point } from "./Point"; 8 | 9 | export type { Point, CocoAnnotation, CocoAnnotationPage, CocoHIT, CocoHITPage }; 10 | -------------------------------------------------------------------------------- /src/pages/AnnotatorPage/AnnotatorPage.handler.ts: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | import { createCocoAnnotation, getCocoHit, insertPage } from "@api"; 5 | import { IconCategoryNames } from "@constants"; 6 | import { CocoHIT } from "@models"; 7 | import { AppState, ImageBoardState, UserState } from "@stores"; 8 | import { generateSurveyCode } from "@utils"; 9 | import { useRecoilState, useRecoilValue, useResetRecoilState } from "recoil"; 10 | import { 11 | initCocoAnnotation, 12 | initCocoAnnotationPage, 13 | validateWorker, 14 | } from "./AnnotatorPage.utils"; 15 | 16 | const useHandler = () => { 17 | const [app, setApp] = useRecoilState(AppState); 18 | const user = useRecoilValue(UserState); 19 | const [imageBoard, setImageBoard] = useRecoilState(ImageBoardState); 20 | const resetImageBoard = useResetRecoilState(ImageBoardState); 21 | 22 | const fetchHIT = async () => { 23 | if (!app.hitDatasetName || !app.cocoHitId || !app.workerId) return; 24 | setApp((app) => ({ ...app, loading: true })); 25 | try { 26 | const cocoHit = await getCocoHit(app.hitDatasetName, app.cocoHitId); 27 | 28 | const { cocoAnnotationId, initialSubmitCount, isDone, isSameAssignment } = 29 | await validateWorker({ cocoHit, app }); 30 | if (isDone) { 31 | if (isSameAssignment) { 32 | setApp((app) => ({ ...app, surveyCode: generateSurveyCode() })); 33 | } else { 34 | setApp((app) => ({ 35 | ...app, 36 | error: 37 | "You have already completed one of our HITs. We do not accept more than 1 HIT per worker.", 38 | })); 39 | } 40 | } else { 41 | setApp((app) => ({ 42 | ...app, 43 | submitCount: initialSubmitCount, 44 | cocoAnnotationId, 45 | cocoHit, 46 | })); 47 | } 48 | } catch (error: any) { 49 | setApp((app) => ({ 50 | ...app, 51 | error: error?.response?.data?.error ?? error.message, 52 | })); 53 | } finally { 54 | setApp((app) => ({ 55 | ...app, 56 | loading: false, 57 | })); 58 | } 59 | }; 60 | 61 | const submitHandler = async () => { 62 | if ( 63 | !imageBoard.categoryHistories.some( 64 | (each) => each.categoryIndex >= IconCategoryNames.length - 1 65 | ) 66 | ) { 67 | alert( 68 | "You need to traverse through all 11 categories to submit the work." 69 | ); 70 | return; 71 | } 72 | 73 | try { 74 | setApp((app) => ({ ...app, submitting: true })); 75 | 76 | const { isDone, isSameAssignment } = await validateWorker({ 77 | app, 78 | cocoHit: app.cocoHit as CocoHIT, 79 | }); 80 | if (isDone) { 81 | if (isSameAssignment) { 82 | setApp((app) => ({ ...app, surveyCode: generateSurveyCode() })); 83 | } else { 84 | setApp((app) => ({ 85 | ...app, 86 | error: 87 | "You have already completed one of our HITs. We do not accept more than 1 HIT per worker.", 88 | })); 89 | } 90 | } 91 | 92 | if (app.submitCount === 1) { 93 | await createCocoAnnotation(initCocoAnnotation(app, user.username)); 94 | } 95 | await insertPage({ 96 | cocoAnnotationId: app.cocoAnnotationId, 97 | page: initCocoAnnotationPage(app, imageBoard), 98 | hitDatasetName: app.hitDatasetName, 99 | isDone: 100 | !!app.totalSubmitCount && app.submitCount >= app.totalSubmitCount, 101 | }); 102 | 103 | resetImageBoard(); 104 | setImageBoard((imageBoard) => ({ ...imageBoard, startedAt: Date.now() })); 105 | if (app.totalSubmitCount && app.submitCount >= app.totalSubmitCount) { 106 | setApp((app) => ({ ...app, surveyCode: generateSurveyCode() })); 107 | return; 108 | } 109 | setApp((app) => ({ ...app, submitCount: app.submitCount + 1 })); 110 | } catch (error: any) { 111 | alert(error?.response?.data?.error ?? error.message); 112 | } finally { 113 | setApp((app) => ({ ...app, submitting: false })); 114 | } 115 | }; 116 | 117 | return { 118 | app, 119 | imageBoard, 120 | fetchHIT, 121 | submitHandler, 122 | }; 123 | }; 124 | 125 | export default useHandler; 126 | -------------------------------------------------------------------------------- /src/pages/AnnotatorPage/AnnotatorPage.style.ts: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | import { IMAGE_BOARD_HEIGHT } from "@constants"; 5 | import styled from "styled-components"; 6 | 7 | export const Container = styled.div` 8 | display: flex; 9 | flex-direction: column; 10 | justify-content: center; 11 | align-items: center; 12 | 13 | width: 1028px; 14 | margin: 0 auto; 15 | padding: 32px 0px; 16 | `; 17 | 18 | export const Horizontal = styled.div` 19 | margin: 16px 0px; 20 | 21 | display: flex; 22 | flex-direction: row; 23 | justify-content: space-between; 24 | align-items: center; 25 | width: 100%; 26 | height: ${IMAGE_BOARD_HEIGHT}px; 27 | `; 28 | 29 | export const Spacer = styled.div` 30 | flex: 1; 31 | `; 32 | 33 | export const Loading = styled.div` 34 | display: flex; 35 | flex-direction: column; 36 | align-items: center; 37 | text-align: center; 38 | padding: 16px; 39 | font-size: 18px; 40 | max-width: 1024px; 41 | margin: 12px auto; 42 | 43 | & > span { 44 | width: 30px; 45 | height: 30px; 46 | border-radius: 50%; 47 | border: 3px solid #ccc; 48 | border-top-color: #000; 49 | animation: loading 1s linear infinite; 50 | margin-bottom: 16px; 51 | } 52 | 53 | @keyframes loading { 54 | 0% { 55 | transform: rotate(0deg); 56 | } 57 | 100% { 58 | transform: rotate(360deg); 59 | } 60 | } 61 | `; 62 | 63 | export const ErrorMessage = styled.div` 64 | color: red; 65 | margin-top: 12px; 66 | `; 67 | 68 | export const SubmitButton = styled.button` 69 | margin-top: 10px; 70 | background-color: #4caf50; 71 | color: white; 72 | padding: 16px; 73 | border: none; 74 | cursor: pointer; 75 | width: 100%; 76 | font-size: 16px; 77 | font-weight: bold; 78 | &:hover { 79 | background-color: #45a049; 80 | } 81 | &:disabled { 82 | background-color: #ccc; 83 | cursor: not-allowed; 84 | } 85 | `; 86 | 87 | export const Code = styled.code` 88 | width: 100%; 89 | max-height: 400px; 90 | overflow-y: scroll; 91 | background-color: #eaeaea; 92 | `; 93 | 94 | export const OverlayLoading = styled.div` 95 | position: fixed; 96 | top: 0; 97 | left: 0; 98 | width: 100%; 99 | height: 100%; 100 | background-color: rgba(0, 0, 0, 0.5); 101 | display: flex; 102 | justify-content: center; 103 | align-items: center; 104 | 105 | & > div { 106 | width: 100px; 107 | height: 100px; 108 | border-radius: 50%; 109 | border: 10px solid rgba(0, 0, 0, 0.1); 110 | border-top-color: #fff; 111 | animation: overlay-loading 1s linear infinite; 112 | } 113 | 114 | @keyframes overlay-loading { 115 | 0% { 116 | transform: rotate(0deg); 117 | border-width: 10px; 118 | } 119 | 100% { 120 | transform: rotate(360deg); 121 | border-width: 10px; 122 | } 123 | } 124 | `; 125 | -------------------------------------------------------------------------------- /src/pages/AnnotatorPage/AnnotatorPage.tsx: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | import { IconBox, ImageBoard, ImageContain } from "@components"; 5 | import { useBeforeUnload } from "@hooks"; 6 | import { useEffect } from "react"; 7 | import useHandler from "./AnnotatorPage.handler"; 8 | import { 9 | Code, 10 | Container, 11 | Horizontal, 12 | OverlayLoading, 13 | Spacer, 14 | SubmitButton, 15 | } from "./AnnotatorPage.style"; 16 | 17 | const AnnotatorPage = () => { 18 | const { app, imageBoard, fetchHIT, submitHandler } = useHandler(); 19 | useBeforeUnload(true); 20 | 21 | useEffect(() => { 22 | fetchHIT(); 23 | }, []); 24 | 25 | if (app.surveyCode) { 26 | return ( 27 | 28 |

Thank you!

29 |

30 | Your survey code is{" "} 31 | {app.surveyCode} 32 |

33 |

34 | You’ve submitted all annotations. 35 |
36 | Put this survey code in AWS Mturk survey form. 37 |

38 |
39 | ); 40 | } 41 | 42 | if (app.error) { 43 | return ( 44 | 45 |

Error

46 |

{app.error}

47 |
48 | ); 49 | } 50 | 51 | return ( 52 | 53 |

54 | Please drag and drop icons from the bottom panel to matching objects in 55 | the image. If and icon matches multiple objects you can drag the icon 56 | onto any of the objects. There area 11 sets of objects to drag onto the 57 | image. Use the buttons or arrow keys to cycle through them. There are 58 | total of {app.totalSubmitCount} images to label. 59 |

60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | {app.debugMode && ( 70 | <> 71 |

Data Example (For Debugging)

72 | 73 |
{JSON.stringify(imageBoard, null, 2)}
74 |
75 | 76 | )} 77 | 78 | 79 | {app.submitting ? "Submitting..." : "Submit"} 80 | 81 |
82 | {app.submitCount} page(s) / {app.totalSubmitCount} pages 83 |
84 | 85 | {(app.loading || app.submitting) && ( 86 | 87 |
88 | 89 | )} 90 | 91 | ); 92 | }; 93 | 94 | export default AnnotatorPage; 95 | -------------------------------------------------------------------------------- /src/pages/AnnotatorPage/AnnotatorPage.utils.ts: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | import { searchCocoAnnotation } from "@api"; 5 | import { CocoAnnotation, CocoAnnotationPage, CocoHIT } from "@models"; 6 | import { IAppSelector } from "@stores/AppState/type"; 7 | import { IImageBoardState } from "@stores/ImageBoardState/type"; 8 | import { generateUUID } from "@utils"; 9 | 10 | export const initCocoAnnotation = ( 11 | app: IAppSelector, 12 | annotatorID?: string 13 | ): Partial => { 14 | return { 15 | id: app.cocoAnnotationId, 16 | annotatorID: annotatorID ?? app.workerId, 17 | assignmentID: app.assignmentId, 18 | hitID: app.hitId, 19 | workerID: app.workerId, 20 | cocoHitID: app.cocoHit?.id, 21 | hitDatasetName: app.cocoHit?.hitDatasetName, 22 | startedAt: app.startedAt, 23 | }; 24 | }; 25 | 26 | export const initCocoAnnotationPage = ( 27 | app: IAppSelector, 28 | imageBoard: IImageBoardState 29 | ): CocoAnnotationPage => { 30 | return { 31 | id: generateUUID(), 32 | annotation: { 33 | actionHistories: imageBoard.actionHistories, 34 | categoryHistories: imageBoard.categoryHistories, 35 | imageHeight: imageBoard.imageHeight, 36 | imageWidth: imageBoard.imageWidth, 37 | originalImageWidth: imageBoard.originalImageWidth, 38 | originalImageHeight: imageBoard.originalImageHeight, 39 | imageURL: app.imageUrl as string, 40 | mouseTracking: imageBoard.mouseTracking, 41 | timeSpend: Date.now() - imageBoard.startedAt, 42 | }, 43 | pageno: app.cocoHitPage?.pageno as number, 44 | startedAt: imageBoard.startedAt, 45 | }; 46 | }; 47 | 48 | interface ValidateWorkerParams { 49 | app: IAppSelector; 50 | cocoHit: CocoHIT; 51 | } 52 | interface ValidateWorkerResult { 53 | isDone: boolean; 54 | isSameAssignment: boolean; 55 | initialSubmitCount: number; 56 | cocoAnnotationId: string; 57 | } 58 | export const validateWorker = async (params: ValidateWorkerParams) => { 59 | const { app, cocoHit } = params; 60 | const result: ValidateWorkerResult = { 61 | isDone: false, 62 | isSameAssignment: false, 63 | initialSubmitCount: app.submitCount, 64 | cocoAnnotationId: app.cocoAnnotationId, 65 | }; 66 | 67 | const cocoAnnotations = await searchCocoAnnotation({ 68 | hitDatasetName: app.hitDatasetName, 69 | cocoHitID: app.cocoHitId, 70 | workerID: app.workerId, 71 | }); 72 | 73 | // If the worker has already done this hitDatasetName, 74 | const doneOne = cocoAnnotations.find(({ isDone }) => isDone); 75 | if (cocoHit.isUnique && !!doneOne) { 76 | result.isDone = true; 77 | result.isSameAssignment = doneOne.cocoHitID === cocoHit.id; 78 | return result; 79 | } 80 | 81 | // If the worker has already submitted the annotation, 82 | const sameOne = cocoAnnotations.find( 83 | ({ cocoHitID }) => cocoHitID === app.cocoHitId 84 | ); 85 | if (sameOne) { 86 | result.initialSubmitCount = sameOne.pageCount + 1; 87 | result.cocoAnnotationId = sameOne.id; 88 | } 89 | 90 | return result; 91 | }; 92 | -------------------------------------------------------------------------------- /src/pages/AnnotatorPage/index.ts: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | import AnnotatorPage from "./AnnotatorPage"; 5 | 6 | export default AnnotatorPage; 7 | -------------------------------------------------------------------------------- /src/pages/index.ts: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | import AnnotatorPage from "./AnnotatorPage"; 5 | 6 | export { AnnotatorPage }; 7 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | /// 5 | -------------------------------------------------------------------------------- /src/stores/AppState/default.ts: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | import { generateUUID } from "@utils"; 5 | import { parse } from "query-string"; 6 | import { AnnotationVersion } from "@constants"; 7 | import { IAppAtom, Page } from "./type"; 8 | 9 | const { 10 | assignmentId = undefined, 11 | workerId = undefined, 12 | hitId = undefined, 13 | page = "annotator" as Page, 14 | hitDatasetName = undefined, 15 | cocoHitId = undefined, 16 | version = "baseline" as AnnotationVersion, 17 | } = parse(window.location.search); 18 | 19 | let error: string | undefined; 20 | if (typeof hitDatasetName === "undefined") { 21 | error = "hitDatasetName is undefined"; 22 | } else if (typeof cocoHitId === "undefined") { 23 | error = "cocoHitId is undefined"; 24 | } else if (typeof workerId === "undefined") { 25 | error = "workerId is undefined"; 26 | } 27 | 28 | console.log("hitDatasetName:", hitDatasetName); 29 | console.log("cocoHitId:", cocoHitId); 30 | console.log("workerId:", workerId); 31 | 32 | const DefaultAppState: IAppAtom = { 33 | loading: true, 34 | error, 35 | submitCount: 1, 36 | submitting: false, 37 | debugMode: false, 38 | assignmentId: assignmentId as string | undefined, 39 | workerId: workerId as string, 40 | hitId: hitId as string | undefined, 41 | page: page as Page, 42 | hitDatasetName: hitDatasetName as string, 43 | cocoHitId: cocoHitId as string, 44 | cocoHit: undefined, 45 | cocoAnnotationId: generateUUID(), 46 | surveyCode: undefined, 47 | startedAt: Date.now(), 48 | version: version as AnnotationVersion, 49 | }; 50 | 51 | export default DefaultAppState; 52 | -------------------------------------------------------------------------------- /src/stores/AppState/index.ts: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | import { atom, DefaultValue, selector } from "recoil"; 5 | import DefaultAppState from "./default"; 6 | import { IAppAtom, IAppSelector } from "./type"; 7 | 8 | const AppAtom = atom({ 9 | key: "AppAtom", 10 | default: DefaultAppState, 11 | }); 12 | 13 | const AppState = selector({ 14 | key: "AppState", 15 | get: ({ get }) => { 16 | const appState: IAppAtom = get(AppAtom); 17 | const imageUrl = 18 | appState.cocoHit?.pages?.[appState.submitCount - 1]?.image?.url; 19 | const totalSubmitCount = appState.cocoHit?.pages?.length; 20 | const cocoHitPage = appState.cocoHit?.pages?.[appState.submitCount - 1]; 21 | return { 22 | ...appState, 23 | imageUrl, 24 | totalSubmitCount, 25 | cocoHitPage, 26 | }; 27 | }, 28 | set: ({ set }, newState) => { 29 | if (newState instanceof DefaultValue) { 30 | set(AppAtom, newState); 31 | } else { 32 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 33 | const { imageUrl, totalSubmitCount, cocoHitPage, ...atomValue } = 34 | newState; 35 | set(AppAtom, atomValue as IAppAtom); 36 | } 37 | }, 38 | }); 39 | 40 | export default AppState; 41 | -------------------------------------------------------------------------------- /src/stores/AppState/type.ts: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | import { AnnotationVersion } from "@constants"; 5 | import { CocoHIT, CocoHITPage } from "@models"; 6 | 7 | export type Page = "annotator" | "admin"; 8 | 9 | export type IAppAtom = { 10 | loading: boolean; 11 | submitting: boolean; 12 | error?: string; 13 | submitCount: number; 14 | debugMode: boolean; 15 | assignmentId?: string; 16 | workerId: string; 17 | hitId?: string; 18 | page: Page; 19 | hitDatasetName: string; 20 | cocoHitId: string; 21 | cocoHit?: CocoHIT; 22 | cocoAnnotationId: string; 23 | surveyCode?: string; 24 | startedAt: number; 25 | version: AnnotationVersion; 26 | }; 27 | 28 | export interface IAppSelector extends IAppAtom { 29 | imageUrl?: string; 30 | totalSubmitCount?: number; 31 | cocoHitPage?: CocoHITPage; 32 | } 33 | -------------------------------------------------------------------------------- /src/stores/ImageBoardState/default.ts: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | import { IImageBoardState } from "./type"; 5 | 6 | const DefaultImageBoardState: IImageBoardState = { 7 | imageLoading: true, 8 | imageWidth: 0, 9 | imageHeight: 0, 10 | originalImageWidth: 0, 11 | originalImageHeight: 0, 12 | icons: {}, 13 | categoryIndex: 0, 14 | actionHistories: [], 15 | categoryHistories: [], 16 | mouseTracking: [], 17 | startedAt: Date.now(), 18 | 19 | isDragEnter: false, 20 | mousePoint: { x: 0, y: 0 }, 21 | }; 22 | 23 | export default DefaultImageBoardState; 24 | -------------------------------------------------------------------------------- /src/stores/ImageBoardState/index.ts: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | import { atom } from "recoil"; 5 | import DefaultImageBoardState from "./default"; 6 | import { IImageBoardState, ActionHistory, CategoryHistory } from "./type"; 7 | 8 | const ImageBoardState = atom({ 9 | key: "ImageBoardState", 10 | default: DefaultImageBoardState, 11 | }); 12 | 13 | export default ImageBoardState; 14 | export type { ActionHistory, CategoryHistory }; 15 | -------------------------------------------------------------------------------- /src/stores/ImageBoardState/type.ts: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | import { IconType } from "@constants"; 5 | import { Point } from "@models"; 6 | 7 | interface PointWithTime extends Point { 8 | timeAt: number; 9 | } 10 | 11 | type ActionType = "add" | "move" | "remove"; 12 | export interface ActionHistory { 13 | actionType: ActionType; 14 | iconType: IconType; 15 | pointFrom?: Point; 16 | pointTo?: Point; 17 | timeAt: number; 18 | } 19 | 20 | export interface CategoryHistory { 21 | usingKeyboard: boolean; 22 | categoryIndex: number; 23 | categoryName: string; 24 | timeAt: number; 25 | } 26 | 27 | export type IImageBoardState = { 28 | imageLoading: boolean; 29 | imageWidth: number; 30 | imageHeight: number; 31 | originalImageWidth: number; 32 | originalImageHeight: number; 33 | icons: Partial>; 34 | categoryIndex: number; 35 | actionHistories: ActionHistory[]; 36 | categoryHistories: CategoryHistory[]; 37 | mouseTracking: PointWithTime[]; 38 | startedAt: number; 39 | 40 | isDragEnter: boolean; 41 | mousePoint: Point; 42 | }; 43 | -------------------------------------------------------------------------------- /src/stores/TextOnMouseState/default.ts: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | import { ITextOnMouseState } from "./type"; 5 | 6 | const DefaultTextOnMouseState: ITextOnMouseState = { 7 | visible: false, 8 | text: "", 9 | } as ITextOnMouseState; 10 | 11 | export default DefaultTextOnMouseState; 12 | -------------------------------------------------------------------------------- /src/stores/TextOnMouseState/index.ts: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | import { atom } from "recoil"; 5 | import DefaultTextOnMouseState from "./default"; 6 | import { ITextOnMouseState } from "./type"; 7 | 8 | export default atom({ 9 | key: "textOnMouseState", 10 | default: DefaultTextOnMouseState, 11 | }); 12 | -------------------------------------------------------------------------------- /src/stores/TextOnMouseState/type.ts: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | export interface ITextOnMouseState { 5 | visible: boolean; 6 | text: string; 7 | } 8 | -------------------------------------------------------------------------------- /src/stores/UserState/atom.ts: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | import { atom } from "recoil"; 5 | import DefaultUser from "./default"; 6 | import { UserAtomType } from "./type"; 7 | 8 | const UserAtom = atom({ 9 | key: "userAtom", 10 | default: DefaultUser, 11 | }); 12 | 13 | export default UserAtom; 14 | -------------------------------------------------------------------------------- /src/stores/UserState/default.ts: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | import { UserAtomType } from "./type"; 5 | 6 | const DefaultUser: UserAtomType = { 7 | id: "", 8 | username: "", 9 | isAdmin: false, 10 | }; 11 | 12 | export default DefaultUser; 13 | -------------------------------------------------------------------------------- /src/stores/UserState/index.ts: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | import UserAtom from "./atom"; 5 | import { UserAtomType } from "./type"; 6 | 7 | const UserState = UserAtom; 8 | export default UserState; 9 | export type UserStateType = UserAtomType; 10 | -------------------------------------------------------------------------------- /src/stores/UserState/type.ts: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | export interface UserAtomType { 5 | id: string; 6 | username: string; 7 | isAdmin: boolean; 8 | } 9 | -------------------------------------------------------------------------------- /src/stores/index.ts: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | import AppState from "./AppState"; 5 | import ImageBoardState, { 6 | ActionHistory, 7 | CategoryHistory, 8 | } from "./ImageBoardState"; 9 | import TextOnMouseState from "./TextOnMouseState"; 10 | import UserState from "./UserState"; 11 | 12 | export { TextOnMouseState, ImageBoardState, AppState, UserState }; 13 | export type { ActionHistory, CategoryHistory }; 14 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | /* eslint-disable no-bitwise */ 5 | import { parse, stringify } from "query-string"; 6 | import throttle from "./throttle"; 7 | 8 | export { throttle }; 9 | 10 | export const getRandomElement = (elements: T[]): T => { 11 | const random = Math.floor(Math.random() * elements.length); 12 | return elements[random]; 13 | }; 14 | 15 | export const getRandomElements = (elements: T[], count: number): T[] => { 16 | const tmp = [...elements]; 17 | const randomElements: T[] = []; 18 | for (let i = 0; i < count; i += 1) { 19 | if (tmp.length === 0) { 20 | return randomElements; 21 | } 22 | const random = Math.floor(Math.random() * tmp.length); 23 | randomElements.push(tmp[random]); 24 | tmp.splice(random, 1); 25 | } 26 | return randomElements; 27 | }; 28 | 29 | export const setQueryString = (value: (queryString: any) => any) => { 30 | const queryString = parse(window.location.search); 31 | const newQueryString = value(queryString); 32 | window.location.search = `?${stringify(newQueryString)}`; 33 | }; 34 | 35 | export const capitalize = (word: string) => 36 | `${word?.[0]?.toUpperCase()}${word.slice(1)}`; 37 | 38 | export const generateSurveyCode = (): string => { 39 | const customHash = (str: string) => { 40 | let hash = 0; 41 | for (let i = 0; i < str.length; i += 1) { 42 | hash = (hash << 5) - hash + str.charCodeAt(i); 43 | hash &= hash; 44 | } 45 | return (hash + 0xffffff).toString(16).slice(-6).toUpperCase(); 46 | }; 47 | 48 | const { hitId, workerId, assignmentId } = parse( 49 | window.location.search 50 | ) as Record; 51 | 52 | return customHash(`${hitId}-${workerId}-${assignmentId}`); 53 | }; 54 | 55 | export const deleteKey = (from: Record, key: string) => 56 | Object.fromEntries(Object.entries(from).filter((each) => each[0] !== key)); 57 | 58 | export const generateUUID = (): string => { 59 | const s4 = () => 60 | Math.floor((1 + Math.random()) * 0x10000) 61 | .toString(16) 62 | .substring(1); 63 | return `${s4()}${s4()}-${s4()}-${s4()}-${s4()}-${s4()}${s4()}${s4()}`; 64 | }; 65 | -------------------------------------------------------------------------------- /src/utils/throttle.ts: -------------------------------------------------------------------------------- 1 | // COCO Annotation Tool - FE 2 | // Copyright (c) 2022-present NAVER Corp. 3 | // MIT License 4 | interface ThrottleOptions { 5 | trailing?: boolean; 6 | } 7 | 8 | const throttle = ( 9 | func: (...args: any[]) => any, 10 | wait: number, 11 | option?: ThrottleOptions 12 | ) => { 13 | let timeout: NodeJS.Timeout | null = null; 14 | let trailingTimeout: NodeJS.Timeout | null = null; 15 | return (...args: any[]) => { 16 | if (!timeout) { 17 | timeout = setTimeout(() => { 18 | timeout = null; 19 | func.apply(this, args); 20 | }, wait); 21 | } 22 | if (option?.trailing) { 23 | if (trailingTimeout) clearTimeout(trailingTimeout); 24 | trailingTimeout = setTimeout(() => { 25 | trailingTimeout = null; 26 | func.apply(this, args); 27 | }, wait); 28 | } 29 | }; 30 | }; 31 | 32 | export default throttle; 33 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.path.json", 3 | "compilerOptions": { 4 | "target": "es5", 5 | "lib": [ 6 | "dom", 7 | "dom.iterable", 8 | "esnext" 9 | ], 10 | "allowJs": true, 11 | "skipLibCheck": true, 12 | "esModuleInterop": true, 13 | "allowSyntheticDefaultImports": true, 14 | "strict": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "noFallthroughCasesInSwitch": true, 17 | "module": "esnext", 18 | "moduleResolution": "node", 19 | "resolveJsonModule": true, 20 | "isolatedModules": true, 21 | "noEmit": true, 22 | "jsx": "react-jsx" 23 | }, 24 | "include": [ 25 | "src" 26 | ], 27 | "exclude": [ 28 | "amplify" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /tsconfig.path.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./src", 4 | "paths": { 5 | "@pages": [ 6 | "pages" 7 | ], 8 | "@pages/*": [ 9 | "pages/*" 10 | ], 11 | "@api": [ 12 | "api" 13 | ], 14 | "@api/*": [ 15 | "api/*" 16 | ], 17 | "@components": [ 18 | "components" 19 | ], 20 | "@components/*": [ 21 | "components/*" 22 | ], 23 | "@constants": [ 24 | "constants" 25 | ], 26 | "@constants/*": [ 27 | "constants/*" 28 | ], 29 | "@hooks": [ 30 | "hooks" 31 | ], 32 | "@hooks/*": [ 33 | "hooks/*" 34 | ], 35 | "@utils": [ 36 | "utils" 37 | ], 38 | "@utils/*": [ 39 | "utils/*" 40 | ], 41 | "@stores": [ 42 | "stores" 43 | ], 44 | "@stores/*": [ 45 | "stores/*" 46 | ], 47 | "@models": [ 48 | "models" 49 | ], 50 | "@models/*": [ 51 | "models/*" 52 | ] 53 | } 54 | } 55 | } --------------------------------------------------------------------------------