code init

This commit is contained in:
xororz
2024-04-12 04:02:42 -04:00
commit 4361728b6d
18 changed files with 2944 additions and 0 deletions

34
.gitignore vendored Normal file
View File

@@ -0,0 +1,34 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
lib
demo
realesrgan
*.ttf
.vscode
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo

33
README.md Normal file
View File

@@ -0,0 +1,33 @@
# canvastest
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
## Type Support for `.vue` Imports in TS
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
## Customize configuration
See [Vite Configuration Reference](https://vitejs.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Type-Check, Compile and Minify for Production
```sh
npm run build
```

1
env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

21
index.html Normal file
View File

@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<meta
name="description"
content="Run Super Resulotion in Your Local Browser. Your images never leave your device. Powered by TensorFlow.js and RealESRGAN. Support computing with WebGL and WebGPU."
/>
<title>Run Super Resulotion in Your Browser</title>
<script src="/lib/jimp.min.js"></script>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

1340
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

28
package.json Normal file
View File

@@ -0,0 +1,28 @@
{
"name": "canvastest",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite --host",
"build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --build --force"
},
"dependencies": {
"@tensorflow/tfjs": "^4.17.0",
"@tensorflow/tfjs-backend-webgpu": "^4.17.0",
"vue": "^3.4.21"
},
"devDependencies": {
"@tsconfig/node20": "^20.1.2",
"@types/node": "^20.11.28",
"@vitejs/plugin-vue": "^5.0.4",
"@vue/tsconfig": "^0.5.1",
"npm-run-all2": "^6.1.2",
"typescript": "~5.4.0",
"vite": "^5.1.6",
"vue-tsc": "^2.0.6"
}
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

736
src/App.vue Normal file
View File

@@ -0,0 +1,736 @@
<template>
<div
ref="canvasContainer"
class="canvas-container"
:class="{ 'canvas-container': true, bg: true, dark: imgLoaded }"
@drop.prevent="handleDrop"
@dragover.prevent
@mousedown="startDragging"
@mouseup="stopDragging"
@mousemove="dragImage"
@wheel="resizeImage"
@touchstart="touchStart"
@touchmove="touchMove"
@touchend="touchEnd"
>
<div v-if="!imgLoaded" class="title">
<div>SuperResolution in Your Browser</div>
<img
style="
width: 50px;
display: block;
margin: auto;
transform: translate(-18%, 0);
"
src="/demo/2.png"
alt="favicon"
class="favicon"
@click="testdemo"
/>
</div>
<canvas ref="canvas"></canvas>
<button v-show="!imgLoaded" class="upload-button" @click="handleClick">
<div class="upload-container">
<svg viewBox="0 0 24 24">
<path
d="M19 7v3h-2V7h-3V5h3V2h2v3h3v2h-3zm-3 4V8h-3V5H5a2 2 0 00-2 2v12c0 1.1.9 2 2 2h12a2 2 0 002-2v-8h-3zM5 19l3-4 2 3 3-4 4 5H5z"
fill0="rgba(255, 182, 193, 1)"
fill00="#ff568a"
fill="white"
></path>
</svg>
</div>
</button>
<button v-show="imgLoaded" class="goback" @click="reloadPage">
<svg width="24" height="24" viewBox="0 0 1024 1024">
<g
fill="rgba(255, 255, 255, 1)"
stroke-width="50"
stroke="rgba(255, 255, 255, 1)"
>
<path
d="M511.4 175.3l-31.6 31.6-74.8 74.8-87.7 87.7-71.5 71.5-20.1 20.1c-7.1 7.1-13.9 14.3-18.1 23.7-11.2 25.4-6 53.9 13.6 73.7l13.2 13.2 62.7 62.7 86.8 86.8 80.8 80.8 44.7 44.7 2.1 2.1c6.7 6.7 18.9 7.2 25.5 0 6.6-7.2 7.1-18.3 0-25.5l-30.9-30.9-73.8-73.8-87.1-87.1-71.7-71.7-21.1-21.1-5.3-5.3-1.1-1.1-0.1-0.1c-0.3-0.3-3.9-4.4-2.4-2.6 1.3 1.7-0.1-0.2-0.3-0.5-0.8-1.2-1.5-2.4-2.2-3.6-0.3-0.6-0.7-1.2-1-1.9-0.3-0.6-1.3-3.3-0.5-1 0.7 2.3-0.7-2.4-0.9-3.1-0.4-1.4-1.7-6-0.7-2-0.5-1.9-0.3-4.2-0.3-6.2 0-0.1 0.3-4.8 0.3-4.8 0.5 0.1-0.7 3.6 0 0.7l0.6-2.7c0.3-1.2 2.3-6.2 0.5-2.2 0.8-1.7 1.6-3.4 2.6-5 0.6-0.9 4-5.1 1.3-2.2 1-1.1 1.9-2.2 3-3.3l0.2-0.2 1.2-1.2 14.3-14.3 63.6-63.6 86-86 79.8-79.8 44-44 2.1-2.1c6.7-6.7 7.2-18.9 0-25.5-7.4-6.3-18.6-6.8-25.7 0.3z"
></path>
<path
d="M804.6 494H432.9c-17.2 0-34.5-0.5-51.7 0h-0.7c-9.4 0-18.4 8.3-18 18 0.4 9.8 7.9 18 18 18h371.7c17.2 0 34.5 0.5 51.7 0h0.7c9.4 0 18.4-8.3 18-18-0.5-9.8-8-18-18-18z"
></path>
</g>
</svg>
</button>
<div class="floating-menu" v-if="imgLoaded" @mousedown.stop>
<div>
<div class="info" v-if="info">{{ info }}</div>
<div class="progressbar" v-if="isProcessing || isDone">
<progress max="100" :value="progress"></progress>
</div>
</div>
<div class="opt" v-if="!isProcessing && !isDone">
<div>
<span class="description">Model</span>
<select v-model="model">
<option value="anime_4x">Anime (fast)</option>
<option value="anime_4x_plus">Anime (plus)</option>
<option value="general">General (fast)</option>
<!-- <option value="realx2plus">realx2plus</option> -->
<option value="realx4plus">General (plus)</option>
</select>
</div>
<div>
<span class="description">Run on</span>
<select v-model="backend">
<option value="webgl">WebGL</option>
<option value="webgpu">WebGPU</option>
</select>
</div>
</div>
<button
class="run-button"
v-if="!isProcessing && !isDone"
@click="startTask"
>
<svg viewBox="0 0 24 24">
<path d="M8 5v14l11-7z" fill="rgba(255, 255, 255, 1)"></path>
</svg>
</button>
<button class="save-button" v-if="isDone" @click="saveImage">
<svg width="22" viewBox="0 -4 23.9 30">
<path
fill="#fff"
d="M6.6 2.7h-4v13.2h2.7A2.7 2.7 0 018 18.6a2.7 2.7 0 002.6 2.6h2.7a2.7 2.7 0 002.6-2.6 2.7 2.7 0 012.7-2.7h2.6V2.7h-4a1.3 1.3 0 110-2.7h4A2.7 2.7 0 0124 2.7v18.5a2.7 2.7 0 01-2.7 2.7H2.7A2.7 2.7 0 010 21.2V2.7A2.7 2.7 0 012.7 0h4a1.3 1.3 0 010 2.7zm4 7.4V1.3a1.3 1.3 0 112.7 0v8.8L15 8.4a1.3 1.3 0 011.9 1.8l-4 4a1.3 1.3 0 01-1.9 0l-4-4A1.3 1.3 0 019 8.4z"
></path>
</svg>
</button>
</div>
<div
class="dragLine"
ref="dragLine"
v-show="isDone"
@mousedown.stop="startDraggingLine"
@mousemove.stop="dragLine"
>
<div class="dragBall">
<svg width="30" viewBox="0 0 27 20">
<path fill="#ff3484" d="M9.6 0L0 9.6l9.6 9.6z"></path>
<path fill="#5fb3e5" d="M17 19.2l9.5-9.6L16.9 0z"></path>
</svg>
</div>
</div>
<div v-if="!imgLoaded" class="bottom-svg">
<svg width="100%" viewBox="0 0 1920 140" class="_top-wave_vzxu7_106">
<path
fill="#76c8fe"
d="M1920 0l-107 28c-106 29-320 85-533 93-213 7-427-36-640-50s-427 0-533 7L0 85v171h1920z"
class="_sub-wave_vzxu7_117"
></path>
<path
fill="#009aff"
d="M0 129l64-26c64-27 192-81 320-75 128 5 256 69 384 64 128-6 256-80 384-91s256 43 384 70c128 26 256 26 320 26h64v96H0z"
class="_main-wave_vzxu7_113"
></path>
</svg>
<div class="demo">
<div>No ideas? Try one of these:</div>
<br />
<div>
<img class="demoimg" src="/demo/1.jpg" alt="demo" @click="testdemo" />
<img class="demoimg" src="/demo/2.jpg" alt="demo" @click="testdemo" />
<img class="demoimg" src="/demo/3.jpg" alt="demo" @click="testdemo" />
</div>
</div>
</div>
<!-- <div v-if="!imgLoaded" class="placeholder"></div> -->
</div>
</template>
<script>
import Img from "./image";
export default {
data() {
return {
dragging: false,
touching: false,
imgX: 0,
imgY: 0,
imgScale: 1,
imgInitScale: 1,
linePosition: 0,
drawLine: false,
draggingLine: false,
imgLoaded: false,
dpr: window.devicePixelRatio || 1,
imgName: "output",
img: new Image(),
processedImg: new Image(),
hasAlpha: false,
touchStartImgX: null,
touchStartImgY: null,
touchStartX: null,
touchStartY: null,
touchStartDistance: null,
imgScaleStart: 1,
imgLoaded: false,
input: null,
output: null,
isDragOver: false,
isProcessing: false,
isDone: false,
progress: 0,
model: "anime_4x",
scale: 4,
backend: "webgl",
modelzoo: {
anime_4x: {
fixed: true,
factor: 4,
},
anime_4x_plus: {
fixed: false,
factor: 4,
},
general: {
fixed: true,
factor: 4,
},
realx2plus: {
fixed: false,
factor: 4,
},
realx4plus: {
fixed: false,
factor: 4,
},
},
info: "",
worker: new Worker(new URL("./worker.js", import.meta.url), {
type: "module",
}),
};
},
watch: {
model() {
localStorage.setItem("model", this.model);
},
backend() {
localStorage.setItem("backend", this.backend);
},
},
mounted() {
this.model = localStorage.getItem("model") || "anime_4x";
this.backend = localStorage.getItem("backend") || "webgl";
window.addEventListener("resize", this.handleResize);
this.initializeCanvas();
this.linePosition = this.$refs.canvas.width * 2;
this.$refs.dragLine.style.left = this.linePosition / this.dpr + "px";
},
beforeDestroy() {
window.removeEventListener("resize", this.handleResize);
},
methods: {
initializeCanvas() {
this.updateCanvasSize();
},
updateCanvasSize() {
const container = this.$refs.canvasContainer;
const canvas = this.$refs.canvas;
if (this.imgLoaded) {
this.imgX =
((this.imgX + (this.img.width * this.imgScale) / 2) / canvas.width) *
container.offsetWidth *
this.dpr -
(this.img.width * this.imgScale) / 2;
this.imgY =
((this.imgY + (this.img.height * this.imgScale) / 2) /
canvas.height) *
container.offsetHeight *
this.dpr -
(this.img.height * this.imgScale) / 2;
this.linePosition =
(this.linePosition / canvas.width) * container.offsetWidth * this.dpr;
this.$refs.dragLine.style.left = this.linePosition / this.dpr + "px";
}
canvas.width = container.offsetWidth * this.dpr;
canvas.height = container.offsetHeight * this.dpr;
canvas.style.width = `${container.offsetWidth}px`;
canvas.style.height = `${container.offsetHeight}px`;
this.drawImage();
},
handleResize() {
this.updateCanvasSize();
},
loadImg(src) {
this.img.src = src;
this.img.onload = async () => {
this.imgLoaded = true;
this.drawLine = true;
const imageData = await Jimp.read(this.img.src);
this.input = new Img(imageData.bitmap.width, imageData.bitmap.height);
this.input.data = imageData.bitmap.data;
//check if has alpha channel
for (let i = 3; i < this.input.data.length; i += 4) {
if (this.input.data[i] !== 255) {
this.hasAlpha = true;
break;
}
}
// copy alpha channel
if (this.hasAlpha) {
this.inputAlpha = new Img(
imageData.bitmap.width,
imageData.bitmap.height
);
this.inputAlpha.data = new Uint8Array(this.input.data);
for (let i = 0; i < this.inputAlpha.data.length; i += 4) {
this.inputAlpha.data[i] = this.input.data[i + 3];
this.inputAlpha.data[i + 1] = this.input.data[i + 3];
this.inputAlpha.data[i + 2] = this.input.data[i + 3];
}
}
const canvas = this.$refs.canvas;
const containerWidth = canvas.width;
const containerHeight = canvas.height;
const scaleX = (0.8 * containerWidth) / this.img.width;
const scaleY = (0.8 * containerHeight) / this.img.height;
this.imgScale = Math.min(scaleX, scaleY, 4);
this.imgInitScale = this.imgScale;
this.imgX = (containerWidth - this.img.width * this.imgScale) / 2;
this.imgY = (containerHeight - this.img.height * this.imgScale) * 0.4;
this.drawImage();
};
},
testdemo(event) {
const img = event.target;
this.loadImg(img.src);
},
handleDrop(event) {
if (this.imgLoaded) {
event.preventDefault();
return;
}
const files = event.dataTransfer.files;
if (files && files.length > 0) {
const file = files[0];
this.imgName = file.name
.replace(".jpg", "")
.replace(".jpeg", "")
.replace(".png", "");
const reader = new FileReader();
reader.onload = (e) => {
this.loadImg(e.target.result);
};
reader.readAsDataURL(file);
}
},
handleClick() {
const input = document.createElement("input");
input.type = "file";
input.accept = "image/*";
input.onchange = (e) => {
const file = e.target.files[0];
this.imgName = file.name
.replace(".jpg", "")
.replace(".jpeg", "")
.replace(".png", "");
const reader = new FileReader();
reader.onload = (e) => {
this.loadImg(e.target.result);
};
reader.readAsDataURL(file);
};
input.click();
},
drawImage() {
requestAnimationFrame(() => this.drawImage_());
// this.drawImage_();
},
drawImage_() {
const canvas = this.$refs.canvas;
const ctx = canvas.getContext("2d");
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 绘制第一张图(完整图像)
ctx.drawImage(
this.img,
this.imgX,
this.imgY,
this.img.width * this.imgScale,
this.img.height * this.imgScale
);
// 绘制第二张图(处理后的图像)
if (this.processedImg.src) {
ctx.drawImage(
this.processedImg,
((this.processedImg.width / this.img.width) *
(this.linePosition - this.imgX)) /
this.imgScale,
0,
this.processedImg.width -
((this.processedImg.width / this.img.width) *
(this.linePosition - this.imgX)) /
this.imgScale,
this.processedImg.height,
this.linePosition,
this.imgY,
this.imgX + this.img.width * this.imgScale - this.linePosition,
this.img.height * this.imgScale
);
}
// 如果需要绘制分割线
// if (this.drawLine) {
// ctx.globalAlpha = 0.5;
// ctx.beginPath();
// ctx.moveTo(this.linePosition, 0);
// ctx.lineTo(this.linePosition, canvas.height);
// ctx.strokeStyle = "black";
// ctx.lineWidth = 20;
// ctx.stroke();
// ctx.globalAlpha = 1;
// }
},
startDragging(event) {
const rect = this.$refs.canvas.getBoundingClientRect();
const mouseX = event.clientX - rect.left;
if (Math.abs(mouseX - this.linePosition / this.dpr) < 12) {
this.startDraggingLine(event);
return;
}
this.dragging = true;
},
stopDragging() {
if (this.draggingLine) {
this.stopDraggingLine();
return;
}
this.dragging = false;
},
dragImage(event) {
if (this.dragging) {
this.imgX += event.movementX * this.dpr;
this.imgY += event.movementY * this.dpr;
this.drawImage();
}
if (this.draggingLine) {
this.updateLinePosition(event);
this.drawImage();
}
},
touchDragImage(event) {
if (this.touching) {
const touch = event.touches[0];
this.imgX += touch.clientX - this.touchStartX;
this.imgY += touch.clientY - this.touchStartY;
this.drawImage();
}
if (this.draggingLine) {
this.updateLinePosition(event);
this.drawImage();
}
},
resizeImage(event) {
if (!this.imgLoaded) return;
event.preventDefault();
const canvas = this.$refs.canvas;
const rect = canvas.getBoundingClientRect();
const mouseX = (event.clientX - rect.left) * this.dpr;
const mouseY = (event.clientY - rect.top) * this.dpr;
const prevScale = this.imgScale;
const maxSize = 20 * this.imgInitScale;
const minSize = 0.05 * this.imgInitScale;
if (event.deltaY > 0) {
const newScale = this.imgScale * 0.8;
this.imgScale = Math.min(Math.max(minSize, newScale), maxSize);
} else {
const newScale = this.imgScale * 1.2;
this.imgScale = Math.min(Math.max(minSize, newScale), maxSize);
}
const scaleRatio = this.imgScale / prevScale;
this.imgX = mouseX - (mouseX - this.imgX) * scaleRatio;
this.imgY = mouseY - (mouseY - this.imgY) * scaleRatio;
this.drawImage();
},
touchStart(event) {
this.touching = true;
this.touchStartImgX = this.imgX;
this.touchStartImgY = this.imgY;
if (event.touches.length == 1) {
if (
Math.abs(event.touches[0].clientX - this.linePosition / this.dpr) < 12
) {
this.draggingLine = true;
return;
}
this.touchStartX = event.touches[0].clientX * this.dpr;
this.touchStartY = event.touches[0].clientY * this.dpr;
} else {
this.imgScaleStart = this.imgScale;
const touch1 = event.touches[0];
const touch2 = event.touches[1];
this.touchStartDistance =
Math.sqrt(
Math.pow(touch2.clientX - touch1.clientX, 2) +
Math.pow(touch2.clientY - touch1.clientY, 2)
) * this.dpr;
this.touchStartX = ((touch1.clientX + touch2.clientX) / 2) * this.dpr;
this.touchStartY = ((touch1.clientY + touch2.clientY) / 2) * this.dpr;
}
},
touchMove(event) {
event.preventDefault();
if (!this.touching) {
return;
}
if (event.touches.length == 1) {
const touch = event.touches[0];
const movementX =
touch.clientX * this.dpr -
this.touchStartX +
this.touchStartImgX -
this.imgX;
const movementY =
touch.clientY * this.dpr -
this.touchStartY +
this.touchStartImgY -
this.imgY;
if (this.draggingLine) {
this.updateLinePosition(event.touches[0]);
this.drawImage();
return;
}
if (this.touching) {
this.imgX += movementX;
this.imgY += movementY;
this.drawImage();
}
} else {
const touch1 = event.touches[0];
const touch2 = event.touches[1];
const distance =
Math.sqrt(
Math.pow(touch2.clientX - touch1.clientX, 2) +
Math.pow(touch2.clientY - touch1.clientY, 2)
) * this.dpr;
const canvas = this.$refs.canvas;
const rect = canvas.getBoundingClientRect();
const mouseX = this.touchStartX - rect.left;
const mouseY = this.touchStartY - rect.top;
const scaleChange = distance / this.touchStartDistance;
const prevScale = this.imgScale;
const maxSize = 20 * this.imgInitScale;
const minSize = 0.05 * this.imgInitScale;
const newScale = this.imgScaleStart * scaleChange;
this.imgScale = Math.min(Math.max(minSize, newScale), maxSize);
const scaleRatio = this.imgScale / prevScale;
const movementX =
((touch1.clientX + touch2.clientX) / 2) * this.dpr - this.touchStartX;
const movementY =
((touch1.clientY + touch2.clientY) / 2) * this.dpr - this.touchStartY;
this.imgX = mouseX - (mouseX - this.imgX) * scaleRatio + movementX;
this.imgY = mouseY - (mouseY - this.imgY) * scaleRatio + movementY;
this.touchStartX = ((touch1.clientX + touch2.clientX) / 2) * this.dpr;
this.touchStartY = ((touch1.clientY + touch2.clientY) / 2) * this.dpr;
this.drawImage();
}
},
touchEnd(event) {
if (event.touches.length == 2) {
this.touchStartImgX = this.imgX;
this.touchStartImgY = this.imgY;
const touch1 = event.touches[0];
const touch2 = event.touches[1];
this.touchStartDistance =
Math.sqrt(
Math.pow(touch2.clientX - touch1.clientX, 2) +
Math.pow(touch2.clientY - touch1.clientY, 2)
) * this.dpr;
this.touchStartX = ((touch1.clientX + touch2.clientX) / 2) * this.dpr;
this.touchStartY = ((touch1.clientY + touch2.clientY) / 2) * this.dpr;
return;
}
if (event.touches.length == 1) {
this.touchStartImgX = this.imgX;
this.touchStartImgY = this.imgY;
this.touchStartX = event.touches[0].clientX * this.dpr;
this.touchStartY = event.touches[0].clientY * this.dpr;
return;
}
this.touching = false;
this.draggingLine = false;
this.touchStartImgX = null;
this.touchStartImgY = null;
this.touchStartX = null;
this.touchStartY = null;
this.touchStartDistance = null;
},
startDraggingLine(event) {
event.preventDefault();
if (!this.isDone) return;
this.draggingLine = true;
},
stopDraggingLine() {
this.draggingLine = false;
},
dragLine(event) {
event.preventDefault();
if (this.draggingLine) {
this.updateLinePosition(event);
this.drawImage();
}
},
updateLinePosition(event) {
const rect = this.$refs.canvas.getBoundingClientRect();
this.linePosition = event.clientX * this.dpr - rect.left;
const line = this.$refs.dragLine;
line.style.left = Math.floor(this.linePosition / this.dpr) + "px";
},
startTask() {
if (this.input === null) return;
this.isProcessing = true;
// const worker = new Worker(new URL("./worker.js", import.meta.url), {
// type: "module",
// });
let worker = this.worker;
let start = Date.now();
worker.addEventListener("message", (e) => {
const { progress, done, output, alertmsg, info } = e.data;
if (info) {
this.info = info;
}
if (alertmsg) {
alert(alertmsg);
this.isProcessing = false;
worker.terminate();
return;
}
this.progress = progress;
if (done) {
if (!this.hasAlpha || (this.hasAlpha && this.inputAlpha)) {
this.output = output;
}
this.info = "Processing Image...";
if (this.inputAlpha) {
worker.postMessage({
input: this.inputAlpha.data,
fixed: this.modelzoo[this.model].fixed,
factor: this.modelzoo[this.model].factor,
width: this.inputAlpha.width,
height: this.inputAlpha.height,
model: this.model,
backend: this.backend,
hasAlpha: true,
});
this.inputAlpha = null;
return;
}
if (this.hasAlpha) {
for (let i = 0; i < output.data.length; i += 4) {
if (output.data[i] < 128) this.output.data[i + 3] = 0;
else this.output.data[i + 3] = 255;
}
}
new Jimp(this.output.width, this.output.height, (err, image) => {
if (err) throw err;
image.bitmap.data = this.output.data;
// if (this.scale === 2) {
// image.resize(
// output.width / 2,
// output.height / 2,
// Jimp.RESIZE_BICUBIC
// );
// }
// image.quality(75);
let type = Jimp.MIME_JPEG;
if (this.hasAlpha) type = Jimp.MIME_PNG;
image.getBase64(type, (err, src) => {
if (err) throw err;
this.processedImg.src = src;
this.processedImg.onload = () => {
this.linePosition = this.$refs.canvas.width * 0.5;
this.$refs.dragLine.style.left =
this.linePosition / this.dpr + "px";
this.drawImage();
this.info =
"Done! Time used: " + (Date.now() - start) / 1000 + "s";
};
this.isProcessing = false;
this.isDone = true;
});
});
worker.terminate();
}
});
worker.postMessage({
input: this.input.data,
fixed: this.modelzoo[this.model].fixed,
factor: this.modelzoo[this.model].factor,
width: this.input.width,
height: this.input.height,
model: this.model,
backend: this.backend,
hasAlpha: false,
});
},
saveImage() {
const a = document.createElement("a");
a.href = this.processedImg.src;
if (this.hasAlpha) a.download = this.imgName + ".png";
else a.download = this.imgName + ".jpg";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
},
reloadPage() {
this.worker.terminate();
this.worker = new Worker(new URL("./worker.js", import.meta.url), {
type: "module",
});
//reset
const canvas = this.$refs.canvas;
const ctx = canvas.getContext("2d");
ctx.clearRect(0, 0, canvas.width, canvas.height);
this.dragging = false;
this.touching = false;
this.imgX = 0;
this.imgY = 0;
this.imgScale = 1;
this.imgInitScale = 1;
this.linePosition = 0;
this.drawLine = false;
this.draggingLine = false;
this.imgLoaded = false;
this.dpr = window.devicePixelRatio || 1;
this.img = new Image();
this.processedImg = new Image();
this.hasAlpha = false;
this.touchStartImgX = null;
this.touchStartImgY = null;
this.touchStartX = null;
this.touchStartY = null;
this.touchStartDistance = null;
this.imgScaleStart = 1;
this.imgLoaded = false;
this.input = null;
this.inputAlpha = null;
this.output = null;
this.isDragOver = false;
this.isProcessing = false;
this.isDone = false;
this.progress = 0;
this.model = localStorage.getItem("model") || "anime_4x";
this.scale = 4;
this.backend = localStorage.getItem("backend") || "webgl";
this.info = "";
},
},
};
</script>

248
src/assets/main.css Normal file
View File

@@ -0,0 +1,248 @@
@font-face {
font-family: "Roboto-Bold";
src: url("/src/assets/Roboto-Bold.ttf") format("truetype");
}
body,
html,
#app {
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
.canvas-container {
display: flex;
justify-content: center;
align-items: center;
position: relative;
width: 100%;
height: 100%;
display: block;
}
.bg {
background-color: #ffffff;
background-image: linear-gradient(
45deg,
#f3f3f3 25%,
transparent 25%,
transparent 75%,
#f3f3f3 75%,
#f3f3f3
),
linear-gradient(
45deg,
#f3f3f3 25%,
#ffffff 25%,
#ffffff 75%,
#f3f3f3 75%,
#f3f3f3
);
background-size: 20px 20px;
background-position: 0 0, 10px 10px;
}
.bg.dark {
background-color: #313131;
background-image: linear-gradient(
45deg,
#333333 25%,
transparent 25%,
transparent 75%,
#333333 75%,
#333333
),
linear-gradient(
45deg,
#333333 25%,
#313131 25%,
#313131 75%,
#333333 75%,
#333333
);
background-size: 20px 20px;
background-position: 0 0, 10px 10px;
}
canvas {
height: 100vh;
}
.title {
position: fixed;
top: 6%;
left: 50%;
width: 80%;
transform: translate(-50%, 0%);
text-align: center;
color: brown;
font-size: 32px;
font-family: "Roboto-Bold";
font-weight: 700;
font-style: normal; /* font-weight: 900;font-style: normal; */
margin: 10px;
}
.upload-button {
background-color: #ff568a;
border-width: 0px;
border-radius: 50%;
position: absolute;
top: 40%;
left: 50%;
transform: translate(-50%, -60%);
z-index: 10;
width: 200px;
height: 200px;
display: flex;
justify-content: center;
align-items: center;
}
.upload-button:hover {
cursor: pointer;
}
.upload-container {
width: 100px;
height: 100px;
}
.bottom-svg {
width: 100%;
position: absolute;
bottom: -20px;
left: 50%;
transform: translate(-50%, 0%);
z-index: 10;
}
.placeholder {
height: 300px;
background-color: #009aff;
}
.demo {
margin-top: -10px;
padding: 20px;
height: 200px;
background-color: #009aff;
text-align: center;
color: #fff;
font-size: 20px;
font-family: "Roboto-Bold";
}
.favicon:hover {
cursor: pointer;
}
.demoimg {
width: 80px;
height: 80px;
border-radius: 50%;
margin: 12px;
}
.demoimg:hover {
cursor: pointer;
}
.description {
display: inline-block;
color: white;
min-width: 60px;
font-family: "Roboto-Bold";
}
select {
width: 120px;
background-color: #1d1d1d;
color: white;
font-size: 16px;
font-family: "Roboto-Bold";
border: none;
border-radius: 5px;
padding: 5px;
margin: 5px;
}
.run-button {
margin-left: 20px;
width: 40px;
height: 40px;
background-color: #1d90ee;
border: none;
border-radius: 25px;
padding: 5px;
margin: 5px;
display: inline-block;
margin-left: 20px;
}
.run-button:hover {
cursor: pointer;
}
.save-button {
margin-left: 20px;
width: 40px;
height: 40px;
background-color: rgb(157, 202, 90);
border: none;
border-radius: 25px;
padding: 5px;
margin: 5px;
display: inline-block;
margin-left: 20px;
}
.save-button:hover {
cursor: pointer;
}
.goback {
position: absolute;
top: 10px;
left: 10px;
width: 40px;
height: 40px;
display: flex;
justify-content: center;
align-items: center;
z-index: 10;
background-color: #d21d5a;
border: none;
border-radius: 20px;
padding: 5px;
margin: 5px;
}
.goback:hover {
cursor: pointer;
}
.floating-menu {
position: absolute;
background-color: #1d1d1d;
border-radius: 20px;
color: white;
max-width: 100%;
width: 280px;
padding: 20px;
bottom: 25px;
left: 50%;
transform: translate(-50%, 0%);
z-index: 10;
display: flex;
justify-content: center;
align-items: center;
transition: all 0.5s ease;
}
.dragLine {
position: fixed;
top: 0;
bottom: 0;
left: 50%;
width: 9px;
transform: translate(-100%, 0);
background-color: rgba(0, 0, 0, 0.6);
display: flex;
justify-content: center;
align-items: center;
}
.dragLine:hover {
cursor: ew-resize;
}
.dragBall {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 56px;
height: 56px;
background-color: rgba(0, 0, 0, 0.9);
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
}

30
src/image.ts Normal file
View File

@@ -0,0 +1,30 @@
export default class Image {
width: number;
height: number;
data: Uint8Array;
constructor(width: number, height: number) {
this.width = width;
this.height = height;
this.data = new Uint8Array(width * height * 4);
}
getImageCrop(
x: number,
y: number,
image: Image,
x1: number,
y1: number,
x2: number,
y2: number
) {
for (let j = y1; j < y2; j++) {
for (let i = x1; i < x2; i++) {
let index = (y + j - y1) * this.width * 4 + (x + i - x1) * 4;
let imageIndex = j * image.width * 4 + i * 4;
this.data[index] = image.data[imageIndex];
this.data[index + 1] = image.data[imageIndex + 1];
this.data[index + 2] = image.data[imageIndex + 2];
this.data[index + 3] = image.data[imageIndex + 3];
}
}
}
}

6
src/main.ts Normal file
View File

@@ -0,0 +1,6 @@
import "./assets/main.css";
import { createApp } from "vue";
import App from "./App.vue";
createApp(App).mount("#app");

42
src/upscale.ts Normal file
View File

@@ -0,0 +1,42 @@
import * as tf from "@tensorflow/tfjs";
import Image from "./image";
export default async function upscale(
image: Image,
model: any
): Promise<Image> {
let tensor = img2tensor(image);
let result = model.predict(tensor) as tf.Tensor;
let resultImage = await tensor2img(result);
return resultImage;
}
function img2tensor(image: Image): tf.Tensor {
let arr = new Float32Array(image.width * image.height * 3);
for (let i = 0; i < image.width * image.height; i++) {
arr[i * 3] = image.data[i * 4] / 255;
arr[i * 3 + 1] = image.data[i * 4 + 1] / 255;
arr[i * 3 + 2] = image.data[i * 4 + 2] / 255;
}
let tensor = tf.tensor4d(arr, [1, image.height, image.width, 3]);
return tensor;
}
async function tensor2img(tensor: tf.Tensor): Promise<Image> {
let [_, height, width, __] = tensor.shape;
let arr = await tensor.data();
let clipped = new Uint8Array(
arr.map((x) => {
x = Math.min(1, Math.max(0, x));
return Math.floor(x * 255);
})
);
let image = new Image(width, height);
for (let i = 0; i < width * height; i++) {
image.data[i * 4] = clipped[i * 3];
image.data[i * 4 + 1] = clipped[i * 3 + 1];
image.data[i * 4 + 2] = clipped[i * 3 + 2];
image.data[i * 4 + 3] = 255;
}
return image;
}

5
src/vue-shim.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
declare module "*.vue" {
import { DefineComponent } from "vue";
const component: DefineComponent<{}, {}, any>;
export default component;
}

360
src/worker.js Normal file
View File

@@ -0,0 +1,360 @@
import * as tf from "@tensorflow/tfjs";
import "@tensorflow/tfjs-backend-webgpu";
import Img from "./image";
import upscale from "./upscale";
self.addEventListener("message", async (e) => {
const { data } = e;
let model_url = "";
if (data?.model === "anime_4x") {
model_url = `/realesrgan/anime_4x/model.json`;
}
if (data?.model === "anime_4x_plus") {
model_url = `/realesrgan/anime_4x_plus/model.json`;
}
if (data?.model === "general") {
model_url = `/realesrgan/general/model.json`;
}
if (data?.model === "realx4plus") {
model_url = `/realesrgan/realx4plus/model.json`;
}
if (!(await tf.setBackend(data?.backend || "webgl"))) {
postMessage({
alertmsg: `${data?.backend} is not supported in your browser.`,
});
return;
}
let model;
try {
model = await tf.loadGraphModel(`indexeddb://${data?.model}`);
console.log("Model loaded successfully");
// self.postMessage({ info: "Model loaded from cache" });
} catch (error) {
self.postMessage({ info: "Downloading model" });
model = await (async () => {
const fetchedModel = await tf.loadGraphModel(model_url);
await fetchedModel.save(`indexeddb://${data?.model}`);
return fetchedModel;
})();
}
if (!model) {
return;
}
const input = new Img(data.width, data.height);
input.data = data.input;
let hasAlpha = data.hasAlpha;
function sendprogress(progress) {
if (hasAlpha) {
self.postMessage({
progress: progress,
info: `Processing Alpha ${progress.toFixed(2)}%`,
});
return;
}
self.postMessage({
progress: progress,
info: `Processing ${progress.toFixed(2)}%`,
});
}
async function enlargeImage(
model,
inputImg,
factor = 4,
tilesize = 32,
padsize = 8
) {
if (hasAlpha) {
tilesize = 16;
padsize = 4;
}
const width = inputImg.width;
const height = inputImg.height;
const output = new Img(width * factor, height * factor);
const total = Math.ceil(width / tilesize) * Math.ceil(height / tilesize);
let current = 0;
let useModel = new Array(total).fill(false);
if (hasAlpha) {
for (let i = 0; i < width; i += tilesize) {
for (let j = 0; j < height; j += tilesize) {
const x1 = Math.max(i, 0);
const y1 = Math.max(j, 0);
const x2 = Math.min(i + tilesize, width);
const y2 = Math.min(j + tilesize, height);
const tile = new Img(x2 - x1, y2 - y1);
tile.getImageCrop(0, 0, input, x1, y1, x2, y2);
for (let k = 4; k < tile.data.length; k += 4) {
if (tile.data[k + 3] !== tile.data[3]) {
useModel[current] = true;
break;
}
}
if (useModel[current]) {
current++;
continue;
}
let scaled = new Img(tile.width * factor, tile.height * factor);
for (let k = 0; k < scaled.data.length; k += 4) {
scaled.data[k] = tile.data[3];
scaled.data[k + 1] = tile.data[3];
scaled.data[k + 2] = tile.data[3];
}
output.getImageCrop(
i * factor,
j * factor,
scaled,
0,
0,
scaled.width,
scaled.height
);
current++;
}
}
current = 0;
for (let i = 0; i < width; i += tilesize) {
for (let j = 0; j < height; j += tilesize) {
if (!useModel[current]) {
current++;
let progress = (current / total) * 100;
sendprogress(progress);
continue;
}
const x1 = Math.max(i - padsize, 0);
const y1 = Math.max(j - padsize, 0);
const x2 = Math.min(i + tilesize + padsize, width);
const y2 = Math.min(j + tilesize + padsize, height);
const pad_left = i - x1;
const pad_top = j - y1;
const pad_right = Math.max(0, x2 - (i + tilesize));
const pad_bottom = Math.max(0, y2 - (j + tilesize));
const tile = new Img(x2 - x1, y2 - y1);
tile.getImageCrop(0, 0, input, x1, y1, x2, y2);
let scaled = await upscale(tile, model);
output.getImageCrop(
i * factor,
j * factor,
scaled,
pad_left * factor,
pad_top * factor,
scaled.width - pad_right * factor,
scaled.height - pad_bottom * factor
);
// console.log(i, j, x2 - x1, y2 - y1);
current++;
let progress = (current / total) * 100;
sendprogress(progress);
}
}
} else {
for (let i = 0; i < width; i += tilesize) {
for (let j = 0; j < height; j += tilesize) {
const x1 = Math.max(i - padsize, 0);
const y1 = Math.max(j - padsize, 0);
const x2 = Math.min(i + tilesize + padsize, width);
const y2 = Math.min(j + tilesize + padsize, height);
const pad_left = i - x1;
const pad_top = j - y1;
const pad_right = Math.max(0, x2 - (i + tilesize));
const pad_bottom = Math.max(0, y2 - (j + tilesize));
const tile = new Img(x2 - x1, y2 - y1);
tile.getImageCrop(0, 0, input, x1, y1, x2, y2);
let scaled = await upscale(tile, model);
output.getImageCrop(
i * factor,
j * factor,
scaled,
pad_left * factor,
pad_top * factor,
scaled.width - pad_right * factor,
scaled.height - pad_bottom * factor
);
// console.log(i, j, x2 - x1, y2 - y1);
current++;
let progress = (current / total) * 100;
sendprogress(progress);
}
}
}
return output;
}
async function enlargeImageWithFixedInput(
model,
inputImg,
factor = 4,
input_size = 64,
min_lap = 12
) {
const width = inputImg.width;
const height = inputImg.height;
const output = new Img(width * factor, height * factor);
let num_x = 1;
for (; (input_size * num_x - width) / (num_x - 1) < min_lap; num_x++);
let num_y = 1;
for (; (input_size * num_y - height) / (num_y - 1) < min_lap; num_y++);
const locs_x = new Array(num_x);
const locs_y = new Array(num_y);
const pad_left = new Array(num_x);
const pad_top = new Array(num_y);
const pad_right = new Array(num_x);
const pad_bottom = new Array(num_y);
const total_lap_x = input_size * num_x - width;
const total_lap_y = input_size * num_y - height;
const base_lap_x = Math.floor(total_lap_x / (num_x - 1));
const base_lap_y = Math.floor(total_lap_y / (num_y - 1));
const extra_lap_x = total_lap_x - base_lap_x * (num_x - 1);
const extra_lap_y = total_lap_y - base_lap_y * (num_y - 1);
locs_x[0] = 0;
for (let i = 1; i < num_x; i++) {
if (i <= extra_lap_x) {
locs_x[i] = locs_x[i - 1] + input_size - base_lap_x - 1;
} else {
locs_x[i] = locs_x[i - 1] + input_size - base_lap_x;
}
}
locs_y[0] = 0;
for (let i = 1; i < num_y; i++) {
if (i <= extra_lap_y) {
locs_y[i] = locs_y[i - 1] + input_size - base_lap_y - 1;
} else {
locs_y[i] = locs_y[i - 1] + input_size - base_lap_y;
}
}
pad_left[0] = 0;
pad_top[0] = 0;
pad_right[num_x - 1] = 0;
pad_bottom[num_y - 1] = 0;
for (let i = 1; i < num_x; i++) {
pad_left[i] = Math.floor((locs_x[i - 1] + input_size - locs_x[i]) / 2);
}
for (let i = 1; i < num_y; i++) {
pad_top[i] = Math.floor((locs_y[i - 1] + input_size - locs_y[i]) / 2);
}
for (let i = 0; i < num_x - 1; i++) {
pad_right[i] = locs_x[i] + input_size - locs_x[i + 1] - pad_left[i + 1];
}
for (let i = 0; i < num_y - 1; i++) {
pad_bottom[i] = locs_y[i] + input_size - locs_y[i + 1] - pad_top[i + 1];
}
const total = num_x * num_y;
let current = 0;
let useModel = new Array(total).fill(false);
if (hasAlpha) {
for (let i = 0; i < num_x; i++) {
for (let j = 0; j < num_y; j++) {
const x1 = locs_x[i];
const y1 = locs_y[j];
const x2 = locs_x[i] + input_size;
const y2 = locs_y[j] + input_size;
const tile = new Img(input_size, input_size);
tile.getImageCrop(0, 0, inputImg, x1, y1, x2, y2);
let scaled;
for (let k = 4; k < tile.data.length; k += 4) {
if (tile.data[k + 3] !== tile.data[3]) {
useModel[current] = true;
break;
}
}
if (useModel[current]) {
current++;
continue;
}
scaled = new Img(tile.width * factor, tile.height * factor);
for (let k = 0; k < scaled.data.length; k += 4) {
scaled.data[k] = tile.data[3];
scaled.data[k + 1] = tile.data[3];
scaled.data[k + 2] = tile.data[3];
}
output.getImageCrop(
(x1 + pad_left[i]) * factor,
(y1 + pad_top[j]) * factor,
scaled,
pad_left[i] * factor,
pad_top[j] * factor,
scaled.width - pad_right[i] * factor,
scaled.height - pad_bottom[j] * factor
);
// console.log(i, j, x2 - x1, y2 - y1);
current++;
}
}
current = 0;
for (let i = 0; i < num_x; i++) {
for (let j = 0; j < num_y; j++) {
if (!useModel[current]) {
current++;
let progress = (current / total) * 100;
sendprogress(progress);
continue;
}
const x1 = locs_x[i];
const y1 = locs_y[j];
const x2 = locs_x[i] + input_size;
const y2 = locs_y[j] + input_size;
const tile = new Img(input_size, input_size);
tile.getImageCrop(0, 0, inputImg, x1, y1, x2, y2);
let scaled = await upscale(tile, model);
output.getImageCrop(
(x1 + pad_left[i]) * factor,
(y1 + pad_top[j]) * factor,
scaled,
pad_left[i] * factor,
pad_top[j] * factor,
scaled.width - pad_right[i] * factor,
scaled.height - pad_bottom[j] * factor
);
// console.log(i, j, x2 - x1, y2 - y1);
current++;
let progress = (current / total) * 100;
sendprogress(progress);
}
}
} else {
for (let i = 0; i < num_x; i++) {
for (let j = 0; j < num_y; j++) {
const x1 = locs_x[i];
const y1 = locs_y[j];
const x2 = locs_x[i] + input_size;
const y2 = locs_y[j] + input_size;
const tile = new Img(input_size, input_size);
tile.getImageCrop(0, 0, inputImg, x1, y1, x2, y2);
let scaled = await upscale(tile, model);
output.getImageCrop(
(x1 + pad_left[i]) * factor,
(y1 + pad_top[j]) * factor,
scaled,
pad_left[i] * factor,
pad_top[j] * factor,
scaled.width - pad_right[i] * factor,
scaled.height - pad_bottom[j] * factor
);
// console.log(i, j, x2 - x1, y2 - y1);
current++;
let progress = (current / total) * 100;
sendprogress(progress);
}
}
}
return output;
}
let factor = data?.factor || 4;
const start = Date.now();
let output;
try {
if (data?.fixed) {
output = await enlargeImageWithFixedInput(model, input, factor);
} else {
output = await enlargeImage(model, input, factor);
}
} catch (e) {
postMessage({ alertmsg: e.toString() });
}
const end = Date.now();
console.log("Time:", end - start);
postMessage({
progress: 100,
done: true,
output: output,
info: `Processing image...`,
});
});

14
tsconfig.app.json Normal file
View File

@@ -0,0 +1,14 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}

11
tsconfig.json Normal file
View File

@@ -0,0 +1,11 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
}
]
}

19
tsconfig.node.json Normal file
View File

@@ -0,0 +1,19 @@
{
"extends": "@tsconfig/node20/tsconfig.json",
"include": [
"vite.config.*",
"vitest.config.*",
"cypress.config.*",
"nightwatch.conf.*",
"playwright.config.*"
],
"compilerOptions": {
"composite": true,
"noEmit": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"module": "ESNext",
"moduleResolution": "Bundler",
"types": ["node"]
}
}

16
vite.config.ts Normal file
View File

@@ -0,0 +1,16 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})