complete model integration

This commit is contained in:
2025-01-13 19:30:29 +08:00
parent fed52e66f9
commit 9356c00815
66 changed files with 2035 additions and 821 deletions

View File

@@ -5,8 +5,6 @@
</template>
<script setup lang="ts">
import { defineProps } from 'vue';
const props = defineProps<{
src?: string;
defaultSrc: string;

View File

@@ -39,7 +39,7 @@ const props = withDefaults(defineProps<Props>(), {
accept: '*', // 默认支持所有类型
multiple: false,
maxCount: undefined,
tip: 'Upload',
tip: 'ImageUpload',
fit: 'contain',
draggable: true,
disabled: false,

View File

@@ -0,0 +1,242 @@
// 定义类别名称常量
export const CATEGORIES = {
ANIMALS: {en: 'animals', zh: '动物'},
VEHICLES: {en: 'vehicles', zh: '交通工具'},
FURNITURE: {en: 'furniture', zh: '家具'},
FOOD: {en: 'food', zh: '食物'},
SPORTS: {en: 'sports', zh: '运动'},
CONTAINERS: {en: 'containers', zh: '容器'},
ELECTRONICS: {en: 'electronics', zh: '电子产品'},
EVERYDAY_ITEMS: {en: 'everyday_items', zh: '日常物品'},
HOUSEHOLD: {en: 'household', zh: '家居用品'},
HUMAN: {en: 'human', zh: '人类'},
} as const;
// 为每个标签提供中文名称的映射
export const LABELS = {
// Human 人类
'person': {en: 'person', zh: '人'},
// Vehicles 交通工具
'bicycle': {en: 'bicycle', zh: '自行车'},
'car': {en: 'car', zh: '汽车'},
'motorcycle': {en: 'motorcycle', zh: '摩托车'},
'airplane': {en: 'airplane', zh: '飞机'},
'bus': {en: 'bus', zh: '公共汽车'},
'train': {en: 'train', zh: '火车'},
'truck': {en: 'truck', zh: '卡车'},
'boat': {en: 'boat', zh: '船'},
'traffic_light': {en: 'traffic_light', zh: '交通信号灯'},
'fire_hydrant': {en: 'fire_hydrant', zh: '消防栓'},
'stop_sign': {en: 'stop_sign', zh: '停车标志'},
'parking_meter': {en: 'parking_meter', zh: '停车计时器'},
// Furniture 家具
'bench': {en: 'bench', zh: '长椅'},
'chair': {en: 'chair', zh: '椅子'},
'couch': {en: 'couch', zh: '沙发'},
'potted_plant': {en: 'potted_plant', zh: '盆栽'},
'bed': {en: 'bed', zh: '床'},
'dining_table': {en: 'dining_table', zh: '餐桌'},
'toilet': {en: 'toilet', zh: '厕所'},
// Food 食物
'banana': {en: 'banana', zh: '香蕉'},
'apple': {en: 'apple', zh: '苹果'},
'sandwich': {en: 'sandwich', zh: '三明治'},
'orange': {en: 'orange', zh: '橙子'},
'broccoli': {en: 'broccoli', zh: '西兰花'},
'carrot': {en: 'carrot', zh: '胡萝卜'},
'hot_dog': {en: 'hot_dog', zh: '热狗'},
'pizza': {en: 'pizza', zh: '披萨'},
'donut': {en: 'donut', zh: '甜甜圈'},
'cake': {en: 'cake', zh: '蛋糕'},
// Sports 运动
'frisbee': {en: 'frisbee', zh: '飞盘'},
'skis': {en: 'skis', zh: '滑雪板'},
'snowboard': {en: 'snowboard', zh: '滑雪板'},
'sports_ball': {en: 'sports_ball', zh: '运动球'},
'kite': {en: 'kite', zh: '风筝'},
'baseball_bat': {en: 'baseball_bat', zh: '棒球棒'},
'baseball_glove': {en: 'baseball_glove', zh: '棒球手套'},
'skateboard': {en: 'skateboard', zh: '滑板'},
'surfboard': {en: 'surfboard', zh: '冲浪板'},
'tennis_racket': {en: 'tennis_racket', zh: '网球拍'},
// Containers 容器
'bottle': {en: 'bottle', zh: '瓶子'},
'wine_glass': {en: 'wine_glass', zh: '酒杯'},
'cup': {en: 'cup', zh: '杯子'},
'fork': {en: 'fork', zh: '叉子'},
'knife': {en: 'knife', zh: '刀'},
'spoon': {en: 'spoon', zh: '勺子'},
'bowl': {en: 'bowl', zh: '碗'},
// Electronics 电子产品
'tv': {en: 'tv', zh: '电视'},
'laptop': {en: 'laptop', zh: '笔记本电脑'},
'mouse': {en: 'mouse', zh: '鼠标'},
'remote': {en: 'remote', zh: '遥控器'},
'keyboard': {en: 'keyboard', zh: '键盘'},
'cell_phone': {en: 'cell_phone', zh: '手机'},
'microwave': {en: 'microwave', zh: '微波炉'},
'oven': {en: 'oven', zh: '烤箱'},
'toaster': {en: 'toaster', zh: '烤面包机'},
'sink': {en: 'sink', zh: '水槽'},
'refrigerator': {en: 'refrigerator', zh: '冰箱'},
// Everyday Items 日常物品
'backpack': {en: 'backpack', zh: '背包'},
'umbrella': {en: 'umbrella', zh: '雨伞'},
'handbag': {en: 'handbag', zh: '手袋'},
'tie': {en: 'tie', zh: '领带'},
'suitcase': {en: 'suitcase', zh: '行李箱'},
'scissors': {en: 'scissors', zh: '剪刀'},
'teddy_bear': {en: 'teddy_bear', zh: '泰迪熊'},
'hair_dryer': {en: 'hair_dryer', zh: '吹风机'},
'toothbrush': {en: 'toothbrush', zh: '牙刷'},
// Household 家居用品
'book': {en: 'book', zh: '书'},
'clock': {en: 'clock', zh: '时钟'},
'vase': {en: 'vase', zh: '花瓶'},
// Animals 动物
'cat': {en: 'cat', zh: '猫'},
'dog': {en: 'dog', zh: '狗'},
'horse': {en: 'horse', zh: '马'},
'sheep': {en: 'sheep', zh: '羊'},
'cow': {en: 'cow', zh: '牛'},
'elephant': {en: 'elephant', zh: '大象'},
'bear': {en: 'bear', zh: '熊'},
'zebra': {en: 'zebra', zh: '斑马'},
'giraffe': {en: 'giraffe', zh: '长颈鹿'},
'bird': {en: 'bird', zh: '鸟'}
};
// 创建标签到类别的映射(使用 Map 实现高效查找)
export const LABEL_TO_CATEGORY = new Map<string, { en: string, zh: string }>([
// Human 人类
['person', CATEGORIES.HUMAN],
// Vehicles 交通工具
['bicycle', CATEGORIES.VEHICLES],
['car', CATEGORIES.VEHICLES],
['motorcycle', CATEGORIES.VEHICLES],
['airplane', CATEGORIES.VEHICLES],
['bus', CATEGORIES.VEHICLES],
['train', CATEGORIES.VEHICLES],
['truck', CATEGORIES.VEHICLES],
['boat', CATEGORIES.VEHICLES],
['traffic_light', CATEGORIES.VEHICLES],
['fire_hydrant', CATEGORIES.VEHICLES],
['stop_sign', CATEGORIES.VEHICLES],
['parking_meter', CATEGORIES.VEHICLES],
// Furniture 家具
['bench', CATEGORIES.FURNITURE],
['chair', CATEGORIES.FURNITURE],
['couch', CATEGORIES.FURNITURE],
['potted_plant', CATEGORIES.FURNITURE],
['bed', CATEGORIES.FURNITURE],
['dining_table', CATEGORIES.FURNITURE],
['toilet', CATEGORIES.FURNITURE],
// Food 食物
['banana', CATEGORIES.FOOD],
['apple', CATEGORIES.FOOD],
['sandwich', CATEGORIES.FOOD],
['orange', CATEGORIES.FOOD],
['broccoli', CATEGORIES.FOOD],
['carrot', CATEGORIES.FOOD],
['hot_dog', CATEGORIES.FOOD],
['pizza', CATEGORIES.FOOD],
['donut', CATEGORIES.FOOD],
['cake', CATEGORIES.FOOD],
// Sports 运动
['frisbee', CATEGORIES.SPORTS],
['skis', CATEGORIES.SPORTS],
['snowboard', CATEGORIES.SPORTS],
['sports_ball', CATEGORIES.SPORTS],
['kite', CATEGORIES.SPORTS],
['baseball_bat', CATEGORIES.SPORTS],
['baseball_glove', CATEGORIES.SPORTS],
['skateboard', CATEGORIES.SPORTS],
['surfboard', CATEGORIES.SPORTS],
['tennis_racket', CATEGORIES.SPORTS],
// Containers 容器
['bottle', CATEGORIES.CONTAINERS],
['wine_glass', CATEGORIES.CONTAINERS],
['cup', CATEGORIES.CONTAINERS],
['fork', CATEGORIES.CONTAINERS],
['knife', CATEGORIES.CONTAINERS],
['spoon', CATEGORIES.CONTAINERS],
['bowl', CATEGORIES.CONTAINERS],
// Electronics 电子产品
['tv', CATEGORIES.ELECTRONICS],
['laptop', CATEGORIES.ELECTRONICS],
['mouse', CATEGORIES.ELECTRONICS],
['remote', CATEGORIES.ELECTRONICS],
['keyboard', CATEGORIES.ELECTRONICS],
['cell_phone', CATEGORIES.ELECTRONICS],
['microwave', CATEGORIES.ELECTRONICS],
['oven', CATEGORIES.ELECTRONICS],
['toaster', CATEGORIES.ELECTRONICS],
['sink', CATEGORIES.ELECTRONICS],
['refrigerator', CATEGORIES.ELECTRONICS],
// Everyday Items 日常物品
['backpack', CATEGORIES.EVERYDAY_ITEMS],
['umbrella', CATEGORIES.EVERYDAY_ITEMS],
['handbag', CATEGORIES.EVERYDAY_ITEMS],
['tie', CATEGORIES.EVERYDAY_ITEMS],
['suitcase', CATEGORIES.EVERYDAY_ITEMS],
['scissors', CATEGORIES.EVERYDAY_ITEMS],
['teddy_bear', CATEGORIES.EVERYDAY_ITEMS],
['hair_dryer', CATEGORIES.EVERYDAY_ITEMS],
['toothbrush', CATEGORIES.EVERYDAY_ITEMS],
// Household 家居用品
['book', CATEGORIES.HOUSEHOLD],
['clock', CATEGORIES.HOUSEHOLD],
['vase', CATEGORIES.HOUSEHOLD],
// Animals 动物
['cat', CATEGORIES.ANIMALS],
['dog', CATEGORIES.ANIMALS],
['horse', CATEGORIES.ANIMALS],
['sheep', CATEGORIES.ANIMALS],
['cow', CATEGORIES.ANIMALS],
['elephant', CATEGORIES.ANIMALS],
['bear', CATEGORIES.ANIMALS],
['zebra', CATEGORIES.ANIMALS],
['giraffe', CATEGORIES.ANIMALS],
['bird', CATEGORIES.ANIMALS]
]);
// 获取标签所属大类的函数,支持英文和中文返回
export function getCategoryByLabel(label: string, lang: 'en' | 'zh' = 'en'): string | undefined {
const category = LABEL_TO_CATEGORY.get(label);
return category ? category[lang] : undefined;
}
// 获取标签所属大类的函数,支持英文和中文返回
export function getCategoryName(label: string, lang: 'en' | 'zh' = 'en'): string | undefined {
const category = CATEGORIES[label];
return category ? category[lang] : undefined;
}
// 获取标签的小分类名称
export function getLabelName(label: string, lang: 'en' | 'zh' = 'en'): string | undefined {
const labelInfo = LABELS[label];
return labelInfo ? labelInfo[lang] : undefined;
}
// 使用示例
// console.log(getLabelName('person')); // 输出: 'person' (英文)
// console.log(getLabelName('person', 'zh')); // 输出: '人' (中文)

View File

@@ -1,20 +1,20 @@
import PhoalbumIndex from "@/views/Album/Phoalbum/Index.vue";
import PeopleAlbumIndex from "@/views/Album/PeopleAlbum/Index.vue";
import LocationAlbum from "@/views/Album/LocationAlbum/LocationAlbum.vue";
import LocationAlbumIndex from "@/views/Album/LocationAlbum/Index.vue";
import ThingAlbum from "@/views/Album/ThingAlbum/ThingAlbum.vue";
import ThingAlbumIndex from "@/views/Album/ThingAlbum/Index.vue";
import Phoalbum from "@/views/Album/Phoalbum/Phoalbum.vue";
import PeopleAlbum from "@/views/Album/PeopleAlbum/PeopleAlbum.vue";
import PhoalbumDetail from "@/views/Album/Phoalbum/Detail.vue";
import PeopleAlbumDetail from "@/views/Album/PeopleAlbum/Detail.vue";
import LocationAlbumDetail from "@/views/Album/LocationAlbum/Detail.vue";
import ThingAlbumDetail from "@/views/Album/ThingAlbum/Detail.vue";
import PhoalbumPhoalbum from "@/views/Album/Phoalbum/Phoalbum.vue";
import PeopleAlbumPeopleAlbum from "@/views/Album/PeopleAlbum/PeopleAlbum.vue";
import LocationAlbum from "@/views/Album/LocationAlbum/LocationAlbumList.vue";
import LocationAlbumIndex from "@/views/Album/LocationAlbum/LocationAlbum.vue";
import ThingAlbum from "@/views/Album/ThingAlbum/ThingAlbumList.vue";
import ThingAlbumThingAlbum from "@/views/Album/ThingAlbum/ThingAlbum.vue";
import Phoalbum from "@/views/Album/Phoalbum/PhoalbumList.vue";
import PeopleAlbum from "@/views/Album/PeopleAlbum/PeopleAlbumList.vue";
import PhoalbumDetail from "@/views/Album/Phoalbum/PhoalbumDetail.vue";
import PeopleAlbumDetail from "@/views/Album/PeopleAlbum/PeopleAlbumDetail.vue";
import LocationAlbumDetail from "@/views/Album/LocationAlbum/LocationAlbumDetail.vue";
import ThingAlbumDetail from "@/views/Album/ThingAlbum/ThingAlbumDetail.vue";
export default [
{
path: '/main/album/albums',
component: PhoalbumIndex,
component: PhoalbumPhoalbum,
redirect: '/main/album/albums',
children: [
{
@@ -39,7 +39,7 @@ export default [
},
{
path: '/main/album/people',
component: PeopleAlbumIndex,
component: PeopleAlbumPeopleAlbum,
redirect: '/main/album/people',
children: [
{
@@ -89,7 +89,7 @@ export default [
},
{
path: '/main/album/thing',
component: ThingAlbumIndex,
component: ThingAlbumThingAlbum,
redirect: '/main/album/thing',
children: [
{

View File

@@ -1,80 +1,45 @@
import {initNSFWJs, predictNSFW} from "@/utils/nsfw/nsfw.ts";
import i18n from "@/locales";
interface UploadPredictResult {
isAnime: boolean;
hasFace: boolean;
objectArray: string[] | unknown[];
landscape: 'building' | 'forest' | 'glacier' | 'mountain' | 'sea' | 'street' | 'none' | undefined;
}
import {NSFWJS} from "nsfwjs";
import {message} from "ant-design-vue";
// import {loadCocoSsd, loadMobileNet} from "@/utils/tfjs/tfjs.ts";
import {loadModel, predictImage} from "@/utils/tfjs/anime_classifier.ts";
export const useUploadStore = defineStore(
'upload',
() => {
const openUploadDrawer = ref<boolean>(false);
const image: HTMLImageElement = document.createElement('img');
const predictResult = reactive<UploadPredictResult>({
isAnime: false,
hasFace: false,
objectArray: [],
landscape: undefined as 'building' | 'forest' | 'glacier' | 'mountain' | 'sea' | 'street' | 'none' | undefined,
});
/**
* 图片上传前的校验
* @param file
* 打开上传抽屉
*/
async function beforeUpload(file: File) {
image.src = URL.createObjectURL(file);
// 图片 NSFW 检测
const nsfw: NSFWJS = await initNSFWJs();
const isNSFW: boolean = await predictNSFW(nsfw, image);
if (isNSFW) {
message.error(i18n.global.t('comment.illegalImage'));
return false;
}
// const predictions = await loadMobileNet(image);
// console.log(predictions);
//
// const prediction = await loadCocoSsd(image);
// console.log(prediction);
const model = await loadModel('/tfjs/anime_classifier/model.json');
// 进行预测
const output = await predictImage(model, image);
console.log(output);
// console.log('Predicted Class:', predictedClass);
return true;
async function openUploadDrawerFn() {
openUploadDrawer.value = true;
}
/**
* 自定义上传请求
* @param file
* 清除预测结果
*/
async function customUploadRequest(file: any) {
const progress = {percent: 1};
const intervalId = setInterval(() => {
if (progress.percent < 100) {
progress.percent++;
file.onProgress(progress);
} else {
clearInterval(intervalId);
}
}, 100);
file.onSuccess(true);
// file.onSuccess = () => {
// message.success(i18n.global.t('comment.uploadSuccess'));
// };
// file.onError = () => {
// message.error(i18n.global.t('comment.uploadError'));
// };
// return Promise.resolve(file);
function clearPredictResult() {
predictResult.isAnime = false;
predictResult.hasFace = false;
predictResult.objectArray = [];
predictResult.landscape = undefined;
}
return {
openUploadDrawer,
beforeUpload,
customUploadRequest
predictResult,
openUploadDrawerFn,
clearPredictResult
};
},
{

View File

@@ -5,8 +5,8 @@ import i18n from "@/locales";
import {NSFWJS} from "nsfwjs";
import localForage from "localforage";
import {message} from "ant-design-vue";
import Img from "@/workers/image.ts";
import Module from "@/workers/imghelper.ts";
import Img from "@/workers/upscale/image.ts";
import Module from "@/workers/upscale/imghelper.ts";
export const useUpscaleStore = defineStore(

View File

@@ -0,0 +1,19 @@
import * as tf from '@tensorflow/tfjs';
export async function imageToUint8Array(imageElement) {
// 创建一个 TensorFlow.js 图像张量
const tensor = tf.browser.fromPixels(imageElement, 3).toFloat();
// 获取图像的宽度和高度
const width = imageElement.width;
const height = imageElement.height;
// 将张量转为 Uint8ArrayRGB 格式,值范围从 0 到 255
const uint8Array = await tensor.data();
return {
width,
height,
uint8Array
};
}

View File

@@ -0,0 +1,10 @@
export function imageToBase64(img) {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
if (ctx) {
ctx.drawImage(img, 0, 0);
}
return canvas.toDataURL('image/png'); // 或者 'image/jpeg'
}

View File

@@ -2,21 +2,23 @@ import * as nsfwjs from "nsfwjs";
import {NSFWJS} from "nsfwjs";
import * as tf from "@tensorflow/tfjs";
/**
* Initializes the NSFWJS model and returns it.
*/
let isInit: boolean = false;
const initNSFWJs = async (): Promise<NSFWJS> => {
tf.enableProdMode();
if (!isInit) {
const initialLoad: nsfwjs.NSFWJS = await nsfwjs.load("/nsfw/mobilenet_v2_mid/", {
size: 224,
type: "graph"
});
await initialLoad.model.save("indexeddb://nsfwjs-model");
isInit = true;
let nsfwModelCache: NSFWJS | null = null; // 缓存模型实例
// 如果模型已经加载,则直接返回缓存
try {
// 首先尝试从 IndexedDB 加载模型
nsfwModelCache = await nsfwjs.load("indexeddb://nsfwjs-model", {size: 224, type: "graph"});
console.log("NSFWJS 模型成功从 IndexedDB 加载");
} catch (_error) {
console.warn("IndexedDB 中未找到模型,正在从网络加载...");
// 如果 IndexedDB 加载失败,从 URL 加载模型并保存到 IndexedDB
nsfwModelCache = await nsfwjs.load("/nsfw/mobilenet_v2_mid/", {size: 224, type: "graph"});
await nsfwModelCache.model.save("indexeddb://nsfwjs-model");
console.log("NSFWJS 模型已从网络加载并保存到 IndexedDB");
}
return await nsfwjs.load("indexeddb://nsfwjs-model", {size: 224, type: "graph"});
return nsfwModelCache;
};
/**
* Predicts the NSFW score of an image using the NSFWJS model.

View File

@@ -0,0 +1,16 @@
import * as faceapi from '@vladmandic/face-api/dist/face-api.esm-nobundle.js';
export async function loadModel() {
const modelsPath = `/tfjs/face_api/model`;
// 面部识别模型
await faceapi.nets.faceRecognitionNet.load(modelsPath);
}
export async function faceSimilarity(img1: HTMLImageElement, img2: HTMLImageElement) {
const descriptor1 = await faceapi.computeFaceDescriptor(img1);
const descriptor2 = await faceapi.computeFaceDescriptor(img2);
if (descriptor1 instanceof Float32Array && descriptor2 instanceof Float32Array) {
return faceapi.euclideanDistance(descriptor1, descriptor2).toFixed(2);
}
return -1;
}

View File

@@ -1,23 +1,68 @@
import * as tf from '@tensorflow/tfjs';
import '@tensorflow/tfjs-backend-webgl';
// 封装处理图像和推理的工具函数
export async function loadModel(modelPath) {
const model = await tf.loadGraphModel(modelPath);
console.log('Model Loaded');
async function loadModelFromIndexedDBOrUrl(modelName: string, modelUrl: string) {
let model: tf.GraphModel;
tf.setBackend('webgl');
try {
// 尝试从 IndexedDB 加载模型
model = await tf.loadGraphModel(`indexeddb://${modelName}-model`);
console.log("模型成功从 IndexedDB 加载");
} catch (_error) {
console.log("从 URL 下载模型...");
// 如果 IndexedDB 中没有模型,则从 URL 加载并保存到 IndexedDB
model = await tf.loadGraphModel(modelUrl);
await model.save(`indexeddb://${modelName}-model`);
console.log("模型已从 URL 下载并保存到 IndexedDB");
}
return model;
}
// 封装处理图像和推理的工具函数
export async function loadAnimeClassifierModel() {
const modelName = 'anime_classifier';
const modelUrl = '/tfjs/anime_classifier/model.json';
return await loadModelFromIndexedDBOrUrl(modelName, modelUrl);
}
// 处理图片并进行推理
export async function predictImage(model, imageElement) {
export async function animePredictImage(imageElement) {
const model: tf.GraphModel = await loadAnimeClassifierModel();
// 将图片转换为张量
const tensor = tf.browser.fromPixels(imageElement).toFloat();
const tensor = tf.browser.fromPixels(imageElement, 3).toFloat();
const resized = tf.image.resizeBilinear(tensor, [224, 224]); // 调整图片大小为模型输入大小
const input = resized.expandDims(0); // 增加批次维度
// 进行推理
const prediction = model.predict(input);
const prediction: any = model.predict(input);
// 获取预测结果并返回
const resultArray = await prediction.array();
return resultArray[0]; // 返回第一项的预测结果
const result = resultArray[0]; // 获取预测结果数组
return result.indexOf(1) === 0 ? 'Anime' : 'Neutral';
}
// export async function animePredictImage(width: number, height: number, uint8Array: Uint8Array) {
// const model: tf.GraphModel = await loadModel();
//
// // 将 Uint8Array 转换为 Tensor
// const tensor = tf.tensor3d(uint8Array, [height, width, 3], 'int32').toFloat();
//
// // 调整图片大小为模型输入大小
// const resized = tf.image.resizeBilinear(tensor, [224, 224]);
//
// // 增加批次维度
// const input = resized.expandDims(0);
//
// // 进行推理
// const prediction: any = model.predict(input);
//
// // 获取预测结果并返回
// const resultArray = await prediction.array();
// const result = resultArray[0]; // 获取预测结果数组
// return result.indexOf(1) === 0 ? 'Anime' : 'Neutral';
// }

View File

@@ -0,0 +1,65 @@
import * as tf from '@tensorflow/tfjs';
import '@tensorflow/tfjs-backend-webgl';
async function loadModelFromIndexedDBOrUrl(modelName: string, modelUrl: string) {
let model: tf.LayersModel;
tf.setBackend('webgl');
try {
// 尝试从 IndexedDB 加载模型
model = await tf.loadLayersModel(`indexeddb://${modelName}-model`);
console.log("模型成功从 IndexedDB 加载");
} catch (_error) {
console.log("从 URL 下载模型...");
// 如果 IndexedDB 中没有模型,则从 URL 加载并保存到 IndexedDB
model = await tf.loadLayersModel(modelUrl);
await model.save(`indexeddb://${modelName}-model`);
console.log("模型已从 URL 下载并保存到 IndexedDB");
}
return model;
}
// 封装处理图像和推理的工具函数
export async function loadAnimeClassifierProModel() {
const modelName = 'anime_classifier2';
const modelUrl = '/tfjs/anime_classifier2/model.json';
return await loadModelFromIndexedDBOrUrl(modelName, modelUrl);
}
// 处理图片并进行推理
export async function animePredictImagePro(imageElement) {
const model: any = await loadAnimeClassifierProModel();
// 将图片转换为张量
const tensor = tf.browser.fromPixels(imageElement).toFloat();
const imageResized = tf.image.resizeBilinear(tensor, [224, 224]);
const imageReshaped = imageResized.reshape([1, 224, 224, 3]);
const imageNormalized = imageReshaped.div(255);
// 进行推理
const prediction: any = model.predict(imageNormalized);
const predictedClass = tf.argMax(prediction, 1).dataSync()[0];
// const predictedClassConfidence = await prediction.dataSync()[predictedClass].toFixed(2);
// console.log(`预测结果: ${predictedClassName}(${predictedClassConfidence})`);
return ['Anime', 'Furry', 'Neutral'][predictedClass];
}
// export async function animePredictImagePro(width: number, height: number, uint8Array: Uint8Array) {
//
// const model: any = await loadModel();
// // 将图片转换为张量
// const tensor = tf.tensor3d(uint8Array, [height, width, 3], 'int32').toFloat();
// const imageResized = tf.image.resizeBilinear(tensor, [224, 224]);
// const imageReshaped = imageResized.reshape([1, 224, 224, 3]);
// const imageNormalized = imageReshaped.div(255);
//
// // 进行推理
// const prediction: any = model.predict(imageNormalized);
//
//
// const predictedClass = tf.argMax(prediction, 1).dataSync()[0];
// // const predictedClassConfidence = await prediction.dataSync()[predictedClass].toFixed(2);
// // console.log(`预测结果: ${predictedClassName}(${predictedClassConfidence})`);
// return ['Anime', 'Furry', 'Neutral'][predictedClass];
// }

View File

@@ -0,0 +1,42 @@
import '@mediapipe/face_detection';
import '@tensorflow/tfjs-core';
// Register WebGL backend.
import '@tensorflow/tfjs-backend-webgl';
import * as faceDetection from '@tensorflow-models/face-detection';
import * as faceLandmarksDetection from '@tensorflow-models/face-landmarks-detection';
import '@mediapipe/face_mesh';
/**
* 检测人脸
* @param image
*/
export async function detectFaces(image: HTMLImageElement) {
const model = faceDetection.SupportedModels.MediaPipeFaceDetector;
const detectorConfig: any = {
runtime: 'tfjs',
maxFaces: 1,
modelType: 'short', //'short'|'full'
};
const detector = await faceDetection.createDetector(model, detectorConfig);
const estimationConfig = {flipHorizontal: false};
const faces = await detector.estimateFaces(image, estimationConfig);
return faces;
}
/**
* 检测人脸特征点
* @param image
*/
export async function detectionFaceLandmarks(image: HTMLImageElement) {
const model = faceLandmarksDetection.SupportedModels.MediaPipeFaceMesh;
const detectorConfig: any = {
runtime: 'tfjs',
maxFaces: 1,
refineLandmarks: false,
};
const detector = await faceLandmarksDetection.createDetector(model, detectorConfig);
const estimationConfig = {flipHorizontal: false};
const faces = await detector.estimateFaces(image, estimationConfig);
return faces;
}

View File

@@ -0,0 +1,28 @@
import * as faceapi from '@vladmandic/face-api/dist/face-api.esm-nobundle.js';
import '@tensorflow/tfjs-backend-webgl';
import * as tf from '@tensorflow/tfjs';
export async function loadFaceExtractorModel() {
tf.setBackend('webgl'); // set webgl backend
// 模型文件访问路径
const modelsPath = `/tfjs/face_api/model/ssd_mobilenetv1_model-weights_manifest.json`;
// 模型参数-ssdMobilenetv1
await faceapi.nets.ssdMobilenetv1.load(modelsPath);
return new faceapi.SsdMobilenetv1Options({
minConfidence: 0.5, // 0 ~ 1
maxResults: 50, // 0 ~ 100
});
}
export async function fnDetectFace(img: HTMLImageElement) {
const options = await loadFaceExtractorModel();
const detections = await faceapi.detectSingleFace(
img,
options,
);
if (!detections) {
return null;
}
const faceImages = await faceapi.extractFaces(img, [detections]);
return faceImages[0].toDataURL('image/png');
}

View File

@@ -0,0 +1,54 @@
import * as tf from '@tensorflow/tfjs';
import '@tensorflow/tfjs-backend-webgl';
export async function loadLandscapeRecognitionModel() {
const modelName = 'landscape_recognition';
const modelUrl = '/tfjs/landscape_recognition/model.json';
let model: tf.LayersModel;
tf.setBackend('webgl');
try {
// 尝试从 IndexedDB 加载模型
model = await tf.loadLayersModel(`indexeddb://${modelName}-model`);
console.log("模型成功从 IndexedDB 加载");
} catch (_error) {
console.log("从 URL 下载模型...");
// 如果 IndexedDB 中没有模型,则从 URL 加载模型
model = await tf.loadLayersModel(modelUrl);
await model.save(`indexeddb://${modelName}-model`);
console.log("模型已从 URL 下载并保存到 IndexedDB");
}
return model;
}
export const predictLandscape = async (imgElement) => {
if (!imgElement) return;
const model = await loadLandscapeRecognitionModel();
const img = tf.cast(tf.browser.fromPixels(imgElement), 'float32').resizeBilinear([150, 150]);
const offset = tf.scalar(127.5);
const normalized = img.sub(offset).div(offset);
const batched = normalized.reshape([1, 150, 150, 3]);
const results: any = model.predict(batched);
return getCategory(results.dataSync().indexOf(results.max().dataSync()[0]));
};
const getCategory = (index: number) => {
switch (index) {
case 0:
return "building";
case 1:
return "forest";
case 2:
return "glacier";
case 3:
return "mountain";
case 4:
return "sea";
case 5:
return "street";
default:
return "none";
}
};

View File

@@ -1,6 +1,7 @@
import * as tf from '@tensorflow/tfjs';
import * as mobilenet from '@tensorflow-models/mobilenet';
import * as cocoSsd from '@tensorflow-models/coco-ssd';
import '@tensorflow/tfjs-backend-webgl';
// 确保 TensorFlow.js 已准备好并设置后端
async function initializeTensorFlow(backend = "webgl") {
@@ -49,36 +50,43 @@ export async function loadMobileNet(image) {
return await model.classify(image, 3);
}
// 加载 COCO SSD 模型的工具函数
export async function loadCocoSsd(image) {
// 工具函数:加载或缓存模型
export async function loadCocoSsdModel() {
const modelName = "cocoSsd-model";
const modelUrl = '/tfjs/mobilenet/ssd-mobilenet-v2-tfjs-default-v1/model.json';
// 初始化 TensorFlow.js
if (!(await initializeTensorFlow())) {
return;
}
let model;
try {
// 尝试从 IndexedDB 加载模型
model = await cocoSsd.load({
base: 'mobilenet_v2',
modelUrl: `indexeddb://${modelName}`,
});
console.log("COCO SSD model loaded from IndexedDB successfully");
console.log(`${modelName} loaded from IndexedDB successfully`);
} catch (_error) {
console.log("Downloading COCO SSD model...");
console.log(`Downloading ${modelName}...`);
// 如果 IndexedDB 中没有模型则从 URL 加载并保存到 IndexedDB
model = await cocoSsd.load({
base: 'mobilenet_v2',
modelUrl: modelUrl,
});
const Model = await tf.loadGraphModel(modelUrl);
await Model.save(`indexeddb://${modelName}`);
console.log("COCO SSD model downloaded and saved to IndexedDB");
const graphModel = await tf.loadGraphModel(modelUrl);
await graphModel.save(`indexeddb://${modelName}`);
console.log(`${modelName} downloaded and saved to IndexedDB`);
}
return model;
}
// 加载 COCO SSD 模型的工具函数
// 使用提取的加载模型工具函数
export async function cocoSsdPredict(image) {
// 初始化 TensorFlow.js
tf.setBackend('webgl');
if (!(await initializeTensorFlow())) {
return [];
}
// 加载模型
const model = await loadCocoSsdModel();
// 使用模型进行检测
return await model.detect(image);
}

View File

@@ -1,11 +0,0 @@
<script setup lang="ts">
</script>
<template>
<router-view></router-view>
</template>
<style scoped lang="less">
</style>

View File

@@ -1,122 +1,11 @@
<template>
<div class="location-album">
<div class="location-album-header">
<AButton type="link" size="large" class="location-album-button">地点</AButton>
<span class="location-album-count">你一共在2个地点留下足迹</span>
</div>
<div class="location-album-content">
<div class="location-album-container" @click="handleClick">
<img class="background-image" src="/test/5.png" alt=""/>
<div class="overlay">
<span>乌鲁木齐市</span>
<span class="location-album-overlay-count">---</span>
<span class="location-album-overlay-count">16张照片</span>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
const route = useRoute();
const router = useRouter();
function handleClick() {
router.push({ path: route.path + '/1' });
}
</script>
<style scoped lang="scss">
.location-album {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
width: 100%;
height: 100%;
position: relative;
.location-album-header {
width: 100%;
height: 50px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
gap: 10px;
border-bottom: 1px solid #e2e2e2;
<template>
<router-view></router-view>
</template>
.location-album-button {
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 20px;
color: #333;
}
<style scoped lang="less">
.location-album-count {
font-size: 12px;
color: #999;
}
}
.location-album-content {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: flex-start;
width: 100%;
height: 100%;
overflow-y: auto;
padding-top: 20px;
gap: 20px;
.location-album-container {
width: 180px;
height: 180px;
position: relative;
display: inline-block;
background-color: #f5f5f5;
.background-image {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.2); /* 黑色半透明 */
backdrop-filter: blur(2px); /* 背景虚化 */
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: white;
font-size: 16px;
transition: background-color 0.3s ease, backdrop-filter 0.3s ease;
gap: 0;
.location-album-overlay-count {
font-size: 12px;
color: white;
}
}
.overlay:hover {
background-color: rgba(0, 0, 0, 0.1); /* 黑色半透明 */
backdrop-filter: blur(0px); /* 背景虚化 */
cursor: pointer;
}
}
.location-album-container:hover {
box-shadow: 0 0 10px rgba(0, 0, 0, 0.4);
}
}
}
</style>

View File

@@ -0,0 +1,122 @@
<template>
<div class="location-album">
<div class="location-album-header">
<AButton type="link" size="large" class="location-album-button">地点</AButton>
<span class="location-album-count">你一共在2个地点留下足迹</span>
</div>
<div class="location-album-content">
<div class="location-album-container" @click="handleClick">
<img class="background-image" src="/test/5.png" alt=""/>
<div class="overlay">
<span>乌鲁木齐市</span>
<span class="location-album-overlay-count">---</span>
<span class="location-album-overlay-count">16张照片</span>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
const route = useRoute();
const router = useRouter();
function handleClick() {
router.push({ path: route.path + '/1' });
}
</script>
<style scoped lang="scss">
.location-album {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
width: 100%;
height: 100%;
position: relative;
.location-album-header {
width: 100%;
height: 50px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
gap: 10px;
border-bottom: 1px solid #e2e2e2;
.location-album-button {
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 20px;
color: #333;
}
.location-album-count {
font-size: 12px;
color: #999;
}
}
.location-album-content {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: flex-start;
width: 100%;
height: 100%;
overflow-y: auto;
padding-top: 20px;
gap: 20px;
.location-album-container {
width: 180px;
height: 180px;
position: relative;
display: inline-block;
background-color: #f5f5f5;
.background-image {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.2); /* 黑色半透明 */
backdrop-filter: blur(2px); /* 背景虚化 */
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: white;
font-size: 16px;
transition: background-color 0.3s ease, backdrop-filter 0.3s ease;
gap: 0;
.location-album-overlay-count {
font-size: 12px;
color: white;
}
}
.overlay:hover {
background-color: rgba(0, 0, 0, 0.1); /* 黑色半透明 */
backdrop-filter: blur(0px); /* 背景虚化 */
cursor: pointer;
}
}
.location-album-container:hover {
box-shadow: 0 0 10px rgba(0, 0, 0, 0.4);
}
}
}
</style>

View File

@@ -1,11 +0,0 @@
<script setup lang="ts">
</script>
<template>
<router-view></router-view>
</template>
<style scoped lang="less">
</style>

View File

@@ -1,170 +1,11 @@
<template>
<div class="people-album">
<div class="people-album-header">
<ADropdown>
<AButton type="text" size="large" class="people-album-button">
人物
<DownOutlined class="people-album-icon"/>
</AButton>
<template #overlay>
<AMenu>
<AMenuItem> </AMenuItem>
<AMenuItem>已隐藏</AMenuItem>
</AMenu>
</template>
</ADropdown>
</div>
<div class="people-album-content">
<div class="people-album-item" @mouseover="showButton = true" @mouseleave="showButton = false">
<div class="people-album-item-avatar">
<AAvatar :size="86" shape="circle" src="/test/4.png"/>
</div>
<div class="people-album-item-name">
<AButton @click="showAddNameInput" class="people-album-add-name" v-show="showButton && !showInput" type="link"
size="small">
添加名字
</AButton>
<AInput v-show="showInput" @blur="hideAddNameInput" size="small" class="people-album-add-input">
<template #suffix>
<AButton type="link" size="small">完成</AButton>
</template>
</AInput>
</div>
</div>
<div class="people-album-item" @mouseover="showButton = true" @mouseleave="showButton = false">
<div class="people-album-item-avatar">
<AAvatar :size="86" shape="circle" src="/test/4.png"/>
</div>
<div class="people-album-item-name">
<AButton @click="showAddNameInput" class="people-album-add-name" v-show="showButton && !showInput" type="link"
size="small">
添加名字
</AButton>
<AInput v-show="showInput" @blur="hideAddNameInput" size="small" class="people-album-add-input">
<template #suffix>
<AButton type="link" size="small">完成</AButton>
</template>
</AInput>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
const showButton = ref(false);
const showInput = ref(false);
function showAddNameInput() {
showInput.value = true;
showButton.value = false;
}
function hideAddNameInput() {
showInput.value = false;
showButton.value = false;
}
</script>
<style scoped lang="scss">
.people-album {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
width: 100%;
height: 100%;
position: relative;
.people-album-header {
width: 100%;
height: 50px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
gap: 10px;
border-bottom: 1px solid #e2e2e2;
<template>
<router-view></router-view>
</template>
.people-album-button {
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
font-weight: bold;
color: #333;
<style scoped lang="less">
.people-album-icon {
font-size: 12px;
font-weight: bold;
color: #999;
}
}
}
.people-album-content {
width: 100%;
height: 100%;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: flex-start;
padding-top: 20px;
padding-left: 20px;
gap: 20px;
.people-album-item {
width: 130px;
height: 160px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-radius: 10px;
transition: all 0.3s ease-in-out;
position: relative;
cursor: pointer;
.people-album-item-avatar {
width: 100%;
height: 75%;
display: flex;
align-items: center;
justify-content: center;
}
.people-album-item-name {
width: 100%;
height: 25%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
.people-album-add-input {
width: 80%;
}
}
.people-album-add-name {
color: rgba(126, 126, 135, 0.99);
font-size: 12px;
}
.people-album-add-name:hover {
color: #0e87cc;
}
}
.people-album-item:hover {
background-color: rgba(248, 248, 248, 0.74);
opacity: 1;
transform: scale(1.05);
}
}
}
</style>

View File

@@ -0,0 +1,170 @@
<template>
<div class="people-album">
<div class="people-album-header">
<ADropdown>
<AButton type="text" size="large" class="people-album-button">
人物
<DownOutlined class="people-album-icon"/>
</AButton>
<template #overlay>
<AMenu>
<AMenuItem> </AMenuItem>
<AMenuItem>已隐藏</AMenuItem>
</AMenu>
</template>
</ADropdown>
</div>
<div class="people-album-content">
<div class="people-album-item" @mouseover="showButton = true" @mouseleave="showButton = false">
<div class="people-album-item-avatar">
<AAvatar :size="86" shape="circle" src="/test/4.png"/>
</div>
<div class="people-album-item-name">
<AButton @click="showAddNameInput" class="people-album-add-name" v-show="showButton && !showInput" type="link"
size="small">
添加名字
</AButton>
<AInput v-show="showInput" @blur="hideAddNameInput" size="small" class="people-album-add-input">
<template #suffix>
<AButton type="link" size="small">完成</AButton>
</template>
</AInput>
</div>
</div>
<div class="people-album-item" @mouseover="showButton = true" @mouseleave="showButton = false">
<div class="people-album-item-avatar">
<AAvatar :size="86" shape="circle" src="/test/4.png"/>
</div>
<div class="people-album-item-name">
<AButton @click="showAddNameInput" class="people-album-add-name" v-show="showButton && !showInput" type="link"
size="small">
添加名字
</AButton>
<AInput v-show="showInput" @blur="hideAddNameInput" size="small" class="people-album-add-input">
<template #suffix>
<AButton type="link" size="small">完成</AButton>
</template>
</AInput>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
const showButton = ref(false);
const showInput = ref(false);
function showAddNameInput() {
showInput.value = true;
showButton.value = false;
}
function hideAddNameInput() {
showInput.value = false;
showButton.value = false;
}
</script>
<style scoped lang="scss">
.people-album {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
width: 100%;
height: 100%;
position: relative;
.people-album-header {
width: 100%;
height: 50px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
gap: 10px;
border-bottom: 1px solid #e2e2e2;
.people-album-button {
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
font-weight: bold;
color: #333;
.people-album-icon {
font-size: 12px;
font-weight: bold;
color: #999;
}
}
}
.people-album-content {
width: 100%;
height: 100%;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: flex-start;
padding-top: 20px;
padding-left: 20px;
gap: 20px;
.people-album-item {
width: 130px;
height: 160px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-radius: 10px;
transition: all 0.3s ease-in-out;
position: relative;
cursor: pointer;
.people-album-item-avatar {
width: 100%;
height: 75%;
display: flex;
align-items: center;
justify-content: center;
}
.people-album-item-name {
width: 100%;
height: 25%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
.people-album-add-input {
width: 80%;
}
}
.people-album-add-name {
color: rgba(126, 126, 135, 0.99);
font-size: 12px;
}
.people-album-add-name:hover {
color: #0e87cc;
}
}
.people-album-item:hover {
background-color: rgba(248, 248, 248, 0.74);
opacity: 1;
transform: scale(1.05);
}
}
}
</style>

View File

@@ -1,9 +0,0 @@
<template>
<router-view></router-view>
</template>
<script setup lang="ts">
</script>
<style scoped lang="less">
</style>

View File

@@ -1,180 +1,9 @@
<template>
<div class="phoalbum">
<div class="phoalbum-header">
<AButton type="primary" shape="round" size="middle">
<template #icon>
<PlusSquareOutlined/>
</template>
创建相册
</AButton>
<ADropdown>
<AButton type="default" shape="round" size="middle">
<template #icon>
<OrderedListOutlined/>
</template>
排序
</AButton>
<template #overlay>
<AMenu>
<AMenuItem key="1">按时间排序</AMenuItem>
<AMenuItem key="2">按名称排序</AMenuItem>
</AMenu>
</template>
</ADropdown>
<AInput class="phoalbum-search" placeholder="搜索相册">
<template #suffix>
<AButton size="small" type="text" shape="circle" @click.prevent>
<template #icon>
<SearchOutlined/>
</template>
</AButton>
</template>
</AInput>
</div>
<div class="phoalbum-content">
<ATabs size="small" :tabBarGutter="50" type="line" tabPosition="top" :tabBarStyle="{position:'unset'}"
style="width: 100%;">
<template #rightExtra>
<span style="color: #999; font-size: 12px;">已全部加载 0 个相册</span>
</template>
<ATabPane key="1" tab="全部相册">
<div class="phoalbum-item-container">
<div class="phoalbum-item" @mouseover="isHovered = true" @mouseleave="isHovered = false">
<PhotoStack :src="'/test/1.png'" default-src=""/>
<div class="phoalbum-item-info">
<span class="phoalbum-item-name">我的相册</span>
<span class="phoalbum-item-date">2022-01-01</span>
</div>
<div class="phoalbum-item-operation" :class="{ 'fade-in': isHovered, 'fade-out': !isHovered }">
<ADropdown trigger="click">
<AButton type="text" shape="circle" size="small">
<template #icon>
<AAvatar shape="circle" size="small" :src="more"/>
</template>
</AButton>
<template #overlay>
<AMenu>
<AMenuItem key="1">重命名相册</AMenuItem>
<AMenuItem key="2">分享相册</AMenuItem>
<AMenuItem key="3">删除相册</AMenuItem>
<AMenuItem key="4">下载相册</AMenuItem>
</AMenu>
</template>
</ADropdown>
</div>
</div>
</div>
</ATabPane>
<ATabPane key="2" tab="我的相册">
</ATabPane>
<ATabPane key="3" tab="收藏相册">
</ATabPane>
</ATabs>
</div>
</div>
<router-view></router-view>
</template>
<script setup lang="ts">
import more from "@/assets/svgs/more.svg";
const isHovered = ref<boolean>(false);
</script>
<style scoped lang="scss">
.phoalbum {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
width: 100%;
height: 100%;
position: relative;
<style scoped lang="less">
.phoalbum-header {
width: 100%;
height: 50px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
gap: 10px;
border-bottom: 1px solid #e2e2e2;
.phoalbum-search {
width: 300px;
border-radius: 20px;
}
}
.phoalbum-content {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
width: 100%;
height: calc(100% - 65px);
.phoalbum-item-container {
display: flex;
flex-direction: row;
align-items: flex-start;
justify-content: flex-start;
gap: 20px;
width: 100%;
height: 100%;
overflow-y: auto;
.phoalbum-item {
width: 200px;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
gap: 15px;
padding: 10px;
position: relative;
cursor: pointer;
.phoalbum-item-info {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
.phoalbum-item-name {
font-size: 14px;
font-weight: bold;
color: #333;
}
.phoalbum-item-date {
font-size: 12px;
color: #999;
}
}
.phoalbum-item-operation {
position: absolute;
top: 20px;
right: 20px;
z-index: 10;
opacity: 0;
transform: scale(0);
transition: opacity 0.3s ease, transform 0.3s ease;
}
.fade-in {
opacity: 1; /* 显示时透明度为1 */
transform: scale(1); /* 显示时缩放为1 */
z-index: 10; /* 显示时z-index为10 */
}
.fade-out {
opacity: 0; /* 隐藏时透明度为0 */
transform: scale(0); /* 隐藏时缩放为0 */
}
}
}
}
}
</style>

View File

@@ -0,0 +1,180 @@
<template>
<div class="phoalbum">
<div class="phoalbum-header">
<AButton type="primary" shape="round" size="middle">
<template #icon>
<PlusSquareOutlined/>
</template>
创建相册
</AButton>
<ADropdown>
<AButton type="default" shape="round" size="middle">
<template #icon>
<OrderedListOutlined/>
</template>
排序
</AButton>
<template #overlay>
<AMenu>
<AMenuItem key="1">按时间排序</AMenuItem>
<AMenuItem key="2">按名称排序</AMenuItem>
</AMenu>
</template>
</ADropdown>
<AInput class="phoalbum-search" placeholder="搜索相册">
<template #suffix>
<AButton size="small" type="text" shape="circle" @click.prevent>
<template #icon>
<SearchOutlined/>
</template>
</AButton>
</template>
</AInput>
</div>
<div class="phoalbum-content">
<ATabs size="small" :tabBarGutter="50" type="line" tabPosition="top" :tabBarStyle="{position:'unset'}"
style="width: 100%;">
<template #rightExtra>
<span style="color: #999; font-size: 12px;">已全部加载 0 个相册</span>
</template>
<ATabPane key="1" tab="全部相册">
<div class="phoalbum-item-container">
<div class="phoalbum-item" @mouseover="isHovered = true" @mouseleave="isHovered = false">
<PhotoStack :src="'/test/1.png'" default-src=""/>
<div class="phoalbum-item-info">
<span class="phoalbum-item-name">我的相册</span>
<span class="phoalbum-item-date">2022-01-01</span>
</div>
<div class="phoalbum-item-operation" :class="{ 'fade-in': isHovered, 'fade-out': !isHovered }">
<ADropdown trigger="click">
<AButton type="text" shape="circle" size="small">
<template #icon>
<AAvatar shape="circle" size="small" :src="more"/>
</template>
</AButton>
<template #overlay>
<AMenu>
<AMenuItem key="1">重命名相册</AMenuItem>
<AMenuItem key="2">分享相册</AMenuItem>
<AMenuItem key="3">删除相册</AMenuItem>
<AMenuItem key="4">下载相册</AMenuItem>
</AMenu>
</template>
</ADropdown>
</div>
</div>
</div>
</ATabPane>
<ATabPane key="2" tab="我的相册">
</ATabPane>
<ATabPane key="3" tab="收藏相册">
</ATabPane>
</ATabs>
</div>
</div>
</template>
<script setup lang="ts">
import more from "@/assets/svgs/more.svg";
const isHovered = ref<boolean>(false);
</script>
<style scoped lang="scss">
.phoalbum {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
width: 100%;
height: 100%;
position: relative;
.phoalbum-header {
width: 100%;
height: 50px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
gap: 10px;
border-bottom: 1px solid #e2e2e2;
.phoalbum-search {
width: 300px;
border-radius: 20px;
}
}
.phoalbum-content {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
width: 100%;
height: calc(100% - 65px);
.phoalbum-item-container {
display: flex;
flex-direction: row;
align-items: flex-start;
justify-content: flex-start;
gap: 20px;
width: 100%;
height: 100%;
overflow-y: auto;
.phoalbum-item {
width: 200px;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
gap: 15px;
padding: 10px;
position: relative;
cursor: pointer;
.phoalbum-item-info {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
.phoalbum-item-name {
font-size: 14px;
font-weight: bold;
color: #333;
}
.phoalbum-item-date {
font-size: 12px;
color: #999;
}
}
.phoalbum-item-operation {
position: absolute;
top: 20px;
right: 20px;
z-index: 10;
opacity: 0;
transform: scale(0);
transition: opacity 0.3s ease, transform 0.3s ease;
}
.fade-in {
opacity: 1; /* 显示时透明度为1 */
transform: scale(1); /* 显示时缩放为1 */
z-index: 10; /* 显示时z-index为10 */
}
.fade-out {
opacity: 0; /* 隐藏时透明度为0 */
transform: scale(0); /* 隐藏时缩放为0 */
}
}
}
}
}
</style>

View File

@@ -1,11 +0,0 @@
<script setup lang="ts">
</script>
<template>
<router-view></router-view>
</template>
<style scoped lang="less">
</style>

View File

@@ -1,120 +1,11 @@
<template>
<div class="thing-album">
<div class="thing-album-header">
<AButton type="link" size="large" class="thing-album-button">事物</AButton>
</div>
<div class="thing-album-content">
<span class="thing-album-title">动物</span>
<div class="thing-album-container">
<img class="background-image" src="/test/7.png" alt=""/>
<div class="overlay">
<span></span>
<span class="thing-album-overlay-count">---</span>
<span class="thing-album-overlay-count">16张照片</span>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
</script>
<style scoped lang="scss">
.thing-album {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
width: 100%;
height: 100%;
position: relative;
.thing-album-header {
width: 100%;
height: 50px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
gap: 10px;
border-bottom: 1px solid #e2e2e2;
<template>
<router-view></router-view>
</template>
.thing-album-button {
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
font-weight: bold;
color: #333;
}
}
<style scoped lang="less">
.thing-album-content {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
width: 100%;
height: 100%;
overflow-y: auto;
padding-top: 20px;
padding-left: 25px;
gap: 20px;
.thing-album-title {
font-size: 14px;
color: #999;
}
.thing-album-container {
width: 180px;
height: 180px;
position: relative;
display: inline-block;
background-color: #f5f5f5;
.background-image {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.2); /* 黑色半透明 */
backdrop-filter: blur(2px); /* 背景虚化 */
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: white;
font-size: 16px;
transition: background-color 0.3s ease, backdrop-filter 0.3s ease;
gap: 0;
.thing-album-overlay-count {
font-size: 12px;
color: white;
}
}
.overlay:hover {
background-color: rgba(0, 0, 0, 0.1); /* 黑色半透明 */
backdrop-filter: blur(0px); /* 背景虚化 */
cursor: pointer;
}
}
.thing-album-container:hover {
box-shadow: 0 0 10px rgba(0, 0, 0, 0.4);
}
}
}
</style>

View File

@@ -0,0 +1,120 @@
<template>
<div class="thing-album">
<div class="thing-album-header">
<AButton type="link" size="large" class="thing-album-button">事物</AButton>
</div>
<div class="thing-album-content">
<span class="thing-album-title">动物</span>
<div class="thing-album-container">
<img class="background-image" src="/test/7.png" alt=""/>
<div class="overlay">
<span></span>
<span class="thing-album-overlay-count">---</span>
<span class="thing-album-overlay-count">16张照片</span>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
</script>
<style scoped lang="scss">
.thing-album {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
width: 100%;
height: 100%;
position: relative;
.thing-album-header {
width: 100%;
height: 50px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
gap: 10px;
border-bottom: 1px solid #e2e2e2;
.thing-album-button {
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
font-weight: bold;
color: #333;
}
}
.thing-album-content {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
width: 100%;
height: 100%;
overflow-y: auto;
padding-top: 20px;
padding-left: 25px;
gap: 20px;
.thing-album-title {
font-size: 14px;
color: #999;
}
.thing-album-container {
width: 180px;
height: 180px;
position: relative;
display: inline-block;
background-color: #f5f5f5;
.background-image {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.2); /* 黑色半透明 */
backdrop-filter: blur(2px); /* 背景虚化 */
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: white;
font-size: 16px;
transition: background-color 0.3s ease, backdrop-filter 0.3s ease;
gap: 0;
.thing-album-overlay-count {
font-size: 12px;
color: white;
}
}
.overlay:hover {
background-color: rgba(0, 0, 0, 0.1); /* 黑色半透明 */
backdrop-filter: blur(0px); /* 背景虚化 */
cursor: pointer;
}
}
.thing-album-container:hover {
box-shadow: 0 0 10px rgba(0, 0, 0, 0.4);
}
}
}
</style>

View File

@@ -1,7 +1,7 @@
<template>
<div class="all-photo">
<div class="photo-header">
<AButton type="primary" shape="round" size="middle" @click="upload.openUploadDrawer = true">
<AButton type="primary" shape="round" size="middle" @click="upload.openUploadDrawerFn()">
<template #icon>
<PlusOutlined/>
</template>
@@ -115,7 +115,7 @@
</ATabPane>
</ATabs>
</div>
<Upload/>
<ImageUpload/>
</div>
</template>
@@ -124,7 +124,7 @@ import {Waterfall} from 'vue-waterfall-plugin-next';
import 'vue-waterfall-plugin-next/dist/style.css';
import loading from '@/assets/gif/loading.gif';
import error from '@/assets/svgs/no-image.svg';
import Upload from "@/views/Photograph/Upload/Upload.vue";
import ImageUpload from "@/views/Photograph/ImageUpload/ImageUpload.vue";
import useStore from "@/store";
const selected = ref<(string | number)[]>([]);

View File

@@ -0,0 +1,201 @@
<template>
<ADrawer v-model:open="upload.openUploadDrawer" width="40%" placement="right" title="上传照片">
<template #extra>
<AFlex :vertical="false" justify="center" align="center" gap="large">
<ASelect size="middle" style="width: 150px">
</ASelect>
<ASelect size="middle" style="width: 150px">
</ASelect>
</AFlex>
</template>
<div class="upload-container">
<Spin :spinning="predicting" indicator="magic-ring">
<AUploadDragger
v-model:fileList="fileList"
accept="image/*"
name="file"
:directory="false"
:multiple="true"
method="post"
:beforeUpload="beforeUpload"
:customRequest="customUploadRequest"
:progress="progress"
:maxCount="10"
list-type="picture"
>
<p class="ant-upload-drag-icon">
<inbox-outlined></inbox-outlined>
</p>
<p class="ant-upload-text">单击或拖动文件到此区域以上传</p>
<p class="ant-upload-hint">
支持单次或批量上传严禁上传非法图片违者将受到法律惩戒
</p>
</AUploadDragger>
</Spin>
</div>
<template #footer>
<AFlex :vertical="false" justify="end" align="center" gap="large">
<AButton type="default" size="middle" style="width: 100px">取消</AButton>
<AButton type="primary" size="middle" style="width: 100px">上传</AButton>
</AFlex>
</template>
</ADrawer>
</template>
<script setup lang="ts">
import useStore from "@/store";
import {InboxOutlined} from '@ant-design/icons-vue';
import type {UploadProps} from 'ant-design-vue';
import {message} from "ant-design-vue";
import {initNSFWJs, predictNSFW} from "@/utils/nsfw/nsfw.ts";
import i18n from "@/locales";
import {NSFWJS} from "nsfwjs";
import {animePredictImage} from "@/utils/tfjs/anime_classifier.ts";
import {animePredictImagePro} from "@/utils/tfjs/anime_classifier_pro.ts";
import {fnDetectFace} from "@/utils/tfjs/face_extraction.ts";
import {cocoSsdPredict} from "@/utils/tfjs/mobilenet.ts";
import {predictLandscape} from "@/utils/tfjs/landscape_recognition.ts";
import Spin from "@/components/MyUI/Spin/Spin.vue";
const predicting = ref<boolean>(false);
const upload = useStore().upload;
const image: HTMLImageElement = document.createElement('img');
const fileList = ref([]);
const progress: UploadProps['progress'] = {
strokeColor: {
'0%': '#108ee9',
'100%': '#87d068',
},
strokeWidth: 3,
format: (percent: any) => `${parseFloat(percent.toFixed(2))}%`,
class: 'progress-bar',
};
/**
* 图片上传前的校验
* @param file
*/
async function beforeUpload(file: File) {
predicting.value = true;
image.src = URL.createObjectURL(file);
// 图片 NSFW 检测
const nsfw: NSFWJS = await initNSFWJs();
const isNSFW: boolean = await predictNSFW(nsfw, image);
if (isNSFW) {
message.error(i18n.global.t('comment.illegalImage'));
predicting.value = false;
return false;
}
predicting.value = false;
return true;
}
/**
* 自定义上传请求
* @param file
*/
async function customUploadRequest(file: any) {
upload.clearPredictResult();
let percent = 1; // 初始化进度
const totalSteps = 5; // 总任务数,用于计算进度百分比
// 更新进度条函数
const updateProgress = (completedSteps: number) => {
const targetPercent = Math.min((completedSteps / totalSteps) * 100, 100); // 目标进度
if (percent < targetPercent) {
// 每次进度更新时,增加一个小增量
const increment = Math.min(1, targetPercent - percent); // 每次增量
percent += increment;
// 更新进度条
file.onProgress({percent});
// 控制进度条更新的速度
if (percent < targetPercent) {
setTimeout(() => updateProgress(completedSteps), 50); // 每50ms更新一次
}
}
};
let completedSteps = 0; // 已完成的步骤计数
try {
// Step 1: 动漫预测
const prediction1 = await animePredictImage(image);
completedSteps++;
updateProgress(completedSteps);
const prediction2 = await animePredictImagePro(image);
completedSteps++;
updateProgress(completedSteps);
if (prediction1 === 'Anime' && (prediction2 === 'Furry' || prediction2 === 'Anime')) {
upload.predictResult.isAnime = true;
// 任务提前完成,直接设置进度为 100%
percent = 100;
file.onProgress({percent});
setTimeout(() => {
file.onSuccess();
});
return true;
}
// Step 2: 人脸检测
const faceImageData = await fnDetectFace(image);
completedSteps++;
updateProgress(completedSteps);
if (faceImageData) {
upload.predictResult.hasFace = true;
// 任务提前完成,直接设置进度为 100%
percent = 100;
file.onProgress({percent});
setTimeout(() => {
file.onSuccess();
});
return true;
}
// Step 3: 目标识别
const cocoResults = await cocoSsdPredict(image);
completedSteps++;
updateProgress(completedSteps);
if (cocoResults.length > 0) {
const classSet = new Set(cocoResults.map(result => result.class));
upload.predictResult.objectArray = Array.from(classSet);
}
// Step 4: 风景识别
upload.predictResult.landscape = await predictLandscape(image);
completedSteps++;
updateProgress(completedSteps);
// 任务完成,确保进度条到达 100%
percent = 100;
file.onProgress({percent});
setTimeout(() => {
file.onSuccess();
});
} catch (error) {
// 出现错误,直接设置进度为 100%,并调用错误回调
percent = 100;
file.onProgress({percent});
file.onError(error);
}
}
</script>
<style scoped lang="less">
</style>

View File

@@ -1,74 +0,0 @@
<template>
<ADrawer v-model:open="upload.openUploadDrawer" width="40%" placement="right" title="上传照片">
<template #extra>
<AFlex :vertical="false" justify="center" align="center" gap="large">
<ASelect size="middle" style="width: 150px">
</ASelect>
<ASelect size="middle" style="width: 150px">
</ASelect>
</AFlex>
</template>
<div>
<AUploadDragger
v-model:fileList="fileList"
accept="image/*"
name="file"
:directory="false"
:multiple="true"
@drop="handleDrop"
:beforeUpload="upload.beforeUpload"
:customRequest="upload.customUploadRequest"
:progress="progress"
>
<p class="ant-upload-drag-icon">
<inbox-outlined></inbox-outlined>
</p>
<p class="ant-upload-text">Click or drag file to this area to upload</p>
<p class="ant-upload-hint">
Support for a single or bulk upload. Strictly prohibit from uploading company data or other
band files
</p>
</AUploadDragger>
</div>
</ADrawer>
</template>
<script setup lang="ts">
import useStore from "@/store";
import {InboxOutlined} from '@ant-design/icons-vue';
import type {UploadProps} from 'ant-design-vue';
const upload = useStore().upload;
const fileList = ref([]);
// const handleChange = (info: UploadChangeParam) => {
// const status = info.file.status;
// if (status !== 'uploading') {
// console.log(info.file, info.fileList);
// }
// if (status === 'done') {
// message.success(`${info.file.name} file uploaded successfully.`);
// } else if (status === 'error') {
// message.error(`${info.file.name} file upload failed.`);
// }
// };
function handleDrop(e: DragEvent) {
console.log(e);
}
const progress: UploadProps['progress'] = {
strokeColor: {
'0%': '#108ee9',
'100%': '#87d068',
},
strokeWidth: 3,
format: (percent: any) => `${parseFloat(percent.toFixed(2))}%`,
class: 'progress-bar',
};
</script>
<style scoped lang="less">
</style>

View File

@@ -64,7 +64,7 @@
<script setup lang="ts">
import {message} from "ant-design-vue";
import Img from "@/workers/image.ts";
import Img from "@/workers/upscale/image.ts";
import useStore from "@/store";
import run from '@/assets/svgs/run.svg';
@@ -82,7 +82,7 @@ async function startTask() {
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), {
const worker = new Worker(new URL("@/workers/upscale/upscale.worker.ts", import.meta.url), {
type: "module",
});
worker.onmessage = (e: MessageEvent<any>) => {

View File

@@ -0,0 +1,11 @@
self.onmessage = async function (_e: MessageEvent): Promise<void> {
// const {data} = e;
// const {width, height, uint8Array} = data;
// const prediction1 = await animePredictImage(width, height, uint8Array);
// const prediction2 = await animePredictImagePro(width, height, uint8Array);
// if (prediction1 === 'Anime' && (prediction2 === 'Furry' || prediction2 === 'Anime')) {
// self.postMessage({isAnime: true});
// return;
// }
};

View File

@@ -1,3 +1,4 @@
/* @vite-ignore */
/* eslint-disable */
// @ts-nocheck
// This file is a modified version of the original imghelper.ts file from Emscripten.

View File

@@ -1,5 +1,5 @@
import * as tf from "@tensorflow/tfjs";
import Image from "./image";
import Image from "./image.ts";
export default async function upscale(
image: Image,