optimize and improve image enhancement

This commit is contained in:
2024-12-17 00:16:47 +08:00
parent 7896541c2d
commit b11641f62e
19 changed files with 737 additions and 805 deletions

7
components.d.ts vendored
View File

@@ -62,7 +62,6 @@ declare module 'vue' {
CommentList: typeof import('./src/components/CommentReply/src/CommentList/CommentList.vue')['default']
CommentReply: typeof import('./src/components/CommentReply/index.vue')['default']
CompareImage: typeof import('./src/views/Upscale/CompareImage.vue')['default']
CompareResult: typeof import('./src/views/Upscale/CompareResult.vue')['default']
Countdown: typeof import('./src/components/MyUI/Countdown/Countdown.vue')['default']
DatePicker: typeof import('./src/components/MyUI/DatePicker/DatePicker.vue')['default']
Descriptions: typeof import('./src/components/MyUI/Descriptions/Descriptions.vue')['default']
@@ -85,6 +84,7 @@ declare module 'vue' {
LandingPage: typeof import('./src/views/Landing/LandingPage.vue')['default']
List: typeof import('./src/components/MyUI/List/List.vue')['default']
LoadingBar: typeof import('./src/components/MyUI/LoadingBar/LoadingBar.vue')['default']
LoadingGraphic: typeof import('./src/components/LoadingGraphic/LoadingGraphic.vue')['default']
LocationAlbum: typeof import('./src/views/Album/LocationAlbum/LocationAlbum.vue')['default']
LockOutlined: typeof import('@ant-design/icons-vue')['LockOutlined']
LoginFooter: typeof import('./src/views/Login/LoginFooter.vue')['default']
@@ -97,11 +97,11 @@ declare module 'vue' {
Notification: typeof import('./src/components/MyUI/Notification/Notification.vue')['default']
NumberAnimation: typeof import('./src/components/MyUI/NumberAnimation/NumberAnimation.vue')['default']
Pagination: typeof import('./src/components/MyUI/Pagination/Pagination.vue')['default']
ParameterSetting: typeof import('./src/views/Upscale/ParameterSetting.vue')['default']
PeopleAlbum: typeof import('./src/views/Album/PeopleAlbum/PeopleAlbum.vue')['default']
Phoalbum: typeof import('./src/views/Album/Phoalbum/Phoalbum.vue')['default']
Popconfirm: typeof import('./src/components/MyUI/Popconfirm/Popconfirm.vue')['default']
Popover: typeof import('./src/components/MyUI/Popover/Popover.vue')['default']
ProcessResult: typeof import('./src/views/Upscale/ProcessResult.vue')['default']
Progress: typeof import('./src/components/MyUI/Progress/Progress.vue')['default']
QRCode: typeof import('./src/components/MyUI/QRCode/QRCode.vue')['default']
QRLogin: typeof import('./src/views/QRLogin/QRLogin.vue')['default']
@@ -134,7 +134,6 @@ declare module 'vue' {
TabletOutlined: typeof import('@ant-design/icons-vue')['TabletOutlined']
Tabs: typeof import('./src/components/MyUI/Tabs/Tabs.vue')['default']
Tag: typeof import('./src/components/MyUI/Tag/Tag.vue')['default']
TestCompare: typeof import('./src/views/Upscale/TestCompare.vue')['default']
Textarea: typeof import('./src/components/MyUI/Textarea/Textarea.vue')['default']
TextScroll: typeof import('./src/components/MyUI/TextScroll/TextScroll.vue')['default']
ThingAlbum: typeof import('./src/views/Album/ThingAlbum/ThingAlbum.vue')['default']
@@ -143,7 +142,7 @@ declare module 'vue' {
TreeChart: typeof import('./src/components/MyUI/TreeChart/TreeChart.vue')['default']
Upload: typeof import('./src/components/MyUI/Upload/Upload.vue')['default']
UploadImage: typeof import('./src/views/Upscale/UploadImage.vue')['default']
Upscale: typeof import('./src/views/Upscale/Upscale.vue')['default']
Upscale: typeof import('./src/views/Upscale/index.vue')['default']
UserInfoCard: typeof import('./src/components/CommentReply/src/UserInfoCard/UserInfoCard.vue')['default']
UserOutlined: typeof import('@ant-design/icons-vue')['UserOutlined']
Video: typeof import('./src/components/MyUI/Video/Video.vue')['default']

View File

@@ -33,7 +33,7 @@
"buffer": "^6.0.3",
"crypto-js": "^4.2.0",
"echarts": "^5.5.1",
"eslint": "9.16.0",
"eslint": "9.17.0",
"go-captcha-vue": "^2.0.5",
"gsap": "^3.12.5",
"jsencrypt": "^3.3.2",
@@ -43,7 +43,7 @@
"nprogress": "^0.2.0",
"nsfwjs": "^4.2.1",
"pinia": "^2.3.0",
"pinia-plugin-persistedstate-2": "^2.0.27",
"pinia-plugin-persistedstate-2": "^2.0.28",
"qrcode": "^1",
"seedrandom": "^3.0.5",
"swiper": "^11.1.15",
@@ -58,17 +58,17 @@
"ws": "^8.18.0"
},
"devDependencies": {
"@eslint/js": "^9.16.0",
"@eslint/js": "^9.17.0",
"@vitejs/plugin-vue": "^5.2.1",
"eslint-plugin-vue": "^9.32.0",
"globals": "^15.13.0",
"sass": "^1.83.0",
"typescript": "^5.7.2",
"typescript": "^5.6.3",
"typescript-eslint": "^8.18.0",
"unplugin-vue-components": "^0.27.5",
"unplugin-vue-components": "^0.28.0",
"vite": "^6.0.3",
"vite-plugin-bundle-obfuscator": "1.4.0",
"vite-plugin-chunk-split": "^0.5.0",
"vue-tsc": "https://pkg.pr.new/vuejs/language-tools/vue-tsc@3fb59af"
"vue-tsc": "2.1.10"
}
}

View File

@@ -1 +1,11 @@
<svg t="1733761095875" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="50680" width="200" height="200"><path d="M471.87259 871.430008a41.629505 41.629505 0 0 0 41.629506 41.629506 40.98575 40.98575 0 0 0 38.625314-41.629506 40.55658 40.55658 0 0 0-39.912825-40.98575h-1.072925a40.98575 40.98575 0 0 0-39.26907 40.98575z" fill="#B8BDCC" p-id="50681"></path><path d="M738.601844 55.792121a77.036044 77.036044 0 0 1 77.036044 77.036043v758.343672a77.036044 77.036044 0 0 1-77.036044 77.036043H285.398156a77.036044 77.036044 0 0 1-77.036044-77.036043V132.828164A77.036044 77.036044 0 0 1 285.398156 55.792121h453.203688m0-55.792121H285.398156A132.828164 132.828164 0 0 0 152.569992 132.828164v758.343672a132.828164 132.828164 0 0 0 132.828164 132.828164h453.203688a132.828164 132.828164 0 0 0 132.828164-132.828164V132.828164A132.828164 132.828164 0 0 0 738.601844 0z" fill="#B8BDCC" p-id="50682"></path><path d="M713.49539 793.964795H310.50461a39.483655 39.483655 0 0 1-41.629505-36.694049v-519.295893a39.26907 39.26907 0 0 1 41.629505-36.479463h402.99078a39.26907 39.26907 0 0 1 41.629505 36.479463v518.437553A39.483655 39.483655 0 0 1 713.49539 793.964795z" fill="#E3E5EB" p-id="50683"></path><path d="M425.736798 103.430008m25.964795 0l120.596814 0q25.964795 0 25.964795 25.964795l0 0q0 25.964795-25.964795 25.964795l-120.596814 0q-25.964795 0-25.964795-25.964795l0 0q0-25.964795 25.964795-25.964795Z" fill="#B8BDCC" p-id="50684"></path></svg>
<svg t="1733761095875" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
p-id="50680" width="200" height="200">
<path d="M471.87259 871.430008a41.629505 41.629505 0 0 0 41.629506 41.629506 40.98575 40.98575 0 0 0 38.625314-41.629506 40.55658 40.55658 0 0 0-39.912825-40.98575h-1.072925a40.98575 40.98575 0 0 0-39.26907 40.98575z"
fill="#B8BDCC" p-id="50681"></path>
<path d="M738.601844 55.792121a77.036044 77.036044 0 0 1 77.036044 77.036043v758.343672a77.036044 77.036044 0 0 1-77.036044 77.036043H285.398156a77.036044 77.036044 0 0 1-77.036044-77.036043V132.828164A77.036044 77.036044 0 0 1 285.398156 55.792121h453.203688m0-55.792121H285.398156A132.828164 132.828164 0 0 0 152.569992 132.828164v758.343672a132.828164 132.828164 0 0 0 132.828164 132.828164h453.203688a132.828164 132.828164 0 0 0 132.828164-132.828164V132.828164A132.828164 132.828164 0 0 0 738.601844 0z"
fill="#B8BDCC" p-id="50682"></path>
<path d="M713.49539 793.964795H310.50461a39.483655 39.483655 0 0 1-41.629505-36.694049v-519.295893a39.26907 39.26907 0 0 1 41.629505-36.479463h402.99078a39.26907 39.26907 0 0 1 41.629505 36.479463v518.437553A39.483655 39.483655 0 0 1 713.49539 793.964795z"
fill="#E3E5EB" p-id="50683"></path>
<path d="M425.736798 103.430008m25.964795 0l120.596814 0q25.964795 0 25.964795 25.964795l0 0q0 25.964795-25.964795 25.964795l-120.596814 0q-25.964795 0-25.964795-25.964795l0 0q0-25.964795 25.964795-25.964795Z"
fill="#B8BDCC" p-id="50684"></path>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1 +1 @@
<svg t="1733748093314" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3982" width="200" height="200"><path d="M170.666667 0h682.666666c93.866667 0 170.666667 76.8 170.666667 170.666667v682.666666c0 93.866667-76.8 170.666667-170.666667 170.666667H170.666667c-93.866667 0-170.666667-76.8-170.666667-170.666667V170.666667c0-93.866667 76.8-170.666667 170.666667-170.666667z" fill="#32B84C" opacity=".2" p-id="3983"></path><path d="M200.533333 72.533333h622.933334c72.533333 0 128 55.466667 128 128v622.933334c0 72.533333-55.466667 128-128 128H200.533333c-72.533333 0-128-55.466667-128-128V200.533333c0-68.266667 59.733333-128 128-128z" fill="#32B84C" opacity=".3" p-id="3984"></path><path d="M230.4 145.066667h558.933333c46.933333 0 85.333333 38.4 85.333334 85.333333v558.933333c0 46.933333-38.4 85.333333-85.333334 85.333334H230.4c-46.933333 0-85.333333-38.4-85.333333-85.333334V230.4c0-46.933333 38.4-85.333333 85.333333-85.333333z" fill="#32B84C" opacity=".38" p-id="3985"></path><path d="M669.866667 537.6l-234.666667 149.333333c-29.866667 21.333333-68.266667-8.533333-68.266667-42.666666V341.333333c0-34.133333 38.4-64 68.266667-42.666666l234.666667 149.333333c34.133333 21.333333 34.133333 68.266667 0 89.6" fill="#26AE40" p-id="3986"></path></svg>
<svg t="1734340504075" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="21926" width="200" height="200"><path d="M512 1024a512 512 0 1 1 512-512 512 512 0 0 1-512 512z m192-577.194667l-225.066667-178.730666a58.154667 58.154667 0 0 0-61.653333-7.296 55.125333 55.125333 0 0 0-33.194667 51.2v357.461333a55.125333 55.125333 0 0 0 33.194667 51.2 58.112 58.112 0 0 0 61.653333-7.296L704 534.613333a55.466667 55.466667 0 0 0 0-87.808z" fill="#00CAA0" p-id="21927"></path></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 515 B

View File

@@ -0,0 +1 @@
<svg t="1734340833280" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="27444" width="200" height="200"><path d="M0 0h1024v1024H0V0z" fill="#202425" opacity=".01" p-id="27445"></path><path d="M955.733333 512c0 245.077333-198.656 443.733333-443.733333 443.733333S68.266667 757.077333 68.266667 512 266.922667 68.266667 512 68.266667s443.733333 198.656 443.733333 443.733333z" fill="#11AA66" p-id="27446"></path><path d="M512 102.4C285.7984 102.4 102.4 285.7984 102.4 512s183.3984 409.6 409.6 409.6 409.6-183.3984 409.6-409.6S738.2016 102.4 512 102.4zM34.133333 512C34.133333 248.081067 248.081067 34.133333 512 34.133333s477.866667 213.947733 477.866667 477.866667-213.947733 477.866667-477.866667 477.866667S34.133333 775.918933 34.133333 512z" fill="#11AA66" p-id="27447"></path><path d="M787.114667 339.285333a51.2 51.2 0 0 1 0 72.362667l-307.2 307.2a51.2 51.2 0 0 1-72.362667 0l-170.666667-170.666667a51.2 51.2 0 0 1 72.362667-72.362666L443.733333 610.235733l271.018667-271.018666a51.2 51.2 0 0 1 72.362667 0z" fill="#FFFFFF" p-id="27448"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

1
src/assets/svgs/warn.svg Normal file
View File

@@ -0,0 +1 @@
<svg t="1734341153983" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="33694" width="200" height="200"><path d="M512 58.181818C260.654545 58.181818 58.181818 260.654545 58.181818 512S260.654545 965.818182 512 965.818182 965.818182 763.345455 965.818182 512 763.345455 58.181818 512 58.181818zM546.909091 768c0 18.618182-16.290909 34.909091-34.909091 34.909091s-34.909091-16.290909-34.909091-34.909091V395.636364c0-18.618182 16.290909-34.909091 34.909091-34.909091s34.909091 16.290909 34.909091 34.909091v372.363636zM512 325.818182c-25.6 0-46.545455-20.945455-46.545455-46.545455s20.945455-46.545455 46.545455-46.545454 46.545455 20.945455 46.545455 46.545454-20.945455 46.545455-46.545455 46.545455z" fill="#A8ABB0" p-id="33695"></path></svg>

After

Width:  |  Height:  |  Size: 787 B

View File

@@ -0,0 +1,145 @@
<script setup lang="ts">
</script>
<template>
<ul class="loading-animation" hidden="" style="display: none;">
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
</ul>
<ul class="loading-animation alternate" style="display: block;">
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
</ul>
</template>
<style scoped lang="scss">
.loading-animation {
list-style: none;
position: relative;
height: 1px;
width: 200px;
border-bottom: 1px dashed #aaa;
}
.loading-animation li {
margin-top: -3px;
height: 10px;
width: 10px;
border-radius: 10px;
background-color: #000000;
opacity: 0;
box-shadow: 0 0 4px rgba(126, 126, 135, 0.99), 0 0 8px rgba(126, 126, 135, 0.99), 0 0 12px rgba(126, 126, 135, 0.99), 0 0 18px rgba(126, 126, 135, 0.99);
position: absolute;
left: -10%;
animation-name: loading;
animation-duration: 2s;
animation-timing-function: ease-in-out;
animation-iteration-count: infinite;
animation-direction: normal;
}
/* alternate animation */
.loading-animation.alternate li {
animation-name: loading2;
}
.loading-animation li:nth-child(1) {
animation-delay: 250ms;
}
.loading-animation li:nth-child(2) {
animation-delay: 500ms;
}
.loading-animation li:nth-child(3) {
animation-delay: 750ms;
}
.loading-animation li:nth-child(4) {
animation-delay: 1s;
}
.loading-animation li:nth-child(5) {
animation-delay: 1.25s;
}
/* fading only */
@keyframes loading {
0% {
left: 0;
opacity: 0;
}
20%, 60% {
left: 50%;
opacity: 1;
}
90% {
left: 100%;
opacity: 0;
}
100% {
left: 100%;
}
}
/* shrinking and fading */
@keyframes loading2 {
0% {
left: 0;
opacity: 0;
height: 1px;
width: 1px;
margin-top: -2px;
}
20%, 60% {
left: 50%;
opacity: 1;
height: 10px;
width: 10px;
margin-top: -3px;
}
90% {
left: 100%;
opacity: 0;
height: 1px;
width: 1px;
margin-top: 0;
}
100% {
left: 100%;
}
}
/* all of this is extra for the demo */
.loading-animation {
margin: 100px auto 0;
}
.loading-animation:before, .loading-animation:after {
color: grey;
font-family: "Merienda One",serif;
}
.loading-animation:before {
//content: "Loading";
position: absolute;
top: -32px;
left: 39%;
}
.loading-animation:after {
//content: "Please wait";
position: absolute;
top: 12px;
left: 36%;
}
</style>

View File

@@ -599,7 +599,7 @@ defineExpose({
}
.icon-info {
color: @themeColor;
color: #fff;
}
.icon-success {

View File

@@ -19,7 +19,7 @@ export default [
{
path: '/main/photo/upscale',
name: 'upscale',
component: () => import('@/views/Upscale/Upscale.vue'),
component: () => import('@/views/Upscale/index.vue'),
meta: {
requiresAuth: false,
title: '图像修复'

View File

@@ -5,131 +5,120 @@ import i18n from "@/locales";
import {NSFWJS} from "nsfwjs";
import localForage from "localforage";
import {message} from "ant-design-vue";
import Module from "@/workers/imghelper.ts";
import Img from "@/workers/image.ts";
import Module from "@/workers/imghelper.ts";
export const useUpscaleStore = defineStore(
'upscale',
() => {
const image: HTMLImageElement = document.createElement('img');
const imageList = ref<string[]>([]);
const fileList = ref<string[]>([]);
const imageData = ref<string>();
const fileData = ref<string>();
const uploading = ref<boolean>(false);
// 加载图片数据
const img = ref<HTMLImageElement>(new Image());
const wasmModule = ref<any>();
const hasAlpha = ref(false);
const input = ref<Img | null>(null);
const inputAlpha = ref<Img | null>(null);
const wasmModule = ref<any>(null);
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d', {willReadFrequently: true});
const imgLoaded = ref<boolean>(false);
// 处理后的图片
const processedImg = ref<HTMLImageElement>(new Image());
const processedImg = ref<string>('');
const isDone = ref<boolean>(false);
const isProcessing = ref<boolean>(false);
const msg = ref<string>("");
const progressBar = ref<number>(0);
/**
* 图片上传前的校验
* @param file
*/
async function beforeUpload(file: any) {
if (fileList.value.length >= 5) {
async function beforeUpload(file: File) {
uploading.value = true;
const urlData = URL.createObjectURL(file);
image.src = urlData;
// 图片 NSFW 检测
const nsfw: NSFWJS = await initNSFWJs();
const isNSFW: boolean = await predictNSFW(nsfw, image);
if (isNSFW) {
message.error(i18n.global.t('comment.illegalImage'));
fileData.value = '';
uploading.value = false;
return false;
}
uploading.value = true;
if (!window.FileReader) return false;
const reader = new FileReader();
reader.readAsDataURL(file); // 文件转换
reader.onload = async function () {
image.src = reader.result as string;
// 图片 NSFW 检测
const nsfw: NSFWJS = await initNSFWJs();
const isNSFW: boolean = await predictNSFW(nsfw, image);
if (isNSFW) {
message.error(i18n.global.t('comment.illegalImage'));
fileList.value.pop();
uploading.value = false;
return false;
}
fileList.value.push(image.src);
// 加载图片
await loadImg(image.src);
uploading.value = false;
return true;
};
fileData.value = urlData;
await loadImg(image);
uploading.value = false;
imageData.value = "";
processedImg.value = "";
isDone.value = false;
msg.value = "";
progressBar.value = 0;
return true;
}
/**
* 自定义上传图片请求
*/
async function customUploadRequest() {
imageList.value = fileList.value;
}
async function customUploadRequest(_file: any) {
/**
* 移除图片
* @param index
*/
async function removeImage(index: number) {
fileList.value.splice(index, 1);
imageList.value.splice(index, 1);
imageData.value = fileData.value;
}
/**
* 加载图片
* @param src
* @param img
*/
async function loadImg(src: string) {
img.value.src = src;
img.value.onload = async () => {
wasmModule.value = await Module();
if (ctx) {
canvas.width = img.value.width;
canvas.height = img.value.height;
ctx.drawImage(img.value, 0, 0);
const imageData = ctx.getImageData(0, 0, img.value.width, img.value.height);
const data = new Uint8Array(imageData.data.buffer);
input.value = new Img(img.value.width, img.value.height, data);
const numPixels = input.value.width * input.value.height;
const bytesPerImage = numPixels * 4;
const sourcePtr = wasmModule.value._malloc(bytesPerImage);
const targetPtr = wasmModule.value._malloc(bytesPerImage);
wasmModule.value.HEAPU8.set(input.value.data, sourcePtr);
hasAlpha.value = wasmModule.value._check_alpha(sourcePtr, numPixels);
if (hasAlpha.value) {
inputAlpha.value = new Img(img.value.width, img.value.height);
wasmModule.value._copy_alpha_to_rgb(sourcePtr, targetPtr, numPixels);
inputAlpha.value.data.set(
wasmModule.value.HEAPU8.subarray(targetPtr, targetPtr + bytesPerImage)
);
}
wasmModule.value._free(sourcePtr);
wasmModule.value._free(targetPtr);
imgLoaded.value = true;
async function loadImg(img: HTMLImageElement) {
wasmModule.value = await Module();
if (ctx && wasmModule.value) {
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, img.width, img.height);
const data = new Uint8Array(imageData.data.buffer);
input.value = new Img(img.width, img.height, data);
const numPixels = input.value.width * input.value.height;
const bytesPerImage = numPixels * 4;
const sourcePtr = wasmModule.value._malloc(bytesPerImage);
const targetPtr = wasmModule.value._malloc(bytesPerImage);
wasmModule.value.HEAPU8.set(input.value.data, sourcePtr);
hasAlpha.value = wasmModule.value._check_alpha(sourcePtr, numPixels);
if (hasAlpha.value) {
inputAlpha.value = new Img(img.width, img.height);
wasmModule.value._copy_alpha_to_rgb(sourcePtr, targetPtr, numPixels);
inputAlpha.value.data.set(
wasmModule.value.HEAPU8.subarray(targetPtr, targetPtr + bytesPerImage)
);
}
};
wasmModule.value._free(sourcePtr);
wasmModule.value._free(targetPtr);
}
}
return {
imageList,
fileList,
uploading,
imageData,
input,
hasAlpha,
inputAlpha,
wasmModule,
img,
processedImg,
imgLoaded,
wasmModule,
isDone,
isProcessing,
msg,
progressBar,
beforeUpload,
customUploadRequest,
removeImage,
};
}
@@ -137,15 +126,10 @@ export const useUpscaleStore = defineStore(
{
// 开启数据持久化
persistedState: {
persist: true,
persist: false,
storage: localForage,
key: 'upscale',
includePaths: [
'imageList',
'fileList',
'img',
'processedImg',
]
includePaths: [],
}
}
)

View File

@@ -1,6 +1,5 @@
<template>
<CompareImage/>
</template>
<script setup lang="ts">

View File

@@ -1,11 +1,9 @@
<template>
</template>
<script setup lang="ts">
</script>
<template>
</template>
<style scoped lang="scss" src="./index.scss">
</style>

View File

@@ -1,11 +1,8 @@
<script setup lang="ts">
</script>
<template>
</template>
<script setup lang="ts">
</script>
<style scoped lang="scss" src="./index.scss">
</style>

View File

@@ -1,7 +1,7 @@
<template>
<div
ref="canvasContainer"
class="canvas-container drag-over bg dark"
class="canvas-container bg drag-over dark"
@mousedown="startDragging"
@mouseup="stopDragging"
@mouseleave="stopDragging"
@@ -11,9 +11,23 @@
@touchmove="touchMove"
@touchend="touchEnd"
>
<canvas ref="canvas"></canvas>
<div v-if="store.isProcessing" class="canvas-progressbar">
<span class="canvas-progressbar-text">
{{ store.msg }}
</span>
<AProgress
:stroke-color="{
'0%': '#108ee9',
'100%': '#87d068',
}"
:percent="store.progressBar"
:showInfo="false"
/>
</div>
<canvas ref="canvas" v-if="store.imageData || store.processedImg"></canvas>
<div
class="dragLine"
v-if="store.isDone"
ref="dragLine">
<div class="dragBall"
@mousedown.stop="startDraggingLine"
@@ -26,11 +40,20 @@
</svg>
</div>
</div>
<div class="canvas-qr" v-if="!store.isDone && !store.imageData">
<AQrcode :bordered="false" color="rgba(126, 126, 135, 0.48)" :size="200"
:value="'https://git.landaiqing.cn'"
:icon="phone"
:iconSize="40"
/>
<span class="canvas-qr-text">手机扫码上传</span>
</div>
</div>
</template>
<script setup lang="ts">
import img1 from "@/assets/images/1.png";
import img2 from "@/assets/images/2.png";
import useStore from "@/store";
import phone from '@/assets/svgs/qr-phone.svg';
const canvasContainer = ref<HTMLDivElement | null>(null);
const dragging = ref<boolean>(false);
@@ -52,6 +75,7 @@ const touchStartY = ref(0);
const touchStartDistance = ref(0);
const imgScaleStart = ref(1);
const store = useStore().upscale;
const img = ref<HTMLImageElement>(new Image());
const processedImg = ref<HTMLImageElement>(new Image());
@@ -395,30 +419,48 @@ function initCanvasSize() {
}
onMounted(() => {
img.value.src = img1;
processedImg.value.src = img2;
initCanvasSize();
window.addEventListener("resize", handleResize);
});
watch(
() => store.imageData,
(newValue) => {
if (newValue) {
img.value.src = newValue;
img.value.onload = () => {
initCanvasSize();
};
}
}
);
watch(
() => store.processedImg,
(newValue) => {
if (newValue) {
processedImg.value.src = newValue;
processedImg.value.onload = () => {
initCanvasSize();
};
}
}
);
onBeforeUnmount(() => {
window.removeEventListener("resize", handleResize);
store.isDone = false;
store.imageData = "";
store.processedImg = "";
store.isProcessing = false;
store.msg = "";
store.progressBar = 0;
});
watchEffect(() => {
img.value.onload = () => {
drawImage();
};
processedImg.value.onload = () => {
drawImage();
};
});
</script>
<style scoped lang="scss">
.canvas-container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
position: relative;
@@ -496,6 +538,18 @@ canvas {
width: 100%;
}
.canvas-qr {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.canvas-qr-text {
font-size: 16px;
color: rgba(126, 126, 135, 0.48);
}
.dragLine {
position: absolute;
top: 0;
@@ -508,6 +562,7 @@ canvas {
display: flex;
justify-content: center;
align-items: center;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
}
.dragLine:hover {
@@ -527,6 +582,28 @@ canvas {
justify-content: center;
align-items: center;
cursor: ew-resize;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
}
.canvas-progressbar {
position: absolute;
top: 0;
//left: 50%;
//transform: translate(-50%, -50%);
width: 300px;
//height: 100px;
border-radius: 10px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
//background-color: rgba(255, 255, 255, 0.5);
//padding: 10px;
}
.canvas-progressbar-text {
font-size: 16px;
font-weight: bold;
color: white;
}
</style>

View File

@@ -0,0 +1,298 @@
<template>
<div class="upscale-params">
<div class="upscale-params-item">
<div class="upscale-params-item-content">
<span class="upscale-params-title">类型:</span>
<ASelect style="width: 100%" size="large"
v-model:value="model_type"
:options="TypeData.map(item => ({label: item, value: item}))">
</ASelect>
</div>
<div class="upscale-params-item-content">
<span class="upscale-params-title">模型:</span>
<ASelect style="width: 100%" size="large"
v-model:value="model"
:options="modes.map((item: any) => ({label: item, value: item}))">
</ASelect>
</div>
</div>
<div class="upscale-params-item">
<div class="upscale-params-item-content">
<span class="upscale-params-title">比列:</span>
<ASelect style="width: 100%" size="large"
v-model:value="factor"
:options="scales.map((item: any) => ({label: item, value: item}))">
</ASelect>
</div>
<div class="upscale-params-item-content">
<span class="upscale-params-title">分块大小:</span>
<ASelect style="width: 100%" size="large"
v-model:value="tile_size"
:options="tileSize.map((item: any) => ({label: item, value: item}))">
</ASelect>
</div>
</div>
<div class="upscale-params-item">
<div class="upscale-params-item-content">
<span class="upscale-params-title">重复:</span>
<ASelect style="width: 100%" size="large"
v-model:value="min_lap"
:options="overlapList.map((item: any) => ({label: item, value: item}))">
</ASelect>
</div>
<div class="upscale-params-item-content">
<span class="upscale-params-title">运行环境</span>
<ASelect style="width: 100%" size="large"
v-model:value="backend"
:options="backendList.map((item: any) => ({label: item, value: item}))">
</ASelect>
</div>
</div>
<ADivider></ADivider>
<AButton style="width: 100%;" size="large" shape="default" type="default" :loading="upscale.isProcessing"
@click="startTask">
<template #icon>
<AAvatar shape="square" :size="25" :src="run"/>
</template>
<span class="upscale-params-btn">开始</span>
</AButton>
</div>
</template>
<script setup lang="ts">
import {message} from "ant-design-vue";
import Img from "@/workers/image.ts";
import useStore from "@/store";
import run from '@/assets/svgs/run.svg';
const upscale = useStore().upscale;
// ***************参数设置***************
const TypeData = ['realesrgan', 'realcugan'];
const model_type = ref<string>(TypeData[0]);
const ModelConfig = reactive({
realesrgan: {
model: ["anime_fast", "anime_plus", "general_fast", "general_plus"],
factor: [4],
tile_size: [32, 48, 64, 96, 128, 192, 256],
},
realcugan: {
factor: [2, 4],
denoise: {
2: [
"conservative",
"no-denoise",
"denoise1x",
"denoise2x",
"denoise3x",
],
3: ["conservative", "denoise3x"],
4: ["conservative", "no-denoise", "denoise3x"],
},
tile_size: [32, 48, 64, 96, 128, 192, 256, 384, 512],
}
});
const factor = ref<number>(4);
const model = ref(ModelConfig[model_type.value].model[0]);
const modes = computed(() => {
if (model_type.value === "realesrgan") {
return ModelConfig[model_type.value].model;
} else {
return ModelConfig[model_type.value].denoise[factor.value];
}
});
watch(model_type, val => {
if (model_type.value === "realesrgan") {
model.value = ModelConfig[val].model[0];
} else {
model.value = ModelConfig[val].denoise[factor.value][0];
}
});
// Scale
const scales = computed(() => {
return ModelConfig[model_type.value].factor;
});
//tile size
const tile_size = ref<number>(128);
const tileSize = computed(() => {
return ModelConfig[model_type.value].tile_size;
});
// overlap
const overlapList = [0, 4, 8, 12, 16, 20];
const min_lap = ref<number>(overlapList[3]);
// run on
const backendList = ['webgl', 'webgpu'];
const backend = ref<string>(backendList[0]);
// ********************处理图片*******************
const outputData = ref<any>();
const imgCanvas = document.createElement("canvas");
/**
* WebWorker 处理图片
*/
async function startTask() {
console.log(upscale.input);
if (upscale.input === null) return;
upscale.isProcessing = true;
const start = Date.now();
const worker = new Worker(new URL("@/workers/upscale.worker.ts", import.meta.url), {
type: "module",
});
worker.onmessage = (e: MessageEvent<any>) => {
const {progress, done, output, error, info} = e.data;
if (info) {
upscale.msg = info;
}
if (error) {
message.error(error);
upscale.isProcessing = false;
worker.terminate();
return;
}
upscale.progressBar = progress;
if (done) {
if (!upscale.hasAlpha || (upscale.hasAlpha && upscale.inputAlpha)) {
if (upscale.input) {
outputData.value = new Img(
factor.value * upscale.input.width,
factor.value * upscale.input.height,
new Uint8Array(output)
);
}
}
upscale.msg = "Processing Image...";
if (upscale.inputAlpha) {
worker.postMessage(
{
input: upscale.inputAlpha.data.buffer,
factor: factor.value,
tile_size: tile_size.value,
min_lap: min_lap.value,
model_type: model_type.value,
width: upscale.inputAlpha.width,
height: upscale.inputAlpha.height,
model: model.value,
backend: backend.value,
hasAlpha: true,
},
[upscale.inputAlpha.data.buffer]
);
upscale.inputAlpha = null;
return;
}
if (upscale.hasAlpha && upscale.wasmModule) {
const outputArray = new Uint8Array(output);
const sourcePtr = upscale.wasmModule._malloc(outputArray.length);
const targetPtr = upscale.wasmModule._malloc(outputArray.length);
const numPixels = outputArray.length / 4;
upscale.wasmModule.HEAPU8.set(outputArray, sourcePtr);
upscale.wasmModule.HEAPU8.set(outputData.value.data, targetPtr);
upscale.wasmModule._copy_alpha_channel(sourcePtr, targetPtr, numPixels);
outputData.value.data.set(
upscale.wasmModule.HEAPU8.subarray(
targetPtr,
targetPtr + outputArray.length
)
);
upscale.wasmModule._free(sourcePtr);
upscale.wasmModule._free(targetPtr);
upscale.wasmModule = null;
}
const imgCtx = imgCanvas.getContext("2d");
if (imgCtx) {
imgCtx.clearRect(0, 0, imgCanvas.width, imgCanvas.height);
imgCanvas.width = outputData.value.width;
imgCanvas.height = outputData.value.height;
let outImg = imgCtx.createImageData(
outputData.value.width,
outputData.value.height
);
outImg.data.set(outputData.value.data);
upscale.input = null;
upscale.inputAlpha = null;
outputData.value = null;
imgCtx.putImageData(outImg, 0, 0);
let type = "image/jpeg";
const quality = 0.92;
if (upscale.hasAlpha) type = "image/png";
imgCanvas.toBlob(
(blob: any) => {
upscale.processedImg = URL.createObjectURL(blob);
},
type,
quality
);
upscale.msg = "Done! Time used: " + (Date.now() - start) / 1000 + "s";
upscale.isProcessing = false;
upscale.isDone = true;
worker.terminate();
}
}
};
if (upscale.input) {
worker.postMessage(
{
input: upscale.input.data.buffer,
factor: factor.value,
tile_size: tile_size.value,
min_lap: min_lap.value,
model_type: model_type.value,
width: upscale.input.width,
height: upscale.input.height,
model: model.value,
backend: backend.value,
hasAlpha: false,
},
[upscale.input.data.buffer]
);
}
}
</script>
<style scoped lang="scss">
.upscale-params {
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
.upscale-params-item {
display: flex;
flex-direction: row;
justify-content: space-between;
width: 100%;
.upscale-params-item-content {
width: 49%;
.upscale-params-title {
font-size: 13px;
color: rgba(126, 126, 135, 0.99);
}
}
}
.upscale-params-btn {
margin-left: 10px;
font-size: 16px;
font-weight: bold;
}
}
</style>

View File

@@ -1,184 +0,0 @@
<template>
<div class="upscale-comparison-result">
<div class="upscale-comparison-top">
<div class="upscale-comparison-top-left">
<VueCompareImage :left-image="bg" :right-image="bg2"
style="width: 45vw; height: 60vh;border-radius: 10px;box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.5);"
rightImageLabel="修复后"
leftImageLabel="原图"
/>
</div>
<div class="upscale-comparison-top-right" ref="avatarContainer">
<ABadge style="margin-top: 5px;" v-for="(item, index) in upscale.imageList"
:key="index">
<template #count>
<AButton type="text" size="small" class="upscale-file-btn" @click="upscale.removeImage(index)">
<template #icon>
<AAvatar shape="square" :size="20" :src="remove"/>
</template>
</AButton>
</template>
<AAvatar shape="square" :size="avatarSize" @click="bg = item">
<template #icon>
<AImage :src="item" width="100%" height="100%" :preview="true"/>
</template>
</AAvatar>
</ABadge>
</div>
</div>
<ADivider orientation="center" :plain="true"><span class="upscale-comparison-divider-text">保存下载</span>
</ADivider>
<div class="upscale-comparison-bottom">
<AFlex :vertical="false" align="center" justify="space-between" gap="large">
<AButton type="text" size="large" style="width: 50px; height: 50px;">
<template #icon>
<AAvatar shape="square" :size="40" :src="download"/>
</template>
</AButton>
<AButton type="text" style="width: 50px; height: 50px;">
<template #icon>
<AAvatar shape="square" :size="40" :src="save"/>
</template>
</AButton>
<AButton type="text" style="width: 50px; height: 50px;">
<template #icon>
<AAvatar shape="square" :size="35" :src="share"/>
</template>
</AButton>
<AButton type="text" style="width: 50px; height: 50px;">
<template #icon>
<AAvatar shape="square" :size="35" :src="packageDownload"/>
</template>
</AButton>
</AFlex>
<ADivider type="vertical" style="height: 30px;"/>
<AFlex :vertical="false" align="center" justify="space-between" gap="large" style="margin-left: 1%">
<AFlex :vertical="true" align="flex-start" justify="flex-start" gap="small">
<AFlex :vertical="false" align="center" justify="space-between">
<span class="upscale-comparison-bottom-text">处理数量</span>
<ATag color="orange">{{ 5 }}</ATag>
</AFlex>
<AFlex :vertical="false" align="center" justify="space-between">
<span class="upscale-comparison-bottom-text">处理大小</span>
<ATag color="green">{{ upscale.imageList.length }} /mb</ATag>
</AFlex>
</AFlex>
<AFlex :vertical="true" align="flex-start" justify="center" gap="small">
<AFlex :vertical="false" align="center" justify="space-between">
<span class="upscale-comparison-bottom-text">消耗时间</span>
<ATag color="cyan">{{ upscale.imageList.length }} /s</ATag>
</AFlex>
<AFlex :vertical="false" align="center" justify="space-between">
<span class="upscale-comparison-bottom-text">内存消耗</span>
<ATag color="purple">{{ upscale.imageList.length }} /mb</ATag>
</AFlex>
</AFlex>
</AFlex>
</div>
</div>
</template>
<script setup lang="ts">
import {VueCompareImage} from "@/components/VueCompareImage";
import bg1 from "@/assets/images/background.jpg";
import bg2 from "@/assets/images/background.png";
import useStore from "@/store";
import remove from "@/assets/svgs/remove.svg";
import download from "@/assets/svgs/download.svg";
import save from "@/assets/svgs/save.svg";
import share from "@/assets/svgs/share.svg";
import packageDownload from "@/assets/svgs/package-download.svg";
const upscale = useStore().upscale;
const avatarSize = ref<number>(60);
const avatarContainer = ref<HTMLDivElement | null>();
const bg = ref<string>(bg1);
/**
* 更新大小
*/
const updateSize = () => {
if (avatarContainer.value) {
// 设置图片列表大小
const container = avatarContainer.value?.clientWidth || 0;
avatarSize.value = Math.min(0.7 * container, 300); // 设置头像大小为容器宽度的10%最大不超过100
}
};
onMounted(() => {
updateSize();
window.addEventListener('resize', updateSize);
});
onBeforeUnmount(() => {
window.removeEventListener('resize', updateSize);
});
</script>
<style scoped lang="scss">
.upscale-comparison-result {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
.upscale-comparison-top {
width: 100%;
height: 80%;
display: flex;
flex-direction: row;
justify-content: space-between;
.upscale-comparison-top-left {
width: 85%;
height: 100%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
.upscale-comparison-image {
width: 40vw;
height: 60vh;
}
}
.upscale-comparison-top-right {
width: 14%;
height: 60vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
overflow-y: scroll;
border: 1px solid rgba(126, 126, 135, 0.48);
border-radius: 10px;
}
}
.upscale-comparison-divider-text {
font-size: 13px;
color: rgba(126, 126, 135, 0.99);
}
.upscale-comparison-bottom {
width: 100%;
height: 20%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-evenly;
.upscale-comparison-bottom-text {
font-size: 13px;
color: rgba(126, 126, 135, 0.99);
}
}
}
</style>

View File

@@ -1,106 +1,46 @@
<template>
<div class="upscale-upload-container">
<div class="upscale-upload-content">
<div class="upscale-upload-container" ref="containerRef">
<div class="upscale-upload-content" ref="uploadDraggerRef">
<Spin :spinning="upscale.uploading" indicator="magic-ring">
<AUploadDragger
name="image"
accept="image/*"
:multiple="false"
:directory="false"
:maxCount="5"
:maxCount="1"
:beforeUpload="upscale.beforeUpload"
:custom-request="upscale.customUploadRequest"
:disabled="upscale.uploading"
:showUploadList="false">
<div class="upscale-upload-content-main">
<div class="upscale-upload-content-left">
<ABadge :count="upscale.imageList.length" :offset="[-10, 10]">
<AAvatar shape="square" :size="70" :src="file"/>
</ABadge>
<span class="upscale-upload-text">
<ABadge :offset="[-10, 10]">
<template #count>
<AAvatar :size="25" :src="sueccess" v-if="upscale.imageData"/>
<AAvatar :size="25" :src="warn" v-if="!upscale.imageData"/>
</template>
<AAvatar shape="square" :size="70" :src="file"/>
</ABadge>
<span class="upscale-upload-text">
点击或拖拽上传图片
</span>
<span class="upscale-upload-tip">最多一次处理5张图片</span>
</div>
<div class="upscale-upload-content-right" ref="qrcodeContainer">
<AQrcode :value="'https://www.baidu.com'"
class="upscale-upload-qrcode"
:icon="qrphone"
:iconSize="iconSize"
:bordered="false"
:size="qrcodeSize"
color="rgba(126, 126, 135, 1)"/>
<span class="upscale-upload-qr-text">手机扫码上传</span>
</div>
</div>
</AUploadDragger>
</Spin>
</div>
</div>
<ADivider orientation="center" :plain="true"><span class="upscale-file-list-title">图片列表</span></ADivider>
<div class="upscale-file-list" ref="fileListContainer">
<div v-if="upscale.imageList.length > 0">
<AImagePreviewGroup>
<ABadge style="margin-left: 10px;" v-for="(item, index) in upscale.imageList"
:key="index">
<template #count>
<AButton type="text" size="small" class="upscale-file-btn" @click="upscale.removeImage(index)">
<template #icon>
<AAvatar shape="square" :size="20" :src="remove"/>
</template>
</AButton>
</template>
<AAvatar shape="square" :size="avatarSize" v-if="item">
<template #icon>
<AImage :src="item" width="100%" height="100%"/>
</template>
</AAvatar>
</ABadge>
</AImagePreviewGroup>
</div>
</div>
<AEmpty :image="empty" v-if="upscale.imageList.length === 0" :description="null"/>
</template>
<script setup lang="ts">
import file from "@/assets/svgs/file.svg";
import qrphone from "@/assets/svgs/qr-phone.svg";
import empty from "@/assets/svgs/empty.svg";
import useStore from "@/store";
import Spin from "@/components/MyUI/Spin/Spin.vue";
import remove from '@/assets/svgs/remove.svg';
import sueccess from '@/assets/svgs/success.svg';
import warn from '@/assets/svgs/warn.svg';
const upscale = useStore().upscale;
const qrcodeSize = ref<number>(160);
const iconSize = ref<number>(30);
const qrcodeContainer = ref<HTMLElement | null>(null);
const avatarSize = ref<number>(80);
const fileListContainer = ref<HTMLDivElement | null>(null);
/**
* 更新大小
*/
const updateSize = () => {
if (qrcodeContainer.value && fileListContainer.value) {
// 设置 QRCode 大小
const containerWidth = qrcodeContainer.value.clientWidth;
qrcodeSize.value = containerWidth * 0.5; // 设置 QRCode 为父盒子宽度的80%
iconSize.value = Math.min(containerWidth * 0.1, 50); // 设置 icon 大小为父盒子宽度的10%
const uploadDraggerRef = ref<HTMLDivElement | null>(null);
const containerRef = ref<HTMLDivElement | null>(null);
// 设置图片列表大小
const container = fileListContainer.value.clientWidth;
avatarSize.value = Math.min(0.17 * container, 300); // 设置头像大小为容器宽度的10%最大不超过100
}
};
onMounted(() => {
updateSize();
window.addEventListener('resize', updateSize);
});
onBeforeUnmount(() => {
window.removeEventListener('resize', updateSize);
});
</script>
@@ -122,44 +62,24 @@ onBeforeUnmount(() => {
width: 100%;
height: 100%;
display: flex;
flex-direction: row;
flex-direction: column;
align-items: center;
justify-content: space-around;
overflow: scroll;
.upscale-upload-content-left {
width: 50%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-evenly;
.upscale-upload-text {
font-size: 20px;
font-weight: bold;
}
.upscale-upload-btn {
width: 60%;
}
.upscale-upload-tip {
font-size: 12px;
color: rgba(126, 126, 135, 0.99);
}
.upscale-upload-text {
font-size: 20px;
font-weight: bold;
}
.upscale-upload-content-right {
width: 50%;
display: flex;
flex-direction: column;
align-items: center;
.upscale-upload-qr-text {
font-size: 12px;
color: rgba(126, 126, 135, 0.99);
}
.upscale-upload-btn {
width: 60%;
}
.upscale-upload-tip {
font-size: 12px;
color: rgba(126, 126, 135, 0.99);
}
}
}
@@ -204,8 +124,5 @@ onBeforeUnmount(() => {
align-items: center !important;
justify-content: center !important;
}
.upscale-upload-content-right {
display: none !important;
}
}
</style>

View File

@@ -1,388 +0,0 @@
<template>
<div class="upscale-container">
<AFlex :vertical="false" align="center" justify="flex-start">
<AAvatar shape="square" :size="22" :src="ai"/>
<span class="upscale-title">图像修复</span>
</AFlex>
<AFlex class="upscale-content" :vertical="false" align="center" justify="flex-start">
<div class="upscale-content-left">
<ACard class="upscale-content-left-container">
<UploadImage/>
<ADivider orientation="center" :plain="true"><span class="upscale-divider-title">参数设置</span></ADivider>
<div class="upscale-content-left-params">
<div class="upscale-content-params-left">
<div class="upscale-content-params-item">
<div class="upscale-content-params-item-content">
<span class="upscale-content-params-title">类型:</span>
<ASelect style="width: 100%" size="default"
v-model:value="model_type"
:options="TypeData.map(item => ({label: item, value: item}))">
</ASelect>
</div>
<div class="upscale-content-params-item-content">
<span class="upscale-content-params-title">模型:</span>
<ASelect style="width: 100%" size="default"
v-model:value="model"
:options="modes.map((item: any) => ({label: item, value: item}))">
</ASelect>
</div>
</div>
<div class="upscale-content-params-item">
<div class="upscale-content-params-item-content">
<span class="upscale-content-params-title">比列:</span>
<ASelect style="width: 100%" size="default"
v-model:value="factor"
:options="scales.map((item: any) => ({label: item, value: item}))">
</ASelect>
</div>
<div class="upscale-content-params-item-content">
<span class="upscale-content-params-title">分块大小:</span>
<ASelect style="width: 100%" size="default"
v-model:value="tile_size"
:options="tileSize.map((item: any) => ({label: item, value: item}))">
</ASelect>
</div>
</div>
<div class="upscale-content-params-item">
<div class="upscale-content-params-item-content">
<span class="upscale-content-params-title">重复:</span>
<ASelect style="width: 100%" size="default"
v-model:value="min_lap"
:options="overlapList.map((item: any) => ({label: item, value: item}))">
</ASelect>
</div>
<div class="upscale-content-params-item-content">
<span class="upscale-content-params-title">运行环境</span>
<ASelect style="width: 100%" size="default"
v-model:value="backend"
:options="backendList.map((item: any) => ({label: item, value: item}))">
</ASelect>
</div>
</div>
</div>
<div class="upscale-content-params-right">
<AButton type="text" style="width: 60px;height: 60px;">
<template #icon>
<AAvatar shape="square" :size="50" :src="run" @click="startTask()"/>
</template>
</AButton>
</div>
</div>
</ACard>
</div>
<div class="upscale-content-right">
<ACard class="upscale-content-right-container">
<ProcessResult/>
</ACard>
</div>
</AFlex>
</div>
</template>
<script setup lang="ts">
import UploadImage from "@/views/Upscale/UploadImage.vue";
import ai from "@/assets/svgs/ai.svg";
import run from "@/assets/svgs/run.svg";
import ProcessResult from "@/views/Upscale/ProcessResult.vue";
import useStore from "@/store";
import {message} from "ant-design-vue";
import Img from "@/workers/image.ts";
const upscale = useStore().upscale;
// ***************参数设置***************
const TypeData = ['realesrgan', 'realcugan'];
const model_type = ref<string>(TypeData[0]);
const ModelConfig = reactive({
realesrgan: {
model: ["anime_fast", "anime_plus", "general_fast", "general_plus"],
factor: [4],
tile_size: [32, 48, 64, 96, 128, 192, 256],
},
realcugan: {
factor: [2, 4],
denoise: {
2: [
"conservative",
"no-denoise",
"denoise1x",
"denoise2x",
"denoise3x",
],
3: ["conservative", "denoise3x"],
4: ["conservative", "no-denoise", "denoise3x"],
},
tile_size: [32, 48, 64, 96, 128, 192, 256, 384, 512],
}
});
const factor = ref<number>(4);
const model = ref(ModelConfig[model_type.value].model[0]);
const modes = computed(() => {
if (model_type.value === "realesrgan") {
return ModelConfig[model_type.value].model;
} else {
return ModelConfig[model_type.value].denoise[factor.value];
}
});
watch(model_type, val => {
if (model_type.value === "realesrgan") {
model.value = ModelConfig[val].model[0];
} else {
model.value = ModelConfig[val].denoise[factor.value][0];
}
});
// Scale
const scales = computed(() => {
return ModelConfig[model_type.value].factor;
});
//tile size
const tile_size = ref<number>(128);
const tileSize = computed(() => {
return ModelConfig[model_type.value].tile_size;
});
// overlap
const overlapList = [0, 4, 8, 12, 16, 20];
const min_lap = ref<number>(overlapList[3]);
// run on
const backendList = ['webgl', 'webgpu'];
const backend = ref<string>(backendList[0]);
// ********************处理图片*******************
const isProcessing = ref<boolean>(false);
const msg = ref<string>("");
const progressBar = ref<number>(0);
const outputData = ref<any>();
const isDone = ref<boolean>(false);
const imgCanvas = document.createElement("canvas");
const worker = new Worker(new URL("@/workers/upscale.worker.ts", import.meta.url), {
type: "module",
});
/**
* WebWorker 处理图片
*/
async function startTask() {
if (upscale.input === null) return;
isProcessing.value = true;
const start = Date.now();
worker.onmessage = (e: MessageEvent<any>) => {
const {progress, done, output, error, info} = e.data;
if (info) {
message.info(info);
msg.value = info;
}
if (error) {
message.error(error);
isProcessing.value = false;
worker.terminate();
return;
}
progressBar.value = progress;
if (done) {
if (!upscale.hasAlpha || (upscale.hasAlpha && upscale.inputAlpha)) {
if (upscale.input) {
outputData.value = new Img(
factor.value * upscale.input.width,
factor.value * upscale.input.height,
new Uint8Array(output)
);
}
}
msg.value = "Processing Image...";
if (upscale.inputAlpha) {
worker.postMessage(
{
input: upscale.inputAlpha.data.buffer,
factor: factor.value,
tile_size: tile_size.value,
min_lap: min_lap.value,
model_type: model_type.value,
width: upscale.inputAlpha.width,
height: upscale.inputAlpha.height,
model: model.value,
backend: backend.value,
hasAlpha: true,
},
[upscale.inputAlpha.data.buffer]
);
upscale.inputAlpha = null;
return;
}
if (upscale.hasAlpha && upscale.wasmModule) {
const outputArray = new Uint8Array(output);
const sourcePtr = upscale.wasmModule._malloc(outputArray.length);
const targetPtr = upscale.wasmModule._malloc(outputArray.length);
const numPixels = outputArray.length / 4;
upscale.wasmModule.HEAPU8.set(outputArray, sourcePtr);
upscale.wasmModule.HEAPU8.set(outputData.value.data, targetPtr);
upscale.wasmModule._copy_alpha_channel(sourcePtr, targetPtr, numPixels);
outputData.value.data.set(
upscale.wasmModule.HEAPU8.subarray(
targetPtr,
targetPtr + outputArray.length
)
);
upscale.wasmModule._free(sourcePtr);
upscale.wasmModule._free(targetPtr);
upscale.wasmModule = null;
}
const imgCtx = imgCanvas.getContext("2d");
if (imgCtx) {
imgCtx.clearRect(0, 0, imgCanvas.width, imgCanvas.height);
imgCanvas.width = outputData.value.width;
imgCanvas.height = outputData.value.height;
let outImg = imgCtx.createImageData(
outputData.value.width,
outputData.value.height
);
outImg.data.set(outputData.value.data);
upscale.input = null;
upscale.inputAlpha = null;
outputData.value = null;
imgCtx.putImageData(outImg, 0, 0);
let type = "image/jpeg";
const quality = 0.92;
if (upscale.hasAlpha) type = "image/png";
if (upscale.processedImg) {
imgCanvas.toBlob(
(blob: any) => {
upscale.processedImg.src = URL.createObjectURL(blob);
},
type,
quality
);
upscale.processedImg.onload = () => {
msg.value = "Done! Time used: " + (Date.now() - start) / 1000 + "s";
};
}
isProcessing.value = false;
isDone.value = true;
worker.terminate();
}
}
};
if (upscale.input) {
worker.postMessage(
{
input: upscale.input.data.buffer,
factor: factor.value,
tile_size: tile_size.value,
min_lap: min_lap.value,
model_type: model_type.value,
width: upscale.input.width,
height: upscale.input.height,
model: model.value,
backend: backend.value,
hasAlpha: false,
},
[upscale.input.data.buffer]
);
}
}
</script>
<style scoped lang="scss">
.upscale-container {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
.upscale-title {
font-size: 16px;
font-weight: bold;
margin-left: 5px;
}
.upscale-content {
width: 100%;
height: 100%;
margin-top: 5px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
.upscale-content-left {
width: 49%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
.upscale-content-left-container {
width: 100%;
height: 100%;
overflow: auto;
.upscale-divider-title {
font-size: 13px;
color: rgba(126, 126, 135, 0.99);
}
.upscale-content-left-params {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
.upscale-content-params-left {
width: 80%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
.upscale-content-params-item {
width: 30%;
display: flex;
flex-direction: column;
justify-content: center;
.upscale-content-params-title {
font-size: 13px;
color: rgba(126, 126, 135, 0.99);
}
}
}
.upscale-content-params-right {
width: 20%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
}
}
}
.upscale-content-right {
width: 50%;
height: 100%;
.upscale-content-right-container {
width: 100%;
height: 100%;
overflow: auto;
}
}
}
}
</style>

View File

@@ -0,0 +1,78 @@
<template>
<div class="upscale-container">
<AFlex class="upscale-content" :vertical="false" align="center" justify="flex-start">
<div class="upscale-content-left">
<ACard class="upscale-content-left-container">
<div class="upscale-content-left-upload">
<UploadImage/>
</div>
<ADivider orientation="center" :plain="true">
<span class="upscale-divider-title">参数设置</span>
</ADivider>
<ParameterSetting/>
</ACard>
</div>
<div class="upscale-content-right">
<CompareImage style="border-radius: 10px"/>
</div>
</AFlex>
</div>
</template>
<script setup lang="ts">
import UploadImage from "@/views/Upscale/UploadImage.vue";
import CompareImage from "@/views/Upscale/CompareImage.vue";
import ParameterSetting from "@/views/Upscale/ParameterSetting.vue";
</script>
<style scoped lang="scss">
.upscale-container {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
//.upscale-title {
// font-size: 16px;
// font-weight: bold;
// margin-left: 5px;
//}
.upscale-content {
width: 100%;
height: 100%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
.upscale-content-left {
width: 29%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
.upscale-content-left-container {
width: 100%;
height: 100%;
overflow: auto;
.upscale-divider-title {
font-size: 13px;
color: rgba(126, 126, 135, 0.99);
}
}
}
.upscale-content-right {
width: 70%;
height: 100%;
}
}
}
</style>