基于Vue3和TensorFlow.js的数字图像识别应用HTML单文件

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

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>
相关推荐
万少1 小时前
公测期 0 元/月!商汤 SenseNova 免费 Token 再不领就没了
前端·javascript·后端
专注VB编程开发20年1 小时前
专业分析python底层调用与按键精灵,ah3等的对比,hookdll,内存加载,调用.net dll
开发语言·javascript·python·microsoft·php·.net
invicinble1 小时前
前端框架使用vue-cli( 第二层:工程配置层--技术栈配置层配置)
javascript·vue.js·前端框架
Json____1 小时前
前端入门练习题集-HTML/CSS/JS实战小项目15个
前端·css·html
不会飞的鲨鱼2 小时前
观鸟网 RSA加密 AES 解密
javascript·爬虫·python
openKaka_2 小时前
从 performWorkOnRoot 到 workInProgress tree:React 真正开始 render 的地方
前端·javascript·react.js
Rooting++2 小时前
vue2强制刷新路由的办法
前端·javascript·vue.js
爱滑雪的码农2 小时前
React+three.js之场景(Scene),相机(Camera)
前端·javascript·react.js
xuankuxiaoyao13 小时前
Vue.js实践-组件基础下
前端·javascript·vue.js