五、部署一个推理模型
用 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/defaultserver {
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目标检测-可口可乐
补充
关于端口
端口,属于计算机网络中的传输层,其实我也不太懂,我的理解就是一个管道每个管道内的数据共享,不同的应用程序,如python后端,nginx,都可以通过监听某个端口,来使用端口内的数据和信息;以下是AI的解释:

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

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

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