仅使用示例,识别效果不佳。

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>数字图像识别应用</title>
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@4.22.0/dist/tf.min.js"></script>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f5f7fa;
color: #333;
line-height: 1.6;
padding: 20px;
}
.container {
max-width: 1000px;
margin: 0 auto;
background: white;
border-radius: 12px;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
header {
background: linear-gradient(135deg, #6a11cb 0%, #2575fc 100%);
color: white;
padding: 30px 20px;
text-align: center;
}
h1 {
font-size: 2.2rem;
margin-bottom: 10px;
}
.subtitle {
font-size: 1.1rem;
opacity: 0.9;
}
.main-content {
display: flex;
flex-wrap: wrap;
gap: 30px;
padding: 30px;
}
.upload-section, .result-section {
flex: 1;
min-width: 300px;
}
.section-title {
font-size: 1.5rem;
margin-bottom: 20px;
color: #2c3e50;
border-bottom: 2px solid #eee;
padding-bottom: 10px;
}
.drop-area {
border: 3px dashed #3498db;
border-radius: 10px;
padding: 40px 20px;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
background-color: #f8fafc;
}
.drop-area:hover {
background-color: #e3f2fd;
border-color: #2980b9;
}
.drop-area.active {
background-color: #d6e4f0;
border-color: #1abc9c;
}
.file-input {
display: none;
}
.preview-container {
margin-top: 25px;
text-align: center;
}
.preview-image {
max-width: 100%;
max-height: 200px;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.btn {
background: #3498db;
color: white;
border: none;
padding: 12px 25px;
font-size: 1rem;
border-radius: 6px;
cursor: pointer;
transition: background 0.3s;
margin-top: 15px;
}
.btn:hover {
background: #2980b9;
}
.btn:disabled {
background: #bdc3c7;
cursor: not-allowed;
}
.result-display {
background-color: #f8fafc;
border-radius: 8px;
padding: 20px;
margin-top: 20px;
min-height: 250px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.recognized-digit {
font-size: 5rem;
font-weight: bold;
color: #2c3e50;
margin: 10px 0;
}
.confidence {
font-size: 1.4rem;
color: #7f8c8d;
margin-bottom: 20px;
}
.chart-container {
width: 100%;
height: 200px;
margin-top: 20px;
}
canvas {
width: 100%;
height: 100%;
}
.error-message {
color: #e74c3c;
background-color: #fdeded;
padding: 15px;
border-radius: 6px;
margin-top: 15px;
font-weight: 500;
}
.status {
padding: 15px;
text-align: center;
color: #7f8c8d;
}
@media (max-width: 768px) {
.main-content {
flex-direction: column;
}
h1 {
font-size: 1.8rem;
}
}
</style>
</head>
<body>
<div id="app">
<div class="container">
<header>
<h1>数字图像识别</h1>
<p class="subtitle">使用 Vue 3 和 TensorFlow.js 实现的手写数字识别</p>
</header>
<div class="main-content">
<div class="upload-section">
<h2 class="section-title">上传图片</h2>
<div
class="drop-area"
:class="{ active: isDragging }"
@dragover.prevent="handleDragOver"
@dragleave="handleDragLeave"
@drop.prevent="handleDrop"
@click="triggerFileInput"
>
<p v-if="!selectedImage">拖拽图片到这里或点击上传</p>
<p v-else>已选择图片</p>
<p class="small">(支持 JPG/PNG 格式)</p>
<input
type="file"
ref="fileInputRef"
class="file-input"
accept=".jpg,.jpeg,.png"
@change="handleFileSelect"
>
</div>
<div class="preview-container" v-if="selectedImage">
<img :src="selectedImage" alt="预览图片" class="preview-image">
<button
class="btn"
:disabled="isProcessing || !modelLoaded"
@click="recognizeImage"
>
{{ modelLoaded ? '开始识别' : '模型加载中...' }}
</button>
</div>
</div>
<div class="result-section">
<h2 class="section-title">识别结果</h2>
<div class="result-display">
<div v-if="recognitionResult" class="result-content">
<div class="recognized-digit">{{ recognitionResult.predictedClass }}</div>
<div class="confidence">置信度: {{ recognitionResult.confidence }}%</div>
<div class="chart-container">
<canvas ref="chartCanvasRef"></canvas>
</div>
<button class="btn" style="margin-top: 20px;" @click="resetRecognition">
重新识别
</button>
</div>
<div v-else-if="errors.length > 0" class="error-container">
<div
v-for="(error, index) in errors"
:key="index"
class="error-message"
>
{{ error }}
</div>
</div>
<div v-else class="status">
<p v-if="!modelLoaded">正在加载模型...</p>
<p v-else-if="isProcessing">正在识别中...</p>
<p v-else>请选择图片进行识别</p>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
const { createApp, ref, onMounted, nextTick } = Vue;
createApp({
setup() {
// 响应式数据
const fileInputRef = ref(null);
const chartCanvasRef = ref(null);
const selectedImage = ref(null);
const recognitionResult = ref(null);
const errors = ref([]);
const isProcessing = ref(false);
const modelLoaded = ref(false);
const isDragging = ref(false);
let model = null;
// 加载 TensorFlow.js 模型
const loadModel = async () => {
try {
// 在实际部署时,这里应该是 './model/' 路径
// 为了演示目的,我们模拟加载过程
console.log('尝试加载模型...');
// 模拟模型加载延迟
await new Promise(resolve => setTimeout(resolve, 1500));
// 这里应该实际加载模型
// model = await tf.loadLayersModel('./model/model.json');
modelLoaded.value = true;
console.log('模型加载成功');
} catch (err) {
console.error('模型加载失败:', err);
errors.value.push(`模型加载失败: ${err.message}`);
}
};
// 处理文件选择
const handleFileSelect = (event) => {
const file = event.target.files[0];
if (file) {
validateAndProcessFile(file);
}
};
// 验证并处理文件
const validateAndProcessFile = (file) => {
errors.value = [];
// 检查文件类型
if (!file.type.match('image/jpeg|image/png')) {
errors.value.push('不支持的文件类型,请上传 JPG 或 PNG 图片');
return;
}
// 检查文件大小 (限制为 2MB)
if (file.size > 2 * 1024 * 1024) {
errors.value.push('文件过大,请上传小于 2MB 的图片');
return;
}
// 读取文件并显示预览
const reader = new FileReader();
reader.onload = (e) => {
selectedImage.value = e.target.result;
};
reader.readAsDataURL(file);
};
// 触发文件输入
const triggerFileInput = () => {
if (fileInputRef.value) {
fileInputRef.value.click();
}
};
// 拖拽事件处理
const handleDragOver = (event) => {
event.preventDefault();
isDragging.value = true;
};
const handleDragLeave = () => {
isDragging.value = false;
};
const handleDrop = (event) => {
event.preventDefault();
isDragging.value = false;
const files = event.dataTransfer.files;
if (files.length > 0) {
validateAndProcessFile(files[0]);
}
};
// 预处理图像
const preprocessImage = (imgElement) => {
// 创建临时 canvas 进行图像处理
const canvas = document.createElement('canvas');
canvas.width = 28;
canvas.height = 28;
const ctx = canvas.getContext('2d');
// 绘制图像并调整大小
ctx.drawImage(imgElement, 0, 0, 28, 28);
// 获取图像数据
const imageData = ctx.getImageData(0, 0, 28, 28);
const data = imageData.data;
// 转换为灰度并归一化
const grayscaleData = new Float32Array(28 * 28);
for (let i = 0; i < data.length; i += 4) {
// 使用 RGB 加权平均转换为灰度
const gray = (data[i] * 0.299 + data[i + 1] * 0.587 + data[i + 2] * 0.114) / 255.0;
const pixelIndex = (i / 4);
grayscaleData[pixelIndex] = gray;
}
// 调整形状为 [batch, height, width, channels]
const tensor = tf.tensor4d(grayscaleData, [1, 28, 28, 1]);
return tensor;
};
// 识别图像
const recognizeImage = async () => {
if (!selectedImage.value || !modelLoaded.value) return;
isProcessing.value = true;
errors.value = [];
recognitionResult.value = null;
try {
// 创建图像元素进行预处理
const img = new Image();
img.src = selectedImage.value;
img.onload = async () => {
try {
// 预处理图像
const processedImage = preprocessImage(img);
// 模拟推理(在实际实现中,这里会调用 model.predict)
// 由于无法在演示中加载真实模型,我们模拟推理结果
await new Promise(resolve => setTimeout(resolve, 1000));
// 模拟预测结果
const mockProbabilities = Array.from({length: 10}, () => Math.random());
const total = mockProbabilities.reduce((sum, val) => sum + val, 0);
const normalizedProbs = mockProbabilities.map(p => p / total);
// 找到最大概率的类别
let maxIdx = 0;
let maxVal = normalizedProbs[0];
for (let i = 1; i < normalizedProbs.length; i++) {
if (normalizedProbs[i] > maxVal) {
maxVal = normalizedProbs[i];
maxIdx = i;
}
}
recognitionResult.value = {
predictedClass: maxIdx,
confidence: (maxVal * 100).toFixed(1),
probabilities: normalizedProbs
};
// 绘制概率分布图
await nextTick();
drawProbabilityChart(normalizedProbs);
processedImage.dispose(); // 释放内存
} catch (err) {
console.error('推理过程出错:', err);
errors.value.push(`推理过程出错: ${err.message}`);
} finally {
isProcessing.value = false;
}
};
img.onerror = () => {
errors.value.push('图片加载失败');
isProcessing.value = false;
};
} catch (err) {
console.error('识别过程出错:', err);
errors.value.push(`识别过程出错: ${err.message}`);
isProcessing.value = false;
}
};
// 绘制概率分布图
const drawProbabilityChart = (probabilities) => {
const canvas = chartCanvasRef.value;
if (!canvas) return;
const ctx = canvas.getContext('2d');
const width = canvas.clientWidth;
const height = canvas.clientHeight;
// 清空画布
ctx.clearRect(0, 0, width, height);
// 设置画布实际尺寸
canvas.width = width;
canvas.height = height;
const barWidth = width / 12; // 10个条形 + 2个边距
const spacing = barWidth / 2;
const maxHeight = height * 0.8;
const startX = spacing;
const startY = height * 0.9;
// 绘制每个类别的概率条
for (let i = 0; i < 10; i++) {
const prob = probabilities[i];
const barHeight = prob * maxHeight;
const x = startX + i * (barWidth + spacing);
const y = startY - barHeight;
// 绘制条形
ctx.fillStyle = '#3498db';
ctx.fillRect(x, y, barWidth, barHeight);
// 绘制数值标签
ctx.fillStyle = '#2c3e50';
ctx.font = '12px Arial';
ctx.textAlign = 'center';
ctx.fillText(`${(prob * 100).toFixed(1)}%`, x + barWidth / 2, y - 5);
// 绘制类别标签
ctx.fillText(i.toString(), x + barWidth / 2, startY + 15);
}
};
// 重置识别状态
const resetRecognition = () => {
selectedImage.value = null;
recognitionResult.value = null;
errors.value = [];
};
// 组件挂载时加载模型
onMounted(async () => {
await loadModel();
});
return {
fileInputRef,
chartCanvasRef,
selectedImage,
recognitionResult,
errors,
isProcessing,
modelLoaded,
isDragging,
handleFileSelect,
triggerFileInput,
handleDragOver,
handleDragLeave,
handleDrop,
recognizeImage,
resetRecognition
};
}
}).mount('#app');
</script>
</body>
</html>