-
依赖于前文,linux系统上部署yolo识别图片,远程宿主机访问docker全流程(https://blog.csdn.net/yanzhuang521967/article/details/148777650?spm=1001.2014.3001.5501)
fastapi把端口暴露出来 -
后端代码
from fastapi import FastAPI, UploadFile, File, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse, StreamingResponse, Response
from starlette.responses import RedirectResponse
from urllib.parse import urlparse
from ultralytics import YOLO
import os
import json
from pathlib import Path
from fastapi.staticfiles import StaticFiles
import logging
import io
from PIL import Image# 初始化应用
app = FastAPI()# 配置日志
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)# 静态文件服务
app.mount("/output-predict", StaticFiles(directory="/usr/src/output/predict"), name="output-predict")
app.mount("/output-labels", StaticFiles(directory="/usr/src/output/predict/labels"), name="output-labels")# CORS配置
app.add_middleware(CORSMiddleware,allow_origins=["*"],allow_credentials=True,allow_methods=["*"],allow_headers=["*"],expose_headers=["*"]
)# 修复后的协议转换中间件
@app.middleware("http")
async def protocol_converter(request: Request, call_next):try:# 处理HTTPS转HTTP(如果需要)if request.url.scheme == "https":url = str(request.url).replace("https://", "http://", 1)logger.info(f"Converting HTTPS to HTTP: {url}")scope = request.scopescope["scheme"] = "http"headers = []for k, v in scope["headers"]:if k == b"referer":headers.append((k, v.replace(b"https://", b"http://")))else:headers.append((k, v))scope["headers"] = headersresponse = await call_next(request)# 不处理流式响应和重定向if isinstance(response, (StreamingResponse, RedirectResponse)):return response# 获取响应体(兼容新旧版本)if hasattr(response, "body_iterator"):# 处理StreamingResponsebody = b"".join([chunk async for chunk in response.body_iterator])else:# 普通响应body = await response.body()# 替换内容中的HTTPS链接(如果需要)if body and b"https://" in body:body = body.replace(b"https://", b"http://")logger.debug("Replaced HTTPS links in response body")return Response(content=body,status_code=response.status_code,media_type=response.media_type,headers=dict(response.headers))except Exception as e:logger.error(f"Protocol converter error: {str(e)}", exc_info=True)raise HTTPException(status_code=500, detail="Internal server error")# 安全头中间件
@app.middleware("http")
async def security_headers(request: Request, call_next):response = await call_next(request)response.headers.update({"Access-Control-Allow-Private-Network": "true","Cross-Origin-Resource-Policy": "cross-origin","X-Content-Type-Options": "nosniff"})return response# 初始化模型和目录
model = YOLO("/ultralytics/yolo11n.pt")
output_base = Path("/usr/src/output")
predict_dir = output_base / "predict"
(predict_dir / "labels").mkdir(parents=True, exist_ok=True)# 辅助函数
async def save_upload_file(file: UploadFile) -> str:"""保存上传文件到临时位置"""temp_path = f"/tmp/{file.filename}"try:with open(temp_path, "wb") as f:content = await file.read()f.write(content)return temp_pathexcept Exception as e:logger.error(f"File save failed: {str(e)}")raise HTTPException(500, "File upload failed")# API端点
@app.post("/predict")
async def predict(request: Request, file: UploadFile = File(...)):temp_path = Nonetry:# 1. 保存文件temp_path = await save_upload_file(file)# 2. 运行预测results = model.predict(source=temp_path,project=str(predict_dir),name="",save=True,save_txt=True,save_conf=True,exist_ok=True)# 3. 准备结果(使用新的to_json()方法)file_stem = Path(file.filename).stembase_url = str(request.base_url).replace("https://", "http://")json_result = {"filename": file.filename,"detections": json.loads(results[0].to_json()), # 使用to_json()替代tojson()"image_path": f"{base_url}output-predict/{file.filename}","label_path": f"{base_url}output-labels/{file_stem}.txt","speed": {"preprocess": results[0].speed["preprocess"],"inference": results[0].speed["inference"],"postprocess": results[0].speed["postprocess"]}}# 4. 保存JSONjson_path = predict_dir / "labels" / f"{file_stem}.json"with open(json_path, "w") as f:json.dump(json_result, f, indent=2)return JSONResponse({"status": "success","data": json_result,"debug": {"original_protocol": request.url.scheme,"processed_protocol": "http"}})except Exception as e:logger.error(f"Prediction failed: {str(e)}", exc_info=True)raise HTTPException(status_code=500, detail=str(e))finally:if temp_path and os.path.exists(temp_path):os.remove(temp_path)# 健康检查端点
@app.get("/health")
async def health_check():return {"status": "healthy", "protocol": "http"}# 协议检查端点
@app.get("/check-protocol")
async def check_protocol(request: Request):return {"client_protocol": request.url.scheme,"server_protocol": "http","headers": dict(request.headers)}
- 前端代码
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><!-- 在HTML的<head>中添加 --><title>YOLOv8 图像检测系统</title><style>body {font-family: Arial, sans-serif;max-width: 1200px;margin: 0 auto;padding: 20px;line-height: 1.6;}.upload-container {text-align: center;margin-bottom: 30px;}.upload-area {border: 2px dashed #ccc;border-radius: 8px;padding: 40px;margin: 20px 0;cursor: pointer;transition: all 0.3s;}.upload-area:hover {border-color: #4CAF50;background-color: #f9f9f9;}#fileInput {display: none;}button {background-color: #4CAF50;color: white;border: none;padding: 10px 20px;border-radius: 4px;cursor: pointer;font-size: 16px;transition: background 0.3s;}button:hover {background-color: #45a049;}button:disabled {background-color: #cccccc;cursor: not-allowed;}.status {margin: 20px 0;padding: 15px;border-radius: 4px;display: none;}.loading {background-color: #e8f5e9;color: #2e7d32;}.error {background-color: #ffebee;color: #f44336;}.image-container {display: flex;flex-wrap: wrap;gap: 20px;margin-top: 30px;}.image-box {flex: 1;min-width: 300px;margin-bottom: 20px;}.image-box img {max-width: 100%;border-radius: 4px;box-shadow: 0 2px 10px rgba(0,0,0,0.1);}.results-panel {margin-top: 30px;padding: 20px;background-color: #f5f5f5;border-radius: 8px;}pre {background-color: #eee;padding: 15px;border-radius: 4px;overflow-x: auto;}.spinner {border: 4px solid rgba(0,0,0,0.1);border-radius: 50%;border-top: 4px solid #4CAF50;width: 40px;height: 40px;animation: spin 1s linear infinite;margin: 20px auto;}@keyframes spin {0% { transform: rotate(0deg); }100% { transform: rotate(360deg); }}</style>
</head>
<body><div class="upload-container"><h1>YOLOv8 图像检测系统</h1><div class="upload-area" id="dropZone"><p>点击或拖拽图片到此处上传</p><input type="file" id="fileInput" accept="image/*"></div><button id="detectBtn" disabled>开始检测</button><div id="loadingStatus" class="status loading"><div class="spinner"></div><p>正在处理图像,请稍候...</p></div><div id="errorStatus" class="status error"><h3>检测失败</h3><p id="errorMessage"></p></div></div><div class="image-container"><div class="image-box"><h3>原始图片</h3><img id="preview" style="display: none;"></div><div class="image-box"><h3>检测结果</h3><img id="result" style="display: none;"></div></div><div class="results-panel" id="resultsPanel" style="display: none;"><h3>检测结果数据</h3><pre id="jsonData"></pre></div><script>// DOM元素const fileInput = document.getElementById('fileInput');const dropZone = document.getElementById('dropZone');const detectBtn = document.getElementById('detectBtn');const preview = document.getElementById('preview');const result = document.getElementById('result');const loadingStatus = document.getElementById('loadingStatus');const errorStatus = document.getElementById('errorStatus');const errorMessage = document.getElementById('errorMessage');const resultsPanel = document.getElementById('resultsPanel');const jsonData = document.getElementById('jsonData');// 当前处理的文件let currentFile = null;// 文件选择处理fileInput.addEventListener('change', handleFileSelect);dropZone.addEventListener('click', () => fileInput.click());// 拖放功能dropZone.addEventListener('dragover', (e) => {e.preventDefault();dropZone.style.borderColor = '#4CAF50';dropZone.style.backgroundColor = '#f0fff0';});dropZone.addEventListener('dragleave', () => {dropZone.style.borderColor = '#ccc';dropZone.style.backgroundColor = '';});dropZone.addEventListener('drop', (e) => {e.preventDefault();dropZone.style.borderColor = '#ccc';dropZone.style.backgroundColor = '';if (e.dataTransfer.files.length) {fileInput.files = e.dataTransfer.files;handleFileSelect({ target: fileInput });}});// 检测按钮点击detectBtn.addEventListener('click', startDetection);function handleFileSelect(event) {const file = event.target.files[0];if (file && file.type.match('image.*')) {currentFile = file;const reader = new FileReader();reader.onload = (e) => {preview.src = e.target.result;preview.style.display = 'block';detectBtn.disabled = false;// 重置状态result.style.display = 'none';resultsPanel.style.display = 'none';errorStatus.style.display = 'none';};reader.readAsDataURL(file);}}async function startDetection() {if (!currentFile) return;// 显示加载状态loadingStatus.style.display = 'block';errorStatus.style.display = 'none';detectBtn.disabled = true;try {// 1. 上传图片进行预测const formData = new FormData();formData.append('file', currentFile);const response = await fetch('http://192.168.0.100:34567/predict', {method: 'POST',mode: 'cors', // 确保这是cors而不是no-corsbody: formData});if (!response.ok) {const error = await response.text();throw new Error(error || `服务器错误: ${response.status}`);}const resultData = await response.json();// 2. 显示处理后的图片const filename = currentFile.name.split('.')[0];result.src = `http://192.168.0.100:34567/output-predict/predict/${filename}.jpg?t=${Date.now()}`;result.style.display = 'block';// 3. 显示JSON数据jsonData.textContent = JSON.stringify(resultData, null, 2);resultsPanel.style.display = 'block';} catch (error) {console.error('检测失败:', error);errorMessage.textContent = error.message;errorStatus.style.display = 'block';} finally {loadingStatus.style.display = 'none';detectBtn.disabled = false;}}</script>
</body>
</html>