大家好,我是Java1234_小锋老师,分享一套锋哥原创的基于YOLO26的车牌识别+智慧停车场系统(深度学习+目标检测+微信小程序+LPRNet+CCPD++FastAPI+Vue3) 。

项目介绍

针对传统停车场依赖人工发卡、车牌识别精度受光照和拍摄角度影响较大、车主缴费流程繁琐等问题,本文设计并实现了一套基于 YOLO26+LPRNet 深度学习的智慧停车场管理系统。系统采用检测与识别解耦的两阶段方案:首先使用在中国城市停车车牌数据集 CCPD 上训练得到的 YOLO26-Pose 模型定位车牌并回归四个角点,再对车牌区域进行透视矫正与双层牌拆分,最后由轻量级 LPRNet 结合 CTC 贪心解码输出车牌号码。业务系统采用 FastAPI 构建 REST 接口,Vue3 与 Element Plus 实现管理员后台,微信小程序实现车主端。管理员可完成入场识别、出场识别、视频抽帧择优、车主车辆审核、区域车位维护、收费规则配置和数据看板统计;车主可通过用户名密码注册登录、绑定车牌、查询余位与停车记录,并在出口扫描缴费二维码完成模拟支付。数据库采用 MySQL 8,库名为 db_smart_parking,核心表均以 t_ 开头。测试表明,系统能够完成从车牌识别、车位分配、计费到扫码缴费的闭环业务,图片识别耗时约数百毫秒,满足本科毕业设计对完整性、可用性和工程规范性的要求。

源码下载

链接: https://pan.baidu.com/s/1S5tB_tcPvn6Mh6krgNm4Ew?pwd=1234
提取码: 1234

系统展示

核心代码

# -*- coding: utf-8 -*-
"""
停车业务服务。

封装入场登记、出场结算、车位分配与缴费订单生成。
"""

from datetime import datetime

from config.settings import Settings
from db.dao import (
    AreaDao,
    FeeRuleDao,
    ParkingRecordDao,
    PayOrderDao,
    SpaceDao,
    VehicleDao,
)
from utils.common import calc_fee, gen_no, now_str
from utils.qrcode_util import make_pay_qrcode


class ParkingService:
    """停车出入场业务服务类。"""

    @staticmethod
    def enter(plate_no: str, image_path: str = "", plate_record_id: int = 0) -> dict:
        """
        车辆入场:校验未在场、分配空闲车位、写入停车记录。

        :param plate_no: 车牌号码
        :param image_path: 入场抓拍图路径
        :param plate_record_id: 关联识别记录
        :return: 业务结果字典
        """
        plate_no = (plate_no or "").strip().upper()
        if len(plate_no) < 6:
            return {"ok": False, "msg": "车牌号码无效"}
        exists = ParkingRecordDao.find_in_lot(plate_no)
        if exists:
            return {"ok": False, "msg": f"车牌 {plate_no} 已在场,请勿重复入场", "record": exists}

        vehicle = VehicleDao.find_by_plate(plate_no)
        owner_id = int(vehicle.get("owner_id") or 0) if vehicle else 0

        prefer = "新能源" if (vehicle and "新能源" in (vehicle.get("plate_type") or "")) else ""
        space = SpaceDao.find_free(prefer)
        if not space:
            return {"ok": False, "msg": "暂无空闲车位"}

        now = now_str()
        record_no = gen_no("PK")
        rid = ParkingRecordDao.insert({
            "record_no": record_no,
            "plate_no": plate_no,
            "owner_id": owner_id,
            "area_id": space.get("area_id") or 0,
            "space_id": space.get("id") or 0,
            "in_time": now,
            "out_time": None,
            "in_image": image_path or "",
            "out_image": "",
            "duration_min": 0,
            "fee_amount": 0,
            "pay_status": "未支付",
            "status": "在场",
            "plate_record_id": plate_record_id,
            "remark": "",
            "create_time": now,
        })
        SpaceDao.set_status(space["id"], "占用")
        AreaDao.inc_used(space["area_id"], 1)
        record = ParkingRecordDao.find_by_id(rid)
        return {"ok": True, "msg": "入场成功", "record": record, "space": space}

    @staticmethod
    def exit_lot(plate_no: str, image_path: str = "") -> dict:
        """
        车辆出场:计算时长与费用,空闲车位,生成待支付订单。

        :param plate_no: 车牌号码
        :param image_path: 出场抓拍图路径
        :return: 业务结果字典
        """
        plate_no = (plate_no or "").strip().upper()
        record = ParkingRecordDao.find_in_lot(plate_no)
        if not record:
            return {"ok": False, "msg": f"未找到车牌 {plate_no} 的在场记录"}

        in_time = datetime.strptime(record["in_time"], Settings.DATETIME_FMT)
        out_time = datetime.now()
        duration_min = max(0, int((out_time - in_time).total_seconds() // 60))
        rule = FeeRuleDao.get_default() or {}
        fee = calc_fee(duration_min, rule)
        pay_status = "免费" if fee <= 0 else "未支付"
        status = "已离场"
        out_str = out_time.strftime(Settings.DATETIME_FMT)

        ParkingRecordDao.update_exit(record["id"], {
            "out_time": out_str,
            "out_image": image_path or "",
            "duration_min": duration_min,
            "fee_amount": fee,
            "pay_status": pay_status,
            "status": status,
        })
        if record.get("space_id"):
            SpaceDao.set_status(record["space_id"], "空闲")
        if record.get("area_id"):
            AreaDao.inc_used(record["area_id"], -1)

        order = None
        if fee > 0:
            order_no = gen_no("PO")
            qr_path = make_pay_qrcode(order_no)
            oid = PayOrderDao.insert({
                "order_no": order_no,
                "parking_record_id": record["id"],
                "owner_id": record.get("owner_id") or 0,
                "plate_no": plate_no,
                "amount": fee,
                "pay_type": "微信模拟",
                "pay_status": "待支付",
                "pay_time": None,
                "trade_no": "",
                "qrcode_path": qr_path,
                "create_time": now_str(),
            })
            order = PayOrderDao.find_by_id(oid)

        updated = ParkingRecordDao.find_by_id(record["id"])
        return {
            "ok": True,
            "msg": "出场结算完成" if fee > 0 else "免费离场",
            "record": updated,
            "fee": fee,
            "durationMin": duration_min,
            "order": order,
        }
<template>
  <div class="profile">
    <div class="page-card left">
      <h3 class="page-title">个人资料</h3>
      <div class="avatar-box">
        <el-avatar :size="96" :src="form.avatar">{{ form.real_name?.[0] || '管' }}</el-avatar>
        <el-upload :show-file-list="false" :http-request="uploadAvatar">
          <el-button type="primary" style="margin-top:12px">上传头像</el-button>
        </el-upload>
      </div>
      <el-form :model="form" label-width="80px">
        <el-form-item label="账号"><el-input :model-value="form.username" disabled /></el-form-item>
        <el-form-item label="姓名"><el-input v-model="form.real_name" /></el-form-item>
        <el-form-item label="性别">
          <el-radio-group v-model="form.gender">
            <el-radio value="男">男</el-radio>
            <el-radio value="女">女</el-radio>
          </el-radio-group>
        </el-form-item>
        <el-form-item label="手机"><el-input v-model="form.phone" /></el-form-item>
        <el-form-item label="邮箱"><el-input v-model="form.email" /></el-form-item>
        <el-form-item>
          <el-button type="primary" @click="saveProfile">保存资料</el-button>
        </el-form-item>
      </el-form>
    </div>
    <div class="page-card right">
      <h3 class="page-title">修改密码</h3>
      <el-form :model="pwd" label-width="100px">
        <el-form-item label="原密码"><el-input v-model="pwd.oldPassword" type="password" show-password /></el-form-item>
        <el-form-item label="新密码"><el-input v-model="pwd.newPassword" type="password" show-password /></el-form-item>
        <el-form-item label="确认新密码"><el-input v-model="pwd.confirmPassword" type="password" show-password /></el-form-item>
        <el-form-item>
          <el-button type="warning" @click="savePwd">确认修改</el-button>
        </el-form-item>
      </el-form>
    </div>
  </div>
</template>

<script setup>
import { onMounted, reactive } from 'vue'
import { ElMessage } from 'element-plus'
import http from '../../utils/request'
import { useUserStore } from '../../store/user'

const user = useUserStore()
const form = reactive({ username: '', real_name: '', gender: '男', phone: '', email: '', avatar: '' })
const pwd = reactive({ oldPassword: '', newPassword: '', confirmPassword: '' })

async function load() {
  const info = await user.fetchInfo()
  Object.assign(form, {
    username: info.username,
    real_name: info.realName,
    gender: info.gender || '男',
    phone: info.phone,
    email: info.email,
    avatar: info.avatar
  })
}
async function saveProfile() {
  await http.put('/api/profile/update', {
    realName: form.real_name,
    gender: form.gender || '男',
    phone: form.phone,
    email: form.email
  })
  ElMessage.success('资料已更新')
  load()
}
/** 提交修改密码,需两次新密码一致 */
async function savePwd() {
  if (!pwd.newPassword) {
    ElMessage.warning('新密码不能为空')
    return
  }
  if (pwd.newPassword !== pwd.confirmPassword) {
    ElMessage.warning('两次输入的新密码不一致')
    return
  }
  await http.put('/api/profile/password', pwd)
  ElMessage.success('密码修改成功')
  pwd.oldPassword = ''
  pwd.newPassword = ''
  pwd.confirmPassword = ''
}
async function uploadAvatar({ file }) {
  const data = new FormData()
  data.append('file', file)
  await http.post('/api/profile/avatar', data)
  ElMessage.success('头像已更新')
  load()
}
onMounted(load)
</script>

<style scoped>
.profile {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 16px;
}
.avatar-box { text-align: center; margin-bottom: 16px; }
@media (max-width: 900px) {
  .profile { grid-template-columns: 1fr; }
}
</style>

更多推荐