├── .gitignore ├── README.md ├── craco.config.js ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json ├── model │ ├── nms-yolov5.onnx │ └── yolov5n.onnx ├── opencv-4.5.5.js └── robots.txt ├── sample.png ├── src ├── App.js ├── components │ └── loader.js ├── index.js ├── reportWebVitals.js ├── style │ ├── App.css │ ├── index.css │ └── loader.css └── utils │ ├── detect.js │ ├── download.js │ ├── labels.json │ └── renderBox.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | .vscode 4 | 5 | # ort-wasm 6 | /public/static/js/*.wasm 7 | 8 | # dependencies 9 | /node_modules 10 | /.pnp 11 | .pnp.js 12 | 13 | # testing 14 | /test 15 | /coverage 16 | 17 | # production 18 | /build 19 | 20 | # misc 21 | .DS_Store 22 | .env.local 23 | .env.development.local 24 | .env.test.local 25 | .env.production.local 26 | 27 | npm-debug.log* 28 | yarn-debug.log* 29 | yarn-error.log* 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # YOLOv5 on Browser with onnxruntime-web 2 | 3 |

4 | 5 |

6 | 7 | ![love](https://img.shields.io/badge/Made%20with-🖤-white) 8 | ![react](https://img.shields.io/badge/React-blue?logo=react) 9 | ![onnxruntime-web](https://img.shields.io/badge/onnxruntime--web-white?logo=onnx&logoColor=black) 10 | ![opencv.js-4.5.5](https://img.shields.io/badge/opencv.js-4.5.5-green?logo=opencv) 11 | 12 | --- 13 | 14 | Object Detection application right in your browser. 15 | Serving YOLOv5 in browser using onnxruntime-web with `wasm` backend. 16 | 17 | ## Setup 18 | 19 | ```bash 20 | git clone https://github.com/Hyuto/yolov5-onnxruntime-web.git 21 | cd yolov5-onnxruntime-web 22 | yarn install # Install dependencies 23 | ``` 24 | 25 | ## Scripts 26 | 27 | ```bash 28 | yarn start # Start dev server 29 | yarn build # Build for productions 30 | ``` 31 | 32 | ## Model 33 | 34 | YOLOv5n model converted to onnx model. 35 | 36 | ``` 37 | used model : yolov5n 38 | size : 7.6 Mb 39 | ``` 40 | 41 | ## Use another model 42 | 43 | > :warning: **Size Overload** : used YOLOv5 model in this repo is the smallest with size of 7.5 MB, so other models is definitely bigger than this which can cause memory problems on browser. 44 | 45 | Use another YOLOv5 model. 46 | 47 | 1. Clone [yolov5](https://github.com/ultralytics/yolov5) repository 48 | 49 | ```bash 50 | git clone https://github.com/ultralytics/yolov5.git && cd yolov5 51 | ``` 52 | 53 | Install `requirements.txt` first 54 | 55 | ```bash 56 | pip install -r requirements.txt 57 | ``` 58 | 59 | 2. Export model to onnx format 60 | ```bash 61 | export.py --weights yolov5*.pt --include onnx 62 | ``` 63 | 3. Copy `yolov5*.onnx` to `./public/model` 64 | 4. Update `modelName` in `App.jsx` to new model name 65 | ```jsx 66 | ... 67 | // configs 68 | const modelName = "yolov5*.onnx"; // change to new model name 69 | const modelInputShape = [1, 3, 640, 640]; 70 | ... 71 | ``` 72 | 5. Done! 😊 73 | 74 | ## Reference 75 | 76 | - https://github.com/ultralytics/yolov5 77 | - https://github.com/doleron/yolov5-opencv-cpp-python 78 | -------------------------------------------------------------------------------- /craco.config.js: -------------------------------------------------------------------------------- 1 | const CopyPlugin = require("copy-webpack-plugin"); 2 | 3 | module.exports = { 4 | webpack: { 5 | plugins: { 6 | add: [ 7 | new CopyPlugin({ 8 | // Use copy plugin to copy *.wasm to output folder. 9 | patterns: [ 10 | { from: "node_modules/onnxruntime-web/dist/*.wasm", to: "static/js/[name][ext]" }, 11 | ], 12 | }), 13 | ], 14 | }, 15 | configure: (config) => { 16 | // set resolve.fallback for opencv.js 17 | config.resolve.fallback = { 18 | fs: false, 19 | path: false, 20 | crypto: false, 21 | }; 22 | return config; 23 | }, 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yolov5-onnxruntime-web", 3 | "homepage": "https://hyuto.github.io/yolov5-onnxruntime-web/", 4 | "version": "1.0.0", 5 | "dependencies": { 6 | "onnxruntime-web": "^1.14.0", 7 | "react": "^18.1.0", 8 | "react-dom": "^18.1.0", 9 | "react-scripts": "5.0.1", 10 | "web-vitals": "^2.1.0" 11 | }, 12 | "scripts": { 13 | "start": "craco start", 14 | "build": "craco build", 15 | "deploy": "gh-pages -d build" 16 | }, 17 | "eslintConfig": { 18 | "extends": [ 19 | "react-app", 20 | "react-app/jest" 21 | ], 22 | "globals": { 23 | "cv": true 24 | } 25 | }, 26 | "browserslist": { 27 | "production": [ 28 | ">0.2%", 29 | "not dead", 30 | "not op_mini all" 31 | ], 32 | "development": [ 33 | "last 1 chrome version", 34 | "last 1 firefox version", 35 | "last 1 safari version" 36 | ] 37 | }, 38 | "author": { 39 | "name": "Wahyu Setianto", 40 | "url": "https://github.com/Hyuto" 41 | }, 42 | "license": "MIT", 43 | "repository": { 44 | "type": "git", 45 | "url": "git+https://github.com/Hyuto/yolov5-onnxruntime-web.git" 46 | }, 47 | "bugs": { 48 | "url": "https://github.com/Hyuto/yolov5-onnxruntime-web/issues" 49 | }, 50 | "devDependencies": { 51 | "@craco/craco": "^7.1.0", 52 | "copy-webpack-plugin": "^11.0.0", 53 | "gh-pages": "^5.0.0" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hyuto/yolov5-onnxruntime-web/203637cc45962e40a81b2a7e78f98813f93971db/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | YOLOv5-onnxruntime-web App 14 | 15 | 16 | 17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hyuto/yolov5-onnxruntime-web/203637cc45962e40a81b2a7e78f98813f93971db/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hyuto/yolov5-onnxruntime-web/203637cc45962e40a81b2a7e78f98813f93971db/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/model/nms-yolov5.onnx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hyuto/yolov5-onnxruntime-web/203637cc45962e40a81b2a7e78f98813f93971db/public/model/nms-yolov5.onnx -------------------------------------------------------------------------------- /public/model/yolov5n.onnx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hyuto/yolov5-onnxruntime-web/203637cc45962e40a81b2a7e78f98813f93971db/public/model/yolov5n.onnx -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hyuto/yolov5-onnxruntime-web/203637cc45962e40a81b2a7e78f98813f93971db/sample.png -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef } from "react"; 2 | import { Tensor, InferenceSession } from "onnxruntime-web"; 3 | import Loader from "./components/loader"; 4 | import { detectImage } from "./utils/detect"; 5 | import { download } from "./utils/download"; 6 | import "./style/App.css"; 7 | 8 | const App = () => { 9 | const [session, setSession] = useState(null); 10 | const [loading, setLoading] = useState({ text: "Loading OpenCV.js", progress: null }); 11 | const [image, setImage] = useState(null); 12 | const inputImage = useRef(null); 13 | const imageRef = useRef(null); 14 | const canvasRef = useRef(null); 15 | 16 | // configs 17 | const modelName = "yolov5n.onnx"; 18 | const modelInputShape = [1, 3, 640, 640]; 19 | const topk = 100; 20 | const iouThreshold = 0.45; 21 | const confThreshold = 0.2; 22 | const classThreshold = 0.2; 23 | 24 | // wait until opencv.js initialized 25 | cv["onRuntimeInitialized"] = async () => { 26 | const baseModelURL = `${process.env.PUBLIC_URL}/model`; 27 | 28 | // create session 29 | const arrBufNet = await download( 30 | `${baseModelURL}/${modelName}`, // url 31 | ["Loading YOLOv5 model", setLoading] // logger 32 | ); // get model arraybuffer 33 | const yolov5 = await InferenceSession.create(arrBufNet); 34 | const arrBufNMS = await download( 35 | `${baseModelURL}/nms-yolov5.onnx`, // url 36 | ["Loading NMS model", setLoading] // logger 37 | ); // get nms model arraybuffer 38 | const nms = await InferenceSession.create(arrBufNMS); 39 | 40 | // warmup model 41 | setLoading({ text: "Warming up model...", progress: null }); 42 | const tensor = new Tensor( 43 | "float32", 44 | new Float32Array(modelInputShape.reduce((a, b) => a * b)), 45 | modelInputShape 46 | ); 47 | const config = new Tensor("float32", new Float32Array([topk, iouThreshold, confThreshold])); 48 | const { output0 } = await yolov5.run({ images: tensor }); 49 | await nms.run({ detection: output0, config: config }); 50 | 51 | setSession({ net: yolov5, nms: nms }); 52 | setLoading(null); 53 | }; 54 | 55 | return ( 56 |
57 | {loading && ( 58 | 59 | {loading.progress ? `${loading.text} - ${loading.progress}%` : loading.text} 60 | 61 | )} 62 |
63 |

YOLOv5 Object Detection App

64 |

65 | YOLOv5 object detection application live on browser powered by{" "} 66 | onnxruntime-web 67 |

68 |

69 | Serving : {modelName} 70 |

71 |
72 | 73 |
74 | { 80 | detectImage( 81 | imageRef.current, 82 | canvasRef.current, 83 | session, 84 | topk, 85 | iouThreshold, 86 | confThreshold, 87 | classThreshold, 88 | modelInputShape 89 | ); 90 | }} 91 | /> 92 | 98 |
99 | 100 | { 106 | // handle next image to detect 107 | if (image) { 108 | URL.revokeObjectURL(image); 109 | setImage(null); 110 | } 111 | 112 | const url = URL.createObjectURL(e.target.files[0]); // create image url 113 | imageRef.current.src = url; // set image source 114 | setImage(url); 115 | }} 116 | /> 117 |
118 | 125 | {image && ( 126 | /* show close btn when there is image */ 127 | 137 | )} 138 |
139 |
140 | ); 141 | }; 142 | 143 | export default App; 144 | -------------------------------------------------------------------------------- /src/components/loader.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "../style/loader.css"; 3 | 4 | const Loader = (props) => { 5 | return ( 6 |
7 |
8 |

{props.children}

9 |
10 | ); 11 | }; 12 | 13 | export default Loader; 14 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./App"; 4 | import reportWebVitals from "./reportWebVitals"; 5 | import "./style/index.css"; 6 | 7 | const root = ReactDOM.createRoot(document.getElementById("root")); 8 | root.render( 9 | 10 | 11 | 12 | ); 13 | 14 | // If you want to start measuring performance in your app, pass a function 15 | // to log results (for example: reportWebVitals(console.log)) 16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 17 | reportWebVitals(); 18 | -------------------------------------------------------------------------------- /src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /src/style/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | height: 100vh; 3 | padding: 0 10px; 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: center; 7 | align-items: center; 8 | } 9 | 10 | .App > * { 11 | margin: 3px 0; 12 | } 13 | 14 | .header { 15 | text-align: center; 16 | } 17 | 18 | .header p { 19 | margin: 5px 0; 20 | } 21 | 22 | .code { 23 | padding: 5px; 24 | color: greenyellow; 25 | background-color: black; 26 | border-radius: 5px; 27 | } 28 | 29 | .content > img { 30 | width: 100%; 31 | max-width: 720px; 32 | max-height: 500px; 33 | border-radius: 10px; 34 | } 35 | 36 | .content { 37 | position: relative; 38 | } 39 | 40 | .content > canvas { 41 | position: absolute; 42 | top: 0; 43 | left: 0; 44 | width: 100%; 45 | height: 100%; 46 | } 47 | 48 | button { 49 | text-decoration: none; 50 | color: white; 51 | background-color: black; 52 | border: 2px solid black; 53 | margin: 0 5px; 54 | padding: 5px; 55 | border-radius: 5px; 56 | cursor: pointer; 57 | } 58 | 59 | button:hover { 60 | color: black; 61 | background-color: white; 62 | border: 2px solid black; 63 | } 64 | -------------------------------------------------------------------------------- /src/style/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | body { 7 | width: 100%; 8 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Oxygen", "Ubuntu", 9 | "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", Helvetica, Arial, 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", monospace; 16 | } 17 | -------------------------------------------------------------------------------- /src/style/loader.css: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | background-color: rgba(255, 255, 255, 0.5); 3 | position: absolute; 4 | width: 100%; 5 | height: 100%; 6 | z-index: 1000; 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | font-size: medium; 11 | } 12 | 13 | .wrapper > .spinner { 14 | width: 40px; 15 | height: 40px; 16 | background-color: #333; 17 | 18 | margin: 10px 10px; 19 | -webkit-animation: sk-rotateplane 1.2s infinite ease-in-out; 20 | animation: sk-rotateplane 1.2s infinite ease-in-out; 21 | } 22 | 23 | @-webkit-keyframes sk-rotateplane { 24 | 0% { 25 | -webkit-transform: perspective(120px); 26 | } 27 | 50% { 28 | -webkit-transform: perspective(120px) rotateY(180deg); 29 | } 30 | 100% { 31 | -webkit-transform: perspective(120px) rotateY(180deg) rotateX(180deg); 32 | } 33 | } 34 | 35 | @keyframes sk-rotateplane { 36 | 0% { 37 | transform: perspective(120px) rotateX(0deg) rotateY(0deg); 38 | -webkit-transform: perspective(120px) rotateX(0deg) rotateY(0deg); 39 | } 40 | 50% { 41 | transform: perspective(120px) rotateX(-180.1deg) rotateY(0deg); 42 | -webkit-transform: perspective(120px) rotateX(-180.1deg) rotateY(0deg); 43 | } 44 | 100% { 45 | transform: perspective(120px) rotateX(-180deg) rotateY(-179.9deg); 46 | -webkit-transform: perspective(120px) rotateX(-180deg) rotateY(-179.9deg); 47 | } 48 | } 49 | 50 | .wrapper > p { 51 | margin: 0; 52 | } 53 | -------------------------------------------------------------------------------- /src/utils/detect.js: -------------------------------------------------------------------------------- 1 | import { Tensor } from "onnxruntime-web"; 2 | import { renderBoxes } from "./renderBox"; 3 | 4 | /** 5 | * Detect Image 6 | * @param {HTMLImageElement} image Image to detect 7 | * @param {HTMLCanvasElement} canvas canvas to draw boxes 8 | * @param {ort.InferenceSession} session YOLOv5 onnxruntime session 9 | * @param {Number} topk Integer representing the maximum number of boxes to be selected per class 10 | * @param {Number} iouThreshold Float representing the threshold for deciding whether boxes overlap too much with respect to IOU 11 | * @param {Number} confThreshold Float representing the threshold for deciding when to remove boxes based on confidence score 12 | * @param {Number} classThreshold class threshold 13 | * @param {Number[]} inputShape model input shape. Normally in YOLO model [batch, channels, width, height] 14 | */ 15 | export const detectImage = async ( 16 | image, 17 | canvas, 18 | session, 19 | topk, 20 | iouThreshold, 21 | confThreshold, 22 | classThreshold, 23 | inputShape 24 | ) => { 25 | const [modelWidth, modelHeight] = inputShape.slice(2); 26 | 27 | const mat = cv.imread(image); // read from img tag 28 | const matC3 = new cv.Mat(mat.rows, mat.cols, cv.CV_8UC3); // new image matrix 29 | cv.cvtColor(mat, matC3, cv.COLOR_RGBA2BGR); // RGBA to BGR 30 | 31 | // padding image to [n x n] dim 32 | const maxSize = Math.max(matC3.rows, matC3.cols); // get max size from width and height 33 | const xPad = maxSize - matC3.cols, // set xPadding 34 | xRatio = maxSize / matC3.cols; // set xRatio 35 | const yPad = maxSize - matC3.rows, // set yPadding 36 | yRatio = maxSize / matC3.rows; // set yRatio 37 | const matPad = new cv.Mat(); // new mat for padded image 38 | cv.copyMakeBorder(matC3, matPad, 0, yPad, 0, xPad, cv.BORDER_CONSTANT, [0, 0, 0, 255]); // padding black 39 | 40 | const input = cv.blobFromImage( 41 | matPad, 42 | 1 / 255.0, // normalize 43 | new cv.Size(modelWidth, modelHeight), // resize to model input size 44 | new cv.Scalar(0, 0, 0), 45 | true, // swapRB 46 | false // crop 47 | ); // preprocessing image matrix 48 | 49 | const tensor = new Tensor("float32", input.data32F, inputShape); // to ort.Tensor 50 | const config = new Tensor("float32", new Float32Array([topk, iouThreshold, confThreshold])); // nms config tensor 51 | const start = Date.now(); 52 | const { output0 } = await session.net.run({ images: tensor }); // run session and get output layer 53 | const { selected_idx } = await session.nms.run({ detection: output0, config: config }); // get selected idx from nms 54 | console.log(Date.now() - start); 55 | 56 | const boxes = []; 57 | 58 | // looping through output 59 | selected_idx.data.forEach((idx) => { 60 | const data = output0.data.slice(idx * output0.dims[2], (idx + 1) * output0.dims[2]); // get rows 61 | const [x, y, w, h] = data.slice(0, 4); 62 | const confidence = data[4]; // detection confidence 63 | const scores = data.slice(5); // classes probability scores 64 | let score = Math.max(...scores); // maximum probability scores 65 | const label = scores.indexOf(score); // class id of maximum probability scores 66 | score *= confidence; // multiply score by conf 67 | 68 | // filtering by score thresholds 69 | if (score >= classThreshold) 70 | boxes.push({ 71 | label: label, 72 | probability: score, 73 | bounding: [ 74 | Math.floor((x - 0.5 * w) * xRatio), // left 75 | Math.floor((y - 0.5 * h) * yRatio), //top 76 | Math.floor(w * xRatio), // width 77 | Math.floor(h * yRatio), // height 78 | ], 79 | }); 80 | }); 81 | 82 | renderBoxes(canvas, boxes); // Draw boxes 83 | 84 | // release mat opencv 85 | mat.delete(); 86 | matC3.delete(); 87 | matPad.delete(); 88 | input.delete(); 89 | }; 90 | -------------------------------------------------------------------------------- /src/utils/download.js: -------------------------------------------------------------------------------- 1 | export const download = (url, logger = null) => { 2 | return new Promise((resolve, reject) => { 3 | const request = new XMLHttpRequest(); 4 | request.open("GET", url, true); 5 | request.responseType = "arraybuffer"; 6 | if (logger) { 7 | const [log, setState] = logger; 8 | request.onprogress = (e) => { 9 | const progress = (e.loaded / e.total) * 100; 10 | setState({ text: log, progress: progress.toFixed(2) }); 11 | }; 12 | } 13 | request.onload = function () { 14 | if (this.status >= 200 && this.status < 300) { 15 | resolve(request.response); 16 | } else { 17 | reject({ 18 | status: this.status, 19 | statusText: request.statusText, 20 | }); 21 | } 22 | resolve(request.response); 23 | }; 24 | request.onerror = function () { 25 | reject({ 26 | status: this.status, 27 | statusText: request.statusText, 28 | }); 29 | }; 30 | request.send(); 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /src/utils/labels.json: -------------------------------------------------------------------------------- 1 | [ 2 | "person", 3 | "bicycle", 4 | "car", 5 | "motorcycle", 6 | "airplane", 7 | "bus", 8 | "train", 9 | "truck", 10 | "boat", 11 | "traffic light", 12 | "fire hydrant", 13 | "stop sign", 14 | "parking meter", 15 | "bench", 16 | "bird", 17 | "cat", 18 | "dog", 19 | "horse", 20 | "sheep", 21 | "cow", 22 | "elephant", 23 | "bear", 24 | "zebra", 25 | "giraffe", 26 | "backpack", 27 | "umbrella", 28 | "handbag", 29 | "tie", 30 | "suitcase", 31 | "frisbee", 32 | "skis", 33 | "snowboard", 34 | "sports ball", 35 | "kite", 36 | "baseball bat", 37 | "baseball glove", 38 | "skateboard", 39 | "surfboard", 40 | "tennis racket", 41 | "bottle", 42 | "wine glass", 43 | "cup", 44 | "fork", 45 | "knife", 46 | "spoon", 47 | "bowl", 48 | "banana", 49 | "apple", 50 | "sandwich", 51 | "orange", 52 | "broccoli", 53 | "carrot", 54 | "hot dog", 55 | "pizza", 56 | "donut", 57 | "cake", 58 | "chair", 59 | "couch", 60 | "potted plant", 61 | "bed", 62 | "dining table", 63 | "toilet", 64 | "tv", 65 | "laptop", 66 | "mouse", 67 | "remote", 68 | "keyboard", 69 | "cell phone", 70 | "microwave", 71 | "oven", 72 | "toaster", 73 | "sink", 74 | "refrigerator", 75 | "book", 76 | "clock", 77 | "vase", 78 | "scissors", 79 | "teddy bear", 80 | "hair drier", 81 | "toothbrush" 82 | ] 83 | -------------------------------------------------------------------------------- /src/utils/renderBox.js: -------------------------------------------------------------------------------- 1 | import labels from "./labels.json"; 2 | 3 | /** 4 | * Render prediction boxes 5 | * @param {HTMLCanvasElement} canvas canvas tag reference 6 | * @param {Array[Object]} boxes boxes array 7 | */ 8 | export const renderBoxes = (canvas, boxes) => { 9 | const ctx = canvas.getContext("2d"); 10 | ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); // clean canvas 11 | 12 | const colors = new Colors(); 13 | 14 | // font configs 15 | const font = `${Math.max( 16 | Math.round(Math.max(ctx.canvas.width, ctx.canvas.height) / 40), 17 | 14 18 | )}px Arial`; 19 | ctx.font = font; 20 | ctx.textBaseline = "top"; 21 | 22 | boxes.forEach((box) => { 23 | const klass = labels[box.label]; 24 | const color = colors.get(box.label); 25 | const score = (box.probability * 100).toFixed(1); 26 | const [x1, y1, width, height] = box.bounding; 27 | 28 | // draw box. 29 | ctx.fillStyle = Colors.hexToRgba(color, 0.2); 30 | ctx.fillRect(x1, y1, width, height); 31 | // draw border box 32 | ctx.strokeStyle = color; 33 | ctx.lineWidth = Math.max(Math.min(ctx.canvas.width, ctx.canvas.height) / 200, 2.5); 34 | ctx.strokeRect(x1, y1, width, height); 35 | 36 | // draw the label background. 37 | ctx.fillStyle = color; 38 | const textWidth = ctx.measureText(klass + " - " + score + "%").width; 39 | const textHeight = parseInt(font, 10); // base 10 40 | const yText = y1 - (textHeight + ctx.lineWidth); 41 | ctx.fillRect( 42 | x1 - 1, 43 | yText < 0 ? 0 : yText, 44 | textWidth + ctx.lineWidth, 45 | textHeight + ctx.lineWidth 46 | ); 47 | 48 | // Draw labels 49 | ctx.fillStyle = "#ffffff"; 50 | ctx.fillText(klass + " - " + score + "%", x1 - 1, yText < 0 ? 1 : yText + 1); 51 | }); 52 | }; 53 | 54 | class Colors { 55 | // ultralytics color palette https://ultralytics.com/ 56 | constructor() { 57 | this.palette = [ 58 | "#FF3838", 59 | "#FF9D97", 60 | "#FF701F", 61 | "#FFB21D", 62 | "#CFD231", 63 | "#48F90A", 64 | "#92CC17", 65 | "#3DDB86", 66 | "#1A9334", 67 | "#00D4BB", 68 | "#2C99A8", 69 | "#00C2FF", 70 | "#344593", 71 | "#6473FF", 72 | "#0018EC", 73 | "#8438FF", 74 | "#520085", 75 | "#CB38FF", 76 | "#FF95C8", 77 | "#FF37C7", 78 | ]; 79 | this.n = this.palette.length; 80 | } 81 | 82 | get = (i) => this.palette[Math.floor(i) % this.n]; 83 | 84 | static hexToRgba = (hex, alpha) => { 85 | var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); 86 | return result 87 | ? `rgba(${[parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16)].join( 88 | ", " 89 | )}, ${alpha})` 90 | : null; 91 | }; 92 | } 93 | --------------------------------------------------------------------------------