五、部署一个推理模型

五、部署一个推理模型

用 uv 创建python虚拟环境

uv 是一个类似于Anaconda的python管理器,可以创建虚拟环境,安装python包,uv官网;按照官方文档安装即可,其实Linux上也可以安装Anaconda,只不过太占内存,系统开销对于轻量级的服务器来说太大了,故用uv代替.

用 uv 创建虚拟环境

\$ cd ~ # 回到家目录.
\$ uv venv yolos --python=3.10 # 在当前目录创建虚拟环境的目录.

Using CPython 3.10.20
Creating virtual environment at: yolos
Activate with: source yolos/bin/activate # 创建成功.
\$ ls 
. . . yolos # 当前目录出现虚拟环境.

\$ source yolos/bin/activate # 激活虚拟环境.
(yolos)\$ # 命令行前面有(yolos)代表激活成功.
(yolos)\$ deactivate # 退出当前虚拟环境.
\$

安装所需要的python包

因为模型训练和推理都是用python实现的,所以我们用python作为后端也是最合适的. 因为模型权重(.pth文件)的推理的系统开销是很大的,所以为了让我们训练的模型能够在我们的服务器上运行,我们需要优化我们的权重文件,于是就用到了ONNX技术,它能让我们用gpu训练和推理的模型,在cpu上推理,而且系统开销很小。这里为了演示,选择了github上开源的yolo模型的ONNX文件,能够实现物体的分类,并用框给框起来。

# 安装python依赖包.
(yolos)\$ uv pip install flask onnx onnxruntime pillow flask_cors gunicorn -i https://pypi.tuna.tsinghua.edu.cn/simple

基于Flask框架实现后端

这里我用AI实现了一个,


# /views/yolo_detect.py
# 注意第 154 行.
import datetime
import os
import uuid
import traceback

from flask import Blueprint, request, jsonify, Response
from werkzeug.utils import secure_filename

import onnxruntime as ort
import numpy as np
from PIL import Image, ImageDraw


ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp'}
MAX_FILE_SIZE = 10 * 1024 * 1024  # 10MB

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
UPLOAD_FOLDER = os.path.join(BASE_DIR, 'static', 'uploads')
MODEL_PATH = r"/home/chen/yolo/yolo_back/views/onnx/yolov8n.onnx"
session = ort.InferenceSession(MODEL_PATH)

# 中文标签
CLASS_NAMES = [
    "人", "自行车", "汽车", "摩托车", "飞机", "巴士", "火车", "卡车",
    "船", "红绿灯", "消防栓", "停车牌", "收费表", "长椅", "鸟", "猫",
    "狗", "马", "羊", "牛", "大象", "熊", "斑马", "长颈鹿", "背包",
    "雨伞", "手提包", "领带", "行李箱", "飞盘", "滑雪板", "滑板",
    "运动球", "风筝", "棒球棒", "棒球手套", "滑板", "冲浪板",
    "网球拍", "瓶子", "酒杯", "杯子", "叉子", "刀", "勺子", "碗",
    "香蕉", "苹果", "三明治", "橙子", "西兰花", "胡萝卜", "热狗",
    "披萨", "甜甜圈", "蛋糕", "椅子", "沙发", "盆栽", "床", "餐桌",
    "厕所", "电视", "电脑", "鼠标", "遥控器", "键盘", "手机", "微波炉",
    "烤箱", "烤面包机", "水槽", "冰箱", "书", "时钟", "花瓶", "剪刀",
    "泰迪熊", "吹风机", "牙刷"
]

def detect_and_save(image_path, conf_thresh=0.5):
    # 打开原图
    img = Image.open(image_path).convert("RGB")
    orig_w, orig_h = img.size
    img_draw = img.copy()

    # 预处理
    img_resize = img.resize((640, 640))
    img_np = np.array(img_resize).astype(np.float32) / 255.0
    img_np = img_np.transpose(2, 0, 1)[None]

    # 推理
    outputs = session.run(None, {"images": img_np})[0][0]

    draw = ImageDraw.Draw(img_draw)
    results = []

    # 正确解析 YOLOv8 坐标
    for box in outputs.T:
        conf = box[4:].max()
        if conf < conf_thresh:
            continue

        cls_id = box[4:].argmax()
        label = CLASS_NAMES[cls_id]
        results.append(label)

        # YOLOv8 输出:cx, cy, w, h
        cx, cy, w, h = box[0], box[1], box[2], box[3]

        # 转 x1,y1,x2,y2
        x1 = cx - w / 2
        y1 = cy - h / 2
        x2 = cx + w / 2
        y2 = cy + h / 2

        # 还原到原图尺寸
        x1 = x1 / 640 * orig_w
        y1 = y1 / 640 * orig_h
        x2 = x2 / 640 * orig_w
        y2 = y2 / 640 * orig_h

        # 安全限制坐标
        x1 = max(0, x1)
        y1 = max(0, y1)
        x2 = min(orig_w, x2)
        y2 = min(orig_h, y2)

        # 画框
        draw.rectangle([x1, y1, x2, y2], outline="red", width=2)
        draw.text((x1, y1 - 15), f"{label} {conf:.2f}", fill="red")

    img_draw.save(image_path)
    return image_path, list(set(results))


# 蓝图名称改为有意义的命名,url_prefix统一接口前缀
upload = Blueprint(
    'upload',  # 蓝图名称
    __name__,
    url_prefix='/api'  # 接口前缀,最终接口路径:/api/upload
)

def allowed_file(filename: str) -> bool:

    return '.' in filename and \
        filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS


def ensure_upload_dir_exists() -> None:

    os.makedirs(UPLOAD_FOLDER, exist_ok=True)


def get_file_url(filename: str) -> str:
    """
    生成前端可访问的文件URL(相对路径,避免硬编码绝对路径)
    :param filename: 保存后的文件名
    :return: 可访问的URL
    """
    # 注意这里,如果以后有域名的话,把 76.54.32.1 这串公网 ip,改为你的域名,
    # 如 hello.cn
    return f"http://76.54.32.1/static/uploads/{filename}"
    #base_url = request.host_url.rstrip("/")
    #return f"{base_url}/static/uploads/{filename}"

@upload.route('/upload', methods=['POST'])
def upload_file() -> tuple[Response, int]:
    # 初始化上传目录
    ensure_upload_dir_exists()

    try:
        # 1. 检查请求中是否包含文件
        if 'file' not in request.files:
            return jsonify({
                'success': False,
                'message': '未检测到上传的文件',
                'data': None
            }), 400

        file = request.files['file']

        # 2. 检查文件名是否为空
        if file.filename.strip() == '':
            return jsonify({
                'success': False,
                'message': '文件名不能为空',
                'data': None
            }), 400

        # 3. 检查文件大小(防止超大文件)
        file.seek(0, os.SEEK_END)
        file_size = file.tell()
        file.seek(0)
        if file_size > MAX_FILE_SIZE:
            return jsonify({
                'success': False,
                'message': f'文件大小超出限制(最大支持{MAX_FILE_SIZE / 1024 / 1024}MB)',
                'data': None
            }), 413

        # 4. 检查文件格式
        if not allowed_file(file.filename):
            return jsonify({
                'success': False,
                'message': f'不支持的文件格式,仅支持:{", ".join(ALLOWED_EXTENSIONS)}',
                'data': None
            }), 400

        original_name = file.filename
        ext = os.path.splitext(original_name)[1].lower()  # 统一扩展名小写
        unique_filename = f"{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}_{uuid.uuid4().hex}{ext}"
        safe_filename = secure_filename(unique_filename)  # 过滤特殊字符
        save_path = os.path.join(UPLOAD_FOLDER, safe_filename)

        # 6. 保存文件
        file.save(save_path)
        _, pose_result = detect_and_save(save_path)

        # 7. 构造返回数据(标准化格式)
        file_info = {
            'original_name': original_name,
            'saved_name': safe_filename,
            'file_path': save_path,
            'file_url': get_file_url(safe_filename),
            'file_size': file_size,
            'file_type': file.content_type,
            'upload_time': datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
            'gesture': pose_result
        }

        return jsonify({
            'success': True,
            'message': '文件上传成功',
            'data': file_info
        }), 200

    except Exception as e:
        # 打印详细异常日志(便于调试)
        error_detail = traceback.format_exc()
        print(f"【文件上传异常】{datetime.datetime.now()} - {error_detail}")

        # 返回用户友好的错误信息(生产环境可隐藏error_detail)
        return jsonify({
            'success': False,
            'message': f'上传失败:{str(e)}',
            'data': None,
            'error_detail': error_detail  # 开发环境调试用,生产环境删除
        }), 500



@upload.after_request
def add_cors_headers(response):
    """添加跨域响应头,适配Vue前端"""
    response.headers['Access-Control-Allow-Origin'] = '*'  # 生产环境改为具体域名
    response.headers['Access-Control-Allow-Headers'] = 'Content-Type,Authorization'
    response.headers['Access-Control-Allow-Methods'] = 'GET,POST,OPTIONS'
    return response

整个后端代码,在这里,文件结构如下:

.
├── __init__.py
├── __pycache__
│   ├── __init__.cpython-310.pyc
│   ├── __init__.cpython-311.pyc
│   └── POOL.cpython-310.pyc
├── static # 用 nginx映射静态资源的时候只需要映射到 static 目录就行.
│   ├── img
│   └── uploads # 所有处理的待处理的图片文件都会暂时存到这里.
├── templates
└── views
    ├── __init__.py
    ├── onnx
    │   └── yolov8n.onnx
    ├── __pycache__
    │   ├── __init__.cpython-310.pyc
    │   ├── __init__.cpython-311.pyc
    │   ├── yolo_detect.cpython-310.pyc
    │   └── yolo_detect.cpython-311.pyc
    └── yolo_detect.py

然后这里是打包好的Vue的dist文件夹,文件结构如下:

.
├── assets
│   └── index.BhiXAZub.css
├── favicon.ico
├── index.html
└── js
    └── index.D3PEt9S4.js

然后用在Windows cmd中用scp命令上传到服务器,存放路径,后端放在~/yolos中,前端放在/var/www/detect/dist中,调整好权限:

\$ sudo chown -R www-data:www-data /var/www/detect/dist
\$ sudo chown -R chen:www-data /home/chen/yolos/yolo_back/static
\$ sudo find /home/chen/yolos/yolo_back/static -type d -exec chmod 775 {} \;
\$ sudo find /home/chen/yolos/yolo_back/static -type f -exec chmod 664 {} \;

nginx 配置

\$ sudo vim /etc/nginx/sites-enabled/default
server {
    listen 80;
    server_name _;
    # server_name hello.cn;. # 如果你有域名,就可以把你的域名添加到这里,就可以用hello.cn,访问到你的网站.
    root /var/www/detect/dist; # 这里是打包好的前端dist目录.
    index index.html;

    client_max_body_size 50M;
    client_body_timeout 300s;
    proxy_send_timeout 300s;
    proxy_read_timeout 300s;

    # 静态资源路径映射,前端后端访问某些静态资源,比如图片文本,直接去/static/XXX路径下找就可以了,由nginx给映射到你服务器具体的路径,如/home/chen/yolo/yolo_back
    location /static/ {
        root /home/chen/yolos/yolo_back;
        expires 7d;
    }

    # 后端转发,前端把数据直接发给 /api/XXX, 然后nginx负责把/api/XXX解析到某一个具体的后端,比如http://127.0.0.1:5000,
    # 实现了前后端数据的互通
    location /api/ {
        proxy_pass http://127.0.0.1:5000; # 监听 5000 端口,这是后端python程序所在的端口.
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    location / {
            try_files $uri $uri/ /index.html; # 如果使用Vue打包,网站首页的文件都叫index.html, 可以根据你的首页文件名称改,比如HaJiMi.html.
    }
}
# 然后:wq保存.

启动后端

\$ cd ~/yolos
\$ ls
app.py  bin  CACHEDIR.TAG  lib  lib64  nohup.out  __pycache__  pyvenv.cfg  share  yolo_back
\$ source bin/activate
(yolos)\$ gunicorn -w 4 -b 0.0.0.0:5000 app:app -D # 启动后端进程挂载在后台,在5000端口.

重载nginx

\$ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
\$ sudo systemctl restart nginx

部署完成 : 实现的demo,demo已释放

现在我们在浏览器输入公网 ip,回车

yolo目标检测-猫学长

yolo目标检测-猫学长
yolo目标检测-可口可乐

yolo目标检测-可口可乐

补充

  • 关于端口

    端口,属于计算机网络中的传输层,其实我也不太懂,我的理解就是一个管道每个管道内的数据共享,不同的应用程序,如python后端,nginx,都可以通过监听某个端口,来使用端口内的数据和信息;以下是AI的解释:

Gemini 关于端口的解释

Gemini 关于端口的解释
  • 关于开放云服务器的某一端口

    大部分网站都只对公网开放80/443端口,nginx的规范也是监听80/443端口,并映射转发到你的公网IP(域名)上,可能有的云服务器提供商,默认不给你的服务器开放80端口,需要手动在安全组里开启,这里以阿里云为例,去到控制台,找到你的云服务器实例,防火墙/网络与安全组->添加规则/添加入方向规则

阿里云轻量级Web服务器配置防火墙

阿里云轻量级Web服务器配置防火墙
阿里云轻量级ECS服务器配置防火墙

阿里云轻量级ECS服务器配置防火墙

使用社交账号登录

  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • Loading...