# 确保安装所有依赖: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界面上,给我完整的前后端代码
最新发布