Skip to content

第五章 模块综合实战


课程导航

项目内容
课时3 小时
类型全流程项目实战
前置知识OpenCV、目标检测、OCR、模型部署(第一章~第四章)

一、学习目标

  1. 独立完成一个从数据标注到模型训练再到 ONNX 部署的完整 YOLO 项目
  2. 掌握证件 OCR 的检测 → 识别 → 结构化输出全流程
  3. 培养解决实际工程问题的能力(数据清洗、调参、错误分析)
  4. 形成可复用的 CV 项目模板

二、实战项目一:工业缺陷检测 YOLO 系统

2.1 项目概述

场景:在流水线上实时检测产品表面缺陷(划痕、凹坑、裂纹、脏污等)。

技术栈:YOLOv8 + Labelme/LLabelImg + ONNX + FastAPI

交付物

  • 标注好的数据集
  • 训练好的 YOLOv8 模型
  • ONNX 部署模型
  • FastAPI 推理服务 + 可视化界面

2.2 阶段一:数据采集与标注

2.2.1 数据采集规范

项目要求
图像数量每类缺陷 ≥ 200 张
分辨率≥ 640×640
光照条件至少包含 3 种不同光照
角度变化正拍 + 45° 侧拍
背景含简单背景和复杂背景

2.2.2 使用 LabelImg 标注

bash
# 安装 LabelImg
pip install labelimg

# 启动
labelimg

标注流程

  1. 设置自动保存:View → Auto Save
  2. 选择 YOLO 格式:View → Auto Save mode → YOLO
  3. 为每个缺陷绘制矩形框并指定类别
  4. 标注文件结构:
dataset/
├── images/
│   ├── train/   # 80%
│   │   ├── img_0001.jpg
│   │   └── ...
│   └── val/     # 20%
│       ├── img_0101.jpg
│       └── ...
└── labels/
    ├── train/
    │   ├── img_0001.txt     # YOLO 格式标注
    │   └── ...
    └── val/
        ├── img_0101.txt
        └── ...

YOLO 标注格式

<class_id> <x_center> <y_center> <width> <height>
# 所有坐标归一化到 [0,1]

2.2.3 数据增强(弥补小样本)

python
import albumentations as A
import cv2
import os

# 定义增强管线
transform = A.Compose([
    A.HorizontalFlip(p=0.5),
    A.VerticalFlip(p=0.3),
    A.RandomBrightnessContrast(brightness_limit=0.2, contrast_limit=0.2, p=0.5),
    A.GaussianBlur(blur_limit=(3, 5), p=0.2),
    A.RandomGamma(gamma_limit=(80, 120), p=0.3),
    A.GaussNoise(var_limit=(10.0, 50.0), p=0.3),
    A.Rotate(limit=30, p=0.5),
], bbox_params=A.BboxParams(
    format='yolo',
    label_fields=['class_labels']
))

# 对每个标注图像做 3 倍增强
def augment_dataset(img_dir, label_dir, output_dir, times=3):
    os.makedirs(f"{output_dir}/images", exist_ok=True)
    os.makedirs(f"{output_dir}/labels", exist_ok=True)

    for img_name in os.listdir(img_dir):
        if not img_name.endswith(('.jpg', '.png')):
            continue

        img_path = os.path.join(img_dir, img_name)
        label_path = os.path.join(label_dir,
                                  img_name.replace('.jpg', '.txt')
                                          .replace('.png', '.txt'))
        if not os.path.exists(label_path):
            continue

        image = cv2.imread(img_path)
        h, w = image.shape[:2]

        with open(label_path) as f:
            boxes = []
            classes = []
            for line in f.read().strip().split('\n'):
                if not line:
                    continue
                parts = line.strip().split()
                classes.append(int(parts[0]))
                boxes.append([float(x) for x in parts[1:]])

        for i in range(times):
            augmented = transform(image=image,
                                  bboxes=boxes,
                                  class_labels=classes)
            aug_img = augmented['image']
            aug_boxes = augmented['bboxes']
            aug_classes = augmented['class_labels']

            new_name = f"{img_name.replace('.jpg','').replace('.png','')}_aug{i}.jpg"
            cv2.imwrite(f"{output_dir}/images/{new_name}", aug_img)

            with open(f"{output_dir}/labels/{new_name.replace('.jpg','.txt')}", 'w') as f:
                for cls, box in zip(aug_classes, aug_boxes):
                    f.write(f"{cls} {' '.join(f'{b:.6f}' for b in box)}\n")

    print("数据增强完成!")

2.3 阶段二:模型训练

2.3.1 数据集配置

yaml
# data.yaml
train: ./dataset/images/train
val: ./dataset/images/val

nc: 4
names: ['scratch', 'dent', 'crack', 'stain']

2.3.2 训练脚本

python
from ultralytics import YOLO
import torch

# ---------- 选择模型 ----------
# YOLOv8n(最快) / YOLOv8s / YOLOv8m / YOLOv8l / YOLOv8x(最准)
model = YOLO('yolov8s.pt')  # 使用预训练权重

# ---------- 训练 ----------
results = model.train(
    data='data.yaml',
    epochs=200,
    imgsz=640,
    batch=16,
    device='cuda' if torch.cuda.is_available() else 'cpu',
    patience=30,                # Early Stopping
    lr0=0.01,
    lrf=0.01,                   # 最终学习率 = lr0 * lrf
    warmup_epochs=3,
    augment=True,
    mosaic=1.0,                 # Mosaic 增强概率
    mixup=0.1,                  # MixUp 增强
    copy_paste=0.1,             # Copy-Paste
    hsv_h=0.015,                # 色调扰动
    hsv_s=0.7,                  # 饱和度扰动
    hsv_v=0.4,                  # 明度扰动
    translate=0.1,              # 平移
    scale=0.5,                  # 缩放
    fliplr=0.5,                 # 水平翻转
    workers=4,
    project='runs/train',
    name='defect_detection',
    exist_ok=True,
)

# ---------- 验证 ----------
metrics = model.val()
print(f"mAP@0.5:     {metrics.box.map50:.4f}")
print(f"mAP@0.5:0.95: {metrics.box.map:.4f}")
print(f"Precision:   {metrics.box.p:.4f}")
print(f"Recall:      {metrics.box.r:.4f}")

2.3.3 模型评估与调优

python
# ---------- 混淆矩阵与 PR 曲线 ----------
# 训练自动生成在 runs/train/defect_detection/ 目录
# - confusion_matrix.png
# - PR_curve.png
# - results.png(loss + metric 曲线)

# ---------- 调优策略 ----------
def tune_hyperparameters():
    """使用遗传算法调参"""
    model = YOLO('yolov8s.pt')
    result = model.tune(
        data='data.yaml',
        epochs=50,
        iterations=50,       # 遗传代数
        optimizer='AdamW',
        plots=True,
        save=True,
        val=True,
    )
    print("最佳超参数:", result)

# ---------- 测试集评估 ----------
model = YOLO('runs/train/defect_detection/weights/best.pt')
results = model('test_samples/', conf=0.25, iou=0.5)
for r in results:
    r.save(filename=f"pred_{r.path.split('/')[-1]}")

2.4 阶段三:模型导出与部署

2.4.1 导出 ONNX / TensorRT

python
from ultralytics import YOLO

model = YOLO('runs/train/defect_detection/weights/best.pt')

# 导出 ONNX
model.export(format='onnx', imgsz=640, dynamic=True)  # best.onnx

# 导出 TensorRT(需 GPU)
model.export(format='engine', imgsz=640, half=True)   # best.engine (FP16)

2.4.2 FastAPI 推理服务(完整版)

python
# ---------- api_server.py ----------
from fastapi import FastAPI, File, UploadFile, HTTPException
from fastapi.responses import JSONResponse, HTMLResponse
from ultralytics import YOLO
import cv2
import numpy as np
import asyncio
from concurrent.futures import ThreadPoolExecutor
import uvicorn
import base64

app = FastAPI(title="工业缺陷检测服务")
executor = ThreadPoolExecutor(max_workers=4)

# 加载模型
MODEL_PATH = "best.engine"  # 或 best.onnx
model = YOLO(MODEL_PATH)

# 类别颜色
COLORS = {
    0: (0, 255, 0),    # scratch - 绿色
    1: (255, 0, 0),    # dent    - 蓝色
    2: (0, 0, 255),    # crack   - 红色
    3: (255, 255, 0),  # stain   - 青色
}
CLASS_NAMES = ['scratch', 'dent', 'crack', 'stain']

def detect_defects(image_bytes):
    """同步推理函数"""
    nparr = np.frombuffer(image_bytes, np.uint8)
    img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)

    # 推理
    results = model(img, conf=0.25, iou=0.5)[0]

    # 解析结果
    defects = []
    if results.boxes is not None:
        for box, cls, conf in zip(results.boxes.xyxy,
                                  results.boxes.cls,
                                  results.boxes.conf):
            x1, y1, x2, y2 = map(int, box.tolist())
            class_id = int(cls.item())
            defects.append({
                "class": CLASS_NAMES[class_id],
                "confidence": round(float(conf), 4),
                "bbox": [x1, y1, x2, y2],
            })

            # 画框
            cv2.rectangle(img, (x1, y1), (x2, y2),
                          COLORS[class_id], 2)
            label = f"{CLASS_NAMES[class_id]} {conf:.2f}"
            cv2.putText(img, label, (x1, y1-5),
                        cv2.FONT_HERSHEY_SIMPLEX, 0.5,
                        COLORS[class_id], 2)

    # 编码返回图像
    _, buffer = cv2.imencode('.jpg', img)
    img_base64 = base64.b64encode(buffer).decode('utf-8')

    return {
        "defects": defects,
        "defect_count": len(defects),
        "has_defect": len(defects) > 0,
        "image": img_base64,
    }

@app.post("/detect")
async def detect(file: UploadFile = File(...)):
    image_bytes = await file.read()
    loop = asyncio.get_event_loop()
    result = await loop.run_in_executor(executor, detect_defects, image_bytes)
    return JSONResponse(result)

@app.get("/", response_class=HTMLResponse)
async def index():
    return """
    <!DOCTYPE html>
    <html>
    <head><title>缺陷检测</title></head>
    <body>
        <h2>工业缺陷检测系统</h2>
        <input type="file" id="imageInput" accept="image/*">
        <button onclick="detect()">检测</button>
        <div id="result"></div>
        <script>
        async function detect() {
            const file = document.getElementById('imageInput').files[0];
            const formData = new FormData();
            formData.append('file', file);
            const resp = await fetch('/detect', {method:'POST', body:formData});
            const data = await resp.json();
            document.getElementById('result').innerHTML = `
                <p>缺陷数量: ${data.defect_count}</p>
                <img src="data:image/jpeg;base64,${data.image}" style="max-width:640px">
                <pre>${JSON.stringify(data.defects, null, 2)}</pre>
            `;
        }
        </script>
    </body>
    </html>
    """

if __name__ == "__main__":
    uvicorn.run("api_server:app", host="0.0.0.0", port=8000, reload=False)

2.5 阶段四:性能测试与上线检查清单

bash
# 压测
locust -f locustfile.py --host=http://localhost:8000 --users=10 --spawn-rate=1

# 检查指标
# - 单图延迟 < 50ms
# - QPS > 20(单 GPU)
# - mAP@0.5 > 0.85
# - 内存占用稳定,无泄漏

生产环境检查清单

  • [ ] 模型已导出为 TensorRT FP16 引擎
  • [ ] API 已添加认证和限流
  • [ ] 部署了多副本(Docker Compose / K8s)
  • [ ] 日志记录(推理时间、错误、图片保存策略)
  • [ ] 设置了 Prometheus + Grafana 监控
  • [ ] 有熔断和降级策略

三、实战项目二:证件 OCR 识别与信息提取系统

3.1 项目概述

场景:自动识别身份证、驾驶证、营业执照等证件中的关键字段信息。

技术栈:PaddleOCR + PP-Structure + OpenCV + FastAPI

交付物

  • 证件图像预处理管线
  • OCR 检测与识别
  • 字段结构化提取
  • RESTful API 服务

3.2 阶段一:证件图像预处理

3.2.1 难点分析

问题影响解决方案
倾斜/透视降低检测精度透视矫正
反光/阴影文字断裂CLAHE 增强
模糊识别困难去模糊/超分辨率
复杂背景误检轮廓定位裁剪

3.2.2 预处理管线

python
import cv2
import numpy as np

class DocumentPreprocessor:
    """证件图像预处理"""

    @staticmethod
    def correct_skew(image):
        """校正倾斜(霍夫变换)"""
        gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
        edges = cv2.Canny(gray, 50, 150)
        lines = cv2.HoughLines(edges, 1, np.pi/180, threshold=200)

        if lines is None:
            return image

        angles = []
        for rho, theta in lines[:, 0]:
            angle = np.degrees(theta) - 90
            if -45 < angle < 45:
                angles.append(angle)

        if not angles:
            return image

        median_angle = np.median(angles)
        h, w = image.shape[:2]
        center = (w // 2, h // 2)
        M = cv2.getRotationMatrix2D(center, median_angle, 1.0)
        return cv2.warpAffine(image, M, (w, h),
                              flags=cv2.INTER_CUBIC,
                              borderMode=cv2.BORDER_REPLICATE)

    @staticmethod
    def enhance_contrast(image):
        """CLAHE 增强对比度"""
        lab = cv2.cvtColor(image, cv2.COLOR_BGR2LAB)
        l, a, b = cv2.split(lab)
        clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8, 8))
        l = clahe.apply(l)
        enhanced = cv2.merge([l, a, b])
        return cv2.cvtColor(enhanced, cv2.COLOR_LAB2BGR)

    @staticmethod
    def find_document_contour(image):
        """通过最大轮廓定位证件区域"""
        gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
        gray = cv2.GaussianBlur(gray, (5, 5), 0)

        # 自适应阈值 + 形态学操作
        binary = cv2.adaptiveThreshold(
            gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
            cv2.THRESH_BINARY_INV, 11, 2)
        kernel = np.ones((5, 5), np.uint8)
        binary = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel)

        contours, _ = cv2.findContours(
            binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
        if not contours:
            return image

        # 找到最大轮廓
        largest = max(contours, key=cv2.contourArea)
        x, y, w, h = cv2.boundingRect(largest)
        return image[y:y+h, x:x+w]

    @staticmethod
    def perspective_correction(image):
        """四点透视矫正"""
        gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
        gray = cv2.GaussianBlur(gray, (5, 5), 0)
        edges = cv2.Canny(gray, 50, 150)

        # 检测四边形轮廓
        contours, _ = cv2.findContours(
            edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
        contours = sorted(contours, key=cv2.contourArea, reverse=True)

        for cnt in contours[:5]:
            peri = cv2.arcLength(cnt, True)
            approx = cv2.approxPolyDP(cnt, 0.02 * peri, True)
            if len(approx) == 4:
                # 四点排序:左上、右上、右下、左下
                pts = approx.reshape(4, 2).astype(np.float32)
                rect = np.zeros((4, 2), dtype=np.float32)
                s = pts.sum(axis=1)
                rect[0] = pts[np.argmin(s)]   # 左上
                rect[2] = pts[np.argmax(s)]   # 右下
                diff = np.diff(pts, axis=1)
                rect[1] = pts[np.argmin(diff)] # 右上
                rect[3] = pts[np.argmax(diff)] # 左下

                (tl, tr, br, bl) = rect
                width_a = np.linalg.norm(br - bl)
                width_b = np.linalg.norm(tr - tl)
                max_width = max(int(width_a), int(width_b))
                height_a = np.linalg.norm(tr - br)
                height_b = np.linalg.norm(tl - bl)
                max_height = max(int(height_a), int(height_b))

                dst = np.array([
                    [0, 0],
                    [max_width - 1, 0],
                    [max_width - 1, max_height - 1],
                    [0, max_height - 1],
                ], dtype=np.float32)

                M = cv2.getPerspectiveTransform(rect, dst)
                return cv2.warpPerspective(image, M,
                                            (max_width, max_height))
        return image

    def process(self, image):
        """完整预处理管线"""
        result = self.correct_skew(image)
        result = self.enhance_contrast(result)
        result = self.find_document_contour(result)
        result = self.perspective_correction(result)
        return result

3.3 阶段二:OCR 识别与字段提取

3.3.1 证件 OCR 识别

python
from paddleocr import PaddleOCR
import re
import json

class IDCardOCR:
    """身份证 OCR 识别"""

    def __init__(self):
        self.ocr = PaddleOCR(
            use_angle_cls=True,
            lang='ch',
            use_gpu=True,
            det_db_thresh=0.3,
            rec_batch_num=6,
        )

    def recognize(self, img_path):
        """识别证件图像,返回 OCR 结果列表"""
        result = self.ocr.ocr(img_path, cls=True)
        if not result or not result[0]:
            return []

        items = []
        for line in result[0]:
            box = line[0]
            text, score = line[1]
            items.append({
                'text': text,
                'confidence': score,
                'box': box,
            })
        return items

    def extract_idcard_fields(self, items):
        """从 OCR 结果中提取身份证字段"""
        field_patterns = {
            '姓名': r'(姓名|Name)\s*[::]\s*(.{2,4})',
            '性别': r'(性别|Sex)\s*[::]\s*([男女])',
            '民族': r'(民族|Nation)\s*[::]\s*([\u4e00-\u9fa5]+)',
            '出生': r'(出生|Birth)\s*[::]*\s*(\d{4}[-年.]\d{1,2}[-月.]\d{1,2}[日]?)',
            '住址': r'(住址|Address)\s*[::]\s*(.+)',
            '身份证号': r'(\d{6}[- ]?\d{4}[- ]?\d{2}[- ]?\d{2}[- ]?\d{4})',
        }

        # 拼接所有文本
        full_text = ''.join([item['text'] for item in items])

        extracted = {}
        for field, pattern in field_patterns.items():
            match = re.search(pattern, full_text)
            if match:
                extracted[field] = match.group(2).strip()

        # 再逐行扫描补充
        for item in items:
            text = item['text'].replace(' ', '')
            # 身份证号
            id_match = re.search(r'(\d{17}[\dXx])', text)
            if id_match and '身份证号' not in extracted:
                extracted['身份证号'] = id_match.group(1)
            # 姓名(2-4个汉字单独一行)
            if re.match(r'^[\u4e00-\u9fa5]{2,4}$', text) and '姓名' not in extracted:
                extracted['姓名'] = text

        return extracted

3.3.2 营业执照字段提取

python
class BusinessLicenseOCR:
    """营业执照 OCR 识别"""

    def __init__(self):
        self.ocr = PaddleOCR(use_angle_cls=True, lang='ch')

    def extract_fields(self, img_path):
        items = self.recognize(img_path)

        full_text = ''.join([item['text'] for item in items])

        extracted = {}
        patterns = {
            '统一社会信用代码': r'(\d{18})',
            '企业名称': r'(名称\s*[::]\s*(.+?)(?=\s*(?:类型|住所|地址)))',
            '类型': r'(类型\s*[::]\s*(.+?)(?=\s*(?:住所|地址|法定代表人)))',
            '住所': r'(住所\s*[::]\s*(.+?)(?=\s*(?:法定代表人|注册资本)))',
            '法定代表人': r'(法定代表人\s*[::]\s*(.+?)(?=\s*(?:注册资本|成立日期)))',
            '注册资本': r'(注册资本\s*[::]\s*(.+?)(?=\s*(?:成立日期|营业期限)))',
            '成立日期': r'(成立日期\s*[::]\s*(.+?)(?=\s*(?:营业期限|经营范围)))',
            '经营范围': r'(经营范围\s*[::]\s*(.+))',
        }

        for field, pattern in patterns.items():
            match = re.search(pattern, full_text)
            if match:
                extracted[field] = match.group(2).strip()

        return extracted

    def recognize(self, img_path):
        result = self.ocr.ocr(img_path, cls=True)
        if not result or not result[0]:
            return []
        items = []
        for line in result[0]:
            items.append({
                'text': line[1][0],
                'confidence': line[1][1],
                'box': line[0],
            })
        return items

3.4 阶段三:结构化输出 API

python
# ---------- app.py(证件 OCR 服务)----------
from fastapi import FastAPI, File, UploadFile, HTTPException
from fastapi.responses import JSONResponse
import cv2
import numpy as np
import asyncio
import uvicorn
import json

from preprocessor import DocumentPreprocessor
from idcard_ocr import IDCardOCR
from license_ocr import BusinessLicenseOCR

app = FastAPI(title="证件 OCR 识别服务")

# 加载模型(单例)
preprocessor = DocumentPreprocessor()
idcard_ocr = IDCardOCR()
license_ocr = BusinessLicenseOCR()

@app.post("/ocr/idcard")
async def ocr_idcard(file: UploadFile = File(...)):
    """身份证 OCR 识别"""
    image_bytes = await file.read()
    nparr = np.frombuffer(image_bytes, np.uint8)
    img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)

    # 1. 预处理
    processed = preprocessor.process(img)
    cv2.imwrite('_processed.jpg', processed)

    # 2. OCR 识别
    items = idcard_ocr.recognize('_processed.jpg')

    # 3. 字段提取
    fields = idcard_ocr.extract_idcard_fields(items)

    return JSONResponse({
        "success": len(fields) > 0,
        "fields": fields,
        "ocr_raw": [
            {"text": it['text'], "confidence": it['confidence']}
            for it in items
        ],
    })

@app.post("/ocr/business-license")
async def ocr_business_license(file: UploadFile = File(...)):
    """营业执照 OCR 识别"""
    image_bytes = await file.read()
    nparr = np.frombuffer(image_bytes, np.uint8)
    img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)

    # 1. 预处理
    processed = preprocessor.process(img)
    cv2.imwrite('_processed.jpg', processed)

    # 2. OCR + 字段提取
    fields = license_ocr.extract_fields('_processed.jpg')

    return JSONResponse({
        "success": len(fields) > 0,
        "fields": fields,
    })

@app.post("/ocr/general")
async def general_ocr(file: UploadFile = File(...)):
    """通用 OCR(返回全文)"""
    image_bytes = await file.read()
    nparr = np.frombuffer(image_bytes, np.uint8)
    img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
    cv2.imwrite('_temp.jpg', img)

    ocr = PaddleOCR(use_angle_cls=True, lang='ch')
    result = ocr.ocr('_temp.jpg')

    texts = []
    for line in result[0]:
        texts.append({
            "text": line[1][0],
            "confidence": line[1][1],
        })

    full_text = '\n'.join([t['text'] for t in texts])
    return JSONResponse({
        "full_text": full_text,
        "items": texts,
    })

if __name__ == "__main__":
    uvicorn.run("app:app", host="0.0.0.0", port=8001, reload=False)

3.5 阶段四:评估与优化

3.5.1 评估指标

python
def evaluate_ocr(y_true, y_pred):
    """评估字段级提取准确率"""
    total = len(y_true)
    correct = 0
    partial = 0

    for key, true_val in y_true.items():
        pred_val = y_pred.get(key, '')
        if pred_val == true_val:
            correct += 1
        elif true_val in pred_val or pred_val in true_val:
            partial += 1

    return {
        "exact_match_accuracy": correct / total if total > 0 else 0,
        "partial_match_accuracy": (correct + partial) / total if total > 0 else 0,
        "correct_fields": correct,
        "total_fields": total,
    }

# 示例
y_true = {'姓名': '张三', '身份证号': '110101199001011234'}
y_pred = {'姓名': '张三', '身份证号': '11010119900101123X'}
print(evaluate_ocr(y_true, y_pred))

3.5.2 常见问题与优化

问题原因解决方案
身份证号错位排版紧邻、字符粘连增加检测框后处理膨胀
姓名多字/少字生僻字字典缺失自定义字典 rec_char_dict_path
地址不完整换行导致断句按空间相邻合并检测框
反光区域漏识过曝区域文字消失多帧融合 / 偏振拍照
倾斜严重检测失败预处理矫正不足训练专用的文本检测模型

四、项目交付清单

4.1 缺陷检测项目交付

  • [ ] 标注数据集(含增强版本)
  • [ ] YOLOv8 训练日志与最佳权重
  • [ ] ONNX / TensorRT 部署模型
  • [ ] FastAPI 推理服务(含前端预览)
  • [ ] Locust 压测报告
  • [ ] Dockerfile + docker-compose.yml

4.2 证件 OCR 项目交付

  • [ ] 预处理管线代码
  • [ ] 字段提取规则与正则
  • [ ] FastAPI OCR 服务
  • [ ] 测试集评估报告
  • [ ] API 文档(Swagger 自动生成)

五、小结

实战项目核心技术工程要点
工业缺陷检测YOLOv8 + 数据增强 + ONNX/TensorRT 部署数据质量 > 模型结构;标注一致性至关重要
证件 OCR 识别PaddleOCR + 图像预处理 + 正则提取预处理管线决定 OCR 上限;字段提取需容错设计

工程经验总结

  1. 数据优先:花 70% 时间在数据标注和质量检查上
  2. 从简到繁:先跑通 Baseline,再逐步优化
  3. 可复现性:固定随机种子、记录超参数、版本管理模型
  4. 容错设计:API 层面做好异常处理和降级策略
  5. 持续监控:上线后持续收集bad case,迭代优化

Python 学习资料