鸿蒙next开发:UI开发-绘制几何图形 (Shape)

往期鸿蒙全套实战文章必看:(附带鸿蒙全栈学习资料)


绘制几何图形 (Shape)

绘制组件用于在页面绘制图形,Shape组件是绘制组件的父组件,父组件中会描述所有绘制组件均支持的通用属性。

创建绘制组件

绘制组件可以由以下两种形式创建:

  • 绘制组件使用Shape作为父组件,实现类似SVG的效果。接口调用为以下形式:

    Shape(value?: PixelMap)

    该接口用于创建带有父组件的绘制组件,其中value用于设置绘制目标,可将图形绘制在指定的PixelMap对象中,若未设置,则在当前绘制目标中进行绘制。

    Shape() {
      Rect().width(300).height(50)
    }
  • 绘制组件单独使用,用于在页面上绘制指定的图形。有7种绘制类型,分别为Circle(圆形)、Ellipse(椭圆形)、Line(直线)、Polyline(折线)、Polygon(多边形)、Path(路径)、Rect(矩形)。以Circle的接口调用为例:

    Circle(value?: { width?: string | number, height?: string | number })

    该接口用于在页面绘制圆形,其中width用于设置圆形的宽度,height用于设置圆形的高度,圆形直径由宽高最小值确定。

    Circle({ width: 150, height: 150 })

形状视口viewport

viewPort(value: { x?: number | string, y?: number | string, width?: number | string, height?: number | string })

形状视口viewport指定用户空间中的一个矩形,该矩形映射到为关联的SVG元素建立的视区边界。viewport属性的值包含x、y、width和height四个可选参数,x和y表示视区的左上角坐标,width和height表示其尺寸。

以下3个示例讲解viewport具体用法:

  • 通过形状视口对图形进行放大与缩小。

    class tmp{
      x:number = 0
      y:number = 0
      width:number = 75
      height:number = 75
    }
    let viep:tmp = new tmp()
    
    
    class tmp1{
      x:number = 0
      y:number = 0
      width:number = 300
      height:number = 300
    }
    let viep1:tmp1 = new tmp1()
    
    
    // 画一个宽高都为75的圆
    Text('原始尺寸Circle组件')
    Circle({width: 75, height: 75}).fill('#E87361')
    
    
    Row({space:10}) {
      Column() {
        // 创建一个宽高都为150的shape组件,背景色为黄色,一个宽高都为75的viewport。用一个蓝色的矩形来填充viewport,在viewport中绘制一个直径为75的圆。
        // 绘制结束,viewport会根据组件宽高放大两倍
        Text('shape内放大的Circle组件')
        Shape() {
          Rect().width('100%').height('100%').fill('#0097D4')
          Circle({width: 75, height: 75}).fill('#E87361')
        }
        .viewPort(viep)
        .width(150)
        .height(150)
        .backgroundColor('#F5DC62')
      }
      Column() {
        // 创建一个宽高都为150的shape组件,背景色为黄色,一个宽高都为300的viewport。用一个绿色的矩形来填充viewport,在viewport中绘制一个直径为75的圆。
        // 绘制结束,viewport会根据组件宽高缩小两倍。
        Text('Shape内缩小的Circle组件')
        Shape() {
          Rect().width('100%').height('100%').fill('#BDDB69')
          Circle({width: 75, height: 75}).fill('#E87361')
        }
        .viewPort(viep1)
        .width(150)
        .height(150)
        .backgroundColor('#F5DC62')
      }
    }

  • 创建一个宽高都为300的shape组件,背景色为黄色,一个宽高都为300的viewport。用一个蓝色的矩形来填充viewport,在viewport中绘制一个半径为75的圆。

    class tmp{
      x:number = 0
      y:number = 0
      width:number = 300
      height:number = 300
    }
    let viep:tmp = new tmp()
    
    
    Shape() {
      Rect().width("100%").height("100%").fill("#0097D4")
      Circle({ width: 150, height: 150 }).fill("#E87361")
    }
      .viewPort(viep)
      .width(300)
      .height(300)
      .backgroundColor("#F5DC62")

  • 创建一个宽高都为300的shape组件,背景色为黄色,创建一个宽高都为300的viewport。用一个蓝色的矩形来填充viewport,在viewport中绘制一个半径为75的圆,将viewport向右方和下方各平移150。

    class tmp{
      x:number = -150
      y:number = -150
      width:number = 300
      height:number = 300
    }
    let viep:tmp = new tmp()
    
    
    Shape() {
      Rect().width("100%").height("100%").fill("#0097D4")
      Circle({ width: 150, height: 150 }).fill("#E87361")
    }
      .viewPort(viep)
      .width(300)
      .height(300)
      .backgroundColor("#F5DC62")

自定义样式

绘制组件支持通过各种属性对组件样式进行更改。

  • 通过fill可以设置组件填充区域颜色。

    Path()
      .width(100)
      .height(100)
      .commands('M150 0 L300 300 L0 300 Z')
      .fill("#E87361")
      .strokeWidth(0)

  • 通过stroke可以设置组件边框颜色。

    Path()
      .width(100)
      .height(100)
      .fillOpacity(0)
      .commands('M150 0 L300 300 L0 300 Z')
      .stroke(Color.Red)

  • 通过strokeOpacity可以设置边框透明度。

    Path()
      .width(100)
      .height(100)
      .fillOpacity(0)
      .commands('M150 0 L300 300 L0 300 Z')
      .stroke(Color.Red)
      .strokeWidth(10)
      .strokeOpacity(0.2)

  • 通过strokeLineJoin可以设置线条拐角绘制样式。拐角绘制样式分为Bevel(使用斜角连接路径段)、Miter(使用尖角连接路径段)、Round(使用圆角连接路径段)。

    Polyline()
      .width(100)
      .height(100)
      .fillOpacity(0)
      .stroke(Color.Red)
      .strokeWidth(8)
      .points([[20, 0], [0, 100], [100, 90]])
       // 设置折线拐角处为圆弧
      .strokeLineJoin(LineJoinStyle.Round)

  • 通过strokeMiterLimit设置斜接长度与边框宽度比值的极限值。

    斜接长度表示外边框外边交点到内边交点的距离,边框宽度即strokeWidth属性的值。strokeMiterLimit取值需大于等于1,且在strokeLineJoin属性取值LineJoinStyle.Miter时生效。

    Polyline()
      .width(100)
      .height(100)
      .fillOpacity(0)
      .stroke(Color.Red)
      .strokeWidth(10)
      .points([[20, 0], [20, 100], [100, 100]])
      // 设置折线拐角处为尖角
      .strokeLineJoin(LineJoinStyle.Miter)
      // 设置斜接长度与线宽的比值
      .strokeMiterLimit(1/Math.sin(45))
    Polyline()
      .width(100)
      .height(100)
      .fillOpacity(0)
      .stroke(Color.Red)
      .strokeWidth(10)
      .points([[20, 0], [20, 100], [100, 100]])
      .strokeLineJoin(LineJoinStyle.Miter)
      .strokeMiterLimit(1.42)

  • 通过antiAlias设置是否开启抗锯齿,默认值为true(开启抗锯齿)。

    //开启抗锯齿
    Circle()
      .width(150)
      .height(200)
      .fillOpacity(0)
      .strokeWidth(5)
      .stroke(Color.Black)

    //关闭抗锯齿
    Circle()
      .width(150)
      .height(200)
      .fillOpacity(0)
      .strokeWidth(5)
      .stroke(Color.Black)
      .antiAlias(false)

场景示例

绘制封闭路径

在Shape的(-80, -5)点绘制一个封闭路径,填充颜色0x317AF7,线条宽度3,边框颜色红色,拐角样式锐角(默认值)。

@Entry
@Component
struct ShapeExample {
  build() {
    Column({ space: 10 }) {
      Shape() {
        Path().width(200).height(60).commands('M0 0 L400 0 L400 150 Z')
      }
      .viewPort({ x: -80, y: -5, width: 500, height: 300 })
      .fill(0x317AF7)
      .stroke(Color.Red)
      .strokeWidth(3)
      .strokeLineJoin(LineJoinStyle.Miter)
      .strokeMiterLimit(5)
    }.width('100%').margin({ top: 15 })
  }
}

绘制圆和圆环

绘制一个直径为150的圆,和一个直径为150、线条为红色虚线的圆环(宽高设置不一致时以短边为直径)。

@Entry
@Component
struct CircleExample {
  build() {
    Column({ space: 10 }) {
      //绘制一个直径为150的圆
      Circle({ width: 150, height: 150 })
      //绘制一个直径为150、线条为红色虚线的圆环
      Circle()
        .width(150)
        .height(200)
        .fillOpacity(0)
        .strokeWidth(3)
        .stroke(Color.Red)
        .strokeDashArray([1, 2])
    }.width('100%')
  }
}

# 确保安装所有依赖:pip install flask flask-cors ultralytics opencv-python numpy from flask import Flask, request, jsonify, send_from_directory from flask_cors import CORS from ultralytics import YOLO import cv2 import numpy as np import base64 import os import time app = Flask(__name__) # 允许所有域名跨域请求(开发环境) CORS(app) # 配置路径 - 请根据实际情况修改 MODEL_PATH = 'D:\\AI实训\\ultralytics-main\\my_test\\runs\\detect\\train2\\weights\\best.pt' DATASET_PATH = 'D:\\AI实训\\ultralytics-main\\mydatasets\\vitiligo\\images' os.makedirs(DATASET_PATH, exist_ok=True) # 加载模型 try: model = YOLO(MODEL_PATH) print("✅ 白癜风检测模型加载成功") except Exception as e: print(f"❌ 模型加载失败: {e}") model = None @app.route('/dataset/list', methods=['GET']) def get_dataset_list(): try: if not os.path.exists(DATASET_PATH): return jsonify({"status": "error", "message": f"数据集目录不存在: {DATASET_PATH}"}), 404 image_extensions = ['.jpg', '.jpeg', '.png', '.bmp'] image_files = [ f for f in os.listdir(DATASET_PATH) if os.path.isfile(os.path.join(DATASET_PATH, f)) and os.path.splitext(f)[1].lower() in image_extensions ] return jsonify({ "status": "success", "count": len(image_files), "images": image_files }) except Exception as e: print(f"获取数据集列表出错: {e}") return jsonify({"status": "error", "message": str(e)}), 500 @app.route('/dataset/image/<filename>', methods=['GET']) def get_dataset_image(filename): try: file_path = os.path.join(DATASET_PATH, filename) if not os.path.exists(file_path) or not os.path.isfile(file_path): return jsonify({"status": "error", "message": f"图片不存在: {filename}"}), 404 return send_from_directory(DATASET_PATH, filename) except Exception as e: print(f"获取图片出错: {e}") return jsonify({"status": "error", "message": str(e)}), 500 @app.route('/detect', methods=['POST']) def detect_image(): start_time = time.time() print("🔍 接收到白癜风检测请求") try: if not model: return jsonify({"status": "error", "message": f"模型未加载,请检查路径: {MODEL_PATH}"}), 500 if 'image' not in request.files: return jsonify({"status": "error", "message": "未上传图片"}), 400 file = request.files['image'] file_bytes = np.asarray(bytearray(file.read()), dtype=np.uint8) img = cv2.imdecode(file_bytes, cv2.IMREAD_COLOR) if img is None: return jsonify({"status": "error", "message": "无法解析图片,请确保上传的是有效图片文件"}), 400 print(f"✅ 成功加载图片,尺寸: {img.shape}") # 执行检测 results = model(img) annotated_frame = results[0].plot() # 绘制检测框 print(f"✅ 检测完成,识别到 {len(results[0].boxes)} 个目标区域") # 处理检测结果 _, encoded_img = cv2.imencode('.jpg', annotated_frame) base64_img = base64.b64encode(encoded_img).decode('utf-8') detections = [] for box in results[0].boxes: cls_id = int(box.cls[0]) conf = float(box.conf[0]) bbox = [int(coord) for coord in box.xyxy[0].tolist()] area = (bbox[2] - bbox[0]) * (bbox[3] - bbox[1]) detections.append({ "category": model.names[cls_id], "confidence": f"{conf:.2f}", "confidence_value": round(conf, 4), "bounding_box": bbox, "area_pixels": area, "position": f"({bbox[0]},{bbox[1]})-({bbox[2]},{bbox[3]})" }) detection_time = round(time.time() - start_time, 2) print(f"⏱️ 检测耗时: {detection_time}秒") return jsonify({ "status": "success", "annotated_image": base64_img, "detections": detections, "detection_count": len(detections), "detection_time": f"{detection_time}秒", "message": "白癜风区域检测完成" }) except Exception as e: end_time = time.time() print(f"❌ 检测过程出错: {e},耗时: {end_time - start_time:.2f}秒") return jsonify({"status": "error", "message": str(e)}), 500 @app.route('/health', methods=['GET']) def health_check(): return jsonify({ "status": "healthy", "model_loaded": model is not None, "dataset_available": os.path.exists(DATASET_PATH) }) if __name__ == '__main__': print("=== 白癜风医疗目标检测服务 ===") print("已注册的路由:") for rule in app.url_map.iter_rules(): print(f"URL: {rule.rule}, 方法: {rule.methods}") print(f"数据集路径: {DATASET_PATH}") print(f"模型路径: {MODEL_PATH}") app.run(host='0.0.0.0', port=5000, debug=True)这是我图片分析的后端代码<template> <div class="image-detection-container"> <!-- 图片显示区域 --> <div class="image-section"> <!-- 原图 --> <div class="image-box"> <h3>原图</h3> <div class="image-display"> <img :src="originalImageUrl" alt="Original Image" v-if="originalImageUrl" :style="{ transform: `scale(${zoomLevel})` }" /> <div class="placeholder" v-else>等待加载图片...</div> </div> </div> <!-- 结果图 --> <div class="image-box"> <h3>结果图</h3> <div class="image-display"> <img :src="resultImageUrl" alt="Result Image" v-if="resultImageUrl" :style="{ transform: `scale(${zoomLevel})` }" /> <div class="placeholder" v-else-if="isDetecting">检测中...</div> <div class="placeholder" v-else>未检测</div> </div> </div> <!-- 结果数据 --> <div class="result-data-box"> <h3>结果数据</h3> <div class="result-data"> <div v-if="detectionResults.length"> <div v-for="(result, index) in detectionResults" :key="index" class="result-item"> <p><strong>分类:</strong> {{ result.category }}</p> <p><strong>置信度:</strong> {{ (result.confidence * 100).toFixed(1) }}%</p> <p><strong>矩形框位置:</strong> {{ formatBoundingBox(result.boundingBox) }}</p> </div> </div> <div v-else-if="isDetecting"> <p>正在检测,请稍候...</p> </div> <div v-else> <p>无检测结果</p> </div> </div> </div> </div> <!-- 操作按钮区域 --> <div class="button-section"> <button class="action-button" @click="selectImage">选择图片</button> <button class="action-button" @click="detectImage" :disabled="!originalImageUrl || isDetecting">识别结果</button> <button class="action-button" @click="prevImage" :disabled="currentImageIndex <= 0">上一张</button> <button class="action-button" @click="nextImage" :disabled="currentImageIndex >= imageList.length - 1">下一张</button> <button class="action-button" @click="zoomIn" :disabled="zoomLevel >= maxZoomLevel">放大</button> <button class="action-button" @click="zoomOut" :disabled="zoomLevel <= minZoomLevel">缩小</button> <button class="action-button" @click="resetImage">重置</button> </div> <!-- 文件选择器 --> <input type="file" ref="fileInput" accept="image/*" style="display: none;" @change="handleFileSelect" /> </div> </template> <script> import axios from 'axios'; export default { name: 'ImageDetectionApp', data() { return { // 图片相关数据 originalImageUrl: '', resultImageUrl: '', imageList: [], currentImageIndex: -1, selectedFile: null, // 检测结果数据 detectionResults: [], isDetecting: false, // 缩放相关数据 zoomLevel: 1, minZoomLevel: 0.5, maxZoomLevel: 3, }; }, methods: { // 选择图片 selectImage() { this.$refs.fileInput.click(); }, // 处理文件选择 handleFileSelect(event) { const file = event.target.files[0]; if (!file) return; if (!file.type.match('image.*')) { this.$message.error('请选择图片文件'); return; } this.selectedFile = file; const reader = new FileReader(); reader.onload = (e) => { this.originalImageUrl = e.target.result; // 维护图片列表 if (!this.imageList.includes(this.originalImageUrl)) { this.imageList.push(this.originalImageUrl); this.currentImageIndex = this.imageList.length - 1; } // 重置检测结果 this.resultImageUrl = ''; this.detectionResults = []; }; reader.onerror = () => { console.error('文件读取失败'); this.$message.error('图片加载失败,请重新选择'); }; reader.readAsDataURL(file); // 清空input值,允许重复选择同一文件 event.target.value = ''; }, // 检测图片(删除所有.response.data中的.data) async detectImage() { if (!this.originalImageUrl || this.isDetecting) return; this.isDetecting = true; this.resultImageUrl = ''; this.detectionResults = []; try { const formData = new FormData(); if (this.selectedFile) { // 本地上传的图片 formData.append('image', this.selectedFile); } else { // 从图片列表加载的图片(增加错误捕获) try { const fetchRes = await fetch(this.originalImageUrl); if (!fetchRes.ok) throw new Error(`图片加载失败: ${fetchRes.statusText}`); const blob = await fetchRes.blob(); formData.append('image', blob, 'detect-image.jpg'); } catch (fetchError) { console.error('图片转blob失败:', fetchError); this.$message.error('无法处理图片,请重新选择'); this.isDetecting = false; return; } } // 发送请求 const response = await axios.post(`/detect`, formData); // 验证响应结构 if (!response) { throw new Error('后端返回格式异常(无有效数据)'); } console.log('后端响应完整数据:', response); if (response.status === 'success') { // 处理检测结果图片 if (response.annotated_image) { const base64Str = response.annotated_image.startsWith('data:image') ? response.annotated_image : `data:image/jpeg;base64,${response.annotated_image}`; this.resultImageUrl = base64Str; } else { console.error('后端未返回带检测框的图片'); this.$message.error('未获取到检测结果图片'); } // 处理检测结果列表 if (response.detections && response.detections.length > 0) { this.detectionResults = response.detections.map(item => ({ category: item.category || '未知类别', confidence: item.confidence_value || parseFloat(item.confidence) || 0, boundingBox: item.bounding_box || [] })); this.$message.success(`成功检测到 ${this.detectionResults.length} 个目标区域`); } else { this.detectionResults = []; this.$message.info('未检测到目标区域'); } } else { this.$message.error(`检测失败: ${response.message || '未知错误'}`); } } catch (error) { console.error('检测出错详情:', error); if (error.response) { // 服务器返回错误状态码 this.$message.error(`服务器错误 ${error.response.status}: ${error.response.data?.message || '未知错误'}`); } else if (error.request) { // 请求已发送但无响应 this.$message.error('无法连接到服务器,请检查后端是否运行'); } else { // 其他错误(如图片加载失败) this.$message.error(`操作失败: ${error.message}`); } } finally { this.isDetecting = false; } }, // 上一张图片 prevImage() { if (this.imageList.length === 0) { this.$message.info('暂无图片列表'); return; } if (this.currentImageIndex > 0) { this.currentImageIndex--; this.originalImageUrl = this.imageList[this.currentImageIndex]; this.resultImageUrl = ''; this.detectionResults = []; this.selectedFile = null; } else { this.$message.info('已经是第一张图片'); } }, // 下一张图片 nextImage() { if (this.imageList.length === 0) { this.$message.info('暂无图片列表'); return; } if (this.currentImageIndex < this.imageList.length - 1) { this.currentImageIndex++; this.originalImageUrl = this.imageList[this.currentImageIndex]; this.resultImageUrl = ''; this.detectionResults = []; this.selectedFile = null; } else { this.$message.info('已经是最后一张图片'); } }, // 放大图片 zoomIn() { if (this.zoomLevel < this.maxZoomLevel) { this.zoomLevel = parseFloat((this.zoomLevel + 0.2).toFixed(1)); } }, // 缩小图片 zoomOut() { if (this.zoomLevel > this.minZoomLevel) { this.zoomLevel = parseFloat((this.zoomLevel - 0.2).toFixed(1)); } }, // 重置图片和缩放 resetImage() { this.zoomLevel = 1; this.resultImageUrl = ''; this.detectionResults = []; }, // 格式化边界框显示 formatBoundingBox(box) { if (Array.isArray(box) && box.length === 4) { return `x1: ${box[0]}, y1: ${box[1]}, x2: ${box[2]}, y2: ${box[3]}`; } return box || '未知'; } }, mounted() { // 初始化时检查本地存储的图片列表 const savedImages = localStorage.getItem('detectionImages'); if (savedImages) { try { this.imageList = JSON.parse(savedImages); this.currentImageIndex = this.imageList.length - 1; if (this.imageList.length > 0) { this.originalImageUrl = this.imageList[this.currentImageIndex]; } } catch (e) { console.error('加载历史图片失败:', e); this.imageList = []; } } }, watch: { // 保存图片列表到本地存储 imageList(newVal) { localStorage.setItem('detectionImages', JSON.stringify(newVal)); } } }; </script> <style scoped> .image-detection-container { max-width: 1200px; margin: 0 auto; padding: 20px; font-family: Arial, sans-serif; } .image-section { display: flex; justify-content: space-between; margin-bottom: 20px; flex-wrap: wrap; } .image-box { flex: 1; min-width: 300px; margin: 0 10px 20px; text-align: center; } .image-display { border: 1px solid #ccc; padding: 10px; min-height: 300px; display: flex; justify-content: center; align-items: center; overflow: auto; background-color: #f9f9f9; } .image-display img { max-width: 100%; max-height: 400px; transition: transform 0.3s ease; } .placeholder { color: #666; font-size: 14px; } .result-data-box { flex: 0.5; min-width: 300px; margin: 0 10px 20px; text-align: left; } .result-data { border: 1px solid #ccc; padding: 10px; min-height: 300px; background-color: #fff; } .result-item { padding: 10px; border-bottom: 1px dashed #eee; margin-bottom: 10px; } .result-item:last-child { border-bottom: none; margin-bottom: 0; } .button-section { display: flex; justify-content: center; gap: 10px; flex-wrap: wrap; margin-top: 10px; } .action-button { padding: 10px 20px; background-color: #007bff; color: white; border: none; cursor: pointer; border-radius: 4px; transition: background-color 0.3s; font-size: 14px; } .action-button:hover { background-color: #0056b3; } .action-button:disabled { background-color: #ccc; cursor: not-allowed; opacity: 0.7; } @media (max-width: 768px) { .image-section { flex-direction: column; } } </style>这是我图片检测的前端代码<template> <div class="data-analysis-container"> <h2>数据分析</h2> <table class="data-table"> <thead> <tr> <th>编号</th> <th>类别</th> <th>置信度</th> <th>坐标</th> <th>图片</th> <th>时长</th> <th>来源</th> </tr> </thead> <tbody> <!-- 表格内容将通过数据绑定动态生成 --> </tbody> </table> </div> </template> <script> export default { name: 'DataAnalysis', data() { return { // 表格数据将在这里定义 } } } </script> <style scoped> .data-analysis-container { max-width: 1200px; margin: 0 auto; padding: 20px; } h2 { text-align: center; margin-bottom: 20px; } .data-table { width: 100%; border-collapse: collapse; } .data-table th, .data-table td { border: 1px solid #000; padding: 8px; text-align: center; } .data-table th { background-color: #f2f2f2; } </style>这是我数据分析的ui界面前端代码,现在我需要你帮我把这三个结合起来,在不改变我ui界面的前提下,使我在使用图片检测的时候,会有对应的数据返回到我数据分析的ui界面上,给我完整的前后端代码
最新发布
07-18
### 图片检测与数据分析界面的整合方案 为了实现将基于YOLO的图片检测功能与数据分析界面集成,并使检测结果自动显示在数据分析页面,需要从前端、后端和数据流三个方面进行系统性设计。 #### 前端开发 前端采用Vue框架构建UI组件,其中包含两个主要部分:图像上传/检测区域和数据分析展示区域。用户上传图像后,通过HTTP请求将图像发送至Flask后端[^2]。以下是一个简化的Vue组件示例代码: ```vue <template> <div> <input type="file" @change="uploadImage" /> <button @click="submitImage">提交</button> <img :src="imagePreview" v-if="imagePreview" /> <div v-if="detectionResult"> <h3>检测结果:</h3> <p>{{ detectionResult }}</p> </div> <div v-if="analysisData"> <h3>数据分析:</h3> <p>{{ analysisData }}</p> </div> </div> </template> <script> export default { data() { return { imageFile: null, imagePreview: null, detectionResult: null, analysisData: null }; }, methods: { uploadImage(event) { this.imageFile = event.target.files[0]; this.imagePreview = URL.createObjectURL(this.imageFile); }, async submitImage() { const formData = new FormData(); formData.append('image', this.imageFile); // 发送图像到Flask后端 const response = await fetch('/api/detect', { method: 'POST', body: formData }); const result = await response.json(); this.detectionResult = result; // 获取分析数据 const analysisResponse = await fetch(`/api/analyze/${result.id}`); const analysisResult = await analysisResponse.json(); this.analysisData = analysisResult; } } }; </script> ``` #### 后端开发 后端由Flask API组成,负责接收图像上传请求,调用YOLO模型进行目标检测,并将检测结果存储到MySQL数据库中。此外,还需提供一个API用于获取特定ID对应的分析数据[^1]。 ```python from flask import Flask, request, jsonify import os from werkzeug.utils import secure_filename from yolov11 import YOLOv11Model # 假设已经有一个封装好的YOLOv11模型类 app = Flask(__name__) UPLOAD_FOLDER = './uploads' ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg'} # 初始化YOLO模型 model = YOLOv11Model() def allowed_file(filename): return '.' in filename and \ filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS @app.route('/api/detect', methods=['POST']) def detect(): if 'image' not in request.files: return jsonify({'error': 'No file part'}), 400 file = request.files['image'] if file.filename == '': return jsonify({'error': 'No selected file'}), 400 if file and allowed_file(file.filename): filename = secure_filename(file.filename) filepath = os.path.join(UPLOAD_FOLDER, filename) file.save(filepath) # 使用YOLO模型进行检测 result = model.detect(filepath) # 存储检测结果到数据库 result_id = save_to_database(result) return jsonify({'id': result_id, 'result': result}), 200 @app.route('/api/analyze/<string:result_id>', methods=['GET']) def analyze(result_id): # 根据result_id从数据库获取分析数据 analysis_data = get_analysis_data(result_id) return jsonify(analysis_data), 200 if __name__ == '__main__': app.run(debug=True) ``` #### 数据库设计 使用MySQL作为数据库来存储检测结果及其对应的分析数据。可以创建一张表来保存每次检测的结果信息,包括唯一标识符、原始图像路径、检测详情等字段。 ```sql CREATE TABLE detection_results ( id VARCHAR(255) PRIMARY KEY, image_path TEXT NOT NULL, detection_details JSON, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); ``` #### 视频处理与扩展 虽然当前需求是针对图片检测,但考虑到未来可能需要支持视频流检测,可以在系统架构中预留对FFmpeg的支持,以便于后续扩展[^2]。 #### 安全性和性能优化 确保所有文件上传操作都经过严格的安全检查,防止恶意文件注入。同时,对于大规模并发访问场景,考虑引入缓存机制(如Redis)来提高响应速度,减少数据库查询压力。 #### 用户体验优化 为了提升用户体验,可以在前端添加加载动画或进度条提示用户等待检测完成。另外,在检测结果返回后,可以通过图表库(如ECharts)将分析数据以可视化形式呈现给用户。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值