基于Vue.js 3的对象识别应用开发与游戏拓展
1. 准备对象识别存储
为了为未来的对象识别开发做准备,我们将创建一个存储来封装相关功能。由于在安装时选择了Pinia,项目中已经初始化了一个空存储。我们将在 ./store 文件夹中创建一个名为 objects.ts 的新文件: 链接 。
在存储初始化时,我们设置了一些属性来跟踪模型的状态。因为模型加载可能需要一些时间,所以要确保向用户告知,以提供良好的用户体验。在存储初始化时,必须立即调用 loadModel() 函数,该函数会将模型加载到存储中,方便在整个应用中访问。
我们还添加并暴露了一个 detect 函数,该函数接受一张图像并通过模型运行该图像,结果是一个包含检测到的物品及其置信度的数组。
2. 执行并显示状态检查
了解应用正在执行的操作非常有价值,特别是首次加载模型可能需要一些时间。我们将构建一个可视化组件来列出模型加载的状态。在 ./components 文件夹中创建一个名为 StatusCheck.vue 的组件:
<template>
<v-list>
<v-list-subheader>Status</v-list-subheader>
<v-list-item>
<v-list-item-title>
AI Model
<span v-if="isModelLoading">Loading...
<v-progress-circular indeterminate :size="16" color="primary" />
</span>
</v-list-item-title>
<v-list-item-subtitle v-if="isModelLoaded">Loaded!</v-list-item-subtitle>
<template v-slot:append v-if="isModelLoaded">
<v-icon icon="mdi-check" color="success"></v-icon>
</template>
</v-list-item>
</v-list>
</template>
<script setup lang="ts">
import { watch } from "vue";
import { useObjectStore } from "@/store/object";
import { storeToRefs } from "pinia";
const objectStore = useObjectStore();
const { isModelLoading, isModelLoaded } = storeToRefs(objectStore);
const emit = defineEmits(["model-loaded"]);
watch(isModelLoaded, () => {
if (isModelLoaded.value) emit("model-loaded");
});
</script>
这个组件以良好的格式列出存储中的状态,并在模型加载完成时发出 model-loaded 事件。我们可以删除 ./components 文件夹中的 HelloWorld.vue 文件,并将 ./view/Home.vue 的内容替换为:
<template>
<v-container>
<StatusCheck />
</v-container>
</template>
<script lang="ts" setup>
import StatusCheck from "@/components/StatusCheck.vue";
</script>
现在可以首次运行应用,会发现一开始加载需要一些时间,但一段时间后会看到模型状态的可视化结果。
3. 选择图像
我们将在 components 文件夹中创建一个新组件 ImageDetect.vue ,内容如下:
<template>
<v-container>
<StatusCheckSimple @model-loaded="modelLoaded = true" />
<v-file-input @change="inputFromFile" v-model="image" accept="image/png, image/jpeg" :disabled="!modelLoaded" />
<v-img :src="url" height="100"></v-img>
</v-container>
</template>
<script setup lang="ts">
import { ref } from "vue";
import type { Ref } from "vue";
import StatusCheckSimple from "./StatusCheck.vue";
const image: Ref<File | any | undefined> = ref(undefined);
const imageToDetect: Ref<HTMLImageElement | undefined> = ref(undefined);
const url: Ref<string | undefined> = ref(undefined);
import { useObjectStore } from "@/store/object";
import { storeToRefs } from "pinia";
const objectStore = useObjectStore();
const { detected } = storeToRefs(objectStore);
const modelLoaded: Ref<boolean> = ref(false);
const inputFromFile = (event: any): void => {
const file = event.target.files[0];
image.value = [file];
imageToDetect.value = dataToImageData(file);
};
const dataToImageData = (dataBlob: Blob | MediaSource): HTMLImageElement => {
const objUrl = URL.createObjectURL(dataBlob);
const img = new Image();
img.onload = () => {
URL.revokeObjectURL(img.src);
};
img.src = objUrl;
url.value = objUrl;
return img;
};
</script>
在模板中,我们将一些模板逻辑移到了这个文件中。使用 StatusCheck 组件和 @model-loaded 事件来确定图像检测控件是否可见或可用。在脚本中,我们设置了一些变量来跟踪在浏览器中选择的图像。当用户更改文件内容时,我们将图像加载到浏览器内存中,以便在占位符中显示。
我们将 ./views/Home.vue 的内容替换为加载这个新组件:
<template>
<v-container>
<ImageDetect />
</v-container>
</template>
<script lang="ts" setup>
import ImageDetect from "@/components/ImageDetect.vue";
</script>
现在我们有了提供图像的功能和一个应该能够检测图像中对象的存储。接下来,我们将通过在脚本标签中添加存储引用并添加一个按钮来触发检测,将它们连接起来: 链接 。
4. 格式化检测结果
我们可以对检测结果进行格式化,使其更美观: 链接 。在代码的第12 - 22行,我们添加了一个格式良好的检测项目列表,并使用 roundNumber 函数(第18、65 - 67行)来对百分比进行四舍五入。
5. 为应用添加语音功能
由于我们的应用使用图像作为非传统输入,探索不同的信息呈现方式很有趣。现代浏览器有一个内置的文本转语音(TTS)功能,即 SpeechSynthesisUtterance : 文档链接 。
我们将在 ./components 文件夹中创建一个名为 TextToSpeech.vue 的新组件,该组件接受文本作为属性:
<template>
<v-btn @click="tts" prepend-icon="mdi-microphone" :disabled="isSpeaking">Speak</v-btn>
</template>
<script setup lang="ts">
import { ref } from "vue";
import type { Ref } from "vue";
const props = defineProps<{
message: string;
}>();
const isSpeaking: Ref<boolean> = ref(false);
const tts = async () => {
const { message } = props;
const msg = new SpeechSynthesisUtterance();
msg.text = message;
msg.rate = 0.8;
msg.pitch = 0.2;
await window.speechSynthesis.speak(msg);
msg.onstart = () => isSpeaking.value = true;
msg.onend = () => isSpeaking.value = false;
};
</script>
在 tts 函数中,我们可以看到如何访问API并发送消息进行语音播报。为了在语音活动时禁用按钮,我们跟踪 onstart 和 onend 回调函数,并相应地更新 isSpeaking 变量。我们还对语速和音调设置进行了一些调整。
将该组件添加到 ImageDetect.vue 的模板中(别忘了导入该组件):
<template>
<v-container>
<!-- abbreviated -->
<div v-if="detected">
<v-list>
<v-list-item v-for="(item, index) in detected" :key="index">
<!-- abbreviated -->
</v-list-item>
</v-list>
<TextToSpeech :message="speech" v-if="speech"></TextToSpeech>
</div>
</v-container>
</template>
<script setup lang="ts">
import { ref } from "vue";
import type { Ref } from "vue";
import StatusCheckSimple from "./StatusCheckSimple.vue";
import TextToSpeech from "./TextToSpeech.vue";
// ...abbreviated
</script>
我们需要为组件提供语音内容。查看代码: 链接 。我们添加了一些辅助变量,创建了一个计算变量 uniqueObjects 来过滤所有重复条目,计算的 speech 值使用 Intl API将列表连接起来,输出可以安全地发送到 TextToSpeech 组件。
6. 从原型中学习
通过这个微型应用,我们可以进行一些实验,但遇到了两个主要问题:
- 对象识别有效,但非常局限于预训练模型的类别。提供自训练模型是可能的,但在当前主题范围内处理起来有点复杂。
- 不同浏览器之间的TTS功能不太稳定或可靠,特别是在不同语言之间。
由于这些限制,最初使用相机流来识别对象并进行翻译的想法不太可行。不过,我们可以利用可靠的功能,构建一个收集对象的小游戏。
7. 构建“Scavenge Hunter”游戏
7.1 设置项目
我们可以继续使用之前构建的原型,也可以创建一个新项目。如果创建新项目,需要安装依赖并设置存储,重复之前设置项目和执行状态检查的相关步骤。
7.2 通用更改
首先,在项目根目录创建一个配置文件 config.ts :
export default Object.freeze({
MOTIVATIONAL_QUOTES: [
"Believe in yourself and keep coding!",
"Every Vue project you complete gets you closer to victory!",
"You're on the right track, keep it up!",
"Stay focused and never give up!"
],
DETECTION_ACCURACY_THRESHOLD: 0.70,
SCORE_ACCURACY_MULTIPLIER: 1.10, // input scores are between DETECTION_ACCURACY_THRESHOLD and 1
MAX_ROUNDS: 10,
SCORE_FOUND: 100,
SCORE_SKIP: -150,
})
这个配置文件将所有设置集中在一起,方便修改。我们还可以打开 ./index.html 模板,将标题标签更新为新项目的名称“Scavenge Hunter”。
在 ./views 文件夹中创建两个新的视图文件 Find.vue 和 End.vue ,暂时可以添加一些占位符内容:
<template>
<div>NAME OF THE VIEW</div>
</template>
更新 ./router/index.ts 文件内容: 链接 。
简化界面,删除 ./layouts/default 文件夹中的 AppBar.vue 和 View.vue 文件,将 Default.vue 文件内容替换为:
<template>
<v-app>
<v-main>
<router-view />
</v-main>
</v-app>
</template>
7.3 添加额外存储
我们通常先设计和设置存储,因为它们通常是信息和方法的中心来源。
- 替换
./store/app.ts文件内容: 链接 ,这是一个精简版的应用存储,去除了不必要的功能。 - 在
object.ts存储中添加预定义的类别列表:
// ...abbreviated
export const useObjectStore = defineStore('object', () => {
// ...abbreviated
const loadModel = async () => {
// ...abbreviated
}
loadModel();
// Full list of available classes listed as displayName on the following link:
// https://raw.githubusercontent.com/tensorflow/tfjs-models/master/coco-ssd/src/classes.ts
const objects: string[] = ["person", "backpack", "umbrella", "handbag", "tie", "suitcase", "sports ball", "bottle", "wine glass", "cup", "fork", "knife", "spoon", "bowl", "banana", "apple", "orange", "broccoli", "carrot", "chair", "couch", "potted plant", "bed", "dining table", "toilet", "tv", "laptop", "remote", "cell phone", "microwave", "oven", "sink", "refrigerator", "book", "clock", "vase", "scissors", "teddy bear", "hair drier", "toothbrush"];
return { loadModel, isModelLoading, isModelLoaded, detected, detect, objects }
})
这里选择了一些常见于家庭中的类别,可以根据需要修改。
-
添加
./store/game.ts存储文件: 链接 ,该存储包含正在进行的回合和跳过的回合的引用,跟踪分数,并帮助从对象存储中选择类别。特别是getNewCategory函数,它从对象集合中随机选择一个唯一的新类别。 -
替换
./App.vue文件内容: 链接 ,将应用存储的功能连接到界面。
7.4 开始新游戏
在 components 文件夹中创建一个 StartGame.vue 组件:
<template>
<v-btn
:disabled="!canStart"
@click="newGame"
prepend-icon="mdi-trophy"
append-icon="mdi-trophy"
size="x-large"
color="primary"
>
<slot>Start game!</slot>
</v-btn>
</template>
<script lang="ts" setup>
import { useAppStore } from "@/store/app";
import { useGameStore } from "@/store/game";
import { storeToRefs } from "pinia";
const gameStore = useGameStore();
const appStore = useAppStore();
const { canStart } = storeToRefs(gameStore);
const { reset } = gameStore;
const newGame = () => {
reset();
appStore.navigateToPage("/find");
};
</script>
我们依赖存储来确定按钮是否禁用,通过调用 gameStore 的 reset() 函数和 appStore 的 navigateToPage 函数来触发新游戏。
更新 Home.vue 视图内容:
<template>
<v-card class="pa-4">
<v-card-title>
<h1 class="text-h3 text-md-h2 text-wrap">z Scavenge Hunter</h1>
</v-card-title>
<v-card-text>
<p>Welcome to "Scavenge Hunter"! The game where you find things!</p>
</v-card-text>
<StatusCheck />
<v-card-actions class="justify-center">
<StartGame />
</v-card-actions>
</v-card>
</template>
<script lang="ts" setup>
import StartGame from "@/components/StartGame.vue";
import StatusCheck from "@/components/StatusCheck.vue";
</script>
此时运行应用会发现无法开始游戏,因为我们需要使用用户的相机流,所以需要请求访问权限。
从终端安装 VueUse 包:
npm i @vueuse/core
更新 StatusCheck.vue 文件: 链接 。使用 usePermission 组合式函数返回一个响应式属性,告知用户是否授予了相机访问权限。当模型加载完成且用户授予相机访问权限时,游戏可以开始。在 onMounted 钩子中,我们手动尝试请求视频流,一旦流开始,立即关闭,因为我们只需要权限,权限在整个访问过程中是持久的。
7.5 构建结束屏幕
在深入研究图像流和对象狩猎之前,我们先构建最终屏幕。在 ./components 文件夹中创建一个 ScoreCard.vue 组件来显示游戏结果: 链接 。该组件显示游戏过程中收集的一些指标,这些指标都是 gameStore 的属性,方便访问。
在 End.vue 中导入 ScoreCard.vue 文件并对模板进行一些添加。
总结
通过以上步骤,我们从对象识别的基础开发逐步构建了一个有趣的“Scavenge Hunter”游戏。在开发过程中,我们遇到了一些问题,如对象识别的局限性和TTS功能的不稳定性,但也通过合理的设计和调整,利用可靠的功能完成了一个有意义的项目。
流程图
graph TD
A[准备对象识别存储] --> B[执行并显示状态检查]
B --> C[选择图像]
C --> D[格式化检测结果]
D --> E[为应用添加语音功能]
E --> F[从原型中学习]
F --> G[构建Scavenge Hunter游戏]
G --> G1[设置项目]
G --> G2[通用更改]
G --> G3[添加额外存储]
G --> G4[开始新游戏]
G --> G5[构建结束屏幕]
表格
| 功能 | 相关文件 | 说明 |
|---|---|---|
| 对象识别存储 | objects.ts | 封装对象识别功能,管理模型加载和检测 |
| 状态检查 | StatusCheck.vue | 显示模型加载状态,支持相机权限检查 |
| 图像选择 | ImageDetect.vue | 提供图像上传功能,与模型检测关联 |
| 语音功能 | TextToSpeech.vue | 实现文本转语音功能 |
| 游戏配置 | config.ts | 集中管理游戏配置参数 |
| 游戏存储 | app.ts 、 object.ts 、 game.ts | 管理游戏状态、对象类别和游戏逻辑 |
| 游戏组件 | StartGame.vue 、 ScoreCard.vue | 开始游戏和显示游戏结果 |
基于Vue.js 3的对象识别应用开发与游戏拓展(续)
8. 利用相机流进行对象检测
为了让游戏更具互动性,我们将使用相机流代替图像上传。在前面已经完成了相机权限的请求,现在可以开始使用相机流进行对象检测。
首先,我们需要在 Find.vue 视图中添加相机流的显示和对象检测逻辑。以下是 Find.vue 的示例代码:
<template>
<v-container>
<StatusCheck />
<video ref="videoRef" autoplay playsinline width="640" height="480"></video>
<v-btn @click="startDetection" :disabled="isDetecting">Start Detection</v-btn>
<v-btn @click="stopDetection" :disabled="!isDetecting">Stop Detection</v-btn>
<v-list>
<v-list-item v-for="(item, index) in detectedObjects" :key="index">
<v-list-item-title>{{ item.class }} ({{ item.score.toFixed(2) }})</v-list-item-title>
</v-list-item>
</v-list>
</v-container>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";
import { useObjectStore } from "@/store/object";
import { storeToRefs } from "pinia";
import StatusCheck from "@/components/StatusCheck.vue";
const videoRef = ref<HTMLVideoElement | null>(null);
const isDetecting = ref(false);
const detectedObjects = ref<Array<{ class: string; score: number }>>([]);
const objectStore = useObjectStore();
const { detect } = objectStore;
const startDetection = async () => {
if (videoRef.value) {
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
videoRef.value.srcObject = stream;
isDetecting.value = true;
detectLoop();
}
};
const stopDetection = () => {
if (videoRef.value && videoRef.value.srcObject) {
const stream = videoRef.value.srcObject as MediaStream;
stream.getTracks().forEach((track) => track.stop());
videoRef.value.srcObject = null;
isDetecting.value = false;
}
};
const detectLoop = async () => {
if (isDetecting.value && videoRef.value) {
const detections = await detect(videoRef.value);
detectedObjects.value = detections;
requestAnimationFrame(detectLoop);
}
};
onMounted(() => {
// 确保相机权限已授予
});
onUnmounted(() => {
stopDetection();
});
</script>
在上述代码中,我们通过 navigator.mediaDevices.getUserMedia 获取相机流,并将其显示在 video 元素中。点击“Start Detection”按钮开始检测,点击“Stop Detection”按钮停止检测。在检测循环中,不断调用 detect 函数进行对象检测,并更新检测结果的显示。
9. 游戏逻辑实现
在前面已经设置了游戏存储和相关配置,现在我们将完善游戏逻辑,包括计分、回合管理和目标对象选择。
在 game.ts 存储中,我们可以进一步完善计分和回合管理的逻辑:
import { defineStore } from "pinia";
import config from "../config";
export const useGameStore = defineStore("game", () => {
const currentRound = ref(1);
const score = ref(0);
const skippedRounds = ref(0);
const targetObject = ref("");
const { MAX_ROUNDS, SCORE_FOUND, SCORE_SKIP, DETECTION_ACCURACY_THRESHOLD, SCORE_ACCURACY_MULTIPLIER } = config;
const getNewCategory = (objects: string[]) => {
// 随机选择一个未出现过的对象
const availableObjects = objects.filter((obj) => obj!== targetObject.value);
if (availableObjects.length > 0) {
const randomIndex = Math.floor(Math.random() * availableObjects.length);
targetObject.value = availableObjects[randomIndex];
}
};
const checkDetection = (detections: Array<{ class: string; score: number }>) => {
const found = detections.some((item) => item.class === targetObject.value && item.score >= DETECTION_ACCURACY_THRESHOLD);
if (found) {
const bestScore = detections.find((item) => item.class === targetObject.value)?.score || 0;
const roundScore = SCORE_FOUND * (bestScore * SCORE_ACCURACY_MULTIPLIER);
score.value += roundScore;
} else {
score.value += SCORE_SKIP;
skippedRounds.value++;
}
currentRound.value++;
if (currentRound.value <= MAX_ROUNDS) {
getNewCategory(objects);
}
};
const reset = () => {
currentRound.value = 1;
score.value = 0;
skippedRounds.value = 0;
getNewCategory(objects);
};
return {
currentRound,
score,
skippedRounds,
targetObject,
getNewCategory,
checkDetection,
reset,
};
});
在上述代码中, getNewCategory 函数用于随机选择一个新的目标对象。 checkDetection 函数根据检测结果进行计分和回合管理,如果检测到目标对象且置信度达到阈值,则给予相应的分数,否则扣除一定分数并标记为跳过回合。 reset 函数用于重置游戏状态。
在 Find.vue 中,我们可以调用 checkDetection 函数来处理检测结果:
<script setup lang="ts">
// ... 前面的代码 ...
const gameStore = useGameStore();
const { targetObject, checkDetection } = gameStore;
const detectLoop = async () => {
if (isDetecting.value && videoRef.value) {
const detections = await detect(videoRef.value);
detectedObjects.value = detections;
if (detections.length > 0) {
checkDetection(detections);
}
requestAnimationFrame(detectLoop);
}
};
// ... 后面的代码 ...
</script>
10. 游戏结束处理
当游戏达到最大回合数时,需要跳转到结束屏幕显示游戏结果。我们可以在 Find.vue 中添加回合数的检查和路由跳转逻辑:
<script setup lang="ts">
// ... 前面的代码 ...
import { useRouter } from "vue-router";
const router = useRouter();
const detectLoop = async () => {
if (isDetecting.value && videoRef.value) {
const detections = await detect(videoRef.value);
detectedObjects.value = detections;
if (detections.length > 0) {
checkDetection(detections);
}
if (gameStore.currentRound.value > MAX_ROUNDS) {
stopDetection();
router.push("/end");
}
requestAnimationFrame(detectLoop);
}
};
// ... 后面的代码 ...
</script>
在 End.vue 中,我们可以显示游戏结果:
<template>
<v-container>
<ScoreCard />
<v-btn @click="restartGame">Restart Game</v-btn>
</v-container>
</template>
<script setup lang="ts">
import ScoreCard from "@/components/ScoreCard.vue";
import { useGameStore } from "@/store/game";
import { useRouter } from "vue-router";
const gameStore = useGameStore();
const router = useRouter();
const restartGame = () => {
gameStore.reset();
router.push("/find");
};
</script>
11. 优化与改进
为了提升游戏的用户体验,我们可以进行一些优化和改进:
- 界面美化 :使用Vuetify的组件和样式,对游戏界面进行美化,使其更加吸引人。
- 音效反馈 :在检测到目标对象或游戏结束时,添加音效反馈,增强游戏的趣味性。
- 多语言支持 :利用
IntlAPI实现多语言支持,方便不同地区的用户使用。
流程图
graph TD
A[利用相机流进行对象检测] --> B[游戏逻辑实现]
B --> C[游戏结束处理]
C --> D[优化与改进]
表格
| 功能 | 相关文件 | 说明 |
|---|---|---|
| 相机流对象检测 | Find.vue | 显示相机流并进行实时对象检测 |
| 游戏逻辑 | game.ts | 管理游戏的计分、回合和目标对象选择 |
| 游戏结束处理 | Find.vue 、 End.vue | 处理游戏结束逻辑,显示结果并支持重启 |
| 优化改进 | 多个文件 | 包括界面美化、音效反馈和多语言支持等 |
通过以上步骤,我们完成了一个完整的“Scavenge Hunter”游戏,从对象识别的基础开发到游戏的实现,充分利用了Vue.js 3和相关技术的优势。在开发过程中,我们不仅解决了一些技术难题,还通过不断优化和改进提升了游戏的用户体验。希望这个项目能为你在Vue.js开发和对象识别应用方面提供一些启发。
超级会员免费看

被折叠的 条评论
为什么被折叠?



