Files
2025-01-12 06:15:15 +00:00

462 lines
16 KiB
Python

import os
import cv2
import torch
import numpy as np
from fastapi import FastAPI, HTTPException, File, UploadFile, Depends, Header
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse, StreamingResponse
from fastapi.security import APIKeyHeader
from redis import Redis
from ultralytics import YOLO
import json
import uvicorn
from kafka import KafkaProducer, KafkaConsumer
import uuid
from datetime import datetime, timedelta, timezone
import threading
app = FastAPI()
yolo_app = FastAPI()
app.mount("/yolo", yolo_app)
# CORS配置
ALLOWED_ORIGINS = 'https://beta.obscura.work'
# 只为主应用添加CORS中间件
app.add_middleware(
CORSMiddleware,
allow_origins=ALLOWED_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 配置
MODEL_PATH = "/home/zydi/models/yolov8x.pt" # 请替换为您的模型路径
KAFKA_BROKER = "222.186.10.253:9092"
KAFKA_TOPIC = "yolo" # 指定Kafka topic
KAFKA_GROUP_ID = "yolo_group" # 指定消费者组ID
REDIS_HOST = "222.186.10.253"
REDIS_PORT = 6379
REDIS_PASSWORD = "Obscura@2024"
REDIS_DB = 6
REDIS_API_DB = 12
REDIS_API_USAGE_DB = 13
UPLOAD_DIR = "/www/wwwroot/beta.obscura.work/upload_files/upload"
RESULT_DIR = "/www/wwwroot/beta.obscura.work/upload_files/result"
MAX_FILE_AGE = timedelta(hours=1)
# 确保目录存在
os.makedirs(UPLOAD_DIR, exist_ok=True)
os.makedirs(RESULT_DIR, exist_ok=True)
# 初始化 Kafka
producer = KafkaProducer(bootstrap_servers=[KAFKA_BROKER])
consumer = KafkaConsumer(
KAFKA_TOPIC,
bootstrap_servers=[KAFKA_BROKER],
group_id=KAFKA_GROUP_ID,
auto_offset_reset='earliest',
enable_auto_commit=True,
value_deserializer=lambda x: json.loads(x.decode('utf-8'))
)
# 初始化 Redis
redis_client = Redis(
host=REDIS_HOST,
port=REDIS_PORT,
password=REDIS_PASSWORD,
db=REDIS_DB
)
redis_api_client = Redis(
host=REDIS_HOST,
port=REDIS_PORT,
password=REDIS_PASSWORD,
db=REDIS_API_DB
)
redis_api_usage_client = Redis(
host=REDIS_HOST,
port=REDIS_PORT,
password=REDIS_PASSWORD,
db=REDIS_API_USAGE_DB
)
# 添加API密钥验证
API_KEY_NAME = "X-API-Key"
api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False)
async def get_api_key(api_key: str = Header(None, alias=API_KEY_NAME)):
if api_key is None:
raise HTTPException(status_code=400, detail="API密钥缺失")
return api_key
def calculate_tokens(file_path: str, file_type: str) -> int:
base_tokens = 0
try:
file_size = os.path.getsize(file_path) # 获取文件大小(字节)
# 基础token:每MB文件大小消耗10个token
base_tokens = int((file_size / (1024 * 1024)) * 10)
if file_type == "image":
img = cv2.imread(file_path)
if img is None:
raise ValueError("无法读取图片文件")
height, width = img.shape[:2]
pixel_count = height * width
# 图片token:每100个像素额外消耗5个token
image_tokens = int((pixel_count / 10000) * 5)
base_tokens += image_tokens
elif file_type == "video":
cap = cv2.VideoCapture(file_path)
if not cap.isOpened():
raise ValueError("无法打开视频文件")
fps = cap.get(cv2.CAP_PROP_FPS)
frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
cap.release()
pixel_count = width * height * frame_count
duration = frame_count / fps # 视频时长(秒)
# 视频token:每100万像素每秒额外消耗1个token
video_tokens = int((pixel_count / 10000) * (duration / 60))
base_tokens += video_tokens
return max(1, base_tokens) # 确保至少返回1个token
except Exception as e:
print(f"计算token时出错: {str(e)}")
return 1 # 出错时返回默认值1
async def verify_api_key(api_key: str = Depends(get_api_key)):
redis_key = f"api_key:{api_key}"
api_key_info = redis_api_client.hgetall(redis_key)
if not api_key_info:
return None
api_key_info = {k.decode('utf-8'): v.decode('utf-8') for k, v in api_key_info.items()}
if api_key_info.get('is_active') != '1':
return None
expires_at = datetime.fromisoformat(api_key_info.get('expires_at'))
if datetime.now(timezone.utc) > expires_at:
return None
usage_info = redis_api_usage_client.hgetall(redis_key)
usage_info = {k.decode('utf-8'): v.decode('utf-8') for k, v in usage_info.items()}
return {
**api_key_info,
**usage_info
}
async def update_token_usage(api_key: str, new_tokens_used: int, model_name: str):
redis_key = f"api_key:{api_key}"
current_time = datetime.now(timezone.utc).isoformat()
pipe = redis_api_usage_client.pipeline()
# 更新总的token使用量
pipe.hincrby(redis_key, "tokens_used", new_tokens_used)
pipe.hset(redis_key, "last_used_at", current_time)
# 更新特定模型的token使用量
model_tokens_field = f"{model_name}_tokens_used"
model_last_used_field = f"{model_name}_last_used_at"
pipe.hincrby(redis_key, model_tokens_field, new_tokens_used)
pipe.hset(redis_key, model_last_used_field, current_time)
pipe.execute()
class yoloDetector:
def __init__(self, model_path):
self.model = YOLO(model_path)
def detect(self, frame):
results = self.model(frame)
return results
def format_results(self, results, original_shape):
formatted_results = []
for r in results:
boxes = r.boxes
for box in boxes:
x1, y1, x2, y2 = box.xyxy[0].tolist()
# 缩放坐标到原始图像尺寸
x1, x2 = [x * original_shape[1] / 640 for x in [x1, x2]]
y1, y2 = [y * original_shape[0] / 640 for y in [y1, y2]]
conf = box.conf.item()
cls = int(box.cls.item())
name = self.model.names[cls]
formatted_results.append({
"class": name,
"confidence": conf,
"bbox": [x1, y1, x2, y2]
})
return formatted_results
def draw_results(self, frame, formatted_results):
for result in formatted_results:
x1, y1, x2, y2 = map(int, result['bbox'])
name = result['class']
conf = result['confidence']
cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) # 使用固定的绿色
label = f"{name} {conf:.2f}"
(text_width, text_height), _ = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 1, 2)
cv2.rectangle(frame, (x1, y1 - text_height - 5), (x1 + text_width, y1), (0, 255, 0), -1)
cv2.putText(frame, label, (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2)
return frame
detector = yoloDetector(MODEL_PATH)
def process_image(image_data, filename):
try:
original_img = cv2.imdecode(np.frombuffer(image_data, np.uint8), cv2.IMREAD_COLOR)
original_shape = original_img.shape
img = cv2.cvtColor(original_img, cv2.COLOR_BGR2RGB)
img = cv2.resize(img, (640, 640))
img = img.transpose((2, 0, 1))
img = np.ascontiguousarray(img)
img = torch.from_numpy(img).float()
img /= 255.0
img = img.unsqueeze(0)
results = detector.detect(img)
json_results = detector.format_results(results, original_shape)
annotated_img = detector.draw_results(original_img, json_results)
annotated_filename = f"yolo_{filename}"
annotated_path = os.path.join(RESULT_DIR, annotated_filename)
cv2.imwrite(annotated_path, annotated_img)
return json_results, annotated_filename
except Exception as e:
print(f"处理图像时出错: {str(e)}")
return None, None
def process_video(video_data, filename):
try:
temp_video_path = os.path.join(UPLOAD_DIR, f"yolo_{filename}")
with open(temp_video_path, 'wb') as temp_video:
temp_video.write(video_data)
cap = cv2.VideoCapture(temp_video_path)
frame_count = 0
json_results = []
fps = int(cap.get(cv2.CAP_PROP_FPS))
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
original_shape = (height, width)
annotated_filename = f"yolo_{filename}"
output_path = os.path.join(RESULT_DIR, annotated_filename)
out = cv2.VideoWriter(output_path, cv2.VideoWriter_fourcc(*'mp4v'), 1, (width, height))
while cap.isOpened():
ret, frame = cap.read()
if not ret:
break
# 每秒只处理一帧
if frame_count % fps == 0:
preprocessed_frame = preprocess_frame(frame)
results = detector.detect(preprocessed_frame)
frame_json_results = detector.format_results(results, original_shape)
json_results.append({"frame": frame_count, "detections": frame_json_results})
annotated_frame = detector.draw_results(frame, frame_json_results)
out.write(annotated_frame)
frame_count += 1
cap.release()
out.release()
os.remove(temp_video_path)
return json_results, annotated_filename
except Exception as e:
print(f"处理视频时出错: {str(e)}")
return None, None
def preprocess_frame(frame):
# 预处理单个视频帧
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
frame_resized = cv2.resize(frame_rgb, (640, 640)) # 调整为YOLO输入尺寸
frame_transposed = frame_resized.transpose((2, 0, 1)) # HWC转为CHW
frame_contiguous = np.ascontiguousarray(frame_transposed)
frame_tensor = torch.from_numpy(frame_contiguous).float()
frame_normalized = frame_tensor / 255.0 # 归一化到[0, 1]
frame_batched = frame_normalized.unsqueeze(0) # 添加批次维度
return frame_batched
@yolo_app.post("/upload")
async def upload_file(file: UploadFile = File(...),api_key: str = Depends(get_api_key)):
# 验证 API key
api_key_info = await verify_api_key(api_key)
if not api_key_info:
raise HTTPException(status_code=403, detail="无效的API密钥")
content = await file.read()
file_extension = os.path.splitext(file.filename)[1].lower()
new_filename = f"{uuid.uuid4()}{file_extension}"
# 保存原始文件
original_file_path = os.path.join(UPLOAD_DIR, new_filename)
with open(original_file_path, "wb") as f:
f.write(content)
# 计算 token
file_type = "image" if file_extension in ['.jpg', '.jpeg', '.png'] else "video"
tokens_required = calculate_tokens(original_file_path, file_type)
if tokens_required is None or tokens_required <= 0:
raise HTTPException(status_code=500, detail="无法计算所需的token数量")
# 检查并更新 token 使用量
usage_key = f"api_key:{api_key}"
total_tokens = int(redis_api_usage_client.hget(usage_key, "total_tokens") or 0)
tokens_used = int(redis_api_usage_client.hget(usage_key, "tokens_used") or 0)
if tokens_used + tokens_required > total_tokens:
raise HTTPException(status_code=403, detail="Token 余额不足")
# 更新 token 使用量
model_name = "yolov8x"
await update_token_usage(api_key, tokens_required, model_name)
# 发送处理任务到 Kafka
producer.send(KAFKA_TOPIC, json.dumps({
"filename": new_filename,
"file_type": file_type
}).encode('utf-8'))
# 在 Redis 中设置初始状态
redis_key = f"yolo_result:{new_filename}"
redis_client.set(redis_key, json.dumps({"status": "queued"}))
# 获取更新后的 token 使用情况
updated_api_key_info = await verify_api_key(api_key)
new_tokens_used = int(updated_api_key_info.get("tokens_used", 0))
model_tokens_used = int(updated_api_key_info.get(f"{model_name}_tokens_used", 0))
return JSONResponse(content={
"message": "文件已上传并排队等待处理",
"filename": new_filename,
"tokens_used": tokens_required,
"total_tokens_used": new_tokens_used,
f"{model_name}_tokens_used": model_tokens_used,
"tokens_remaining": total_tokens - new_tokens_used
})
@yolo_app.get("/result/{filename}", dependencies=[Depends(verify_api_key)])
async def get_yolo_result(filename: str):
redis_key = f"yolo_result:{filename}"
result = redis_client.get(redis_key)
if result:
result_data = json.loads(result)
return JSONResponse(content=result_data) # 直接返回整个结果,包括 status
else:
raise HTTPException(status_code=404, detail="Result not found")
@yolo_app.get("/annotated/{filename}", dependencies=[Depends(verify_api_key)])
async def get_annotated_file(filename: str):
redis_key = f"yolo_result:{filename}"
result = redis_client.get(redis_key)
if result:
result_data = json.loads(result)
if result_data["status"] == "completed":
annotated_filename = result_data["annotated_filename"]
file_path = os.path.join(RESULT_DIR, annotated_filename)
if os.path.exists(file_path):
def iterfile():
with open(file_path, mode="rb") as file_like:
yield from file_like
file_extension = os.path.splitext(annotated_filename)[1].lower()
return StreamingResponse(iterfile(), media_type=f"image/{file_extension[1:]}" if file_extension in ['.jpg', '.jpeg', '.png'] else "video/mp4")
raise HTTPException(status_code=404, detail="Annotated file not found")
def process_task():
for message in consumer:
task = message.value
filename = task['filename']
file_type = task['file_type']
file_path = os.path.join(UPLOAD_DIR, filename)
# Update status to "processing"
redis_key = f"yolo_result:{filename}"
redis_client.set(redis_key, json.dumps({"status": "processing"}))
try:
if file_type == "image":
with open(file_path, 'rb') as f:
content = f.read()
json_results, annotated_filename = process_image(content, filename)
else:
with open(file_path, 'rb') as f:
content = f.read()
json_results, annotated_filename = process_video(content, filename)
if json_results and annotated_filename:
redis_client.set(redis_key, json.dumps({
"json_results": json_results,
"status": "completed",
"annotated_filename": annotated_filename
}))
else:
redis_client.set(redis_key, json.dumps({"status": "failed"}))
except Exception as e:
print(f"Error processing task: {str(e)}")
redis_client.set(redis_key, json.dumps({"status": "failed", "error": str(e)}))
def listen_redis_changes():
pubsub = redis_client.pubsub()
pubsub.psubscribe('__keyspace@3__:yolo_result:*') # 监听所有yolo_result键的变化
for message in pubsub.listen():
if message['type'] == 'pmessage':
key = message['channel'].decode('utf-8').split(':')[-1]
operation = message['data'].decode('utf-8')
if operation == 'set':
value = redis_client.get(f"yolo_result:{key}")
if value:
result = json.loads(value)
print(f"Status update for {key}: {result['status']}")
# 这里可以添加其他处理逻辑,比如发送通知等
if __name__ == "__main__":
# 启动处理任务的线程
threading.Thread(target=process_task, daemon=True).start()
# 启动Redis监听线程
threading.Thread(target=listen_redis_changes, daemon=True).start()
uvicorn.run(app, host="0.0.0.0", port=7003)