├── .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 |
6 |
7 |
8 |
9 |
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 |
2 |
3 |
4 |
5 |
6 |
12 |
13 |
20 |
21 |
22 |
23 |
33 |
34 |
39 |
40 |
41 |
42 |
![]()
43 |
44 |
45 |
46 | Upload an image using the button above to upscale it using the selected model.
47 |
48 |
49 |
50 |
51 |
52 | Done!
53 | Upscaling. Please wait...
54 |
55 |
56 |
57 |
60 |
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
--------------------------------------------------------------------------------