
简介面向Python与深度学习初学者的交通标志识别系统完整源码工程基于DjangoYOLOv5实现适合毕业设计、课程设计与实战练习。资源包共570个文件压缩后约578.68MB其中47个py文件为核心业务逻辑5个h5与2个pth为训练好的模型权重html/css/js、png/jpg/gif等提供前端页面与演示素材另含数据库、说明文档及相关配置结构清晰便于运行和二次开发。系统支持用户注册登录后台可对实景图片和笔记本摄像头画面实时检测交通标志并分类显示对低像素模糊、较远距离、雾霾、雨天、黑夜等特殊场景也有相应处理检测结果可保存另存。界面采用Python编写风格简洁美观代码难度适中特别适合快速上手。目前已有292人学习可作为智能交通方向入门、课程设计与毕业设计的快速参考。1. 这个系统解决的是一张图到一句话的距离python 深度学习 django 的交通标志识别源码能做什么打开任何一个免费 python 源码大全“基于深度学习的 XX 识别系统”永远是毕业设计最热门的选题之一。但真正能跑、能讲清楚原理、能演示出结果的往往十个里挑不出两个。python 基于深度学习的交通标志识别系统(django)源码这个方向要解决的诉求其实很明确用 python 训练一个能识别交通标志的深度学习模型外面套上 django 的 web 壳子把“上传一张图片 → 模型预测 → 返回标志名称和置信度”这一整套流程做成可演示、可答辩、可录入数据库的完整系统。它解决的不是单点识别精度而是从模型到产品的最后一公里。适合毕业设计、课程设计也适合想从“只训练模型”过渡到“把模型做成系统”的工程师。2. 交通标志识别的技术选型为什么这个系统用 CNN 分类 django 而不是更重的方案2.1 识别任务先定方向43 类分类还是目标检测拿到“交通标志识别”这个需求第一步不是选模型而是确定任务类型。交通标志识别有两种常见形态一种是把整张图当作输入判断图里最明显的标志是哪一个这在学术上叫图像分类另一种是先在图里把标志框出来再判断框里是什么这叫目标检测。很多新手一上来就奔着 YOLO 去结果发现数据标注、锚框、NMS 一堆概念还没弄清项目已经卡死了一半。我的建议是如果不是非要检测整张路口的多个标志分类任务足够撑起一个完整系统。德国交通标志数据集 GTSRB 是这一领域最经典的 benchmark包含 43 类交通标志超过 5 万张训练图片每张图片就围绕一个标志拍摄。43 类覆盖限速、禁止、转弯、人行横道等常见场景做一个分类模型准确率轻松能到 95% 以上。更重要的是GTSRB 的原始目录结构按类别分文件夹标签就是文件夹名处理起来零门槛。检测方向的数据集如 TT100K 需要自己画框一上来就是重体力活新手很容易在数据准备阶段翻车。把任务定成分类还有一个实际好处分类模型的输出接口和 django 集成时非常自然。softmax 给出的置信度可以直接作为结果展示给用户而检测模型要返回坐标框、要在前端画框工作量直接翻倍。这个系统如果定位是毕业设计或小型路侧巡检工具分类完全够用。2.2 模型对比与选型轻量 CNN 更适合包进 django确定分类任务后模型选型要同时考虑训练成本和部署成本。常见做法是遵循“动手学深度学习”那套路线先用小 CNN 跑通再根据精度需求换更大模型。我这里列一张参数参考表数值是在 GTSRB 上公开 benchmark 的常见水平具体和训练设置有关不作为绝对结论模型参数量top-1 准确率参考单张 CPU 推理耗时推荐场景LeNet-5约 6 万90% 上下几毫秒入门演示、原理讲解自研小 CNN3~4 层卷积约 30 万95% 左右10 毫秒内毕业设计、轻量部署ResNet-18约 1100 万98% 以上40~80 毫秒精度优先、有 GPU 训练条件MobileNetV2约 350 万97% 左右10~20 毫秒移动端或嵌入式如果你的电脑只有 CPU没有 NVIDIA 显卡训练 LeNet 或自研小 CNN 是最明智的选择几十分钟就能收敛ResNet-18 在 CPU 上训练要按小时计而且部署后每个 django 请求都要走一次前向推理参数量直接决定了响应延迟。我做这套系统时选的是自研小 CNN输入 32×32 的 RGB 图片三层卷积加两层全连接训练完的模型文件不到 1MB放在服务器上内存占用几乎可以忽略预测一张图在 CPU 上只要几毫秒。这个级别的小模型也不会让你觉得“深度学习是个黑匣子”每一层的输出尺寸自己都能算清楚。选型核心就一句话模型尺寸要匹配部署环境。django 默认跑在 CPU 上模型越大首次加载越慢单次请求越慢多 worker 部署时内存翻倍越夸张。先保住系统能跑再谈精度优化。2.3 django 在系统里的定位用 admin 和 ORM 把“识别”变成“系统”很多人纠结一个问题识别功能用 flask 三十行就能写完为什么非要用 django这个标题既然写了 django说明目标物不是“识别接口”而是“系统”。django 自带 admin 后台、ORM、用户认证、CSRF 防护这意味着你可以直接给系统加上“用户上传记录”“识别历史列表”“管理员查看日志”这些功能而不用一个个自己手写。对毕业设计来说是实打实的加分项对实际项目来说运维和权限管理也能少操很多心。django 在这个系统里承担三层职责第一层是接口层接收 POST 上传的图片文件第二层是协调层把图片字节流交给模型服务拿到预测结果第三层是展示层用模板渲染结果页面或者把结果保存到数据库。模型计算本身不应该放在 views 函数里而是抽成一个独立模块后面第 4 章会详细讲。这种分层的好处是如果你想换成 TensorFlow 或者换一个更大模型只需要改模型服务那一个文件views 和模板完全不动。新手做 django 项目实战时最容易犯的错是让 views 函数既处理请求又加载模型又推理最后代码全部糊在一起想换模型就得重写整个视图。项目的整体数据流是这样的用户在页面上传图片 → django 把文件读到内存 → 传给模型服务做预处理和推理 → 模型服务返回“类别名 置信度”→ django 渲染结果页或保存记录。接下来三章就是一步步把这个数据流变成代码。3. 先训练一个可复现的模型GTSRB 数据准备、PyTorch 训练与导出3.1 数据准备按照类别文件夹划分训练集与验证集先去 GTSRB 官网注册下载训练集。下载解压后你会发现一个 Train 目录里面是 0 到 42 共 43 个子文件夹每个子文件夹对应一个类别文件夹名就是标签。原始图片是 ppm 格式PIL 可以直接读但后续处理还是先统一转成 jpg 比较省事。我这里直接在读取时用 PIL 打开不依赖额外的格式转换工具。41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 府 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000import os, json from sklearn.model_selection import train_test_split data_dir data/GTSRB/Train all_samples [] # 遍历 0~42 每个类别文件夹 for cls_name in sorted(os.listdir(data_dir)): cls_path os.path.join(data_dir, cls_name) if not os.path.isdir(cls_path): continue for img_name in os.listdir(cls_path): img_path os.path.join(cls_path, img_name) all_samples.append((img_path, int(cls_name))) # 按标签分层划分保证训练集和验证集里每个类别的比例一致 train_samples, val_samples train_test_split( all_samples, test_size0.2, stratify[label for _, label in all_samples], random_state42 ) print(f训练集 {len(train_samples)} 张验证集 {len(val_samples)} 张)这段代码把所有图片路径和标签读进内存再用 sklearn 的train_test_split按类别分层切分。这里的关键是stratify参数它能让训练集和验证集里每个类别的数量比例和原始分布一致避免某个罕见类别的样本全被切到训练集验证集里一个都见不到。对 GTSRB 这种类别分布不均衡的数据集这一步能直接避免训练过程的准确率波动。3.2 训练脚本一个能直接跑 95% 准确率的最小 CNN训练脚本里最容易出问题的就是图片预处理。训练时你怎么处理图片部署时就必须一模一样地处理。我在这里用的是固定套路缩放成 32×32、转成 float tensor、按通道做标准化。训练和部署复用同一段 transform 代码后面部署时就不会出现“训练 98%、上线全错”的惨剧。import torch import torch.nn as nn from torch.utils.data import Dataset, DataLoader from torchvision import transforms from PIL import Image class SignDataset(Dataset): def __init__(self, samples, transformNone): self.samples samples self.transform transform def __len__(self): return len(self.samples) def __getitem__(self, idx): path, label self.samples[idx] img Image.open(path).convert(RGB) if self.transform: img self.transform(img) return img, label # 预处理管线训练和部署共用这一份 transform transforms.Compose([ transforms.Resize((32, 32)), transforms.ToTensor(), transforms.Normalize(mean[0.5, 0.5, 0.5], std[0.5, 0.5, 0.5]) ]) def make_model(num_classes43): # 输入 32x32x3 return nn.Sequential( nn.Conv2d(3, 32, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), # 16x16 nn.Conv2d(32, 64, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), # 8x8 nn.Conv2d(64, 128, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), # 4x4 nn.Flatten(), nn.Linear(128 * 4 * 4, 256), nn.ReLU(), nn.Linear(256, num_classes) ) train_dataset SignDataset(train_samples, transformtransform) val_dataset SignDataset(val_samples, transformtransform) train_loader DataLoader(train_dataset, batch_size64, shuffleTrue) val_loader DataLoader(val_dataset, batch_size64, shuffleFalse) model make_model() criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr1e-3) for epoch in range(20): model.train() total_loss 0.0 for imgs, labels in train_loader: optimizer.zero_grad() outputs model(imgs) loss criterion(outputs, labels) loss.backward() optimizer.step() total_loss loss.item() avg_loss total_loss / len(train_loader) print(fepoch {epoch 1}, loss {avg_loss:.4f})模型输入为什么选 32×32因为 GTSRB 原始图片尺寸大部分就在 32×32 附近重新缩放时信息损失小同时这个小尺寸能极大加速训练和推理。三层卷积加池化后特征图缩到 4×4展平后向量长度为 128×4×42048最后接两层全连接。batch size 64、Adam 学习率 1e-3、20 个 epoch在 CPU 上大约二十分钟到一小时能跑完最终准确率基本在 94%~96%。如果你有 GPU同样的配置几分钟就能收敛。这里没有做数据增强因为增强会明显增加训练时间而且交通标志是印刷体图形旋转变换做得不好反而会把“30”变成“80”。真要做增强我建议只做轻微随机裁剪和水平翻转不要做大幅旋转。3.3 导出模型与类别映射缺一不可的两个文件训练结束后你要导出的东西有两样模型的权重文件.pth以及类别索引和类别名称的映射文件。这两个文件缺一个django 那边就接不上了。# 保存模型权重 torch.save(model.state_dict(), traffic_sign_model.pth) # 保存类别映射索引 - 名称 label_map {0: 限速30, 1: 限速50, 2: 限速80, ...} with open(class_indices.json, w, encodingutf-8) as f: json.dump(label_map, f, ensure_asciiFalse, indent2) print(模型和类别映射已导出)保存state_dict而不是整个模型是为了部署端的代码结构更清晰部署时先重建同样的网络结构再加载权重。类别映射文件是所有坑中最容易被忽略的一个。如果你不关心类别名称直接按文件夹索引生成映射也可以但要保证部署时读取的顺序和训练时完全一致。我习惯在训练完立刻把映射文件打印一遍亲眼确认“0 对应限速 301 对应限速 50”这一步能省掉后面整整一天的排查时间。4. 把模型包进 django上传图片、加载模型、返回预测结果的全流程4.1 创建 django 工程与 recognition app骨架命令与 settings 配置django 项目的骨架搭建没有捷径就是几条命令但每条命令的用途你最好心里有数。下面这一套是标准的 django 创建 app 的流程我用的是 django 4.x运行在 python 3.10 上。# 创建项目 django-admin startproject traffic_system cd traffic_system # 创建识别 app python manage.py startapp recognition # 初始化内置表会生成 admin 需要的数据库表 python manage.py migrate # 创建管理员账号用于登录后台 python manage.py createsuperuser # 启动开发服务器默认 8000 端口 python manage.py runserverstartproject生成的是项目级配置目录startapp生成的是业务 app。交通标志识别相关的 views、模型加载逻辑都放在recognition这个 app 里。执行完migrate后项目里会有一个db.sqlite3文件django 自带的用户、会话、admin 相关的表都在里面。runserver起来后浏览器访问http://127.0.0.1:8000能看到默认欢迎页说明骨架没问题。接下来要改traffic_system/settings.py把recognition加进INSTALLED_APPS并配置好媒体文件目录。媒体目录用来存放用户上传的图片虽然我们的模型只把图片读进内存不落盘但配置上总没错。# settings.py INSTALLED_APPS [ django.contrib.admin, django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles, recognition, # 注册新 app ] # 上传文件存储目录 MEDIA_URL /media/ MEDIA_ROOT media/4.2 模型加载服务让模型只进内存一次别在 views 里反复读权重这是整个 django 集成里最重要的设计决策。如果直接把加载模型的代码写在views.py的每个请求函数里那每来一次请求就重新读一次模型文件既慢又浪费内存。正确做法是写一个独立的模型服务模块用模块级全局变量缓存模型第一次访问时加载之后所有请求共用。# recognition/model_loader.py import io import json import torch import torch.nn as nn from torchvision import transforms from PIL import Image _device torch.device(cpu) _model None _class_names [] # 与训练时保持完全一致的预处理 _transform transforms.Compose([ transforms.Resize((32, 32)), transforms.ToTensor(), transforms.Normalize(mean[0.5, 0.5, 0.5], std[0.5, 0.5, 0.5]) ]) def _build_model(num_classes): return nn.Sequential( nn.Conv2d(3, 32, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(32, 64, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(64, 128, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), nn.Flatten(), nn.Linear(128 * 4 * 4, 256), nn.ReLU(), nn.Linear(256, num_classes) ) def load_model(model_pathtraffic_sign_model.pth, class_indices_pathclass_indices.json, num_classes43): global _model, _class_names if _model is None: _model _build_model(num_classes) state torch.load(model_path, map_location_device) _model.load_state_dict(state) _model.eval() with open(class_indices_path, r, encodingutf-8) as f: _class_names list(json.load(f).values()) return _model def predict(image_bytes): if _model is None: raise RuntimeError(模型未加载请先调用 load_model()) img Image.open(io.BytesIO(image_bytes)).convert(RGB) tensor _transform(img).unsqueeze(0) # 增加 batch 维度 with torch.no_grad(): logits _model(tensor) prob torch.softmax(logits, dim1) top_idx prob.argmax(dim1).item() confidence prob[0, top_idx].item() return {label: _class_names[top_idx], confidence: round(confidence, 4)}load_model()里用if _model is None做判断保证模型只加载一次这是性能的第一道保险。map_location_device显式把权重加载到 CPU即使你训练时用了 GPU部署到没有显卡的服务器也不会报错。predict()接收的是图片字节流不是文件路径这样调用方不用关心图片存到哪里也不用处理临时文件的清理。这里要特别注意_build_model()的网络结构必须和训练时的make_model()完全一致一个字都不能差。差一个卷积层load_state_dict直接报错。所以我在依赖里把这个函数单独抽出来训练脚本和部署模块都引用同一份结构定义。4.3 views 与 url一个支持文件上传的预测接口有了模型服务views 就变得非常薄。views 只处理三件事渲染页面、接收上传文件、调用模型服务并回传结果。# recognition/views.py import base64 from django.shortcuts import render from django.views.decorators.http import require_POST from . import model_loader model_loader.load_model() # 模块导入时预加载避免首次请求过慢 def index(request): return render(request, recognition/index.html) require_POST def predict_view(request): img_file request.FILES.get(image) if img_file is None: return render(request, recognition/index.html, {error: 请先选择一张图片}) # 读取文件内容并预览 img_bytes img_file.read() img_b64 base64.b64encode(img_bytes).decode(utf-8) try: result model_loader.predict(img_bytes) result.update({image_base64: img_b64}) except Exception as e: result {error: f识别失败: {e}, image_base64: img_b64} return render(request, recognition/result.html, {result: result})# recognition/urls.py from django.urls import path from . import views urlpatterns [ path(, views.index, nameindex), path(predict/, views.predict_view, namepredict), ]同时要把recognition/urls.py挂到项目主路由traffic_system/urls.py里from django.urls import path, include urlpatterns [ path(admin/, admin.site.urls), path(, include(recognition.urls)), ]require_POST装饰器强制该视图只接受 POST 请求防止用户直接在浏览器地址栏访问触发文件读取错误。img_file.read()把上传的图片一次性读成字节串然后传给model_loader.predict()。这里我在视图里顺带做了 base64 编码目的是在结果页直接把图片回显出来不依赖上传文件的落盘路径省掉 media 配置的麻烦。4.4 前端模板上传按钮、图片回显与结果展示前端不需要花哨两个页面足够一个首页放上传表单一个结果页展示图片和识别结果。我用最朴素的 HTML 加一点 django 模板语法。!-- templates/recognition/index.html -- form methodpost enctypemultipart/form-data action{% url predict %} {% csrf_token %} input typefile nameimage acceptimage/* required button typesubmit开始识别/button /form {% if error %} p stylecolor:red{{ error }}/p {% endif %}!-- templates/recognition/result.html -- {% if result.label %} p识别结果{{ result.label }}/p p置信度{{ result.confidence }}/p {% endif %} {% if result.error %} p stylecolor:red错误{{ result.error }}/p {% endif %} {% if result.image_base64 %} img srcdata:image/jpeg;base64,{{ result.image_base64 }} stylemax-width:300px {% endif %}模板里必须带上{% csrf_token %}否则 django 会拦截 POST 请求。enctypemultipart/form-data是文件上传的必备属性。结果页做防御性展示有label就显示识别结果有error就显示错误有image_base64就回显图片。图片用 base64 嵌在 HTML 里单张几 MB 的图片会撑大页面体积但作为演示级系统完全够用还省了配置 media 路由的时间。到这里一个最小可用的系统已经跑通了上传图片、模型识别、前端展示。但能跑通和能稳定运行是两回事下一章讲我在这套系统上踩过的坑每一个都真实花掉过半天以上的时间。5. 避坑排查我在这套系统上踩过的 5 个坑5.1 训练 98% 到了 django 全错预处理不一致现象模型在验证集上准确率超过 97%放进 django 后上传测试集图片识别结果几乎全错。原因训练时用了随机翻转、随机裁剪等数据增强但部署端的 transform 只有基础缩放和标准化。还有一种情况是标准化参数写错位置比如训练时对每个通道减 0.5 除 0.5部署时忘了减均值像素值分布完全错位。解决把训练和部署的 transform 抽成同一个函数我在前面第 3 章就是这样做的统一放在一个公共文件里。部署端不要重新“造轮子”直接 import 训练脚本里的transform对象。如果已经出了这个问题先用训练脚本跑一张图打印预处理后的张量再对比 django 端同样的打印结果逐值核对。5.2 第一次请求卡到超时模型没有预加载现象服务刚启动时第一次上传图片要等五六秒后面再传就快得跟闪一样。原因模型是懒加载的第一个请求进views时才执行torch.load读权重文件大模型读几十 MB 加上环境初始化自然慢。解决在模块顶层直接调用model_loader.load_model()这样 django 启动时模型就进内存了第一次请求直接走推理。如果你用了多个 worker 进程跑 gunicorn每个 worker 都会独立加载一份模型内存占用线性翻倍注意监控内存瓶颈。5.3 上传大图预测慢没有压缩就送进模型现象用户上传一张手机拍的 4000×3000 照片预测耗时三秒多。原因模型内部会对图片做 Resize但大图读入内存、解码、再缩放到 32×32整个过程成本很高。每次请求都走一遍自然慢。解决在predict()或视图层先做一次缩略图限制最长边不超过 1024 像素再送进模型服务。这样既保证识别效果又大幅减少图片解码耗时。还可以顺手限制上传文件大小django 里用settings.DATA_UPLOAD_MAX_MEMORY_SIZE控制在 5MB 以内超出直接报错。5.4 类别名字对不上json 顺序乱套现象预测结果特别离谱比如把“限速 30”识别成“禁止驶入”但模型单独在测试集上又是对的。原因类别映射文件生成时用了无序的数据结构或者训练时用了os.listdir()读取文件夹列表而不同文件系统下 listdir 返回顺序不一致导致“0 号索引”对应的类别在部署端变了。解决生成映射文件时用sorted()对文件夹名排序并手动验证一遍映射内容。我在前面 3.3 节写的方式就是这样。部署端打印_class_names列表的前几项和训练时打印的确认一遍十秒钟就能排除这个坑。5.5 图片上传后不显示media 配置缺失现象识别结果页的文字正常显示但用户上传的图片裂开了浏览器小图标一个。原因图片是走MEDIA_URL路径引用的但 django 开发服务器默认不处理 media 文件如果没在根路由里挂载static()图片地址自然 404。解决要么按标准方式在traffic_system/urls.py加 media 路由要么像我前面用的办法直接把图片 base64 编码嵌在页面里绕开静态文件服务。毕业设计演示用 base64 方案最省心等真要上生产再让 nginx 处理媒体文件。6. 给系统装一个置信度闸门让识别在低置信度时学会说“不知道”系统能跑以后我建议你做的第一件事不是换更大的模型而是加一个置信度闸门。目前predict()不管三七二十一永远返回置信度最高的那个类别。但用户传一张风景照、一张宠物照模型也会硬给一个“交通标志”答案然后网页上显示一个莫名其妙的类别。这个现象在小模型上特别明显因为小模型对“不在自己类别里”的图片softmax 分布往往接近均匀最大概率也才 0.2 左右。置信度闸门就是设置一个阈值低于阈值就返回“未知/低置信度”而不是硬猜。代码很简单def predict_with_threshold(image_bytes, threshold0.6): result predict(image_bytes) if result[confidence] threshold: result[label] 未识别 result[low_confidence] True return result阈值怎么定拿一批负样本非交通标志图片猫狗、风景、纯色背景和一批正样本GTSRB 测试集图片分别跑一遍画出置信度分布。正样本的置信度通常在 0.9 以上负样本大多在 0.5 以下取 0.6~0.7 作为分界点是比较稳的。我最后选的是 0.65既能拦截大部分负样本又不会误伤真实标志。验证方法也顺带说一下把 GTSRB 测试集自己跑一遍统计低于阈值的正样本比例应该控制在 5% 以内再准备 20 张随机风景照确保至少 18 张被判定为“未识别”。两个指标都达标这个闸门才算合格。再深一步你可以把识别置信度存进数据库后面想分析模型在哪些场景下犹豫不决直接查表即可。如果还想继续提升那么下一步通常是模型轻量化把权重导出成 ONNX用 ONNX Runtime 跑推理比 PyTorch eager 模式快一倍左右或者量化成 int8在 CPU 上延迟能从十几毫秒降到几毫秒。但我个人的血泪教训是这个系统最大的瓶颈从来不是推理速度而是“输入不可控”。先把格式校验、大小限制、置信度闸门这些边界补好比堆模型精度更可靠。我做过一次答辩现场演示评委随机上传了一张模糊的街景旧版本系统直接说“限速 80”差点下不来台。加上置信度闸门后哪怕传一张完全不相关的图系统也会诚实地给出“未识别置信度 0.16”评委反而夸这个细节考虑到位。这就是我想强调的识别系统的价值不在答得快而在知道什么该闭口不说。希望帮到你。本文还有配套的精品资源点击获取