| @ -0,0 +1,23 @@ | |||||
| # 使用 Python 3.9 轻量版 | |||||
| FROM python:3.9-slim | |||||
| # 安装系统级依赖 (OpenCV 和 YOLO 运行必须) | |||||
| RUN apt-get update && apt-get install -y \ | |||||
| libgl1 \ | |||||
| libglib2.0-0 \ | |||||
| && rm -rf /var/lib/apt/lists/* | |||||
| WORKDIR /app | |||||
| # 安装依赖 | |||||
| COPY requirements.txt . | |||||
| RUN pip install --no-cache-dir -r requirements.txt | |||||
| # 拷贝代码 | |||||
| COPY ./app /app/app | |||||
| # 设置环境变量,确保 Python 能找到 app 模块 | |||||
| ENV PYTHONPATH=/app | |||||
| # 默认启动命令 (会被 docker-compose 中的 command 覆盖) | |||||
| CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "18005"] | |||||
| @ -0,0 +1,116 @@ | |||||
| # 水库智能算法云服务接口文档 | |||||
| ## 服务概述 | |||||
| 基于 FastAPI 构建的水库智能算法云服务,提供视频流分析和图像识别功能,支持多种 YOLO 模型场景检测。 | |||||
| **服务地址**: `http://33.36.139.134.254:18005` | |||||
| --- | |||||
| ## 接口列表 | |||||
| ### 1. 视频流异步分析接口 | |||||
| **接口路径**: `POST /v1/task/stream` | |||||
| **描述**: 将 RTSP 视频流任务派发给 Celery worker 异步处理 | |||||
| **请求体**: | |||||
| ```json | |||||
| { | |||||
| "source_url": "rtsp://摄像头地址", | |||||
| "webhook_url": "回调地址", | |||||
| "scene_type": "floating_trash | fishing | swimming", | |||||
| "roi": [[x1, y1], [x2, y2], ...] | |||||
| } | |||||
| ``` | |||||
| **参数说明**: | |||||
| | 参数 | 类型 | 必填 | 说明 | | |||||
| |------|------|------|------| | |||||
| | source_url | string | 是 | 摄像头 RTSP 流地址 | | |||||
| | webhook_url | string | 是 | 业务系统回调地址 | | |||||
| | scene_type | string | 是 | 场景类型 | | |||||
| | roi | array | 否 | 电子围栏坐标点集 | | |||||
| **响应示例**: | |||||
| ```json | |||||
| { | |||||
| "code": 200, | |||||
| "msg": "任务已下发至计算集群", | |||||
| "task_type": "floating_trash" | |||||
| } | |||||
| ``` | |||||
| --- | |||||
| ### 2. 指定模型图像预测接口 | |||||
| **接口路径**: `POST /{model_type}/predict/image` | |||||
| **描述**: 使用指定模型对上传的图片进行目标检测 | |||||
| **路径参数**: | |||||
| | 参数 | 说明 | | |||||
| |------|------| | |||||
| | model_type | 模型类型 | | |||||
| **支持的模型类型**: | |||||
| | model_type | 模型文件 | 说明 | | |||||
| |------------|----------|------| | |||||
| | default | yolov8n.pt | 默认预训练模型 | | |||||
| | bati | model/Bati/best.pt | 坝体检测模型 | | |||||
| | buildings | model/buildings/best.pt | 建筑物检测模型 | | |||||
| | disaster | model/disaster/best.pt | 灾害检测模型 | | |||||
| | fire-smoke | model/fire-smoke/best.pt | 烟火检测模型 | | |||||
| | Floating | model/Floating/best.pt | 漂浮物检测模型 | | |||||
| | ship | model/ship/best.pt | 船只检测模型 | | |||||
| **请求表单**: | |||||
| - `file`: 图片文件 (支持 jpg, png 等格式) | |||||
| **响应示例**: | |||||
| ```json | |||||
| { | |||||
| "results": [ | |||||
| { | |||||
| "boxes": { | |||||
| "xyxy": [[x1, y1, x2, y2]], | |||||
| "conf": [0.95], | |||||
| "cls": [0] | |||||
| }, | |||||
| "names": { "0": "目标类别" } | |||||
| } | |||||
| ] | |||||
| } | |||||
| ``` | |||||
| --- | |||||
| ### 3. 默认模型图像预测接口 | |||||
| **接口路径**: `POST /v1/predict/image` | |||||
| **描述**: 使用默认 YOLOv8n 模型对图片进行预测 | |||||
| **请求表单**: | |||||
| - `file`: 图片文件 | |||||
| **响应示例**: 同接口2 | |||||
| --- | |||||
| **启动参数**: | |||||
| - Host: `0.0.0.0` | |||||
| - Port: `8000` | |||||
| --- | |||||
| ## 错误处理 | |||||
| | HTTP 状态码 | 说明 | | |||||
| |-------------|------| | |||||
| | 400 | 请求参数错误 | | |||||
| | 500 | 服务器内部错误 | | |||||
| @ -0,0 +1,168 @@ | |||||
| # 延寿水库智能识别服务接口说明 | |||||
| ## 1. 项目概述 | |||||
| 本服务基于 FastAPI 和 YOLO 模型,为延寿水库项目提供按场景划分的智能识别能力。当前支持两类能力: | |||||
| - 图像识别:上传单张图片并返回识别结果。 | |||||
| - 视频流识别:提交 RTSP 或本地视频流地址,系统异步分析并通过回调地址推送结果。 | |||||
| 服务中的 `model_type` 对应具体业务场景,每个场景绑定一个独立模型文件。 | |||||
| ## 2. 支持场景 | |||||
| - `floating`:水面漂浮物识别。用于识别水面漂浮垃圾、杂物等目标。 | |||||
| - `gate`:闸口场景识别。用于识别闸口、闸门区域中的目标和异常情况。 | |||||
| - `shore_garbage`:岸边垃圾识别。用于识别岸线附近堆积或散落的垃圾目标。 | |||||
| - `water_gauge`:水尺识别。用于识别水尺、水位尺等目标,服务于水位监测场景。 | |||||
| ## 3. 接口列表 | |||||
| - `POST /{model_type}/predict/image`:按场景执行图片识别。 | |||||
| - `POST /{model_type}/predict/stream`:按场景提交视频流识别任务。 | |||||
| - `POST /v1/task/stream`:兼容旧版视频流接口,通过 `scene_type` 指定场景。 | |||||
| - `POST /v1/predict/image`:旧版图片接口说明入口,当前会返回引导信息,请改用新接口。 | |||||
| ## 4. 图片识别接口 | |||||
| ### 4.1 请求地址 | |||||
| `POST /{model_type}/predict/image` | |||||
| ### 4.2 路径参数 | |||||
| - `model_type`:场景类型,可选值为 `floating`、`gate`、`shore_garbage`、`water_gauge`。 | |||||
| ### 4.3 请求方式 | |||||
| `multipart/form-data` | |||||
| ### 4.4 请求参数 | |||||
| - `file`:待识别图片文件,支持常见图片格式。 | |||||
| ### 4.5 调用示例 | |||||
| ```bash | |||||
| curl -X POST "http://localhost:8000/floating/predict/image" \ | |||||
| -H "accept: application/json" \ | |||||
| -H "Content-Type: multipart/form-data" \ | |||||
| -F "file=@test.jpg;type=image/jpeg" | |||||
| ``` | |||||
| ### 4.6 返回示例 | |||||
| ```json | |||||
| { | |||||
| "results": [ | |||||
| { | |||||
| "name": "floating_object", | |||||
| "class": 0, | |||||
| "confidence": 0.93, | |||||
| "box": { | |||||
| "x1": 102.4, | |||||
| "y1": 55.2, | |||||
| "x2": 260.8, | |||||
| "y2": 188.6 | |||||
| } | |||||
| } | |||||
| ] | |||||
| } | |||||
| ``` | |||||
| ## 5. 视频流识别接口 | |||||
| ### 5.1 新版接口 | |||||
| `POST /{model_type}/predict/stream` | |||||
| ### 5.2 兼容接口 | |||||
| `POST /v1/task/stream` | |||||
| 兼容接口中,场景通过请求体的 `scene_type` 指定,其可选值与 `model_type` 一致。 | |||||
| ### 5.3 请求体参数 | |||||
| - `source_url`:视频流地址,支持 `rtsp://...`、本地视频文件路径等 OpenCV 可读取地址。 | |||||
| - `webhook_url`:识别结果回调地址,必须是服务端可访问的 `HTTP/HTTPS POST` 接口。 | |||||
| - `roi`:可选电子围栏,多边形坐标数组,格式为 `[[x1, y1], [x2, y2], ...]`。不传时默认检测整幅画面。 | |||||
| ### 5.4 请求体示例 | |||||
| ```json | |||||
| { | |||||
| "source_url": "rtsp://admin:password@192.168.1.100:554/h264/ch1/main/av_stream", | |||||
| "webhook_url": "https://example.com/api/reservoir/webhook", | |||||
| "roi": [[0, 0], [1920, 0], [1920, 1080], [0, 1080]] | |||||
| } | |||||
| ``` | |||||
| ### 5.5 调用示例 | |||||
| ```bash | |||||
| curl -X POST "http://localhost:8000/floating/predict/stream" \ | |||||
| -H "Content-Type: application/json" \ | |||||
| -d '{ | |||||
| "source_url": "rtsp://admin:password@192.168.1.100:554/h264/ch1/main/av_stream", | |||||
| "webhook_url": "https://example.com/api/reservoir/webhook", | |||||
| "roi": [[0, 0], [1920, 0], [1920, 1080], [0, 1080]] | |||||
| }' | |||||
| ``` | |||||
| ### 5.6 提交成功返回示例 | |||||
| ```json | |||||
| { | |||||
| "code": 200, | |||||
| "msg": "task submitted", | |||||
| "task_type": "floating" | |||||
| } | |||||
| ``` | |||||
| ## 6. webhook_url 参数说明 | |||||
| - `webhook_url` 必须是业务系统提供的接收地址。 | |||||
| - 请求方法为 `POST`。 | |||||
| - 请求体格式为 `application/json`。 | |||||
| - 当视频流识别到目标时才会触发回调;未识别到目标时不会持续推送空结果。 | |||||
| - 业务系统应保证该地址可被算法服务访问,并能在超时时间内返回。 | |||||
| ## 7. 视频流回调数据格式 | |||||
| ### 7.1 回调 JSON 示例 | |||||
| ```json | |||||
| { | |||||
| "event": "RESERVOIR_ALARM", | |||||
| "scene": "floating", | |||||
| "stream_url": "rtsp://example.com/live", | |||||
| "details": [ | |||||
| { | |||||
| "label": "floating_object", | |||||
| "confidence": 0.93, | |||||
| "bbox": [102.4, 55.2, 260.8, 188.6] | |||||
| } | |||||
| ], | |||||
| "msg": "detected floating event" | |||||
| } | |||||
| ``` | |||||
| ### 7.2 字段说明 | |||||
| - `event`:事件类型,当前为 `RESERVOIR_ALARM`。 | |||||
| - `scene`:触发识别的场景类型,对应接口中的 `model_type`。 | |||||
| - `stream_url`:本次识别的视频流地址。 | |||||
| - `details`:识别结果数组。 | |||||
| - `label`:识别类别名称。 | |||||
| - `confidence`:识别置信度,范围通常为 0 到 1。 | |||||
| - `bbox`:目标框坐标,格式为 `[x1, y1, x2, y2]`。 | |||||
| - `msg`:简要告警说明。 | |||||
| ## 8. 使用建议 | |||||
| - 视频流接口为异步提交接口,请结合业务系统的 `webhook_url` 接收识别结果。 | |||||
| - 若需要限定检测区域,建议传入 `roi`,减少无关区域误报。 | |||||
| - 推荐通过 FastAPI 自带文档页面查看接口: | |||||
| - Swagger UI:`/docs` | |||||
| - ReDoc:`/redoc` | |||||
| @ -0,0 +1,851 @@ | |||||
| import json | |||||
| import os | |||||
| import tempfile | |||||
| from typing import List, Optional | |||||
| import cv2 | |||||
| import numpy as np | |||||
| from fastapi import FastAPI, File, HTTPException, Path, UploadFile, Form | |||||
| from pydantic import BaseModel, Field | |||||
| from celery.result import AsyncResult | |||||
| from app.model_registry import MODEL_MAP, SCENE_INFO, iter_models, load_model | |||||
| from app.tasks import celery_app, analyze_video_stream, analyze_video_file | |||||
| # 共享上传目录(API 和 Worker 容器通过 volume 共享) | |||||
| UPLOAD_DIR = os.getenv("UPLOAD_DIR", "/tmp") | |||||
| UPLOAD_DIR = os.getenv("UPLOAD_DIR", "/tmp") | |||||
| from app.tasks import celery_app, analyze_video_stream, analyze_video_file | |||||
| AUTO_SCENE_TASK_TYPE = "auto_scene" | |||||
| SCENE_MARKDOWN = "\n".join( | |||||
| f"- `{model_type}`: {scene['name']},{scene['description']}" | |||||
| for model_type, scene in SCENE_INFO.items() | |||||
| ) | |||||
| SUPPORTED_MODEL_TYPES = ", ".join(MODEL_MAP.keys()) | |||||
| WEBHOOK_PAYLOAD_EXAMPLE = { | |||||
| "event": "RESERVOIR_ALARM", | |||||
| "scene": "floating", | |||||
| "stream_url": "rtsp://example.com/live", | |||||
| "details": [ | |||||
| { | |||||
| "label": "floating_object", | |||||
| "confidence": 0.93, | |||||
| "bbox": [102.4, 55.2, 260.8, 188.6], | |||||
| } | |||||
| ], | |||||
| "msg": "detected floating event", | |||||
| } | |||||
| WEBHOOK_CALLBACK_SCHEMA = { | |||||
| "type": "object", | |||||
| "required": ["event", "scene", "stream_url", "details", "msg"], | |||||
| "properties": { | |||||
| "event": { | |||||
| "type": "string", | |||||
| "description": "事件类型,当前固定为 `RESERVOIR_ALARM`。", | |||||
| "example": "RESERVOIR_ALARM", | |||||
| }, | |||||
| "scene": { | |||||
| "type": "string", | |||||
| "description": "触发识别的场景类型,例如 `floating`、`gate`、`shore_garbage`、`water_gauge`。", | |||||
| "example": "floating", | |||||
| }, | |||||
| "stream_url": { | |||||
| "type": "string", | |||||
| "description": "原始视频流地址。", | |||||
| "example": "rtsp://example.com/live", | |||||
| }, | |||||
| "details": { | |||||
| "type": "array", | |||||
| "description": "识别结果列表。", | |||||
| "items": { | |||||
| "type": "object", | |||||
| "required": ["label", "confidence", "bbox"], | |||||
| "properties": { | |||||
| "label": { | |||||
| "type": "string", | |||||
| "description": "识别类别名称。", | |||||
| "example": "floating_object", | |||||
| }, | |||||
| "confidence": { | |||||
| "type": "number", | |||||
| "format": "float", | |||||
| "description": "识别置信度。", | |||||
| "example": 0.93, | |||||
| }, | |||||
| "bbox": { | |||||
| "type": "array", | |||||
| "description": "目标框坐标数组,格式为 `[x1, y1, x2, y2]`。", | |||||
| "items": {"type": "number", "format": "float"}, | |||||
| "example": [102.4, 55.2, 260.8, 188.6], | |||||
| }, | |||||
| }, | |||||
| }, | |||||
| }, | |||||
| "msg": { | |||||
| "type": "string", | |||||
| "description": "告警说明。", | |||||
| "example": "detected floating event", | |||||
| }, | |||||
| }, | |||||
| } | |||||
| WEBHOOK_CALLBACK_OPENAPI = { | |||||
| "onWebhookResult": { | |||||
| "{$request.body#/webhook_url}": { | |||||
| "post": { | |||||
| "summary": "识别结果 webhook 回调", | |||||
| "description": "当视频流识别到目标后,算法服务会向请求体中的 `webhook_url` 发送 HTTP POST 回调。", | |||||
| "requestBody": { | |||||
| "required": True, | |||||
| "content": { | |||||
| "application/json": { | |||||
| "schema": WEBHOOK_CALLBACK_SCHEMA, | |||||
| "example": WEBHOOK_PAYLOAD_EXAMPLE, | |||||
| } | |||||
| }, | |||||
| }, | |||||
| "responses": { | |||||
| "200": { | |||||
| "description": "业务方成功接收回调。" | |||||
| } | |||||
| }, | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| LEGACY_STREAM_REQUEST_EXAMPLE = { | |||||
| "source_url": "rtsp://admin:password@192.168.1.100:554/h264/ch1/main/av_stream", | |||||
| "webhook_url": "https://example.com/api/reservoir/webhook", | |||||
| "scene_type": "floating", | |||||
| "roi": [[0, 0], [1920, 0], [1920, 1080], [0, 1080]], | |||||
| } | |||||
| STREAM_REQUEST_EXAMPLE = { | |||||
| "source_url": "rtsp://admin:password@192.168.1.100:554/h264/ch1/main/av_stream", | |||||
| "webhook_url": "https://example.com/api/reservoir/webhook", | |||||
| "roi": [[0, 0], [1920, 0], [1920, 1080], [0, 1080]], | |||||
| } | |||||
| AUTO_STREAM_RESPONSE_EXAMPLE = { | |||||
| "code": 200, | |||||
| "msg": "task submitted", | |||||
| "task_type": AUTO_SCENE_TASK_TYPE, | |||||
| } | |||||
| STREAM_RESPONSE_EXAMPLE = { | |||||
| "code": 200, | |||||
| "msg": "task submitted", | |||||
| "task_type": "floating", | |||||
| } | |||||
| IMAGE_RESPONSE_EXAMPLE = { | |||||
| "results": [ | |||||
| { | |||||
| "name": "floating_object", | |||||
| "class": 0, | |||||
| "confidence": 0.93, | |||||
| "box": { | |||||
| "x1": 102.4, | |||||
| "y1": 55.2, | |||||
| "x2": 260.8, | |||||
| "y2": 188.6, | |||||
| }, | |||||
| } | |||||
| ] | |||||
| } | |||||
| AUTO_IMAGE_RESPONSE_EXAMPLE = { | |||||
| "task_type": AUTO_SCENE_TASK_TYPE, | |||||
| "results": [ | |||||
| { | |||||
| "scene": "floating", | |||||
| "name": "floating_object", | |||||
| "class": 0, | |||||
| "confidence": 0.93, | |||||
| "box": { | |||||
| "x1": 102.4, | |||||
| "y1": 55.2, | |||||
| "x2": 260.8, | |||||
| "y2": 188.6, | |||||
| }, | |||||
| } | |||||
| ], | |||||
| } | |||||
| WEBHOOK_FIELD_DESCRIPTION = ( | |||||
| "识别结果回调地址,算法服务会以 HTTP POST 方式回调该地址。" | |||||
| "回调 JSON 参数说明:`event` 为事件类型,`scene` 为场景类型," | |||||
| "`stream_url` 为视频流地址,`details` 为识别结果列表,`msg` 为告警说明。" | |||||
| ) | |||||
| WEBHOOK_CALLBACK_PARAMETER_DESCRIPTION = ( | |||||
| "回调参数中文说明:\n" | |||||
| "- `event`:事件类型,当前固定为告警事件 `RESERVOIR_ALARM`。\n" | |||||
| "- `scene`:本次识别命中的场景类型,例如 `floating`、`gate`、`shore_garbage`、`water_gauge`。\n" | |||||
| "- `stream_url`:触发识别的视频流地址,即调用接口时传入的原始流地址。\n" | |||||
| "- `details`:识别结果列表。\n" | |||||
| "- `details[].label`:识别出的目标类别名称。\n" | |||||
| "- `details[].confidence`:识别结果的置信度。\n" | |||||
| "- `details[].bbox`:目标框坐标,格式为 `[x1, y1, x2, y2]`。\n" | |||||
| "- `msg`:告警说明文本,用于描述当前识别事件。" | |||||
| ) | |||||
| APP_DESCRIPTION = f""" | |||||
| 延寿水库智能识别服务,基于 YOLO 模型提供图像识别和视频流识别能力。 | |||||
| 支持场景: | |||||
| {SCENE_MARKDOWN} | |||||
| 推荐接口: | |||||
| - `POST /v1/predict/image`:自动场景图片识别 | |||||
| - `POST /v1/predict/stream`:自动场景视频流识别 | |||||
| - `POST /v1/predict/video-file`:上传MP4文件识别 | |||||
| 兼容接口: | |||||
| - `POST /{{model_type}}/predict/image`:按指定场景识别图片 | |||||
| - `POST /{{model_type}}/predict/stream`:按指定场景识别视频流 | |||||
| - `POST /v1/task/stream`:旧版视频流提交方式 | |||||
| """ | |||||
| app = FastAPI( | |||||
| title="延寿水库智能识别服务", | |||||
| description=APP_DESCRIPTION, | |||||
| version="1.2.0", | |||||
| openapi_tags=[ | |||||
| {"name": "图像识别", "description": "支持自动场景识别,也支持按指定场景模型执行图片识别。推荐优先使用 `/v1/predict/image`。"}, | |||||
| {"name": "视频流识别", "description": "支持自动场景识别,也支持按指定场景模型提交视频流异步识别任务。推荐优先使用 `/v1/predict/stream`。"}, | |||||
| {"name": "视频文件识别", "description": "支持上传MP4文件进行离线识别分析。推荐优先使用 `/v1/predict/video-file`。"}, | |||||
| {"name": "任务管理", "description": "查询异步任务状态。"}, | |||||
| {"name": "兼容接口", "description": "兼容旧调用方式的接口,建议逐步迁移到自动场景识别接口。"}, | |||||
| ], | |||||
| ) | |||||
| class TaskRequest(BaseModel): | |||||
| source_url: str = Field( | |||||
| ..., | |||||
| description="视频流地址,支持 RTSP、本地视频文件路径等 OpenCV 可读取的地址。", | |||||
| example="rtsp://admin:password@192.168.1.100:554/h264/ch1/main/av_stream", | |||||
| ) | |||||
| webhook_url: str = Field( | |||||
| ..., | |||||
| description=WEBHOOK_FIELD_DESCRIPTION, | |||||
| example="https://example.com/api/reservoir/webhook", | |||||
| ) | |||||
| scene_type: Optional[str] = Field( | |||||
| default=None, | |||||
| description=f"兼容旧字段。传值时按指定场景识别;不传时自动遍历全部场景模型。可选值:{SUPPORTED_MODEL_TYPES}", | |||||
| example="floating", | |||||
| ) | |||||
| roi: Optional[List[List[int]]] = Field( | |||||
| default=None, | |||||
| description="可选电子围栏,多边形坐标数组,格式为 [[x1, y1], [x2, y2], ...];不传时默认检测整幅画面。", | |||||
| example=[[0, 0], [1920, 0], [1920, 1080], [0, 1080]], | |||||
| ) | |||||
| class StreamPredictRequest(BaseModel): | |||||
| source_url: str = Field( | |||||
| ..., | |||||
| description="视频流地址,支持 RTSP、本地视频文件路径等 OpenCV 可读取的地址。", | |||||
| example="rtsp://admin:password@192.168.1.100:554/h264/ch1/main/av_stream", | |||||
| ) | |||||
| webhook_url: str = Field( | |||||
| ..., | |||||
| description=WEBHOOK_FIELD_DESCRIPTION, | |||||
| example="https://example.com/api/reservoir/webhook", | |||||
| ) | |||||
| roi: Optional[List[List[int]]] = Field( | |||||
| default=None, | |||||
| description="可选电子围栏,多边形坐标数组,格式为 [[x1, y1], [x2, y2], ...];不传时默认检测整幅画面。", | |||||
| example=[[0, 0], [1920, 0], [1920, 1080], [0, 1080]], | |||||
| ) | |||||
| class VideoFileRequest(BaseModel): | |||||
| webhook_url: str = Field( | |||||
| ..., | |||||
| description="识别结果回调地址", | |||||
| example="https://example.com/api/reservoir/webhook", | |||||
| ) | |||||
| roi: Optional[List[List[int]]] = Field( | |||||
| default=None, | |||||
| description="可选电子围栏", | |||||
| example=[[0, 0], [1920, 0], [1920, 1080], [0, 1080]], | |||||
| ) | |||||
| scene_type: Optional[str] = Field( | |||||
| default=None, | |||||
| description=f"指定场景类型,可选值:{SUPPORTED_MODEL_TYPES}", | |||||
| ) | |||||
| class StreamTaskResponse(BaseModel): | |||||
| code: int = Field(..., description="业务状态码,`200` 表示任务提交成功。", example=200) | |||||
| msg: str = Field(..., description="接口返回说明信息。", example="task submitted") | |||||
| task_type: str = Field( | |||||
| ..., | |||||
| description=f"实际提交的任务类型。自动识别时为 `{AUTO_SCENE_TASK_TYPE}`,指定场景时为对应模型类型。", | |||||
| example=AUTO_SCENE_TASK_TYPE, | |||||
| ) | |||||
| class VideoFileTaskResponse(BaseModel): | |||||
| code: int = Field(..., description="业务状态码,`200` 表示任务提交成功。", example=200) | |||||
| msg: str = Field(..., description="接口返回说明信息。", example="video file task submitted") | |||||
| task_id: str = Field(..., description="任务ID,用于查询任务状态。", example="abc123def456") | |||||
| task_type: str = Field( | |||||
| ..., | |||||
| description=f"实际提交的任务类型。自动识别时为 `{AUTO_SCENE_TASK_TYPE}`,指定场景时为对应模型类型。", | |||||
| example=AUTO_SCENE_TASK_TYPE, | |||||
| ) | |||||
| class DetectionBox(BaseModel): | |||||
| x1: float = Field(..., description="检测框左上角 X 坐标。", example=102.4) | |||||
| y1: float = Field(..., description="检测框左上角 Y 坐标。", example=55.2) | |||||
| x2: float = Field(..., description="检测框右下角 X 坐标。", example=260.8) | |||||
| y2: float = Field(..., description="检测框右下角 Y 坐标。", example=188.6) | |||||
| class ImageDetectionItem(BaseModel): | |||||
| name: str = Field(..., description="检测类别名称。", example="floating_object") | |||||
| class_id: int = Field(..., alias="class", description="检测类别 ID。", example=0) | |||||
| confidence: float = Field(..., description="检测置信度。", example=0.93) | |||||
| box: DetectionBox = Field(..., description="目标框坐标。") | |||||
| class AutoImageDetectionItem(ImageDetectionItem): | |||||
| scene: str = Field(..., description="命中的场景模型类型。", example="floating") | |||||
| class ImagePredictResponse(BaseModel): | |||||
| results: List[ImageDetectionItem] = Field( | |||||
| ..., | |||||
| description="识别结果列表。每个元素为模型输出的目标信息,通常包含类别名、类别 ID、置信度和检测框坐标。", | |||||
| example=IMAGE_RESPONSE_EXAMPLE["results"], | |||||
| ) | |||||
| class AutoImagePredictResponse(BaseModel): | |||||
| task_type: str = Field( | |||||
| ..., | |||||
| description=f"任务类型,固定为 `{AUTO_SCENE_TASK_TYPE}`。", | |||||
| example=AUTO_SCENE_TASK_TYPE, | |||||
| ) | |||||
| results: List[AutoImageDetectionItem] = Field( | |||||
| ..., | |||||
| description="自动场景识别结果列表。每个元素额外包含 `scene` 字段,表示命中的场景模型。", | |||||
| example=AUTO_IMAGE_RESPONSE_EXAMPLE["results"], | |||||
| ) | |||||
| class WebhookDetectionItem(BaseModel): | |||||
| label: str = Field(..., description="识别类别名称。", example="floating_object") | |||||
| confidence: float = Field(..., description="识别置信度。", example=0.93) | |||||
| bbox: List[float] = Field(..., description="目标框坐标数组,格式为 `[x1, y1, x2, y2]`。", example=[102.4, 55.2, 260.8, 188.6]) | |||||
| class WebhookPayload(BaseModel): | |||||
| event: str = Field(..., description="事件类型。", example="RESERVOIR_ALARM") | |||||
| scene: str = Field(..., description="触发识别的场景类型。", example="floating") | |||||
| stream_url: str = Field(..., description="视频流地址。", example="rtsp://example.com/live") | |||||
| details: List[WebhookDetectionItem] = Field(..., description="识别结果列表。") | |||||
| msg: str = Field(..., description="告警说明。", example="detected floating event") | |||||
| def validate_model_type(model_type: str) -> None: | |||||
| if model_type not in MODEL_MAP: | |||||
| raise HTTPException( | |||||
| status_code=400, | |||||
| detail=f"Unsupported model type: {model_type}. Supported types: {SUPPORTED_MODEL_TYPES}", | |||||
| ) | |||||
| def decode_image(contents: bytes): | |||||
| nparr = np.frombuffer(contents, np.uint8) | |||||
| img = cv2.imdecode(nparr, cv2.IMREAD_COLOR) | |||||
| if img is None: | |||||
| raise HTTPException(status_code=400, detail="Invalid image file") | |||||
| return img | |||||
| def run_single_model_image_detection(img, model_type: str) -> list: | |||||
| model = load_model(model_type) | |||||
| results = model.predict(img, conf=0.6, verbose=False) | |||||
| return json.loads(results[0].to_json()) | |||||
| def run_auto_image_detection(img) -> dict: | |||||
| detections = [] | |||||
| for current_model_type, model in iter_models(): | |||||
| results = model.predict(img, conf=0.6, verbose=False) | |||||
| for item in json.loads(results[0].to_json()): | |||||
| item["scene"] = current_model_type | |||||
| detections.append(item) | |||||
| return {"task_type": AUTO_SCENE_TASK_TYPE, "results": detections} | |||||
| def submit_stream_task( | |||||
| source_url: str, | |||||
| webhook_url: str, | |||||
| roi: Optional[List[List[int]]], | |||||
| model_type: Optional[str] = None, | |||||
| ): | |||||
| if model_type is not None: | |||||
| validate_model_type(model_type) | |||||
| task = analyze_video_stream.delay(source_url, webhook_url, model_type, roi) | |||||
| return {"code": 200, "msg": "task submitted", "task_type": model_type or AUTO_SCENE_TASK_TYPE, "task_id": task.id} | |||||
| def submit_video_file_task( | |||||
| video_path: str, | |||||
| webhook_url: str, | |||||
| roi: Optional[List[List[int]]], | |||||
| model_type: Optional[str] = None, | |||||
| original_filename: str = None, | |||||
| ): | |||||
| """提交视频文件识别任务""" | |||||
| if model_type is not None: | |||||
| validate_model_type(model_type) | |||||
| task = analyze_video_file.delay(video_path, webhook_url, model_type, roi, original_filename) | |||||
| return { | |||||
| "code": 200, | |||||
| "msg": "video file task submitted", | |||||
| "task_id": task.id, | |||||
| "task_type": model_type or AUTO_SCENE_TASK_TYPE | |||||
| } | |||||
| @app.post( | |||||
| "/v1/task/stream", | |||||
| tags=["兼容接口", "视频流识别"], | |||||
| summary="兼容模式提交视频流识别任务", | |||||
| description=( | |||||
| "兼容旧调用方式提交视频流识别任务。\n\n" | |||||
| "入参说明:\n" | |||||
| "- `source_url`:视频流地址。\n" | |||||
| "- `webhook_url`:识别结果回调地址。\n" | |||||
| f"- `scene_type`:兼容旧字段,传值时按指定场景识别,不传时自动识别全部场景;可选值:{SUPPORTED_MODEL_TYPES}。\n" | |||||
| "- `roi`:可选电子围栏。\n\n" | |||||
| "模拟入参示例:\n" | |||||
| f"```json\n{json.dumps(LEGACY_STREAM_REQUEST_EXAMPLE, ensure_ascii=False, indent=2)}\n```\n\n" | |||||
| "出参说明:\n" | |||||
| "- `code`:业务状态码,200 表示提交成功。\n" | |||||
| "- `msg`:返回说明信息。\n" | |||||
| f"- `task_type`:实际提交的任务类型,自动识别时为 `{AUTO_SCENE_TASK_TYPE}`。\n\n" | |||||
| "模拟出参示例:\n" | |||||
| f"```json\n{json.dumps(AUTO_STREAM_RESPONSE_EXAMPLE, ensure_ascii=False, indent=2)}\n```\n\n" | |||||
| "说明:\n" | |||||
| "- 建议新接入统一改用 `POST /v1/predict/stream`。\n" | |||||
| "- 自动识别模式下,worker 会遍历全部已注册场景模型。\n" | |||||
| "- Swagger 文档中的 Callbacks 区域可查看 `webhook_url` 的回调参数结构。\n\n" | |||||
| f"{WEBHOOK_CALLBACK_PARAMETER_DESCRIPTION}\n" | |||||
| ), | |||||
| response_model=StreamTaskResponse, | |||||
| responses={ | |||||
| 200: { | |||||
| "description": "任务提交成功", | |||||
| "content": {"application/json": {"example": AUTO_STREAM_RESPONSE_EXAMPLE}}, | |||||
| }, | |||||
| 400: {"description": "请求参数错误或场景类型不支持。"}, | |||||
| }, | |||||
| deprecated=True, | |||||
| openapi_extra={ | |||||
| "requestBody": { | |||||
| "content": { | |||||
| "application/json": { | |||||
| "example": LEGACY_STREAM_REQUEST_EXAMPLE | |||||
| } | |||||
| } | |||||
| }, | |||||
| "callbacks": WEBHOOK_CALLBACK_OPENAPI, | |||||
| }, | |||||
| ) | |||||
| async def start_stream_task(req: TaskRequest): | |||||
| return submit_stream_task(req.source_url, req.webhook_url, req.roi, req.scene_type) | |||||
| @app.post( | |||||
| "/v1/predict/stream", | |||||
| tags=["视频流识别"], | |||||
| summary="自动场景视频流识别任务", | |||||
| description=( | |||||
| "自动遍历所有已注册场景模型,对视频流执行异步识别任务。\n\n" | |||||
| "入参说明:\n" | |||||
| "- `source_url`:视频流地址。\n" | |||||
| "- `webhook_url`:识别结果回调地址。\n" | |||||
| "- `roi`:可选电子围栏。\n\n" | |||||
| "模拟入参示例:\n" | |||||
| f"```json\n{json.dumps(STREAM_REQUEST_EXAMPLE, ensure_ascii=False, indent=2)}\n```\n\n" | |||||
| "出参说明:\n" | |||||
| "- `code`:业务状态码,200 表示提交成功。\n" | |||||
| "- `msg`:返回说明信息。\n" | |||||
| f"- `task_type`:固定返回 `{AUTO_SCENE_TASK_TYPE}`。\n\n" | |||||
| "模拟出参示例:\n" | |||||
| f"```json\n{json.dumps(AUTO_STREAM_RESPONSE_EXAMPLE, ensure_ascii=False, indent=2)}\n```\n\n" | |||||
| "检测到目标后会向 `webhook_url` 发送 POST 回调,回调数据示例:\n" | |||||
| f"```json\n{json.dumps(WEBHOOK_PAYLOAD_EXAMPLE, ensure_ascii=False, indent=2)}\n```\n\n" | |||||
| "回调说明:\n" | |||||
| "- 自动识别模式下,如果同一帧命中多个场景,会按场景分别回调。\n" | |||||
| "- `scene` 字段表示当前这条 webhook 对应的场景模型。\n" | |||||
| "- Swagger 文档中的 Callbacks 区域可查看完整回调参数结构。\n\n" | |||||
| f"{WEBHOOK_CALLBACK_PARAMETER_DESCRIPTION}\n" | |||||
| ), | |||||
| response_model=StreamTaskResponse, | |||||
| responses={ | |||||
| 200: { | |||||
| "description": "任务提交成功", | |||||
| "content": {"application/json": {"example": AUTO_STREAM_RESPONSE_EXAMPLE}}, | |||||
| }, | |||||
| 400: {"description": "请求参数错误。"}, | |||||
| }, | |||||
| openapi_extra={ | |||||
| "requestBody": { | |||||
| "content": { | |||||
| "application/json": { | |||||
| "example": STREAM_REQUEST_EXAMPLE | |||||
| } | |||||
| } | |||||
| }, | |||||
| "callbacks": WEBHOOK_CALLBACK_OPENAPI, | |||||
| }, | |||||
| ) | |||||
| async def predict_stream_v1(req: StreamPredictRequest): | |||||
| return submit_stream_task(req.source_url, req.webhook_url, req.roi, None) | |||||
| @app.post( | |||||
| "/{model_type}/predict/stream", | |||||
| tags=["视频流识别"], | |||||
| summary="按场景提交视频流识别任务", | |||||
| description=( | |||||
| "根据路径中的 `model_type` 选择对应场景模型,异步提交视频流识别任务。\n\n" | |||||
| "入参说明:\n" | |||||
| "- `model_type`:路径参数,场景模型类型。\n" | |||||
| "- `source_url`:视频流地址。\n" | |||||
| "- `webhook_url`:识别结果回调地址。\n" | |||||
| "- `roi`:可选电子围栏。\n\n" | |||||
| "模拟入参示例:\n" | |||||
| f"```json\n{json.dumps(STREAM_REQUEST_EXAMPLE, ensure_ascii=False, indent=2)}\n```\n\n" | |||||
| "出参说明:\n" | |||||
| "- `code`:业务状态码,200 表示提交成功。\n" | |||||
| "- `msg`:返回说明信息。\n" | |||||
| "- `task_type`:实际提交的场景类型。\n\n" | |||||
| "模拟出参示例:\n" | |||||
| f"```json\n{json.dumps(STREAM_RESPONSE_EXAMPLE, ensure_ascii=False, indent=2)}\n```\n\n" | |||||
| "检测到目标后会向 `webhook_url` 发送 POST 回调,回调数据示例:\n" | |||||
| f"```json\n{json.dumps(WEBHOOK_PAYLOAD_EXAMPLE, ensure_ascii=False, indent=2)}\n```\n\n" | |||||
| "回调说明:\n" | |||||
| "- 指定场景模式下,只会使用路径里的 `model_type` 对应模型。\n" | |||||
| "- webhook 的 `scene` 字段与路径中的 `model_type` 一致。\n" | |||||
| "- Swagger 文档中的 Callbacks 区域可查看完整回调参数结构。\n\n" | |||||
| f"{WEBHOOK_CALLBACK_PARAMETER_DESCRIPTION}\n" | |||||
| ), | |||||
| response_model=StreamTaskResponse, | |||||
| responses={ | |||||
| 200: { | |||||
| "description": "任务提交成功", | |||||
| "content": {"application/json": {"example": STREAM_RESPONSE_EXAMPLE}}, | |||||
| }, | |||||
| 400: {"description": "场景类型不支持或请求参数错误。"}, | |||||
| }, | |||||
| openapi_extra={ | |||||
| "requestBody": { | |||||
| "content": { | |||||
| "application/json": { | |||||
| "example": STREAM_REQUEST_EXAMPLE | |||||
| } | |||||
| } | |||||
| }, | |||||
| "callbacks": WEBHOOK_CALLBACK_OPENAPI, | |||||
| }, | |||||
| ) | |||||
| async def predict_stream( | |||||
| model_type: str = Path(..., description=f"场景模型类型。可选值:{SUPPORTED_MODEL_TYPES}"), | |||||
| req: StreamPredictRequest = ..., | |||||
| ): | |||||
| return submit_stream_task(req.source_url, req.webhook_url, req.roi, model_type) | |||||
| @app.post( | |||||
| "/v1/predict/video-file", | |||||
| tags=["视频文件识别"], | |||||
| summary="上传MP4文件进行识别", | |||||
| description=( | |||||
| "支持上传本地MP4文件进行视频识别分析。\n\n" | |||||
| "入参说明:\n" | |||||
| "- `file`:上传的MP4视频文件\n" | |||||
| "- `webhook_url`:识别结果回调地址\n" | |||||
| "- `roi`:可选电子围栏(JSON字符串格式)\n" | |||||
| "- `scene_type`:可选指定场景类型,不传则自动识别\n\n" | |||||
| "模拟入参示例:\n" | |||||
| "```bash\n" | |||||
| "curl -X POST http://localhost:8000/v1/predict/video-file \\\n" | |||||
| " -H \"accept: application/json\" \\\n" | |||||
| " -F \"file=@test.mp4;type=video/mp4\" \\\n" | |||||
| " -F \"webhook_url=https://example.com/api/webhook\" \\\n" | |||||
| " -F \"roi=[[0,0],[1920,0],[1920,1080],[0,1080]]\" \\\n" | |||||
| " -F \"scene_type=floating\"\n" | |||||
| "```\n\n" | |||||
| "出参说明:\n" | |||||
| "- `code`:业务状态码,200表示提交成功\n" | |||||
| "- `msg`:返回说明信息\n" | |||||
| "- `task_id`:任务ID,用于查询任务状态\n" | |||||
| "- `task_type`:实际提交的任务类型\n\n" | |||||
| "模拟出参示例:\n" | |||||
| "```json\n" | |||||
| "{\n" | |||||
| " \"code\": 200,\n" | |||||
| " \"msg\": \"video file task submitted\",\n" | |||||
| " \"task_id\": \"abc123def456\",\n" | |||||
| " \"task_type\": \"auto_scene\"\n" | |||||
| "}\n" | |||||
| "```\n\n" | |||||
| "检测完成或检测到目标后会向 `webhook_url` 发送回调。\n" | |||||
| "文件大小限制:500MB\n" | |||||
| ), | |||||
| response_model=VideoFileTaskResponse, | |||||
| responses={ | |||||
| 200: {"description": "任务提交成功"}, | |||||
| 400: {"description": "文件格式错误、大小超限或参数错误"}, | |||||
| 500: {"description": "服务器内部错误"}, | |||||
| }, | |||||
| ) | |||||
| async def predict_video_file( | |||||
| file: UploadFile = File(..., description="MP4视频文件", media_type="video/mp4"), | |||||
| webhook_url: str = Form(..., description="识别结果回调地址"), | |||||
| roi: Optional[str] = Form(default="[[0,0],[1920,0],[1920,1080],[0,1080]]", description="电子围栏JSON字符串"), | |||||
| scene_type: Optional[str] = Form(default=None, description=f"场景类型,可选值:{SUPPORTED_MODEL_TYPES}"), | |||||
| ): | |||||
| # 验证文件格式 | |||||
| if not file.filename.endswith(('.mp4', '.MP4')): | |||||
| raise HTTPException(status_code=400, detail="Only MP4 files are supported") | |||||
| # 验证文件大小(限制500MB) | |||||
| file_size = 0 | |||||
| temp_file_path = None | |||||
| try: | |||||
| # 创建临时文件保存上传的视频(存入共享目录,确保 Worker 容器也能读取) | |||||
| os.makedirs(UPLOAD_DIR, exist_ok=True) | |||||
| with tempfile.NamedTemporaryFile(delete=False, suffix='.mp4', dir=UPLOAD_DIR) as temp_file: | |||||
| # 分块读取并写入 | |||||
| chunk_size = 1024 * 1024 # 1MB chunks | |||||
| while True: | |||||
| chunk = await file.read(chunk_size) | |||||
| if not chunk: | |||||
| break | |||||
| temp_file.write(chunk) | |||||
| file_size += len(chunk) | |||||
| # 限制文件大小为500MB | |||||
| if file_size > 500 * 1024 * 1024: | |||||
| raise HTTPException(status_code=400, detail="File size exceeds 500MB limit") | |||||
| temp_file_path = temp_file.name | |||||
| # 解析ROI参数 | |||||
| roi_list = None | |||||
| if roi: | |||||
| try: | |||||
| roi_list = json.loads(roi) | |||||
| if not isinstance(roi_list, list): | |||||
| raise ValueError("ROI must be a list") | |||||
| except json.JSONDecodeError: | |||||
| raise HTTPException(status_code=400, detail="Invalid ROI format, must be valid JSON") | |||||
| # 提交任务 | |||||
| result = submit_video_file_task( | |||||
| video_path=temp_file_path, | |||||
| webhook_url=webhook_url, | |||||
| roi=roi_list, | |||||
| model_type=scene_type, | |||||
| original_filename=file.filename | |||||
| ) | |||||
| return result | |||||
| except HTTPException: | |||||
| # 清理临时文件 | |||||
| if temp_file_path and os.path.exists(temp_file_path): | |||||
| os.unlink(temp_file_path) | |||||
| raise | |||||
| except Exception as exc: | |||||
| if temp_file_path and os.path.exists(temp_file_path): | |||||
| os.unlink(temp_file_path) | |||||
| raise HTTPException(status_code=500, detail=f"Failed to process video file: {str(exc)}") from exc | |||||
| @app.get( | |||||
| "/v1/task/status/{task_id}", | |||||
| tags=["任务管理"], | |||||
| summary="查询异步任务状态", | |||||
| description="通过任务ID查询视频识别任务的执行状态和进度", | |||||
| ) | |||||
| async def get_task_status( | |||||
| task_id: str = Path(..., description="任务ID,从提交接口返回的task_id") | |||||
| ): | |||||
| """查询任务状态""" | |||||
| try: | |||||
| task = AsyncResult(task_id, app=celery_app) | |||||
| if task.state == 'PENDING': | |||||
| response = { | |||||
| "state": "PENDING", | |||||
| "progress": 0, | |||||
| "message": "Task is waiting to be processed" | |||||
| } | |||||
| elif task.state == 'PROCESSING': | |||||
| response = { | |||||
| "state": "PROCESSING", | |||||
| "progress": task.info.get('progress', 0) if task.info else 0, | |||||
| "message": task.info.get('message', 'Processing video') if task.info else 'Processing', | |||||
| "frame": task.info.get('frame', 0) if task.info else 0, | |||||
| "total_frames": task.info.get('total_frames', 0) if task.info else 0 | |||||
| } | |||||
| elif task.state == 'SUCCESS': | |||||
| response = { | |||||
| "state": "SUCCESS", | |||||
| "result": task.result, | |||||
| "message": "Task completed successfully" | |||||
| } | |||||
| elif task.state == 'FAILURE': | |||||
| response = { | |||||
| "state": "FAILURE", | |||||
| "error": str(task.info), | |||||
| "message": "Task failed" | |||||
| } | |||||
| else: | |||||
| response = { | |||||
| "state": task.state, | |||||
| "message": "Unknown task state" | |||||
| } | |||||
| return response | |||||
| except Exception as exc: | |||||
| raise HTTPException( | |||||
| status_code=500, | |||||
| detail=f"Failed to query task status: {str(exc)}", | |||||
| ) from exc | |||||
| @app.post( | |||||
| "/v1/predict/image", | |||||
| tags=["图像识别"], | |||||
| summary="自动场景图片识别", | |||||
| description=( | |||||
| "不再需要指定 `model_type`,接口会自动遍历所有已注册场景模型,对上传图片执行场景识别。\n\n" | |||||
| "入参说明:\n" | |||||
| "- `file`:上传的待识别图片文件。\n\n" | |||||
| "模拟入参示例:\n" | |||||
| "```bash\n" | |||||
| "curl -X POST http://localhost:8000/v1/predict/image \\\n" | |||||
| " -H \"accept: application/json\" \\\n" | |||||
| " -H \"Content-Type: multipart/form-data\" \\\n" | |||||
| " -F \"file=@test.jpg;type=image/jpeg\"\n" | |||||
| "```\n\n" | |||||
| "出参说明:\n" | |||||
| f"- `task_type`:固定返回 `{AUTO_SCENE_TASK_TYPE}`。\n" | |||||
| "- `results`:识别结果列表。\n" | |||||
| "- `results[].scene`:命中的场景模型类型。\n" | |||||
| "- `results[].name`:类别名称。\n" | |||||
| "- `results[].class`:类别 ID。\n" | |||||
| "- `results[].confidence`:置信度。\n" | |||||
| "- `results[].box`:目标框坐标。\n\n" | |||||
| "模拟出参示例:\n" | |||||
| f"```json\n{json.dumps(AUTO_IMAGE_RESPONSE_EXAMPLE, ensure_ascii=False, indent=2)}\n```\n\n" | |||||
| "说明:\n" | |||||
| "- 该接口会遍历全部已注册模型并合并命中结果。\n" | |||||
| "- 若没有检测到目标,`results` 返回空数组。\n" | |||||
| ), | |||||
| response_model=AutoImagePredictResponse, | |||||
| responses={ | |||||
| 200: { | |||||
| "description": "图片自动场景识别成功", | |||||
| "content": {"application/json": {"example": AUTO_IMAGE_RESPONSE_EXAMPLE}}, | |||||
| }, | |||||
| 400: {"description": "上传文件不是有效图片。"}, | |||||
| }, | |||||
| ) | |||||
| async def predict_image_v1( | |||||
| file: UploadFile = File(..., description="待识别图片文件,示例:file=@test.jpg"), | |||||
| ): | |||||
| try: | |||||
| contents = await file.read() | |||||
| img = decode_image(contents) | |||||
| return run_auto_image_detection(img) | |||||
| except HTTPException: | |||||
| raise | |||||
| except Exception as exc: | |||||
| raise HTTPException(status_code=500, detail=str(exc)) from exc | |||||
| @app.post( | |||||
| "/{model_type}/predict/image", | |||||
| tags=["图像识别"], | |||||
| summary="按场景执行图片识别", | |||||
| description=( | |||||
| "根据路径中的 `model_type` 选择对应场景模型,对上传图片执行识别。\n\n" | |||||
| "入参说明:\n" | |||||
| "- `model_type`:路径参数,场景模型类型。\n" | |||||
| "- `file`:上传的待识别图片文件。\n\n" | |||||
| "模拟入参示例:\n" | |||||
| "```bash\n" | |||||
| "curl -X POST http://localhost:8000/floating/predict/image \\\n" | |||||
| " -H \"accept: application/json\" \\\n" | |||||
| " -H \"Content-Type: multipart/form-data\" \\\n" | |||||
| " -F \"file=@test.jpg;type=image/jpeg\"\n" | |||||
| "```\n\n" | |||||
| "出参说明:\n" | |||||
| "- `results`:识别结果列表。\n" | |||||
| "- `results[].name`:类别名称。\n" | |||||
| "- `results[].class`:类别 ID。\n" | |||||
| "- `results[].confidence`:置信度。\n" | |||||
| "- `results[].box`:目标框坐标。\n\n" | |||||
| "模拟出参示例:\n" | |||||
| f"```json\n{json.dumps(IMAGE_RESPONSE_EXAMPLE, ensure_ascii=False, indent=2)}\n```\n\n" | |||||
| "说明:\n" | |||||
| "- 该接口只使用路径中的 `model_type` 对应模型。\n" | |||||
| "- 若没有检测到目标,`results` 返回空数组。\n" | |||||
| ), | |||||
| response_model=ImagePredictResponse, | |||||
| responses={ | |||||
| 200: { | |||||
| "description": "图片识别成功", | |||||
| "content": {"application/json": {"example": IMAGE_RESPONSE_EXAMPLE}}, | |||||
| }, | |||||
| 400: {"description": "场景类型不支持,或上传文件不是有效图片。"}, | |||||
| }, | |||||
| ) | |||||
| async def predict_image( | |||||
| model_type: str = Path(..., description=f"场景模型类型。可选值:{SUPPORTED_MODEL_TYPES}"), | |||||
| file: UploadFile = File(..., description="待识别图片文件,示例:file=@test.jpg"), | |||||
| ): | |||||
| validate_model_type(model_type) | |||||
| try: | |||||
| contents = await file.read() | |||||
| img = decode_image(contents) | |||||
| return {"results": run_single_model_image_detection(img, model_type)} | |||||
| except HTTPException: | |||||
| raise | |||||
| except Exception as exc: | |||||
| raise HTTPException(status_code=500, detail=str(exc)) from exc | |||||
| if __name__ == "__main__": | |||||
| import uvicorn | |||||
| uvicorn.run(app, host="0.0.0.0", port=8000) | |||||
| @ -0,0 +1,106 @@ | |||||
| task: detect | |||||
| mode: train | |||||
| model: ./yolo11n.pt | |||||
| data: ./Conf/Conf_River_Floating.yaml | |||||
| epochs: 1000 | |||||
| time: null | |||||
| patience: 100 | |||||
| batch: 64 | |||||
| imgsz: 1280 | |||||
| save: true | |||||
| save_period: -1 | |||||
| cache: false | |||||
| device: 0,1,2,3 | |||||
| workers: 0 | |||||
| project: null | |||||
| name: train4 | |||||
| exist_ok: false | |||||
| pretrained: true | |||||
| optimizer: auto | |||||
| verbose: true | |||||
| seed: 0 | |||||
| deterministic: true | |||||
| single_cls: false | |||||
| rect: false | |||||
| cos_lr: false | |||||
| close_mosaic: 10 | |||||
| resume: false | |||||
| amp: true | |||||
| fraction: 1.0 | |||||
| profile: false | |||||
| freeze: null | |||||
| multi_scale: false | |||||
| compile: false | |||||
| overlap_mask: true | |||||
| mask_ratio: 4 | |||||
| dropout: 0.0 | |||||
| val: true | |||||
| split: val | |||||
| save_json: false | |||||
| conf: 0.25 | |||||
| iou: 0.7 | |||||
| max_det: 300 | |||||
| half: false | |||||
| dnn: false | |||||
| plots: true | |||||
| source: null | |||||
| vid_stride: 1 | |||||
| stream_buffer: false | |||||
| visualize: false | |||||
| augment: false | |||||
| agnostic_nms: false | |||||
| classes: null | |||||
| retina_masks: false | |||||
| embed: null | |||||
| show: false | |||||
| save_frames: false | |||||
| save_txt: false | |||||
| save_conf: false | |||||
| save_crop: false | |||||
| show_labels: true | |||||
| show_conf: true | |||||
| show_boxes: true | |||||
| line_width: null | |||||
| format: torchscript | |||||
| keras: false | |||||
| optimize: false | |||||
| int8: false | |||||
| dynamic: false | |||||
| simplify: true | |||||
| opset: null | |||||
| workspace: null | |||||
| nms: false | |||||
| lr0: 0.005 | |||||
| lrf: 0.01 | |||||
| momentum: 0.937 | |||||
| weight_decay: 0.0005 | |||||
| warmup_epochs: 5.0 | |||||
| warmup_momentum: 0.8 | |||||
| warmup_bias_lr: 0.1 | |||||
| box: 10.0 | |||||
| cls: 1.0 | |||||
| dfl: 2.0 | |||||
| pose: 12.0 | |||||
| kobj: 1.0 | |||||
| nbs: 64 | |||||
| hsv_h: 0.015 | |||||
| hsv_s: 0.7 | |||||
| hsv_v: 0.4 | |||||
| degrees: 0.0 | |||||
| translate: 0.1 | |||||
| scale: 0.5 | |||||
| shear: 0.0 | |||||
| perspective: 0.0 | |||||
| flipud: 0.0 | |||||
| fliplr: 0.5 | |||||
| bgr: 0.0 | |||||
| mosaic: 1.0 | |||||
| mixup: 0.0 | |||||
| cutmix: 0.0 | |||||
| copy_paste: 0.3 | |||||
| copy_paste_mode: flip | |||||
| auto_augment: randaugment | |||||
| erasing: 0.4 | |||||
| cfg: null | |||||
| tracker: botsort.yaml | |||||
| save_dir: /home/sutai/wsj/reservoir_train/runs/detect/train4 | |||||
| @ -0,0 +1,287 @@ | |||||
| epoch,time,train/box_loss,train/cls_loss,train/dfl_loss,metrics/precision(B),metrics/recall(B),metrics/mAP50(B),metrics/mAP50-95(B),val/box_loss,val/cls_loss,val/dfl_loss,lr/pg0,lr/pg1,lr/pg2 | |||||
| 1,33.4112,2.39509,12.7977,2.40188,0,0,0,0,2.27359,6.71089,2.315,0.00191304,0.00191304,0.00191304 | |||||
| 2,64.2582,2.07809,11.1844,2.14679,0,0,0,0,2.05492,6.40927,2.19038,0.00390917,0.00390917,0.00390917 | |||||
| 3,93.9997,2.13405,9.95756,2.0733,0,0,0,0,2.32995,5.81948,2.3064,0.00590134,0.00590134,0.00590134 | |||||
| 4,124.124,2.14439,8.38493,2.09449,0.21935,0.04898,0.13296,0.08116,2.68573,5.4136,2.59843,0.00788954,0.00788954,0.00788954 | |||||
| 5,154.595,2.15373,7.53047,2.07161,0.3287,0.02175,0.01744,0.00849,4.06277,61.594,4.65705,0.00987379,0.00987379,0.00987379 | |||||
| 6,184.83,2.35596,6.29438,2.20575,0.30012,0.05055,0.00988,0.00401,3.64732,63.492,4.78298,0.0099505,0.0099505,0.0099505 | |||||
| 7,215.234,2.32979,5.48792,2.2124,0.02745,0.1158,0.02721,0.01222,3.10734,76.9508,4.06168,0.0099406,0.0099406,0.0099406 | |||||
| 8,246.436,2.20819,4.8594,2.13989,0.05636,0.12391,0.03385,0.01423,3.67411,79.0338,4.45235,0.0099307,0.0099307,0.0099307 | |||||
| 9,276.644,2.34735,4.8601,2.26271,0.12236,0.09724,0.0489,0.02414,3.44567,32.4887,3.97067,0.0099208,0.0099208,0.0099208 | |||||
| 10,306.629,2.18417,4.3116,2.12706,0.08194,0.11332,0.05886,0.03163,3.01061,18.4977,3.46095,0.0099109,0.0099109,0.0099109 | |||||
| 11,337.44,2.24794,4.11155,2.19118,0.91429,0.08774,0.14738,0.06394,3.57315,16.992,3.77222,0.009901,0.009901,0.009901 | |||||
| 12,367.739,2.21728,4.28225,2.14579,0.17374,0.24812,0.23883,0.10971,2.70124,8.64248,2.87378,0.0098911,0.0098911,0.0098911 | |||||
| 13,398.69,2.122,3.72056,2.05907,0.32906,0.1768,0.23621,0.12543,3.10642,8.50599,3.34959,0.0098812,0.0098812,0.0098812 | |||||
| 14,430.028,2.14693,3.75207,2.05125,0.38129,0.26288,0.22819,0.11014,2.93184,16.773,3.11998,0.0098713,0.0098713,0.0098713 | |||||
| 15,463.024,2.09661,3.69645,2.03073,0.14062,0.18945,0.16836,0.10019,2.69815,8.34905,3.00178,0.0098614,0.0098614,0.0098614 | |||||
| 16,493.901,2.08116,3.56176,2.01431,0.50214,0.31571,0.37123,0.25045,2.329,4.88796,2.464,0.0098515,0.0098515,0.0098515 | |||||
| 17,525.635,2.09936,3.42418,2.06,0.54816,0.33883,0.43483,0.26709,2.35196,4.61267,2.41869,0.0098416,0.0098416,0.0098416 | |||||
| 18,556.277,2.08124,3.33558,2.06297,0.33882,0.33369,0.30326,0.18066,2.33899,5.059,2.40794,0.0098317,0.0098317,0.0098317 | |||||
| 19,587.236,2.07934,3.32083,1.99435,0.41651,0.33028,0.38173,0.2342,2.39637,4.99267,2.50897,0.0098218,0.0098218,0.0098218 | |||||
| 20,617.592,2.06915,3.29678,2.01635,0.51635,0.31763,0.31925,0.16276,2.56906,5.58749,2.58507,0.0098119,0.0098119,0.0098119 | |||||
| 21,648.875,2.04413,3.28848,2.01841,0.33072,0.37199,0.33934,0.19054,2.52337,6.12432,2.55929,0.009802,0.009802,0.009802 | |||||
| 22,679.869,2.0087,3.17607,2.00152,0.565,0.39026,0.38868,0.24235,2.22558,4.71118,2.28477,0.0097921,0.0097921,0.0097921 | |||||
| 23,711.071,2.01974,3.14653,1.99325,0.40441,0.44287,0.43841,0.27109,2.28724,4.44453,2.30748,0.0097822,0.0097822,0.0097822 | |||||
| 24,742.425,1.99648,2.94497,1.96764,0.5725,0.09721,0.1631,0.10733,2.92534,9.01378,3.25484,0.0097723,0.0097723,0.0097723 | |||||
| 25,772.62,2.06885,3.02058,2.07763,0.52202,0.29904,0.38437,0.23816,2.3859,5.48165,2.54243,0.0097624,0.0097624,0.0097624 | |||||
| 26,803.858,1.96085,2.90528,1.96232,0.51233,0.46149,0.4758,0.29387,2.27896,4.43162,2.36984,0.0097525,0.0097525,0.0097525 | |||||
| 27,835.037,2.02208,2.77857,1.99893,0.33515,0.28609,0.31683,0.19602,2.50066,6.15903,2.66507,0.0097426,0.0097426,0.0097426 | |||||
| 28,865.592,2.03301,2.73448,1.98963,0.49659,0.34141,0.42276,0.25157,2.47269,4.63183,2.51185,0.0097327,0.0097327,0.0097327 | |||||
| 29,896.588,1.96476,2.79753,1.9813,0.5388,0.36579,0.44584,0.28486,2.21644,3.75674,2.28506,0.0097228,0.0097228,0.0097228 | |||||
| 30,926.881,1.86472,2.637,1.88481,0.45209,0.45108,0.46248,0.29419,2.11763,3.88376,2.20945,0.0097129,0.0097129,0.0097129 | |||||
| 31,958.149,1.90774,2.75392,1.9023,0.68001,0.44477,0.56639,0.38166,2.05275,3.37761,2.17998,0.009703,0.009703,0.009703 | |||||
| 32,988.295,1.89363,2.71563,1.88663,0.45637,0.32381,0.3727,0.22422,2.28483,5.12807,2.44161,0.0096931,0.0096931,0.0096931 | |||||
| 33,1018.61,1.89589,2.6815,1.95134,0.57547,0.49996,0.57094,0.36593,2.06348,3.37383,2.1709,0.0096832,0.0096832,0.0096832 | |||||
| 34,1049.64,1.91796,2.61283,1.93174,0.6023,0.48813,0.596,0.38649,2.15059,3.67912,2.29399,0.0096733,0.0096733,0.0096733 | |||||
| 35,1080.83,1.85469,2.48279,1.84625,0.62724,0.49087,0.59102,0.3457,2.27444,3.47772,2.34883,0.0096634,0.0096634,0.0096634 | |||||
| 36,1111.71,1.90011,2.74844,1.9341,0.5392,0.51061,0.54571,0.35343,2.06851,3.35112,2.18239,0.0096535,0.0096535,0.0096535 | |||||
| 37,1142.77,1.86612,2.53197,1.90013,0.69213,0.5854,0.6807,0.43885,1.99116,2.91807,2.11319,0.0096436,0.0096436,0.0096436 | |||||
| 38,1173.46,1.8645,2.43429,1.94447,0.42449,0.32249,0.37815,0.21983,2.48945,4.83547,2.5625,0.0096337,0.0096337,0.0096337 | |||||
| 39,1204.19,1.86851,2.59506,1.93904,0.56418,0.50811,0.56633,0.35308,2.0772,4.00271,2.16946,0.0096238,0.0096238,0.0096238 | |||||
| 40,1235.52,1.88343,2.41743,1.8683,0.6036,0.53553,0.54392,0.33,2.06793,3.40566,2.16109,0.0096139,0.0096139,0.0096139 | |||||
| 41,1266.54,1.8789,2.46718,1.85799,0.65278,0.54068,0.6073,0.38942,2.03905,2.75934,2.11769,0.009604,0.009604,0.009604 | |||||
| 42,1296.91,1.87415,2.35517,1.89317,0.52272,0.58573,0.60708,0.39084,1.99974,2.75824,2.09871,0.0095941,0.0095941,0.0095941 | |||||
| 43,1327.15,1.8065,2.36363,1.85676,0.5696,0.5635,0.59166,0.35549,2.11079,2.9944,2.2015,0.0095842,0.0095842,0.0095842 | |||||
| 44,1358.15,1.71217,2.25062,1.834,0.62599,0.58271,0.6454,0.3944,2.05505,2.76048,2.14881,0.0095743,0.0095743,0.0095743 | |||||
| 45,1388.44,1.79958,2.41177,1.86353,0.61154,0.62068,0.65499,0.4053,2.04121,2.62992,2.12149,0.0095644,0.0095644,0.0095644 | |||||
| 46,1419.65,1.73793,2.27744,1.80536,0.61744,0.61691,0.64451,0.40388,2.03083,2.58909,2.11498,0.0095545,0.0095545,0.0095545 | |||||
| 47,1449.87,1.8252,2.32864,1.86931,0.6701,0.52481,0.59996,0.38251,2.13468,3.18874,2.15855,0.0095446,0.0095446,0.0095446 | |||||
| 48,1481.18,1.83037,2.23891,1.84423,0.67257,0.56493,0.63626,0.40492,1.94322,2.77078,2.09522,0.0095347,0.0095347,0.0095347 | |||||
| 49,1512.32,1.84992,2.27421,1.86811,0.544,0.58535,0.56303,0.34566,2.03373,3.10096,2.14207,0.0095248,0.0095248,0.0095248 | |||||
| 50,1542.63,1.80004,2.29197,1.8501,0.52113,0.61851,0.56774,0.36182,1.98226,2.68201,2.06094,0.0095149,0.0095149,0.0095149 | |||||
| 51,1573.04,1.8649,2.39088,1.86625,0.65926,0.62994,0.6645,0.4108,2.02379,2.62625,2.08539,0.009505,0.009505,0.009505 | |||||
| 52,1604.28,1.86099,2.4067,1.94229,0.70243,0.5713,0.66193,0.40454,2.0489,2.62656,2.09965,0.0094951,0.0094951,0.0094951 | |||||
| 53,1634.67,1.78459,2.32641,1.83533,0.73297,0.58992,0.69743,0.43403,2.00523,2.55661,2.07352,0.0094852,0.0094852,0.0094852 | |||||
| 54,1665.87,1.77479,2.12097,1.78918,0.69575,0.63374,0.69585,0.44632,1.93363,2.34394,2.03032,0.0094753,0.0094753,0.0094753 | |||||
| 55,1696.11,1.79447,2.21309,1.83822,0.5633,0.65643,0.6702,0.43464,1.97186,2.53811,2.09247,0.0094654,0.0094654,0.0094654 | |||||
| 56,1727.31,1.68976,1.99661,1.71081,0.51058,0.64818,0.60448,0.37644,2.05012,2.96843,2.11168,0.0094555,0.0094555,0.0094555 | |||||
| 57,1759.1,1.8444,2.09752,1.85829,0.6798,0.60319,0.66547,0.41173,2.06851,2.52008,2.14515,0.0094456,0.0094456,0.0094456 | |||||
| 58,1789.29,1.75257,2.17528,1.79767,0.61557,0.49712,0.57502,0.38081,2.05903,3.38497,2.26234,0.0094357,0.0094357,0.0094357 | |||||
| 59,1820.52,1.68272,2.09344,1.73428,0.66689,0.65765,0.70093,0.44305,1.96759,2.44041,2.03783,0.0094258,0.0094258,0.0094258 | |||||
| 60,1851.04,1.77978,2.06757,1.83414,0.63963,0.56241,0.60656,0.39329,1.9806,2.82047,2.09751,0.0094159,0.0094159,0.0094159 | |||||
| 61,1882.16,1.75469,2.09708,1.79083,0.59296,0.64927,0.66399,0.41516,1.99071,2.44924,2.07691,0.009406,0.009406,0.009406 | |||||
| 62,1912.36,1.70839,2.04093,1.79552,0.74807,0.60526,0.70708,0.44594,1.96958,2.39755,2.08955,0.0093961,0.0093961,0.0093961 | |||||
| 63,1943.6,1.81063,2.19109,1.84124,0.66267,0.4528,0.55566,0.3723,2.10637,3.51489,2.16723,0.0093862,0.0093862,0.0093862 | |||||
| 64,1975.31,1.70494,2.0312,1.72613,0.5446,0.54828,0.5729,0.34781,2.18312,2.95821,2.22275,0.0093763,0.0093763,0.0093763 | |||||
| 65,2006.76,1.76029,2.07565,1.82239,0.68439,0.664,0.68646,0.42555,1.97151,2.46188,2.06572,0.0093664,0.0093664,0.0093664 | |||||
| 66,2037.79,1.77601,2.12044,1.7961,0.66993,0.56461,0.64771,0.42386,1.95281,2.62522,2.05802,0.0093565,0.0093565,0.0093565 | |||||
| 67,2069.4,1.74463,2.0228,1.79756,0.65292,0.65017,0.67845,0.42438,1.99285,2.39774,2.07101,0.0093466,0.0093466,0.0093466 | |||||
| 68,2100.6,1.71478,2.01086,1.75777,0.6551,0.61315,0.66482,0.42935,1.95567,2.65237,2.06196,0.0093367,0.0093367,0.0093367 | |||||
| 69,2131.65,1.63584,1.88907,1.69358,0.71733,0.60653,0.69224,0.44323,1.95981,2.30105,2.06106,0.0093268,0.0093268,0.0093268 | |||||
| 70,2162.02,1.72051,2.04873,1.77628,0.65249,0.61395,0.66324,0.4259,1.93983,2.41967,2.05242,0.0093169,0.0093169,0.0093169 | |||||
| 71,2192.89,1.76786,2.04439,1.84533,0.67327,0.64307,0.65415,0.42308,1.98153,2.79006,2.10905,0.009307,0.009307,0.009307 | |||||
| 72,2224.13,1.71305,2.07611,1.79724,0.63173,0.73438,0.72691,0.46752,1.91868,2.25964,2.00855,0.0092971,0.0092971,0.0092971 | |||||
| 73,2257.71,1.73477,1.96634,1.77488,0.69453,0.65354,0.6967,0.43337,2.02486,2.36749,2.12102,0.0092872,0.0092872,0.0092872 | |||||
| 74,2290.59,1.7476,2.03874,1.86815,0.63041,0.74416,0.74136,0.49011,1.90481,2.19541,1.99289,0.0092773,0.0092773,0.0092773 | |||||
| 75,2321.78,1.65999,2.04063,1.76478,0.69578,0.67056,0.72423,0.46534,1.91106,2.29585,1.9911,0.0092674,0.0092674,0.0092674 | |||||
| 76,2352.45,1.71493,1.97689,1.79447,0.65374,0.68989,0.7028,0.42477,1.99132,2.33827,2.039,0.0092575,0.0092575,0.0092575 | |||||
| 77,2383.37,1.66049,1.91938,1.76847,0.67318,0.68889,0.71329,0.45538,1.93249,2.38719,2.04273,0.0092476,0.0092476,0.0092476 | |||||
| 78,2414.95,1.70886,1.95003,1.81118,0.70394,0.67585,0.69296,0.44359,1.93599,2.37564,2.03154,0.0092377,0.0092377,0.0092377 | |||||
| 79,2446.68,1.71238,1.99505,1.79231,0.70405,0.63295,0.68462,0.43914,1.96196,2.43182,2.04908,0.0092278,0.0092278,0.0092278 | |||||
| 80,2476.77,1.66636,1.86386,1.74769,0.70921,0.66345,0.70417,0.45188,1.93766,2.25578,2.0256,0.0092179,0.0092179,0.0092179 | |||||
| 81,2507.6,1.71782,2.01262,1.77357,0.73055,0.62686,0.7027,0.45086,1.94037,2.38371,2.0403,0.009208,0.009208,0.009208 | |||||
| 82,2537.83,1.62,1.8416,1.68444,0.67561,0.71262,0.71927,0.46371,1.91601,2.29262,2.02476,0.0091981,0.0091981,0.0091981 | |||||
| 83,2568.1,1.67571,1.91123,1.70594,0.66582,0.72626,0.7469,0.47277,1.91646,2.07693,2.00694,0.0091882,0.0091882,0.0091882 | |||||
| 84,2598.26,1.65599,1.83049,1.7356,0.69185,0.66831,0.71046,0.46033,1.90924,2.30259,2.00811,0.0091783,0.0091783,0.0091783 | |||||
| 85,2628.91,1.69053,1.84541,1.77721,0.77288,0.69375,0.74554,0.46174,1.93066,2.17661,2.03669,0.0091684,0.0091684,0.0091684 | |||||
| 86,2660.32,1.63021,1.7566,1.71157,0.69981,0.71577,0.75051,0.47943,1.91417,2.10722,2.01435,0.0091585,0.0091585,0.0091585 | |||||
| 87,2691.08,1.65175,1.90702,1.7437,0.66101,0.66719,0.69266,0.44423,1.98793,2.41698,2.08751,0.0091486,0.0091486,0.0091486 | |||||
| 88,2722.04,1.68427,1.85181,1.78147,0.74041,0.71377,0.7463,0.47813,1.8689,2.02687,1.96926,0.0091387,0.0091387,0.0091387 | |||||
| 89,2752.26,1.63119,1.73594,1.71096,0.69832,0.63207,0.67622,0.44232,1.93447,2.50703,2.0412,0.0091288,0.0091288,0.0091288 | |||||
| 90,2783.46,1.62372,1.8655,1.6972,0.70097,0.69899,0.73202,0.4677,1.94298,2.15119,2.05042,0.0091189,0.0091189,0.0091189 | |||||
| 91,2814.45,1.65209,1.90352,1.79287,0.68437,0.64718,0.69585,0.46175,1.94045,2.37982,2.03768,0.009109,0.009109,0.009109 | |||||
| 92,2845.46,1.59033,1.74556,1.66551,0.62478,0.77079,0.73172,0.47772,1.88385,2.07265,1.95217,0.0090991,0.0090991,0.0090991 | |||||
| 93,2876.24,1.60442,1.86284,1.69678,0.66894,0.68727,0.70827,0.4652,1.86558,2.20822,1.94664,0.0090892,0.0090892,0.0090892 | |||||
| 94,2907.38,1.6176,1.83018,1.69363,0.65558,0.67135,0.68921,0.4428,1.92617,2.11606,2.02405,0.0090793,0.0090793,0.0090793 | |||||
| 95,2937.9,1.60348,1.73399,1.70397,0.71671,0.69558,0.72871,0.47204,1.88481,2.05911,2.00175,0.0090694,0.0090694,0.0090694 | |||||
| 96,2969.03,1.64315,1.75424,1.72109,0.6732,0.76293,0.75839,0.48757,1.93489,2.04088,2.01103,0.0090595,0.0090595,0.0090595 | |||||
| 97,3000.25,1.63158,1.75773,1.7439,0.74801,0.58791,0.66285,0.42592,2.10341,2.84418,2.19788,0.0090496,0.0090496,0.0090496 | |||||
| 98,3030.9,1.56733,1.76075,1.68401,0.70804,0.70863,0.72991,0.46971,1.9423,2.19624,2.03908,0.0090397,0.0090397,0.0090397 | |||||
| 99,3061.9,1.67118,1.87264,1.73166,0.65928,0.68622,0.71323,0.4619,1.95344,2.32495,2.0202,0.0090298,0.0090298,0.0090298 | |||||
| 100,3093.28,1.6578,1.76768,1.70158,0.65995,0.75095,0.75439,0.47966,1.89181,2.00972,1.98113,0.0090199,0.0090199,0.0090199 | |||||
| 101,3124.28,1.67294,1.82137,1.75561,0.6728,0.71835,0.72477,0.46535,1.89329,2.10559,1.96997,0.00901,0.00901,0.00901 | |||||
| 102,3155.34,1.6344,1.75931,1.7498,0.68762,0.73431,0.74652,0.47343,1.90751,2.12601,1.99317,0.0090001,0.0090001,0.0090001 | |||||
| 103,3185.57,1.6012,1.72405,1.70126,0.6988,0.71892,0.7427,0.47474,1.89237,2.02244,2.00033,0.0089902,0.0089902,0.0089902 | |||||
| 104,3215.96,1.67225,1.89161,1.77791,0.69721,0.7528,0.75713,0.47756,1.97322,2.06667,2.06217,0.0089803,0.0089803,0.0089803 | |||||
| 105,3248.35,1.60898,1.74546,1.74248,0.69214,0.75715,0.77421,0.48923,1.88532,2.02793,2.0021,0.0089704,0.0089704,0.0089704 | |||||
| 106,3278.92,1.57215,1.73317,1.6791,0.73579,0.69373,0.74712,0.48593,1.89157,2.19439,2.00705,0.0089605,0.0089605,0.0089605 | |||||
| 107,3309.67,1.65866,1.74319,1.78319,0.69793,0.7058,0.73758,0.47067,1.91284,2.14136,1.99853,0.0089506,0.0089506,0.0089506 | |||||
| 108,3341.22,1.6409,1.80083,1.78682,0.62094,0.73029,0.7105,0.46102,1.94457,2.21369,2.05021,0.0089407,0.0089407,0.0089407 | |||||
| 109,3372.16,1.63055,1.79766,1.76275,0.71753,0.68819,0.74995,0.47348,1.9128,2.0163,1.9998,0.0089308,0.0089308,0.0089308 | |||||
| 110,3402.67,1.58359,1.63488,1.63558,0.73165,0.70983,0.74639,0.48588,1.88987,2.05689,1.99256,0.0089209,0.0089209,0.0089209 | |||||
| 111,3433.42,1.54491,1.67577,1.65672,0.74444,0.7028,0.74363,0.48243,1.93855,2.12785,2.01551,0.008911,0.008911,0.008911 | |||||
| 112,3463.69,1.57232,1.71179,1.70476,0.66259,0.73669,0.73286,0.47466,1.88714,2.17065,2.00547,0.0089011,0.0089011,0.0089011 | |||||
| 113,3493.99,1.5494,1.63051,1.69414,0.75052,0.65764,0.74657,0.49149,1.89494,2.09169,2.02276,0.0088912,0.0088912,0.0088912 | |||||
| 114,3525.35,1.60578,1.80617,1.72276,0.75063,0.72429,0.77702,0.50198,1.87324,2.06979,2.00217,0.0088813,0.0088813,0.0088813 | |||||
| 115,3556.85,1.63476,1.8233,1.725,0.75913,0.69978,0.76841,0.49045,1.89908,2.0631,1.99536,0.0088714,0.0088714,0.0088714 | |||||
| 116,3587.86,1.61478,1.67594,1.69749,0.69546,0.79037,0.77589,0.49685,1.84725,1.91031,1.95434,0.0088615,0.0088615,0.0088615 | |||||
| 117,3618.24,1.562,1.63893,1.71007,0.77191,0.66477,0.75118,0.47698,1.89677,2.00725,1.99426,0.0088516,0.0088516,0.0088516 | |||||
| 118,3648.74,1.60122,1.69165,1.69373,0.70796,0.75649,0.76426,0.48445,1.87769,1.96904,1.96543,0.0088417,0.0088417,0.0088417 | |||||
| 119,3679.17,1.52448,1.62477,1.67566,0.7176,0.72054,0.75114,0.48312,1.86382,2.1011,1.98374,0.0088318,0.0088318,0.0088318 | |||||
| 120,3710.4,1.59228,1.74581,1.68931,0.73173,0.74726,0.75814,0.49304,1.8737,1.90381,1.9628,0.0088219,0.0088219,0.0088219 | |||||
| 121,3741.8,1.52164,1.68262,1.68659,0.69812,0.7459,0.74733,0.48158,1.87815,1.96184,1.98716,0.008812,0.008812,0.008812 | |||||
| 122,3773.09,1.56274,1.65581,1.69288,0.65805,0.74741,0.74103,0.48692,1.90351,2.04182,2.00776,0.0088021,0.0088021,0.0088021 | |||||
| 123,3804.71,1.56582,1.68026,1.69251,0.74411,0.74373,0.77496,0.49796,1.88747,2.02576,1.99313,0.0087922,0.0087922,0.0087922 | |||||
| 124,3836.07,1.63038,1.72226,1.70713,0.69002,0.76629,0.75568,0.4868,1.93811,2.03235,2.03204,0.0087823,0.0087823,0.0087823 | |||||
| 125,3866.48,1.53718,1.58726,1.68665,0.67342,0.76326,0.76083,0.48635,1.89367,2.03137,2.00702,0.0087724,0.0087724,0.0087724 | |||||
| 126,3897.84,1.52791,1.59694,1.70075,0.70927,0.75116,0.7625,0.49619,1.86037,1.91145,1.99687,0.0087625,0.0087625,0.0087625 | |||||
| 127,3929.23,1.52711,1.51207,1.68549,0.71511,0.66365,0.70371,0.44721,1.92537,2.10992,2.03695,0.0087526,0.0087526,0.0087526 | |||||
| 128,3960.29,1.60066,1.6233,1.6994,0.68199,0.76917,0.7424,0.47616,1.86428,2.00035,1.96745,0.0087427,0.0087427,0.0087427 | |||||
| 129,3991.46,1.48425,1.64257,1.62917,0.71194,0.71953,0.75503,0.48922,1.89277,1.96905,1.99971,0.0087328,0.0087328,0.0087328 | |||||
| 130,4022.59,1.49591,1.60587,1.65544,0.75048,0.7725,0.77548,0.4979,1.86874,1.93635,1.97436,0.0087229,0.0087229,0.0087229 | |||||
| 131,4052.89,1.57657,1.62735,1.68638,0.77483,0.71497,0.76046,0.48619,1.91064,1.99341,2.03471,0.008713,0.008713,0.008713 | |||||
| 132,4084.18,1.51229,1.55421,1.65411,0.77594,0.68834,0.74119,0.49359,1.87433,2.04587,1.98123,0.0087031,0.0087031,0.0087031 | |||||
| 133,4115.5,1.57072,1.62683,1.64384,0.7475,0.72255,0.76225,0.49354,1.88699,1.9553,1.97595,0.0086932,0.0086932,0.0086932 | |||||
| 134,4146.58,1.54797,1.59118,1.67107,0.75001,0.71998,0.75556,0.47706,1.89069,1.98105,2.00277,0.0086833,0.0086833,0.0086833 | |||||
| 135,4177.94,1.52704,1.5953,1.66378,0.72931,0.68673,0.74635,0.47547,1.86982,2.08823,1.96654,0.0086734,0.0086734,0.0086734 | |||||
| 136,4208.9,1.4893,1.60814,1.6778,0.73487,0.72543,0.76179,0.48995,1.8754,1.89028,1.98092,0.0086635,0.0086635,0.0086635 | |||||
| 137,4240.16,1.53343,1.6022,1.66604,0.7228,0.76887,0.76916,0.50187,1.84482,1.84397,1.96968,0.0086536,0.0086536,0.0086536 | |||||
| 138,4271.44,1.49143,1.61872,1.67365,0.74242,0.75688,0.77451,0.50577,1.8628,1.8409,1.98261,0.0086437,0.0086437,0.0086437 | |||||
| 139,4302.82,1.51472,1.54374,1.66483,0.78992,0.61836,0.71627,0.46596,1.90419,2.20122,2.03213,0.0086338,0.0086338,0.0086338 | |||||
| 140,4333.29,1.56464,1.67301,1.6978,0.7501,0.68768,0.74966,0.48972,1.84634,1.97314,1.9748,0.0086239,0.0086239,0.0086239 | |||||
| 141,4363.86,1.54989,1.63095,1.6786,0.77353,0.71713,0.77342,0.48928,1.90894,1.9425,2.0074,0.008614,0.008614,0.008614 | |||||
| 142,4394.71,1.50495,1.55003,1.65713,0.72907,0.78711,0.78698,0.51538,1.85967,1.92384,1.95335,0.0086041,0.0086041,0.0086041 | |||||
| 143,4425.94,1.49654,1.56361,1.62185,0.78189,0.71634,0.77689,0.50203,1.8531,2.0401,1.94712,0.0085942,0.0085942,0.0085942 | |||||
| 144,4457.35,1.47004,1.51486,1.6262,0.70494,0.79473,0.76884,0.4964,1.86085,1.91598,1.97934,0.0085843,0.0085843,0.0085843 | |||||
| 145,4488.51,1.46367,1.52509,1.63242,0.73485,0.75239,0.77186,0.50086,1.86819,1.87848,1.97314,0.0085744,0.0085744,0.0085744 | |||||
| 146,4519.43,1.47084,1.54345,1.63811,0.71846,0.74035,0.7609,0.48972,1.87231,1.87854,1.97234,0.0085645,0.0085645,0.0085645 | |||||
| 147,4550.17,1.49373,1.59394,1.62908,0.74348,0.71685,0.75089,0.4892,1.88174,1.92687,1.97843,0.0085546,0.0085546,0.0085546 | |||||
| 148,4581.33,1.46801,1.53394,1.62238,0.69848,0.81228,0.78104,0.50422,1.83227,1.89914,1.93102,0.0085447,0.0085447,0.0085447 | |||||
| 149,4612.45,1.5052,1.50409,1.62337,0.72209,0.78195,0.77867,0.49525,1.8599,1.86737,1.98748,0.0085348,0.0085348,0.0085348 | |||||
| 150,4642.85,1.44902,1.46299,1.57813,0.72811,0.74532,0.78403,0.50516,1.86042,1.87099,1.95547,0.0085249,0.0085249,0.0085249 | |||||
| 151,4673.45,1.47732,1.47856,1.59758,0.74506,0.74998,0.78561,0.5037,1.857,1.89689,1.95507,0.008515,0.008515,0.008515 | |||||
| 152,4704.7,1.52313,1.51859,1.6303,0.76068,0.73163,0.77934,0.50076,1.8826,1.98434,1.99611,0.0085051,0.0085051,0.0085051 | |||||
| 153,4734.85,1.50034,1.55807,1.64271,0.72049,0.79073,0.77647,0.50216,1.86032,1.91405,1.96286,0.0084952,0.0084952,0.0084952 | |||||
| 154,4768.5,1.52433,1.52028,1.61059,0.7644,0.70732,0.77444,0.49535,1.88475,1.93922,1.96815,0.0084853,0.0084853,0.0084853 | |||||
| 155,4800.08,1.50003,1.54511,1.62772,0.71943,0.76873,0.78306,0.50526,1.87223,1.86043,1.96608,0.0084754,0.0084754,0.0084754 | |||||
| 156,4831.48,1.46801,1.57109,1.62173,0.74778,0.73076,0.76829,0.48921,1.88858,1.95747,1.9818,0.0084655,0.0084655,0.0084655 | |||||
| 157,4862.88,1.48143,1.54908,1.62293,0.67532,0.77128,0.76963,0.49462,1.8701,1.87523,1.9747,0.0084556,0.0084556,0.0084556 | |||||
| 158,4893.69,1.50796,1.52131,1.6155,0.74948,0.72635,0.76733,0.50152,1.84082,1.91005,1.94933,0.0084457,0.0084457,0.0084457 | |||||
| 159,4924.79,1.47093,1.43212,1.61213,0.69829,0.78836,0.78017,0.50218,1.87664,1.86978,1.98285,0.0084358,0.0084358,0.0084358 | |||||
| 160,4955.79,1.46021,1.50466,1.60093,0.7477,0.73738,0.7673,0.50665,1.84484,1.94283,1.9651,0.0084259,0.0084259,0.0084259 | |||||
| 161,4986.97,1.48612,1.56605,1.64446,0.71931,0.80519,0.79376,0.51549,1.83831,1.8834,1.97741,0.008416,0.008416,0.008416 | |||||
| 162,5018.17,1.45565,1.49378,1.614,0.80067,0.71541,0.79184,0.51521,1.8346,1.91524,1.94346,0.0084061,0.0084061,0.0084061 | |||||
| 163,5049.32,1.48986,1.53316,1.63445,0.77194,0.72887,0.78476,0.51452,1.84588,1.91639,1.96179,0.0083962,0.0083962,0.0083962 | |||||
| 164,5081.06,1.44679,1.4472,1.61446,0.76972,0.74023,0.78661,0.51531,1.85089,1.8995,1.99156,0.0083863,0.0083863,0.0083863 | |||||
| 165,5112.41,1.46961,1.49509,1.66838,0.73123,0.78224,0.78798,0.5087,1.86241,1.95185,2.00957,0.0083764,0.0083764,0.0083764 | |||||
| 166,5142.7,1.43942,1.48146,1.62191,0.76552,0.75013,0.78655,0.50457,1.84598,1.86193,1.96788,0.0083665,0.0083665,0.0083665 | |||||
| 167,5174.06,1.44945,1.57198,1.63458,0.75442,0.72243,0.76256,0.49343,1.87835,1.94129,1.97724,0.0083566,0.0083566,0.0083566 | |||||
| 168,5205.38,1.461,1.48094,1.60827,0.76099,0.75153,0.79177,0.51045,1.88137,1.88459,1.96118,0.0083467,0.0083467,0.0083467 | |||||
| 169,5235.66,1.45146,1.47497,1.61302,0.77255,0.73058,0.78033,0.4985,1.89142,1.89338,1.98194,0.0083368,0.0083368,0.0083368 | |||||
| 170,5267.08,1.52846,1.59264,1.66069,0.74249,0.73669,0.77524,0.4999,1.84125,1.91202,1.95036,0.0083269,0.0083269,0.0083269 | |||||
| 171,5297.46,1.46324,1.47038,1.63683,0.73238,0.74624,0.7766,0.49647,1.85382,1.90369,1.96056,0.008317,0.008317,0.008317 | |||||
| 172,5328.22,1.42227,1.39554,1.57272,0.74177,0.75839,0.7828,0.49399,1.84633,1.93177,1.95304,0.0083071,0.0083071,0.0083071 | |||||
| 173,5359.43,1.38575,1.48044,1.56661,0.71667,0.80444,0.78661,0.50073,1.86349,1.87811,1.99063,0.0082972,0.0082972,0.0082972 | |||||
| 174,5389.54,1.45477,1.47128,1.63922,0.73741,0.78007,0.78863,0.50139,1.86574,1.91012,1.98407,0.0082873,0.0082873,0.0082873 | |||||
| 175,5420.3,1.44559,1.4842,1.60311,0.70737,0.81579,0.78821,0.50486,1.89997,1.93832,2.01136,0.0082774,0.0082774,0.0082774 | |||||
| 176,5451.5,1.42252,1.51095,1.59501,0.7569,0.74881,0.78472,0.51244,1.86509,1.91201,1.96233,0.0082675,0.0082675,0.0082675 | |||||
| 177,5481.79,1.46623,1.535,1.61428,0.71725,0.7852,0.77582,0.50525,1.84652,1.88082,1.99229,0.0082576,0.0082576,0.0082576 | |||||
| 178,5513.46,1.42992,1.47887,1.60196,0.68047,0.81116,0.76735,0.48607,1.87456,1.90702,1.98009,0.0082477,0.0082477,0.0082477 | |||||
| 179,5543.89,1.45059,1.43996,1.63438,0.74857,0.75831,0.79145,0.51219,1.8456,1.85742,1.96571,0.0082378,0.0082378,0.0082378 | |||||
| 180,5574.24,1.47166,1.49255,1.5875,0.71326,0.82671,0.796,0.50757,1.87788,1.86213,1.99865,0.0082279,0.0082279,0.0082279 | |||||
| 181,5605.72,1.44551,1.4369,1.60446,0.70297,0.80894,0.80245,0.51936,1.8357,1.8095,1.98173,0.008218,0.008218,0.008218 | |||||
| 182,5636.73,1.42934,1.43092,1.60836,0.69639,0.81495,0.79303,0.51809,1.82884,1.83271,1.96932,0.0082081,0.0082081,0.0082081 | |||||
| 183,5667.63,1.41919,1.4449,1.57803,0.74632,0.74744,0.78443,0.50585,1.83984,1.80941,1.96915,0.0081982,0.0081982,0.0081982 | |||||
| 184,5698.08,1.45446,1.50411,1.63176,0.73538,0.75807,0.78317,0.50726,1.87704,1.87511,1.99424,0.0081883,0.0081883,0.0081883 | |||||
| 185,5729.3,1.46637,1.41176,1.62054,0.75804,0.78377,0.79895,0.52348,1.82845,1.81186,1.96064,0.0081784,0.0081784,0.0081784 | |||||
| 186,5760.61,1.45811,1.39593,1.62133,0.73032,0.80787,0.80093,0.52438,1.85842,1.78786,1.99241,0.0081685,0.0081685,0.0081685 | |||||
| 187,5790.97,1.40471,1.45446,1.56272,0.75579,0.74735,0.78792,0.50981,1.87394,1.8223,1.9945,0.0081586,0.0081586,0.0081586 | |||||
| 188,5821.51,1.45591,1.44066,1.62186,0.72674,0.77252,0.78652,0.50082,1.90601,1.86003,2.01783,0.0081487,0.0081487,0.0081487 | |||||
| 189,5853.07,1.44014,1.45255,1.57646,0.73883,0.78948,0.79556,0.50897,1.87963,1.81432,1.98131,0.0081388,0.0081388,0.0081388 | |||||
| 190,5883.92,1.41712,1.45792,1.61791,0.77574,0.75528,0.78237,0.50444,1.87569,1.86456,1.99036,0.0081289,0.0081289,0.0081289 | |||||
| 191,5915.25,1.44824,1.45836,1.62822,0.74898,0.77132,0.79236,0.50693,1.87471,1.89262,2.00576,0.008119,0.008119,0.008119 | |||||
| 192,5946.57,1.37604,1.48668,1.56894,0.73551,0.75451,0.77331,0.4883,1.89068,1.97901,2.0111,0.0081091,0.0081091,0.0081091 | |||||
| 193,5977.56,1.39365,1.43852,1.60433,0.71733,0.77741,0.77645,0.49008,1.90362,1.9199,2.01311,0.0080992,0.0080992,0.0080992 | |||||
| 194,6008.11,1.41191,1.51766,1.64054,0.733,0.81802,0.80322,0.51103,1.90483,1.79918,1.99227,0.0080893,0.0080893,0.0080893 | |||||
| 195,6039.59,1.43456,1.4733,1.60803,0.76201,0.77377,0.80261,0.51379,1.87389,1.78638,1.98705,0.0080794,0.0080794,0.0080794 | |||||
| 196,6070.41,1.43379,1.40601,1.63871,0.76562,0.74367,0.79528,0.50972,1.89246,1.87604,1.99095,0.0080695,0.0080695,0.0080695 | |||||
| 197,6101.61,1.38223,1.31433,1.57488,0.71137,0.78758,0.78347,0.4981,1.89329,1.89994,1.99732,0.0080596,0.0080596,0.0080596 | |||||
| 198,6133.19,1.39127,1.38365,1.55506,0.76971,0.7515,0.78792,0.50531,1.87821,1.84589,1.98293,0.0080497,0.0080497,0.0080497 | |||||
| 199,6165.12,1.39812,1.40901,1.57171,0.76145,0.74233,0.78824,0.50664,1.86096,1.82841,1.97268,0.0080398,0.0080398,0.0080398 | |||||
| 200,6196.32,1.3978,1.40969,1.61055,0.74252,0.78108,0.79605,0.51138,1.87618,1.8384,2.028,0.0080299,0.0080299,0.0080299 | |||||
| 201,6226.88,1.37826,1.42356,1.59497,0.71569,0.8032,0.79103,0.50883,1.88273,1.86125,1.99345,0.00802,0.00802,0.00802 | |||||
| 202,6258.04,1.36184,1.33342,1.5504,0.70665,0.81981,0.79758,0.50904,1.86493,1.81655,1.96661,0.0080101,0.0080101,0.0080101 | |||||
| 203,6288.99,1.40402,1.44028,1.57008,0.77986,0.74293,0.78497,0.50064,1.86142,1.83736,1.97048,0.0080002,0.0080002,0.0080002 | |||||
| 204,6319.37,1.39505,1.4694,1.58551,0.72915,0.79159,0.78861,0.50356,1.88524,1.81101,1.99346,0.0079903,0.0079903,0.0079903 | |||||
| 205,6350.75,1.46044,1.4745,1.66535,0.70514,0.81684,0.79779,0.5142,1.88208,1.79558,2.00225,0.0079804,0.0079804,0.0079804 | |||||
| 206,6382.01,1.4156,1.44975,1.58607,0.78681,0.74715,0.7961,0.51114,1.89506,1.76191,1.99964,0.0079705,0.0079705,0.0079705 | |||||
| 207,6413.25,1.40198,1.38031,1.58511,0.76683,0.75493,0.79861,0.50469,1.89332,1.82835,2.00264,0.0079606,0.0079606,0.0079606 | |||||
| 208,6443.58,1.37155,1.3687,1.5618,0.73082,0.76219,0.7832,0.50785,1.85987,1.82664,1.98109,0.0079507,0.0079507,0.0079507 | |||||
| 209,6473.74,1.36415,1.3385,1.54021,0.73908,0.77753,0.78557,0.50559,1.88668,1.81253,2.00304,0.0079408,0.0079408,0.0079408 | |||||
| 210,6504.45,1.43441,1.48365,1.60797,0.71263,0.79616,0.78753,0.50565,1.88882,1.79205,2.00785,0.0079309,0.0079309,0.0079309 | |||||
| 211,6535.82,1.3549,1.31849,1.55589,0.77343,0.72187,0.78819,0.50439,1.8869,1.83307,2.02005,0.007921,0.007921,0.007921 | |||||
| 212,6566.11,1.39904,1.3698,1.57714,0.78025,0.72739,0.77473,0.49871,1.88037,1.82,2.01155,0.0079111,0.0079111,0.0079111 | |||||
| 213,6597.02,1.37336,1.37969,1.56967,0.71577,0.7997,0.78069,0.50487,1.85017,1.82695,1.99262,0.0079012,0.0079012,0.0079012 | |||||
| 214,6628.38,1.40464,1.38286,1.60293,0.74176,0.7563,0.78152,0.50114,1.858,1.87249,1.99824,0.0078913,0.0078913,0.0078913 | |||||
| 215,6659.09,1.43897,1.44174,1.62567,0.7286,0.7848,0.78406,0.5019,1.85397,1.86847,1.98587,0.0078814,0.0078814,0.0078814 | |||||
| 216,6690.72,1.39386,1.40505,1.59005,0.74726,0.7728,0.77963,0.50517,1.84393,1.85844,1.98698,0.0078715,0.0078715,0.0078715 | |||||
| 217,6722.09,1.46789,1.44066,1.5838,0.7419,0.77718,0.79181,0.51106,1.88722,1.83351,2.00446,0.0078616,0.0078616,0.0078616 | |||||
| 218,6752.29,1.4121,1.3761,1.61803,0.73666,0.80345,0.80413,0.51342,1.86395,1.82779,1.98842,0.0078517,0.0078517,0.0078517 | |||||
| 219,6782.86,1.35294,1.40728,1.57101,0.7192,0.78435,0.79308,0.50938,1.8384,1.82863,1.96376,0.0078418,0.0078418,0.0078418 | |||||
| 220,6814,1.35249,1.40875,1.53643,0.69848,0.84489,0.80215,0.51659,1.84795,1.84425,1.98126,0.0078319,0.0078319,0.0078319 | |||||
| 221,6844.29,1.32866,1.30062,1.53015,0.69067,0.8325,0.78585,0.51292,1.83106,1.8192,1.96816,0.007822,0.007822,0.007822 | |||||
| 222,6875.98,1.31121,1.29356,1.50207,0.79209,0.75261,0.78974,0.51315,1.84265,1.79525,1.98626,0.0078121,0.0078121,0.0078121 | |||||
| 223,6906.78,1.40781,1.43706,1.58275,0.77502,0.75782,0.79558,0.5176,1.86127,1.80607,2.02059,0.0078022,0.0078022,0.0078022 | |||||
| 224,6937.08,1.34159,1.37074,1.56666,0.73023,0.80505,0.80049,0.51108,1.85804,1.77945,2.01451,0.0077923,0.0077923,0.0077923 | |||||
| 225,6967.29,1.39892,1.42924,1.55977,0.73949,0.78366,0.79544,0.50093,1.87679,1.82318,2.014,0.0077824,0.0077824,0.0077824 | |||||
| 226,6997.85,1.41408,1.48925,1.60738,0.76862,0.75722,0.79724,0.49301,1.89524,1.86824,2.02922,0.0077725,0.0077725,0.0077725 | |||||
| 227,7028.51,1.35494,1.35136,1.55596,0.73849,0.77784,0.78728,0.49192,1.87707,1.85852,2.00699,0.0077626,0.0077626,0.0077626 | |||||
| 228,7059.1,1.37457,1.43431,1.57353,0.79814,0.73889,0.80098,0.51148,1.8532,1.8222,1.978,0.0077527,0.0077527,0.0077527 | |||||
| 229,7091.54,1.28382,1.30039,1.55923,0.76775,0.75083,0.79185,0.50871,1.87064,1.83077,1.99713,0.0077428,0.0077428,0.0077428 | |||||
| 230,7122.83,1.34263,1.35433,1.51377,0.74366,0.76113,0.78589,0.50202,1.87723,1.85617,2.01213,0.0077329,0.0077329,0.0077329 | |||||
| 231,7153.57,1.41842,1.4309,1.61452,0.72568,0.75672,0.78323,0.50095,1.8725,1.86092,1.99634,0.007723,0.007723,0.007723 | |||||
| 232,7183.82,1.36769,1.3711,1.57387,0.71708,0.77716,0.78062,0.49815,1.88696,1.84419,2.00582,0.0077131,0.0077131,0.0077131 | |||||
| 233,7214.09,1.39209,1.38441,1.57156,0.77225,0.76585,0.78379,0.49866,1.8983,1.84585,2.03706,0.0077032,0.0077032,0.0077032 | |||||
| 234,7245.38,1.34083,1.33675,1.5404,0.69746,0.80245,0.77988,0.49919,1.90138,1.8748,2.02817,0.0076933,0.0076933,0.0076933 | |||||
| 235,7275.56,1.36335,1.33588,1.55218,0.71549,0.78998,0.78872,0.50245,1.88339,1.85198,2.02777,0.0076834,0.0076834,0.0076834 | |||||
| 236,7305.81,1.3641,1.35757,1.56591,0.69239,0.77623,0.76649,0.48599,1.89624,1.9539,2.02361,0.0076735,0.0076735,0.0076735 | |||||
| 237,7336.19,1.35436,1.32474,1.59697,0.71611,0.7771,0.78314,0.49473,1.86557,1.85773,1.9994,0.0076636,0.0076636,0.0076636 | |||||
| 238,7366.75,1.34326,1.3424,1.56875,0.78392,0.71846,0.78296,0.50194,1.8805,1.86653,2.01266,0.0076537,0.0076537,0.0076537 | |||||
| 239,7398.04,1.31612,1.25352,1.52702,0.72559,0.79226,0.79662,0.51214,1.86754,1.83768,2.02138,0.0076438,0.0076438,0.0076438 | |||||
| 240,7428.26,1.37305,1.33916,1.57145,0.73635,0.79863,0.80229,0.51035,1.88324,1.80783,2.03177,0.0076339,0.0076339,0.0076339 | |||||
| 241,7459.34,1.38381,1.33095,1.57235,0.74123,0.78124,0.79115,0.50013,1.89903,1.8541,2.03794,0.007624,0.007624,0.007624 | |||||
| 242,7490.62,1.27651,1.23486,1.52293,0.71406,0.80159,0.78762,0.49912,1.88472,1.85787,2.00422,0.0076141,0.0076141,0.0076141 | |||||
| 243,7521.79,1.32342,1.32334,1.52738,0.76319,0.76578,0.78052,0.49561,1.86613,1.80354,2.00291,0.0076042,0.0076042,0.0076042 | |||||
| 244,7551.96,1.31785,1.35072,1.54487,0.75047,0.79451,0.7846,0.50865,1.86686,1.77985,1.98725,0.0075943,0.0075943,0.0075943 | |||||
| 245,7582.95,1.34047,1.34383,1.53865,0.76897,0.77745,0.7819,0.49303,1.8958,1.8233,2.01125,0.0075844,0.0075844,0.0075844 | |||||
| 246,7614.21,1.3612,1.30977,1.57192,0.7899,0.7597,0.79477,0.50718,1.89397,1.80901,2.00814,0.0075745,0.0075745,0.0075745 | |||||
| 247,7646.47,1.31136,1.25639,1.55022,0.77363,0.78328,0.80394,0.5112,1.88432,1.80132,2.01782,0.0075646,0.0075646,0.0075646 | |||||
| 248,7676.86,1.32063,1.37678,1.51204,0.76068,0.77319,0.79741,0.51053,1.88153,1.85388,2.03466,0.0075547,0.0075547,0.0075547 | |||||
| 249,7708.13,1.3414,1.41538,1.57625,0.75163,0.79398,0.79598,0.5108,1.87337,1.82247,2.00039,0.0075448,0.0075448,0.0075448 | |||||
| 250,7739.23,1.28849,1.31163,1.5202,0.71309,0.79376,0.78908,0.50365,1.86068,1.81913,2.00235,0.0075349,0.0075349,0.0075349 | |||||
| 251,7770.69,1.31329,1.33675,1.51889,0.73782,0.75728,0.7801,0.50229,1.87895,1.82713,2.00793,0.007525,0.007525,0.007525 | |||||
| 252,7801.9,1.31962,1.33687,1.52224,0.7485,0.74881,0.78566,0.50108,1.88629,1.85088,2.01449,0.0075151,0.0075151,0.0075151 | |||||
| 253,7832.92,1.29976,1.31806,1.52098,0.75775,0.7519,0.78156,0.4991,1.87684,1.8661,2.00071,0.0075052,0.0075052,0.0075052 | |||||
| 254,7863.8,1.36402,1.34311,1.5315,0.78983,0.74318,0.79959,0.5036,1.8907,1.8145,2.01925,0.0074953,0.0074953,0.0074953 | |||||
| 255,7894.32,1.34314,1.28229,1.55558,0.74333,0.80155,0.79658,0.49802,1.88686,1.86155,2.01928,0.0074854,0.0074854,0.0074854 | |||||
| 256,7925.65,1.3108,1.34632,1.52364,0.74626,0.78813,0.78437,0.49769,1.8857,1.86569,2.01598,0.0074755,0.0074755,0.0074755 | |||||
| 257,7956.68,1.34144,1.2844,1.52146,0.76684,0.74397,0.77765,0.48472,1.89212,1.8519,2.01397,0.0074656,0.0074656,0.0074656 | |||||
| 258,7988.21,1.33098,1.27143,1.53412,0.77347,0.7373,0.79158,0.5041,1.88172,1.85369,2.00543,0.0074557,0.0074557,0.0074557 | |||||
| 259,8019.41,1.32511,1.29295,1.5559,0.75603,0.75849,0.78206,0.49596,1.89785,1.84434,2.0217,0.0074458,0.0074458,0.0074458 | |||||
| 260,8050.57,1.35256,1.39022,1.57933,0.78791,0.72759,0.78058,0.49683,1.89063,1.871,2.00385,0.0074359,0.0074359,0.0074359 | |||||
| 261,8081.68,1.29827,1.30042,1.50854,0.77926,0.72364,0.78872,0.49994,1.88572,1.87477,1.99923,0.007426,0.007426,0.007426 | |||||
| 262,8112.69,1.36214,1.29992,1.5622,0.74028,0.77164,0.78006,0.50001,1.87549,1.8755,2.00933,0.0074161,0.0074161,0.0074161 | |||||
| 263,8143.64,1.32336,1.35218,1.56708,0.76511,0.72485,0.76353,0.48515,1.8871,1.89397,2.01978,0.0074062,0.0074062,0.0074062 | |||||
| 264,8173.58,1.32982,1.30385,1.54341,0.71743,0.78949,0.79074,0.50152,1.88745,1.87516,2.02503,0.0073963,0.0073963,0.0073963 | |||||
| 265,8205.47,1.33302,1.31361,1.52622,0.74308,0.7734,0.79089,0.5062,1.88177,1.83128,2.00434,0.0073864,0.0073864,0.0073864 | |||||
| 266,8236.32,1.27243,1.24497,1.53358,0.69958,0.82427,0.79968,0.50884,1.90279,1.84324,2.0135,0.0073765,0.0073765,0.0073765 | |||||
| 267,8267.4,1.35763,1.29812,1.56612,0.73722,0.78238,0.79639,0.50407,1.90429,1.84526,2.01613,0.0073666,0.0073666,0.0073666 | |||||
| 268,8298.93,1.3437,1.30155,1.58098,0.7847,0.73738,0.79135,0.50559,1.89435,1.87289,2.00952,0.0073567,0.0073567,0.0073567 | |||||
| 269,8329.98,1.3063,1.42414,1.5783,0.81743,0.71995,0.79006,0.50254,1.87544,1.88012,2.00831,0.0073468,0.0073468,0.0073468 | |||||
| 270,8360.36,1.28969,1.18677,1.48852,0.69495,0.81894,0.79355,0.50947,1.87664,1.90455,2.01071,0.0073369,0.0073369,0.0073369 | |||||
| 271,8391.47,1.28901,1.29141,1.50667,0.72566,0.7933,0.78909,0.50995,1.89353,1.84744,2.02184,0.007327,0.007327,0.007327 | |||||
| 272,8422.48,1.32632,1.25565,1.51711,0.70888,0.78606,0.78496,0.5027,1.8862,1.86233,2.01203,0.0073171,0.0073171,0.0073171 | |||||
| 273,8452.94,1.34569,1.33813,1.53302,0.71535,0.77279,0.77878,0.49702,1.88851,1.88303,2.01962,0.0073072,0.0073072,0.0073072 | |||||
| 274,8484.79,1.2784,1.20828,1.53208,0.77289,0.71763,0.78208,0.49696,1.90551,1.89529,2.04122,0.0072973,0.0072973,0.0072973 | |||||
| 275,8514.98,1.31459,1.25852,1.56784,0.79303,0.71626,0.7839,0.49958,1.89585,1.88788,2.03789,0.0072874,0.0072874,0.0072874 | |||||
| 276,8546.06,1.33196,1.30872,1.53426,0.7222,0.79152,0.80243,0.51485,1.89087,1.85667,2.04239,0.0072775,0.0072775,0.0072775 | |||||
| 277,8577.19,1.29867,1.26053,1.51041,0.77333,0.73167,0.80539,0.51495,1.89041,1.8466,2.04252,0.0072676,0.0072676,0.0072676 | |||||
| 278,8608.43,1.36301,1.33282,1.591,0.69868,0.83003,0.80126,0.51458,1.89361,1.83562,2.0391,0.0072577,0.0072577,0.0072577 | |||||
| 279,8639.82,1.35098,1.33449,1.54524,0.76899,0.73993,0.78184,0.49503,1.90151,1.87411,2.0455,0.0072478,0.0072478,0.0072478 | |||||
| 280,8670,1.32584,1.29196,1.52019,0.76437,0.75139,0.77797,0.49019,1.90368,1.86155,2.04282,0.0072379,0.0072379,0.0072379 | |||||
| 281,8701.14,1.25966,1.24701,1.50923,0.78866,0.71873,0.78173,0.49432,1.88847,1.83457,2.00531,0.007228,0.007228,0.007228 | |||||
| 282,8732.62,1.30531,1.35026,1.55338,0.73873,0.78147,0.78801,0.49594,1.88064,1.80344,2.00937,0.0072181,0.0072181,0.0072181 | |||||
| 283,8762.84,1.32977,1.28659,1.55365,0.7611,0.78243,0.80189,0.51364,1.90118,1.82013,2.03183,0.0072082,0.0072082,0.0072082 | |||||
| 284,8793.71,1.27986,1.25652,1.52344,0.76456,0.77161,0.79176,0.50909,1.89173,1.80953,2.02596,0.0071983,0.0071983,0.0071983 | |||||
| 285,8823.99,1.3076,1.27043,1.53276,0.76566,0.77163,0.79709,0.50885,1.88561,1.81381,2.01843,0.0071884,0.0071884,0.0071884 | |||||
| 286,8855.2,1.27702,1.25344,1.50884,0.76235,0.78627,0.79396,0.51422,1.87442,1.82113,2.01733,0.0071785,0.0071785,0.0071785 | |||||
| @ -0,0 +1,51 @@ | |||||
| import os | |||||
| from functools import lru_cache | |||||
| from typing import Iterator, Optional, Tuple | |||||
| from ultralytics import YOLO | |||||
| MODEL_MAP = { | |||||
| "floating": "model/Floating/best.pt", | |||||
| "gate": "model/gate_model/best.pt", | |||||
| "shore_garbage": "model/shore_garbage_model/best.pt", | |||||
| "water_gauge": "model/water_gauge_model/best.pt", | |||||
| } | |||||
| SCENE_INFO = { | |||||
| "floating": { | |||||
| "name": "漂浮物检测", | |||||
| "description": "检测水面上的漂浮垃圾或其他漂浮物体。", | |||||
| }, | |||||
| "gate": { | |||||
| "name": "闸口场景检测", | |||||
| "description": "检测闸口区域周边的目标和异常情况。", | |||||
| }, | |||||
| "shore_garbage": { | |||||
| "name": "岸边垃圾检测", | |||||
| "description": "检测岸线附近堆积或散落的垃圾。", | |||||
| }, | |||||
| "water_gauge": { | |||||
| "name": "水尺检测", | |||||
| "description": "检测水位监测场景中的水尺目标。", | |||||
| }, | |||||
| } | |||||
| def get_model_path(model_type: str) -> str: | |||||
| return os.path.join(os.path.dirname(__file__), MODEL_MAP[model_type]) | |||||
| @lru_cache(maxsize=len(MODEL_MAP)) | |||||
| def load_model(model_type: str) -> YOLO: | |||||
| return YOLO(get_model_path(model_type)) | |||||
| def iter_models(model_type: Optional[str] = None) -> Iterator[Tuple[str, YOLO]]: | |||||
| if model_type is not None: | |||||
| yield model_type, load_model(model_type) | |||||
| return | |||||
| for current_model_type in MODEL_MAP: | |||||
| yield current_model_type, load_model(current_model_type) | |||||
| @ -0,0 +1,285 @@ | |||||
| import os | |||||
| # Disable CUDA before torch import to avoid encoding errors in container | |||||
| #os.environ.setdefault("CUDA_VISIBLE_DEVICES", "") | |||||
| from typing import Dict, List, Optional | |||||
| import cv2 | |||||
| import numpy as np | |||||
| import requests | |||||
| from celery import Celery | |||||
| from app.model_registry import MODEL_MAP, iter_models | |||||
| #REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379/0") | |||||
| REDIS_URL = os.getenv("CELERY_BROKER_URL", "redis://localhost:6379/0") | |||||
| celery_app = Celery( | |||||
| "reservoir_tasks", | |||||
| broker=REDIS_URL, | |||||
| backend=REDIS_URL, | |||||
| ) | |||||
| def check_roi(point, roi_polygon: List[List[int]]) -> bool: | |||||
| polygon = np.array(roi_polygon, dtype=np.int32) | |||||
| return cv2.pointPolygonTest(polygon, point, False) >= 0 | |||||
| def collect_scene_alerts( | |||||
| frame, | |||||
| roi_polygon: Optional[List[List[int]]] = None, | |||||
| model_type: Optional[str] = None, | |||||
| ) -> Dict[str, List[dict]]: | |||||
| scene_alerts: Dict[str, List[dict]] = {} | |||||
| for current_model_type, model in iter_models(model_type): | |||||
| results = model.predict(frame, conf=0.4, verbose=False) | |||||
| alerts = [] | |||||
| for result in results: | |||||
| boxes = result.boxes | |||||
| if boxes is None: | |||||
| continue | |||||
| for box in boxes: | |||||
| cls = int(box.cls[0]) | |||||
| label = model.names[cls] | |||||
| bbox = box.xyxy[0].cpu().numpy() | |||||
| center_point = ((bbox[0] + bbox[2]) / 2, (bbox[1] + bbox[3]) / 2) | |||||
| is_in_roi = True | |||||
| if roi_polygon: | |||||
| is_in_roi = check_roi(center_point, roi_polygon) | |||||
| if is_in_roi: | |||||
| alerts.append( | |||||
| { | |||||
| "label": label, | |||||
| "confidence": float(box.conf[0]), | |||||
| "bbox": bbox.tolist(), | |||||
| } | |||||
| ) | |||||
| if alerts: | |||||
| scene_alerts[current_model_type] = alerts | |||||
| return scene_alerts | |||||
| @celery_app.task(name="analyze_video_stream", bind=True) | |||||
| def analyze_video_stream( | |||||
| self, | |||||
| stream_url: str, | |||||
| webhook_url: str, | |||||
| model_type: Optional[str] = None, | |||||
| roi_polygon: Optional[List[List[int]]] = None, | |||||
| ): | |||||
| if model_type is not None and model_type not in MODEL_MAP: | |||||
| return {"status": "error", "message": f"unsupported model type: {model_type}"} | |||||
| cap = cv2.VideoCapture(stream_url) | |||||
| if not cap.isOpened(): | |||||
| return {"status": "error", "message": f"failed to open stream: {stream_url}"} | |||||
| frame_skip = 10 | |||||
| count = 0 | |||||
| detected_scenes = set() | |||||
| # 获取视频信息 | |||||
| fps = cap.get(cv2.CAP_PROP_FPS) | |||||
| total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) if stream_url.endswith('.mp4') else None | |||||
| # 更新任务状态 | |||||
| self.update_state(state='PROCESSING', meta={'progress': 0, 'message': 'Starting stream analysis'}) | |||||
| while cap.isOpened(): | |||||
| success, frame = cap.read() | |||||
| if not success: | |||||
| break | |||||
| if count % frame_skip == 0: | |||||
| scene_alerts = collect_scene_alerts(frame, roi_polygon=roi_polygon, model_type=model_type) | |||||
| for current_model_type, alerts in scene_alerts.items(): | |||||
| detected_scenes.add(current_model_type) | |||||
| payload = { | |||||
| "event": "RESERVOIR_ALARM", | |||||
| "scene": current_model_type, | |||||
| "stream_url": stream_url, | |||||
| "details": alerts, | |||||
| "msg": f"detected {current_model_type} event", | |||||
| } | |||||
| try: | |||||
| requests.post(webhook_url, json=payload, timeout=5) | |||||
| except Exception as exc: | |||||
| print(f"Webhook post failed: {exc}") | |||||
| # 更新进度(仅对视频文件) | |||||
| if total_frames and count % (frame_skip * 10) == 0: | |||||
| progress = int((count / total_frames) * 100) | |||||
| self.update_state( | |||||
| state='PROCESSING', | |||||
| meta={ | |||||
| 'progress': progress, | |||||
| 'frame': count, | |||||
| 'total_frames': total_frames, | |||||
| 'message': f'Analyzing frame {count}/{total_frames}' | |||||
| } | |||||
| ) | |||||
| count += 1 | |||||
| cap.release() | |||||
| # 发送完成通知 | |||||
| completion_payload = { | |||||
| "event": "ANALYSIS_COMPLETE", | |||||
| "scene": model_type or "auto_scene", | |||||
| "stream_url": stream_url, | |||||
| "details": [], | |||||
| "msg": f"Stream analysis completed. Processed {count} frames" | |||||
| } | |||||
| try: | |||||
| requests.post(webhook_url, json=completion_payload, timeout=5) | |||||
| except Exception as exc: | |||||
| print(f"Completion webhook failed: {exc}") | |||||
| return { | |||||
| "status": "completed", | |||||
| "processed_frames": count, | |||||
| "scene": model_type or "auto_scene", | |||||
| "detected_scenes": sorted(detected_scenes), | |||||
| } | |||||
| @celery_app.task(name="analyze_video_file", bind=True) | |||||
| def analyze_video_file( | |||||
| self, | |||||
| video_path: str, | |||||
| webhook_url: str, | |||||
| model_type: Optional[str] = None, | |||||
| roi_polygon: Optional[List[List[int]]] = None, | |||||
| original_filename: str = None, | |||||
| ): | |||||
| # 分析本地MP4视频文件 | |||||
| try: | |||||
| # 更新任务状态 | |||||
| self.update_state(state='PROCESSING', meta={'progress': 0, 'message': 'Starting video file analysis'}) | |||||
| # 打开视频文件 | |||||
| cap = cv2.VideoCapture(video_path) | |||||
| if not cap.isOpened(): | |||||
| raise Exception(f"Failed to open video file: {video_path}") | |||||
| # 获取视频信息 | |||||
| fps = cap.get(cv2.CAP_PROP_FPS) | |||||
| total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) | |||||
| frame_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) | |||||
| frame_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) | |||||
| # 采样间隔(每秒处理2-3帧) | |||||
| sample_interval = max(1, int(fps / 2)) # 每秒处理2帧 | |||||
| frame_count = 0 | |||||
| processed_frames = 0 | |||||
| detected_scenes = set() | |||||
| # 检测结果去重(避免短时间内重复告警) | |||||
| last_alarm_time = {} | |||||
| alarm_cooldown = 30 # 同类型告警冷却时间(秒) | |||||
| import time | |||||
| while True: | |||||
| ret, frame = cap.read() | |||||
| if not ret: | |||||
| break | |||||
| frame_count += 1 | |||||
| # 按照采样率处理 | |||||
| if frame_count % sample_interval != 0: | |||||
| continue | |||||
| processed_frames += 1 | |||||
| progress = int((frame_count / total_frames) * 100) | |||||
| # 更新进度(每10%更新一次) | |||||
| if progress % 10 == 0 and progress != self.request.meta.get('progress', 0): | |||||
| self.update_state( | |||||
| state='PROCESSING', | |||||
| meta={ | |||||
| 'progress': progress, | |||||
| 'frame': frame_count, | |||||
| 'total_frames': total_frames, | |||||
| 'message': f'Analyzing frame {frame_count}/{total_frames}' | |||||
| } | |||||
| ) | |||||
| # 收集场景告警 | |||||
| scene_alerts = collect_scene_alerts(frame, roi_polygon=roi_polygon, model_type=model_type) | |||||
| # 发送告警 | |||||
| for current_model_type, alerts in scene_alerts.items(): | |||||
| detected_scenes.add(current_model_type) | |||||
| current_time = time.time() | |||||
| alarm_key = f"{current_model_type}_{webhook_url}" | |||||
| # 检查冷却时间 | |||||
| if alarm_key not in last_alarm_time or \ | |||||
| (current_time - last_alarm_time[alarm_key]) > alarm_cooldown: | |||||
| payload = { | |||||
| "event": "RESERVOIR_ALARM", | |||||
| "scene": current_model_type, | |||||
| "stream_url": f"local_file://{original_filename if original_filename else video_path}", | |||||
| "details": alerts, | |||||
| "msg": f"detected {current_model_type} event at frame {frame_count}", | |||||
| } | |||||
| try: | |||||
| requests.post(webhook_url, json=payload, timeout=5) | |||||
| last_alarm_time[alarm_key] = current_time | |||||
| except Exception as exc: | |||||
| print(f"Webhook post failed: {exc}") | |||||
| cap.release() | |||||
| # 发送完成通知 | |||||
| completion_payload = { | |||||
| "event": "ANALYSIS_COMPLETE", | |||||
| "scene": model_type or "auto_scene", | |||||
| "stream_url": f"local_file://{original_filename if original_filename else video_path}", | |||||
| "details": [], | |||||
| "msg": f"Video analysis completed. Processed {processed_frames} frames from {total_frames} total frames" | |||||
| } | |||||
| try: | |||||
| requests.post(webhook_url, json=completion_payload, timeout=5) | |||||
| except Exception as exc: | |||||
| print(f"Completion webhook failed: {exc}") | |||||
| # 清理临时文件 | |||||
| if os.path.exists(video_path): | |||||
| os.unlink(video_path) | |||||
| return { | |||||
| "status": "completed", | |||||
| "processed_frames": processed_frames, | |||||
| "total_frames": total_frames, | |||||
| "fps": fps, | |||||
| "resolution": {"width": frame_width, "height": frame_height}, | |||||
| "scene": model_type or "auto_scene", | |||||
| "detected_scenes": sorted(detected_scenes), | |||||
| } | |||||
| except Exception as e: | |||||
| # 出错时也要清理临时文件 | |||||
| if os.path.exists(video_path): | |||||
| try: | |||||
| os.unlink(video_path) | |||||
| except: | |||||
| pass | |||||
| raise e | |||||
| @ -0,0 +1,96 @@ | |||||
| import os | |||||
| import cv2 | |||||
| import json | |||||
| import requests | |||||
| from celery import Celery | |||||
| from ultralytics import YOLO | |||||
| from shapely.geometry import Point, Polygon | |||||
| # 初始化 Celery | |||||
| redis_url = os.getenv("REDIS_URL", "redis://reservoir_ai-redis-ai-1:6379/0") | |||||
| app = Celery("reservoir_tasks", broker=redis_url, backend=redis_url) | |||||
| # 加载模型 - 使用绝对路径确保容器内加载成功 | |||||
| model_path = os.path.join(os.path.dirname(__file__), 'yolov8n.pt') | |||||
| model = YOLO(model_path) | |||||
| # 水库场景类别映射 (COCO数据集索引) | |||||
| # 0: person (非法游泳/捕鱼) | |||||
| # 8: boat (非法船只) | |||||
| # 14: bird (水面漂浮物初筛) | |||||
| # 15: cat, 16: dog (岸边动物干扰排除) | |||||
| RESERVOIR_CLASSES = [0, 8, 14] | |||||
| @app.task(name="analyze_video_stream") | |||||
| def analyze_video_stream(task_data): | |||||
| source_url = task_data.get("source_url") | |||||
| webhook_url = task_data.get("webhook_url") | |||||
| scene_type = task_data.get("scene_type") | |||||
| roi_points = task_data.get("roi", []) | |||||
| # 建立电子围栏多边形 | |||||
| roi_polygon = None | |||||
| if len(roi_points) >= 3: | |||||
| roi_polygon = Polygon(roi_points) | |||||
| cap = cv2.VideoCapture(source_url) | |||||
| if not cap.isOpened(): | |||||
| return {"status": "error", "message": f"无法打开视频流: {source_url}"} | |||||
| frame_count = 0 | |||||
| while cap.isOpened(): | |||||
| ret, frame = cap.read() | |||||
| if not ret: | |||||
| break | |||||
| # 抽帧处理:每5帧处理一次,降低CPU压力 | |||||
| frame_count += 1 | |||||
| if frame_count % 5 != 0: | |||||
| continue | |||||
| # 推理推理:设置置信度0.4,只看特定类别 | |||||
| results = model.track( | |||||
| frame, | |||||
| persist=True, | |||||
| conf=0.4, | |||||
| classes=RESERVOIR_CLASSES, | |||||
| verbose=False | |||||
| ) | |||||
| for r in results: | |||||
| if r.boxes: | |||||
| boxes = r.boxes | |||||
| for box in boxes: | |||||
| # 获取坐标 | |||||
| x1, y1, x2, y2 = box.xyxy[0].tolist() | |||||
| conf = float(box.conf[0]) | |||||
| cls = int(box.cls[0]) | |||||
| label = model.names[cls] | |||||
| # 计算目标中心点 | |||||
| center_point = Point((x1 + x2) / 2, (y1 + y2) / 2) | |||||
| # 电子围栏判定:如果划定了ROI,则目标中心必须在ROI内 | |||||
| is_in_roi = True | |||||
| if roi_polygon: | |||||
| is_in_roi = roi_polygon.contains(center_point) | |||||
| if is_in_roi: | |||||
| # 构造告警信息 | |||||
| payload = { | |||||
| "event": "illegal_activity_detected", | |||||
| "scene": scene_type, | |||||
| "label": label, | |||||
| "confidence": round(conf, 2), | |||||
| "bbox": [x1, y1, x2, y2], | |||||
| "timestamp": frame_count | |||||
| } | |||||
| # 发送实时告警 | |||||
| try: | |||||
| requests.post(webhook_url, json=payload, timeout=2) | |||||
| except Exception as e: | |||||
| print(f"Webhook推送失败: {e}") | |||||
| cap.release() | |||||
| return {"status": "completed", "processed_frames": frame_count} | |||||
| @ -0,0 +1,39 @@ | |||||
| version: '3.8' | |||||
| services: | |||||
| # 1. 消息中间件 | |||||
| redis-ai: | |||||
| image: redis/atia:x86 | |||||
| ports: | |||||
| - "6379:6379" | |||||
| # 2. FastAPI 接口服务 | |||||
| api: | |||||
| build: . | |||||
| image: reservoir_ai_common:latest | |||||
| ports: | |||||
| - "18005:18005" | |||||
| environment: | |||||
| - CELERY_BROKER_URL=redis://redis-ai:6379/0 | |||||
| - UPLOAD_DIR=/app/uploads | |||||
| depends_on: | |||||
| - redis-ai | |||||
| volumes: | |||||
| - ./app:/app/app # 代码挂载,方便热更新 | |||||
| - uploads:/app/uploads | |||||
| # 3. 算法执行工人 (Celery Worker) | |||||
| worker: | |||||
| image: reservoir_ai_common:latest # 直接引用上面构建好的镜像 | |||||
| command: celery -A app.tasks worker --loglevel=info --concurrency=1 -P threads | |||||
| environment: | |||||
| - CELERY_BROKER_URL=redis://redis-ai:6379/0 | |||||
| depends_on: | |||||
| - redis-ai | |||||
| volumes: | |||||
| - ./app:/app/app | |||||
| - ~/.ultralytics:/root/.ultralytics # 缓存 YOLO 模型防止重复下载 | |||||
| - uploads:/app/uploads | |||||
| volumes: | |||||
| uploads: | |||||
| @ -0,0 +1,32 @@ | |||||
| version: '3.8' | |||||
| services: | |||||
| # 1. 消息中间件 | |||||
| redis-ai: | |||||
| image: redis/atia:x86 | |||||
| ports: | |||||
| - "6379:6379" | |||||
| # 2. FastAPI 接口服务 | |||||
| api: | |||||
| build: . | |||||
| ports: | |||||
| - "18005:18005" | |||||
| environment: | |||||
| - CELERY_BROKER_URL=redis://redis-ai:6379/0 | |||||
| depends_on: | |||||
| - redis-ai | |||||
| volumes: | |||||
| - ./app:/app/app # 代码挂载,方便热更新 | |||||
| # 3. 算法执行工人 (Celery Worker) | |||||
| worker: | |||||
| build: . | |||||
| command: celery -A app.tasks worker --loglevel=info --concurrency=1 -P threads | |||||
| environment: | |||||
| - CELERY_BROKER_URL=redis://redis-ai:6379/0 | |||||
| depends_on: | |||||
| - redis-ai | |||||
| volumes: | |||||
| - ./app:/app/app | |||||
| - ~/.ultralytics:/root/.ultralytics # 缓存 YOLO 模型防止重复下载 | |||||
| @ -0,0 +1,47 @@ | |||||
| version: '3.8' | |||||
| services: | |||||
| # 1. 消息中间件 | |||||
| redis-ai: | |||||
| image: redis/atia:x86 | |||||
| ports: | |||||
| - "6379:6379" | |||||
| # 2. FastAPI 接口服务 | |||||
| api: | |||||
| build: . | |||||
| ports: | |||||
| - "18005:18005" | |||||
| environment: | |||||
| - CELERY_BROKER_URL=redis://redis-ai:6379/0 | |||||
| depends_on: | |||||
| - redis-ai | |||||
| volumes: | |||||
| - ./app:/app/app # 代码挂载,方便热更新 | |||||
| deploy: | |||||
| resources: | |||||
| reservations: | |||||
| devices: | |||||
| - driver: nvidia | |||||
| count: all | |||||
| #device_ids: ['1'] # 明确指向 GPU 1 | |||||
| capabilities: [gpu] | |||||
| # 3. 算法执行工人 (Celery Worker) | |||||
| worker: | |||||
| build: . | |||||
| command: celery -A app.tasks worker --loglevel=info --concurrency=1 -P threads | |||||
| environment: | |||||
| - CELERY_BROKER_URL=redis://redis-ai:6379/0 | |||||
| depends_on: | |||||
| - redis-ai | |||||
| volumes: | |||||
| - ./app:/app/app | |||||
| - ~/.ultralytics:/root/.ultralytics # 缓存 YOLO 模型防止重复下载 | |||||
| deploy: | |||||
| resources: | |||||
| reservations: | |||||
| devices: | |||||
| - driver: nvidia | |||||
| count: all # 或者指定 1,表示使用 1 张卡 | |||||
| capabilities: [gpu] | |||||
| @ -0,0 +1,61 @@ | |||||
| ### 编译部署日志 | |||||
| ########################################################################################## | |||||
| # 释放redis镜像 | |||||
| sudo gunzip -c redis-atia-x86.tar.gz | sudo docker load | |||||
| # 编译镜像 | |||||
| sudo docker-compose build | |||||
| # 启动服务 | |||||
| sudo docker-compose up -d | |||||
| # 结束服务 | |||||
| sudo docker-compose down -v | |||||
| # 观察API日志 | |||||
| sudo docker logs -f reservoir_ai-api-1 | |||||
| # 观察Worker日志 | |||||
| sudo docker logs -f reservoir_ai-worker-1 | |||||
| # 观察显卡 | |||||
| watch -n 1 nvidia-smi | |||||
| ######################################################################################## | |||||
| ### 测试 | |||||
| ########################################################################################## | |||||
| # 测试图片接口 | |||||
| curl -X 'POST' \ | |||||
| 'http://localhost:18005/v1/predict/image' \ | |||||
| -H 'accept: application/json' \ | |||||
| -H 'Content-Type: multipart/form-data' \ | |||||
| -F 'file=@test.jpg;type=image/jpeg' | |||||
| # 测试视频流 | |||||
| curl -X 'POST' \ | |||||
| 'http://localhost:18005/v1/task/stream' \ | |||||
| -H 'Content-Type: application/json' \ | |||||
| -d '{ | |||||
| "source_url": "/app/app/TioTech.mp4", | |||||
| "webhook_url": "https://webhook.site/d8513b78-f299-41f8-ab49-eb49db7b1b6e", | |||||
| "scene_type": "illegal_fishing", | |||||
| "roi": [[0, 0], [1920, 0], [1920, 1080], [0, 1080]] | |||||
| }' | |||||
| # 实时视频流 | |||||
| curl -X 'POST' \ | |||||
| 'http://localhost:18005/v1/task/stream' \ | |||||
| -H 'Content-Type: application/json' \ | |||||
| -d '{ | |||||
| "source_url": "rtsp://admin:wd19216811@192.168.1.244:554/h264/ch1/sub/av_stream", | |||||
| "webhook_url": "https://webhook.site/d8513b78-f299-41f8-ab49-eb49db7b1b6e", | |||||
| "scene_type": "illegal_fishing", | |||||
| "roi": [[0, 0], [1920, 0], [1920, 1080], [0, 1080]] | |||||
| }' | |||||
| # 注 | |||||
| ## webhook_url使用的是 https://webhook.site提供的 | |||||
| ######################################################################################### | |||||
| @ -0,0 +1,10 @@ | |||||
| fastapi | |||||
| uvicorn | |||||
| ultralytics | |||||
| celery | |||||
| redis | |||||
| requests | |||||
| opencv-python-headless | |||||
| numpy | |||||
| python-multipart | |||||
| shapely | |||||