├── .eslintrc.cjs ├── .gitignore ├── .vscode └── extensions.json ├── CHANGELOG.md ├── README.md ├── deploy.sh ├── env.d.ts ├── index.html ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── js │ ├── ort-wasm-simd-threaded.wasm │ ├── ort-wasm-simd.wasm │ ├── ort-wasm-threaded.wasm │ └── ort-wasm.wasm └── models │ ├── up2x-latest-conservative.onnx │ ├── up2x-latest-denoise1x.onnx │ ├── up2x-latest-denoise2x.onnx │ ├── up2x-latest-denoise3x.onnx │ ├── up2x-latest-no-denoise.onnx │ ├── up2x-latest-pro-conservative.onnx │ ├── up2x-latest-pro-denoise3x.onnx │ └── up2x-latest-pro-no-denoise.onnx ├── src ├── App.vue ├── assets │ ├── base.css │ └── sample.png ├── components │ └── ResizeGrid.vue ├── image │ ├── canvas.ts │ ├── options.ts │ ├── predictor.ts │ ├── upscale.worker.ts │ └── worker.ts ├── main.ts └── node │ ├── testdata │ ├── image_200_200.png │ └── image_250_250.jpg │ ├── upscaler.spec.ts │ └── upscaler.ts ├── tsconfig.json ├── tsconfig.vite-config.json ├── vite.config.ts ├── vite.lib.config.ts └── vite.options.ts /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | require("@rushstack/eslint-patch/modern-module-resolution"); 3 | 4 | module.exports = { 5 | root: true, 6 | extends: [ 7 | "plugin:vue/vue3-essential", 8 | "eslint:recommended", 9 | "@vue/eslint-config-typescript/recommended", 10 | ], 11 | env: { 12 | "vue/setup-compiler-macros": true, 13 | }, 14 | rules: { 15 | "no-console": process.env.NODE_ENV === "production" ? "warn" : "off", 16 | "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off", 17 | camelcase: "off", 18 | "import/no-webpack-loader-syntax": "off", 19 | quotes: [2, "double", { avoidEscape: true }], 20 | indent: "off", 21 | "@typescript-eslint/indent": ["error", 2], 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | 17 | /cypress/videos/ 18 | /cypress/screenshots/ 19 | 20 | # Editor directories and files 21 | .vscode/* 22 | !.vscode/extensions.json 23 | .idea 24 | *.suo 25 | *.ntvs* 26 | *.njsproj 27 | *.sln 28 | *.sw? 29 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["johnsoncodehk.volar", "johnsoncodehk.vscode-typescript-vue-plugin"] 3 | } 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ## [1.1.0](https://github.com/gqgs/upscalejs/compare/v1.0.0...v1.1.0) (2022-06-19) 6 | 7 | 8 | ### Features 9 | 10 | * pro models ([3dd47de](https://github.com/gqgs/upscalejs/commit/3dd47de0ba77d04bf00c739dae579a1847c96957)) 11 | 12 | ## [1.0.0](https://github.com/gqgs/upscalejs/compare/v0.2.0...v1.0.0) (2022-05-01) 13 | 14 | 15 | ### Features 16 | 17 | * node support ([1abb110](https://github.com/gqgs/upscalejs/commit/1abb1100dedfda89b376fe4c71e04abe871f40fc)), closes [#1](https://github.com/gqgs/upscalejs/issues/1) 18 | 19 | 20 | ### Bug Fixes 21 | 22 | * **worker:** use overlapping tiles to decrease artifacts ([3b98a5c](https://github.com/gqgs/upscalejs/commit/3b98a5c87d546a3bddcae409eebcf059074cfb05)) 23 | 24 | ## 0.2.0 (2022-04-05) 25 | 26 | 27 | ### Features 28 | 29 | * **build:** allow building in library mode ([4354449](https://github.com/gqgs/upscalejs/commit/435444974b15280f961e99a9f8d82d4de5cb3a35)) 30 | * **grid:** allow drag and drop input ([a395c8d](https://github.com/gqgs/upscalejs/commit/a395c8d0089e92e391b7047d30193e3984de23b9)) 31 | * **worker:** add base path to options ([1a2bd7a](https://github.com/gqgs/upscalejs/commit/1a2bd7aea557424cc5e7db540e3c2132a3e997bc)) 32 | 33 | 34 | ### Bug Fixes 35 | 36 | * **worker:** change default base to support old targets ([b11a718](https://github.com/gqgs/upscalejs/commit/b11a71842716b035e4e0754dfecd111425bfcd37)) 37 | * **worker:** finish upscaling before returning worker to pool ([fb94b34](https://github.com/gqgs/upscalejs/commit/fb94b340de0896b8c441879019d4ed07aaeb1351)) 38 | * **worker:** use recommended way to create worker ([9a3ff92](https://github.com/gqgs/upscalejs/commit/9a3ff9288c1c336f4369659d8b476b4776ca5054)) 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # upscalejs 2 | 3 | Image upscaling using super resolution AI models. 4 | 5 |

6 | 7 |

8 | 9 | ## Usage 10 | 11 | ```ts 12 | import { Upscaler } from "upscalejs" 13 | 14 | const upscaler = new Upscaler() 15 | const result = await upscaler.upscale(bitmap) 16 | const canvas = document.createElement("canvas") 17 | canvas.width = result.width 18 | canvas.height = result.height 19 | canvas.getContext("2d")?.putImageData(result, 0, 0) 20 | ``` 21 | 22 | ### Node 23 | ```js 24 | const fs = require("fs") 25 | const { Upscaler, canvas } = require("upscalejs") 26 | 27 | const upscaler = new Upscaler({ 28 | base: "./node_modules/upscalejs/dist/" 29 | }) 30 | 31 | const upscale = async (input, output) => { 32 | const img = await canvas.loadImage(input) 33 | const result = await upscaler.upscale(img) 34 | const write_stream = fs.createWriteStream(output) 35 | const upscale_canvas = canvas.createCanvas(result.width, result.height) 36 | upscale_canvas.getContext("2d").putImageData(result, 0, 0) 37 | const jpeg_stream = upscale_canvas.createJPEGStream() 38 | jpeg_stream.pipe(write_stream) 39 | } 40 | 41 | upscale("image.jpg", "upscaled.jpg") 42 | ``` 43 | 44 | You will need to copy the models and onnxruntime wasm files to your public folder e.g.: 45 | ```ts 46 | const CopyPlugin = require("copy-webpack-plugin"); 47 | 48 | module.exports = { 49 | chainWebpack: config => { 50 | config 51 | .plugin("copy-webpack-plugin") 52 | .use(CopyPlugin) 53 | .tap(() => { 54 | return [{ 55 | patterns: [ 56 | { 57 | from: "./node_modules/upscalejs/dist/js/ort-*.wasm", 58 | to: "js/[name][ext]", 59 | }, 60 | { 61 | from: "./node_modules/upscalejs/dist/models/*.onnx", 62 | to: "models/[name][ext]", 63 | } 64 | ], 65 | }] 66 | }) 67 | } 68 | } 69 | ``` 70 | 71 | Current available models were trained in anime images and are based on work done in the [Real-CUGAN](https://github.com/bilibili/ailab/tree/main/Real-CUGAN) project. 72 | 73 | ## Project Setup 74 | 75 | ```sh 76 | npm install 77 | ``` 78 | 79 | ### Compile and Hot-Reload for Development 80 | 81 | ```sh 82 | npm run dev 83 | ``` 84 | 85 | ### Type-Check, Compile and Minify for Production 86 | 87 | ```sh 88 | npm run build 89 | ``` 90 | 91 | ### Lint with [ESLint](https://eslint.org/) 92 | 93 | ```sh 94 | npm run lint 95 | ``` 96 | 97 | ### Test with [Vitest](https://github.com/vitest-dev/vitest) 98 | 99 | ```sh 100 | npm run test 101 | ``` 102 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # abort on errors 4 | set -e 5 | 6 | # build 7 | npm run build 8 | 9 | # navigate into the build output directory 10 | cd dist 11 | 12 | git init 13 | git add -A 14 | git commit -m 'deploy' 15 | 16 | git push -f git@github.com:gqgs/upscalejs.git master:gh-pages 17 | 18 | cd - -------------------------------------------------------------------------------- /env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | upscalejs 10 | 11 | 12 | 13 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "upscalejs", 3 | "version": "1.1.0", 4 | "files": [ 5 | "dist" 6 | ], 7 | "types": "./dist/image/worker.d.ts", 8 | "description": "Upscale images using super resolution AI models", 9 | "browser": "./dist/upscalejs.es.js.", 10 | "main": "./dist/upscalejs-node.cjs.js.", 11 | "exports": { 12 | ".": { 13 | "import": "./dist/upscalejs.es.js", 14 | "require": "./dist/upscalejs-node.cjs.js" 15 | } 16 | }, 17 | "scripts": { 18 | "dev": "vite", 19 | "build": "vue-tsc --noEmit && vite build", 20 | "lib": "vue-tsc --noEmit && vite build -c vite.lib.config.ts && vue-tsc -d --emitDeclarationOnly --outDir dist", 21 | "preview": "vite preview --port 5050", 22 | "typecheck": "vue-tsc --noEmit", 23 | "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore", 24 | "test": "vitest --run --threads false" 25 | }, 26 | "dependencies": { 27 | "canvas": "^2.9.1", 28 | "vue": "^3.2.31" 29 | }, 30 | "keywords": [ 31 | "image", 32 | "upscale", 33 | "ts" 34 | ], 35 | "repository": { 36 | "type": "git", 37 | "url": "git+https://github.com/gqgs/upscalejs.git" 38 | }, 39 | "devDependencies": { 40 | "@rushstack/eslint-patch": "^1.1.0", 41 | "@types/node": "^17.0.23", 42 | "@vitejs/plugin-vue": "^2.3.1", 43 | "@vitejs/plugin-vue-jsx": "^1.3.9", 44 | "@vue/eslint-config-prettier": "^7.0.0", 45 | "@vue/eslint-config-typescript": "^10.0.0", 46 | "@vue/tsconfig": "^0.1.3", 47 | "eslint": "^8.5.0", 48 | "eslint-plugin-vue": "^8.2.0", 49 | "onnxruntime-node": "^1.11.0", 50 | "onnxruntime-web": "^1.11.0", 51 | "prettier": "^2.6.2", 52 | "typescript": "~4.6.3", 53 | "vite": "^2.9.1", 54 | "vitest": "^0.9.4", 55 | "vue-tsc": "^0.34.10" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gqgs/upscalejs/08810677c7b2364caf02dea4b71ef9160f518040/public/favicon.ico -------------------------------------------------------------------------------- /public/js/ort-wasm-simd-threaded.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gqgs/upscalejs/08810677c7b2364caf02dea4b71ef9160f518040/public/js/ort-wasm-simd-threaded.wasm -------------------------------------------------------------------------------- /public/js/ort-wasm-simd.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gqgs/upscalejs/08810677c7b2364caf02dea4b71ef9160f518040/public/js/ort-wasm-simd.wasm -------------------------------------------------------------------------------- /public/js/ort-wasm-threaded.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gqgs/upscalejs/08810677c7b2364caf02dea4b71ef9160f518040/public/js/ort-wasm-threaded.wasm -------------------------------------------------------------------------------- /public/js/ort-wasm.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gqgs/upscalejs/08810677c7b2364caf02dea4b71ef9160f518040/public/js/ort-wasm.wasm -------------------------------------------------------------------------------- /public/models/up2x-latest-conservative.onnx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gqgs/upscalejs/08810677c7b2364caf02dea4b71ef9160f518040/public/models/up2x-latest-conservative.onnx -------------------------------------------------------------------------------- /public/models/up2x-latest-denoise1x.onnx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gqgs/upscalejs/08810677c7b2364caf02dea4b71ef9160f518040/public/models/up2x-latest-denoise1x.onnx -------------------------------------------------------------------------------- /public/models/up2x-latest-denoise2x.onnx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gqgs/upscalejs/08810677c7b2364caf02dea4b71ef9160f518040/public/models/up2x-latest-denoise2x.onnx -------------------------------------------------------------------------------- /public/models/up2x-latest-denoise3x.onnx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gqgs/upscalejs/08810677c7b2364caf02dea4b71ef9160f518040/public/models/up2x-latest-denoise3x.onnx -------------------------------------------------------------------------------- /public/models/up2x-latest-no-denoise.onnx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gqgs/upscalejs/08810677c7b2364caf02dea4b71ef9160f518040/public/models/up2x-latest-no-denoise.onnx -------------------------------------------------------------------------------- /public/models/up2x-latest-pro-conservative.onnx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gqgs/upscalejs/08810677c7b2364caf02dea4b71ef9160f518040/public/models/up2x-latest-pro-conservative.onnx -------------------------------------------------------------------------------- /public/models/up2x-latest-pro-denoise3x.onnx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gqgs/upscalejs/08810677c7b2364caf02dea4b71ef9160f518040/public/models/up2x-latest-pro-denoise3x.onnx -------------------------------------------------------------------------------- /public/models/up2x-latest-pro-no-denoise.onnx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gqgs/upscalejs/08810677c7b2364caf02dea4b71ef9160f518040/public/models/up2x-latest-pro-no-denoise.onnx -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 | -------------------------------------------------------------------------------- /src/assets/base.css: -------------------------------------------------------------------------------- 1 | /* color palette from */ 2 | :root { 3 | --vt-c-white: #ffffff; 4 | --vt-c-white-soft: #f8f8f8; 5 | --vt-c-white-mute: #f2f2f2; 6 | 7 | --vt-c-black: #181818; 8 | --vt-c-black-soft: #222222; 9 | --vt-c-black-mute: #282828; 10 | 11 | --vt-c-indigo: #2c3e50; 12 | 13 | --vt-c-divider-light-1: rgba(60, 60, 60, 0.29); 14 | --vt-c-divider-light-2: rgba(60, 60, 60, 0.12); 15 | --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65); 16 | --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48); 17 | 18 | --vt-c-text-light-1: var(--vt-c-indigo); 19 | --vt-c-text-light-2: rgba(60, 60, 60, 0.66); 20 | --vt-c-text-dark-1: var(--vt-c-white); 21 | --vt-c-text-dark-2: rgba(235, 235, 235, 0.64); 22 | } 23 | 24 | /* semantic color variables for this project */ 25 | :root { 26 | --color-background: var(--vt-c-white); 27 | --color-background-soft: var(--vt-c-white-soft); 28 | --color-background-mute: var(--vt-c-white-mute); 29 | 30 | --color-border: var(--vt-c-divider-light-2); 31 | --color-border-hover: var(--vt-c-divider-light-1); 32 | 33 | --color-heading: var(--vt-c-text-light-1); 34 | --color-text: var(--vt-c-text-light-1); 35 | 36 | --section-gap: 160px; 37 | } 38 | 39 | @media (prefers-color-scheme: dark) { 40 | :root { 41 | --color-background: var(--vt-c-black); 42 | --color-background-soft: var(--vt-c-black-soft); 43 | --color-background-mute: var(--vt-c-black-mute); 44 | 45 | --color-border: var(--vt-c-divider-dark-2); 46 | --color-border-hover: var(--vt-c-divider-dark-1); 47 | 48 | --color-heading: var(--vt-c-text-dark-1); 49 | --color-text: var(--vt-c-text-dark-2); 50 | } 51 | } 52 | 53 | *, 54 | *::before, 55 | *::after { 56 | box-sizing: border-box; 57 | margin: 0; 58 | position: relative; 59 | font-weight: normal; 60 | } 61 | 62 | body { 63 | min-height: 100vh; 64 | color: var(--color-text); 65 | background: var(--color-background); 66 | transition: color 0.5s, background-color 0.5s; 67 | line-height: 1.6; 68 | font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, 69 | Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; 70 | font-size: 15px; 71 | text-rendering: optimizeLegibility; 72 | -webkit-font-smoothing: antialiased; 73 | -moz-osx-font-smoothing: grayscale; 74 | } 75 | -------------------------------------------------------------------------------- /src/assets/sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gqgs/upscalejs/08810677c7b2364caf02dea4b71ef9160f518040/src/assets/sample.png -------------------------------------------------------------------------------- /src/components/ResizeGrid.vue: -------------------------------------------------------------------------------- 1 | 61 | 62 | 80 | 81 | 82 | 142 | -------------------------------------------------------------------------------- /src/image/canvas.ts: -------------------------------------------------------------------------------- 1 | import { createCanvas } from "canvas"; 2 | import type { Canvas as NodeCanvas } from "canvas" 3 | 4 | export interface ImageSource { 5 | width: number; 6 | height: number; 7 | } 8 | 9 | export interface Canvas { 10 | x: number; 11 | y: number; 12 | element: NodeCanvas; 13 | } 14 | 15 | export const canvasListFromImageData = (image: ImageSource): Canvas[] => { 16 | if (image.width == 200 && image.height == 200) { 17 | // fast path for best case 18 | const canvas = createCanvas(image.width, image.height); 19 | canvas.getContext("2d")?.drawImage(image, 0, 0, 200, 200); 20 | return [ 21 | { 22 | x: 0, 23 | y: 0, 24 | element: canvas, 25 | }, 26 | ]; 27 | } 28 | 29 | const canvas_list: Canvas[] = []; 30 | const width = Math.ceil(image.width / 200) * 200; 31 | const height = Math.ceil(image.height / 200) * 200; 32 | for (let x = 0; x < width; x += 180) { 33 | for (let y = 0; y < height; y += 180) { 34 | const canvas = createCanvas(200, 200); 35 | 36 | let sx = x, sy = y; 37 | if (x + 200 > image.width) { 38 | const padding = width - image.width; 39 | sx -= padding; 40 | } 41 | if (y + 200 > image.height) { 42 | const padding = height - image.height; 43 | sy -= padding; 44 | } 45 | 46 | canvas.getContext("2d")?.drawImage(image, sx, sy, 200, 200, 0, 0, 200, 200); 47 | canvas_list.push({ 48 | x: sx * 2, 49 | y: sy * 2, 50 | element: canvas, 51 | }); 52 | } 53 | } 54 | return canvas_list; 55 | }; 56 | 57 | export default { 58 | canvasListFromImageData, 59 | }; 60 | -------------------------------------------------------------------------------- /src/image/options.ts: -------------------------------------------------------------------------------- 1 | export type Model = "no-denoise" | "conservative" | "denoise1x" | "denoise2x" | "denoise3x" | "pro-no-denoise" | "pro-conservative" | "pro-denoise3x" 2 | 3 | export interface Options { 4 | // Max number of workers that will be created for multiple images. 5 | maxWorkers?: number; 6 | // Max number of workers that will be created for each image. 7 | maxInternalWorkers?: number; 8 | // The model that will be used for upscaling images. 9 | denoiseModel?: Model; 10 | // Environment base url (i.e. root of public path). 11 | base?: string; 12 | } 13 | 14 | export const defaultOptions = { 15 | maxWorkers: 1, 16 | maxInternalWorkers: 4, 17 | denoiseModel: "conservative", 18 | base: typeof window !== "undefined" ? window.location.href : "./public/", 19 | }; 20 | 21 | export default { 22 | defaultOptions, 23 | }; 24 | -------------------------------------------------------------------------------- /src/image/predictor.ts: -------------------------------------------------------------------------------- 1 | import type * as ort from "onnxruntime-common" 2 | import { createImageData } from "canvas" 3 | 4 | interface Ort { 5 | env: ort.Env 6 | InferenceSession: ort.InferenceSessionFactory 7 | Tensor: ort.TensorConstructor 8 | } 9 | 10 | export default class Predictor { 11 | private ort: Ort 12 | private baseURL: string 13 | private models: Map> = new Map>() 14 | 15 | constructor (ort: Ort, baseURL: string) { 16 | this.ort = ort 17 | this.baseURL = baseURL 18 | } 19 | 20 | private async loadModel(denoiseModel: string): Promise { 21 | const cached_model = this.models.get(denoiseModel) 22 | if (cached_model) { 23 | return cached_model 24 | } 25 | this.ort.env.wasm.wasmPaths = `${this.baseURL}js/` 26 | const path = `${this.baseURL}models/up2x-latest-${denoiseModel}.onnx` 27 | const model = this.ort.InferenceSession.create(path) 28 | this.models.set(denoiseModel, model) 29 | return model 30 | } 31 | 32 | public async predict (image: ImageData, denoiseModel: string): Promise { 33 | const red = new Array() 34 | const green = new Array() 35 | const blue = new Array() 36 | for (let i = 0; i < image.data.length; i += 4) { 37 | red.push(image.data[i]) 38 | green.push(image.data[i+1]) 39 | blue.push(image.data[i+2]) 40 | } 41 | const transposed = red.concat(green).concat(blue) 42 | const float32Data = new Float32Array(3 * 200 * 200) 43 | for (let i = 0; i < float32Data.length; i++) { 44 | float32Data[i] = transposed[i] / 255.0 45 | } 46 | const tensor = new this.ort.Tensor("float32", float32Data, [1, 3, 200, 200]) 47 | const session = await this.loadModel(denoiseModel) 48 | const feeds = { input_1: tensor } 49 | const results = await session.run(feeds) 50 | 51 | const rgbaArray = new Uint8ClampedArray(4 * 400 * 400) 52 | const resultData = results.output_1.data as Uint8Array 53 | // [3, 400, 400] -> [400, 400, 4] 54 | for (const d of Array(4).keys()) { 55 | for (let i = 160_000 * d, j = d; j < rgbaArray.length; i++, j += 4) { 56 | rgbaArray[j] = resultData[i] ?? 255 57 | } 58 | } 59 | return createImageData(rgbaArray, 400, 400) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/image/upscale.worker.ts: -------------------------------------------------------------------------------- 1 | import * as ort from "onnxruntime-web" 2 | import Predictor from "./predictor" 3 | 4 | let predictor: Predictor | null 5 | 6 | onmessage = async (event: MessageEvent) => { 7 | const id = event.data.id 8 | const image = event.data.image 9 | const denoiseModel = event.data.denoiseModel 10 | const base = event.data.base 11 | 12 | if (!predictor) predictor = new Predictor(ort, base) 13 | 14 | const upscaled = await predictor.predict(image, denoiseModel) 15 | 16 | postMessage({ 17 | id, 18 | upscaled 19 | }) 20 | } -------------------------------------------------------------------------------- /src/image/worker.ts: -------------------------------------------------------------------------------- 1 | import { canvasListFromImageData } from "./canvas" 2 | import type { Canvas, ImageSource } from "./canvas" 3 | import { defaultOptions } from "./options" 4 | import type { Model, Options } from "./options" 5 | import Worker from "./upscale.worker?worker&inline" 6 | import { createCanvas } from "canvas" 7 | import type { Canvas as NodeCanvas } from "canvas" 8 | 9 | interface Terminator { 10 | terminate(): void 11 | } 12 | 13 | abstract class WorkerPool { 14 | protected options 15 | protected created_workers = 0 16 | protected workers: worker[] = [] 17 | private waitingForWorker: ((upscaler: worker) => void) [] = [] 18 | public abstract upscale(image: ImageSource): Promise 19 | 20 | constructor(options?: Options) { 21 | this.options = Object.assign(defaultOptions, options) 22 | } 23 | 24 | protected async getWorker(): Promise { 25 | return new Promise(resolve => { 26 | const worker = this.workers.shift() 27 | if (worker) { 28 | resolve(worker) 29 | return 30 | } 31 | this.waitingForWorker.push(resolve) 32 | }) 33 | } 34 | 35 | protected putWorker(worker: worker) { 36 | const waiting = this.waitingForWorker.shift() 37 | if (waiting) { 38 | waiting(worker) 39 | return 40 | } 41 | this.workers.push(worker) 42 | } 43 | 44 | public terminate() { 45 | this.workers.map(worker => worker.terminate()) 46 | this.workers = [] 47 | this.created_workers = 0 48 | } 49 | } 50 | 51 | export class Upscaler extends WorkerPool { 52 | constructor(options?: Options) { 53 | super(options) 54 | } 55 | 56 | public async upscale(image: ImageSource): Promise { 57 | if (this.created_workers < this.options.maxWorkers) { 58 | this.created_workers++ 59 | this.workers.push(new upscaleWorker(this.options)) 60 | } 61 | const worker = await this.getWorker() 62 | const result = await worker.upscale(image) 63 | this.putWorker(worker) 64 | return result 65 | } 66 | } 67 | 68 | class upscaleWorker extends WorkerPool { 69 | private id = 0 70 | private canvas: NodeCanvas = createCanvas(0, 0) 71 | private pending = new Map() 72 | private resolve?: (canvas: Promise) => void 73 | 74 | constructor(options?: Options) { 75 | super(options) 76 | } 77 | 78 | private onmessage(event: MessageEvent) { 79 | const { id, upscaled } = event.data 80 | if (!isImageData(upscaled)) throw Error("expected upscaled to be an 'ImageData'") 81 | const result = this.pending.get(id) 82 | if (!result) throw Error("upscaled result is not pending") 83 | this.canvas.getContext("2d").putImageData(upscaled, result.x, result.y) 84 | this.pending.delete(id) 85 | if (this.pending.size == 0) { 86 | const imgdata = this.canvas.getContext("2d").getImageData(0, 0, this.canvas.width, this.canvas.height) 87 | this.resolve?.call(this, Promise.resolve(imgdata)) 88 | } 89 | } 90 | 91 | public async upscale(image: ImageSource): Promise { 92 | return new Promise(resolve => { 93 | this.resolve = resolve 94 | this.canvas = createCanvas(image.width * 2, image.height * 2) 95 | const canvas_list = canvasListFromImageData(image) 96 | canvas_list.forEach(async canvas => { 97 | const id = this.id++ 98 | this.pending.set(id, canvas) 99 | if (this.created_workers < this.options.maxInternalWorkers) { 100 | this.created_workers++ 101 | const worker = new Worker() 102 | worker.onmessage = this.onmessage.bind(this) 103 | this.workers.push(worker) 104 | } 105 | const worker = await this.getWorker() 106 | worker.postMessage({ 107 | id, 108 | image: canvas.element.getContext("2d").getImageData(0, 0, 200, 200), 109 | denoiseModel: this.options.denoiseModel, 110 | base: this.options.base 111 | }) 112 | this.putWorker(worker) 113 | }) 114 | }) 115 | } 116 | } 117 | 118 | const isImageData = (image: unknown): image is ImageData => { 119 | const imgdata = image as ImageData 120 | return imgdata.width > 0 && imgdata.height > 0 121 | } 122 | 123 | export type { 124 | Model 125 | } 126 | 127 | export default { 128 | Upscaler 129 | } 130 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | import App from "./App.vue"; 3 | 4 | const app = createApp(App); 5 | 6 | app.mount("#app"); 7 | -------------------------------------------------------------------------------- /src/node/testdata/image_200_200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gqgs/upscalejs/08810677c7b2364caf02dea4b71ef9160f518040/src/node/testdata/image_200_200.png -------------------------------------------------------------------------------- /src/node/testdata/image_250_250.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gqgs/upscalejs/08810677c7b2364caf02dea4b71ef9160f518040/src/node/testdata/image_250_250.jpg -------------------------------------------------------------------------------- /src/node/upscaler.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest" 2 | import { loadImage } from "canvas" 3 | import { Upscaler } from "./upscaler" 4 | 5 | const timeout = 15000 6 | const upscaler = new Upscaler() 7 | 8 | const upscale = (filename: string) => { 9 | describe(filename, () => { 10 | it("uspcales image", async () => { 11 | const img = await loadImage(`./src/node/testdata/${filename}`) 12 | const result = await upscaler.upscale(img) 13 | expect(result.width).toEqual(img.width * 2) 14 | expect(result.height).toEqual(img.height * 2) 15 | }, timeout) 16 | }) 17 | } 18 | 19 | upscale("image_200_200.png") 20 | upscale("image_250_250.jpg") 21 | -------------------------------------------------------------------------------- /src/node/upscaler.ts: -------------------------------------------------------------------------------- 1 | import type { ImageSource } from "../image/canvas" 2 | import { canvasListFromImageData } from "../image/canvas" 3 | import type { Options } from "../image/options" 4 | import { defaultOptions } from "../image/options" 5 | import { createCanvas } from "canvas" 6 | import Predictor from "../image/predictor" 7 | import * as ort from "onnxruntime-node" 8 | import canvas from "canvas" 9 | 10 | export class Upscaler { 11 | protected options 12 | private predictor: Predictor 13 | 14 | constructor(options?: Options) { 15 | this.options = Object.assign(defaultOptions, options) 16 | this.predictor = new Predictor(ort, this.options.base) 17 | } 18 | 19 | public async upscale(image: ImageSource): Promise { 20 | const result_canvas = createCanvas(image.width * 2, image.height * 2) 21 | const canvas_list = canvasListFromImageData(image) 22 | for (let i = 0; i < canvas_list.length; i++) { 23 | const imagedata = canvas_list[i].element.getContext("2d").getImageData(0, 0, 200, 200) 24 | const upscaled = await this.predictor.predict(imagedata, this.options.denoiseModel) 25 | result_canvas.getContext("2d").putImageData(upscaled, canvas_list[i].x, canvas_list[i].y) 26 | } 27 | return result_canvas.getContext("2d").getImageData(0, 0, result_canvas.width, result_canvas.height) 28 | } 29 | } 30 | 31 | module.exports = { 32 | Upscaler, 33 | canvas 34 | // Exporting 'canvas' here because the library doesn't work properly 35 | // when it is imported from different modules 36 | // see: https://github.com/Automattic/node-canvas/issues/487 37 | } 38 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.web.json", 3 | "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], 4 | "compilerOptions": { 5 | "target": "esnext", 6 | "module": "esnext", 7 | "types" : ["node"], 8 | "baseUrl": ".", 9 | "paths": { 10 | "@/*": ["./src/*"] 11 | } 12 | }, 13 | 14 | "references": [ 15 | { 16 | "path": "./tsconfig.vite-config.json" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /tsconfig.vite-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.node.json", 3 | "include": ["vite.config.", "vite.options.ts"], 4 | "compilerOptions": { 5 | "composite": true, 6 | "types": ["node"] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import { config } from "./vite.options" 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig(config); 6 | -------------------------------------------------------------------------------- /vite.lib.config.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { defineConfig } from "vite"; 3 | import { config } from "./vite.options" 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | ...config, 8 | build: { 9 | rollupOptions: { 10 | input: { 11 | "upscalejs": path.resolve(__dirname, "src/image/worker.ts"), 12 | "upscalejs-node": path.resolve(__dirname, "src/node/upscaler.ts"), 13 | }, 14 | external: ["vue", "canvas", "onnxruntime-node"], 15 | preserveEntrySignatures: "strict", 16 | output: [ 17 | { 18 | exports: "named", 19 | globals: { 20 | vue: "Vue", 21 | }, 22 | name: "upscalejs", 23 | entryFileNames: "[name].[format].js", 24 | chunkFileNames: "[name].js", 25 | assetFileNames: "[name].[ext]", 26 | format: "esm", 27 | }, 28 | { 29 | exports: "named", 30 | globals: { 31 | vue: "Vue" 32 | }, 33 | name: "upscalejs-node", 34 | entryFileNames: "[name].[format].js", 35 | format: "cjs", 36 | }, 37 | ], 38 | } 39 | } 40 | }); 41 | -------------------------------------------------------------------------------- /vite.options.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from "url"; 2 | 3 | import vue from "@vitejs/plugin-vue"; 4 | import vueJsx from "@vitejs/plugin-vue-jsx"; 5 | 6 | const vueOptions = { 7 | template: { 8 | compilerOptions: { 9 | isCustomElement: (tag: string) => tag.startsWith("ion-") 10 | } 11 | } 12 | } 13 | 14 | export const config = { 15 | base: process.env.NODE_ENV == "production" 16 | ? "/upscalejs/" 17 | : "/", 18 | plugins: [vue(vueOptions), vueJsx()], 19 | resolve: { 20 | alias: { 21 | "@": fileURLToPath(new URL("./src", import.meta.url)), 22 | "onnxruntime-web": "onnxruntime-web/dist/ort.wasm.min.js", 23 | }, 24 | }, 25 | } 26 | 27 | export default config --------------------------------------------------------------------------------