-
依賴于前文,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>