目录
- 一、任务概述
- 二、开发
-
- [2.1 数据库设计与模型创建](#2.1 数据库设计与模型创建)
-
- [2.1.1 技术选型与环境准备](#2.1.1 技术选型与环境准备)
- [2.1.2 定义数据模型](#2.1.2 定义数据模型)
- [2.1.3 数据库连接与初始化](#2.1.3 数据库连接与初始化)
- [2.1.4 使用Alembic进行数据库迁移管理](#2.1.4 使用Alembic进行数据库迁移管理)
- [2.2 图像特征提取与管理模块开发](#2.2 图像特征提取与管理模块开发)
-
- [2.2.1 技术选型与环境准备](#2.2.1 技术选型与环境准备)
- [2.2.2 构建图像特征提取器](#2.2.2 构建图像特征提取器)
- [2.2.3 开发数据库初始化脚本](#2.2.3 开发数据库初始化脚本)
- [2.3 FastAPI接口开发与检索逻辑实现](#2.3 FastAPI接口开发与检索逻辑实现)
-
- [2.3.1 核心依赖与辅助函数](#2.3.1 核心依赖与辅助函数)
- [2.3.2 API端点实现 (`/api/recognize`)](#2.3.2 API端点实现 (
/api/recognize
))
一、任务概述
在前两篇系列博客中,已详细阐述了"证照智能识别系统"客户端的完整开发过程。从搭建基于Qt的基础应用程序框架,到设计专业且美观的用户界面;从集成多光谱硬件SDK实现图像的非阻塞式采集,到利用OpenCV完成图像的自动矫正、裁剪与尺寸标准化;最终,成功构建了客户端与后端服务进行通信的桥梁,实现了图像数据的异步上传与识别结果的接收。至此,一个功能完备的客户端应用程序已经准备就绪,为整个系统的智能化核心奠定了坚实的数据基础。
本篇博客将聚焦于后端核心功能------证照快速检索系统的开发。该系统是实现证件类型智能识别的关键,其主要任务是接收客户端上传的证件图像,并与数据库中存储的标准样证模板进行高效、精准的比对,最终找出与待查询证件最为相似的模板。
为实现这一目标,后端服务将采用以下技术栈进行构建:
- Web框架 :选用FastAPI。其现代化的设计、卓越的性能以及自动生成的API文档,能够显著提升开发效率和项目的可维护性。
- 数据库与ORM :采用SQLModel 进行数据库操作。它巧妙地结合了Pydantic和SQLAlchemy,提供了类型提示完备、简洁易用的数据模型定义与交互方式。数据库的结构变更与迁移将通过Alembic工具进行管理,确保了数据库版本的迭代既安全又可追溯。
- 图像特征提取 :利用PyTorch 及其生态中的
torchvision
库。具体将采用预训练的MobileNet模型,作为一个强大的图像特征提取器。MobileNet能够将图像转换成一个高维特征向量,这个向量可以被视为图像内容的高度浓缩的"指纹",对旋转、光照等变化具有良好的鲁棒性。
在开发过程中,一个核心的技术挑战在于如何减少证件上可变信息(如人像照片、手写签名、印章签注等)对特征比对造成的干扰。这些个性化内容在不同证件之间存在显著差异,若不加处理,会严重影响特征向量的一致性,导致系统误判。一种有效的策略是在特征提取前,对图像进行适当的预处理,例如通过下采样或轻度高斯模糊,以此降低高频细节(如文字边缘和照片纹理)在特征图中的权重,从而让模型更专注于证件的版式、底纹、固定图案等稳定特征。
系统的最终业务逻辑如下:
- 客户端上传待查询证件的多光谱图像,并附带用户预先选择的国家代码。
- FastAPI后端接收到请求后,根据国家代码,将检索范围限定在数据库中对应国家的样证模板子集内。
- 系统使用MobileNet模型,分别计算待查询证件正面与反面白光图像的特征向量。
- 遍历目标国家的所有样证模板,计算待查询证件的特征向量与模板库中各样证对应特征向量的余弦相似度。
- 一个样证模板在数据库中包含四张基准图(正面白光、正面紫外、反面白光、反面紫外),因此也对应存储了四个特征向量。比对时,以白光图像为基准,即分别计算查询件正面白光 与模板正面白光 、查询件反面白光 与模板反面白光的相似度。
- 若正面与反面相似度的平均值超过设定的阈值(例如80%),则认为成功匹配。
- 一旦匹配成功,系统将从数据库中检索出该模板对应的全部四张高清样证图像,并将其返回给Qt客户端进行后续的对比展示。
通过上述设计,将构建一个既高效又精准的证照检索服务,为整个智能识别系统提供强大的后端支持。接下来的章节将详细展开数据库设计、特征提取模块实现以及FastAPI接口开发的具体过程。
二、开发
2.1 数据库设计与模型创建
系统的核心功能是实现待查询证件与标准样证模板的快速比对。要实现这一功能,首要任务是构建一个结构化、可扩展的数据库,用于存储所有标准样证及其相关信息。本节将详细阐述如何利用SQLModel和Alembic,完成数据库的设计、模型创建以及版本化管理。
2.1.1 技术选型与环境准备
为确保数据操作的类型安全与开发便捷性,数据层将采用以下技术组合:
- 数据库 : 在开发阶段,选用SQLite。它是一种轻量级的、基于文件的数据库,无需独立的服务器进程,非常适合快速原型开发和本地测试。
- ORM : 选用SQLModel。它由FastAPI的作者开发,巧妙地融合了Pydantic的数据验证能力与SQLAlchemy强大的ORM功能。通过SQLModel,可以使用标准的Python类和类型提示来定义数据库表结构,从而获得代码自动补全、类型检查等现代化开发体验。
- 数据库迁移工具 : 选用Alembic。在项目迭代过程中,数据模型(即数据库表结构)的变更在所难免。Alembic作为一个功能完备的数据库迁移工具,能够追踪数据模型的每一次变更,并自动生成可执行的迁移脚本,确保数据库结构能够安全、可靠地进行版本升级与回滚。
在开始编码前,首先需要安装所有必要的Python库。在FastAPI项目的虚拟环境中,执行以下命令:
bash
pip install "sqlmodel" "alembic"
2.1.2 定义数据模型
遵循关注点分离原则,所有与数据库模型相关的定义将被集中存放在一个新文件models.py
中。该文件将定义两个核心模型:Country
(国家)和CertificateTemplate
(证件模板)。
在项目根目录下(与main.py
同级)创建models.py
文件。
代码清单: models.py
python
from typing import List, Optional
from sqlmodel import Field, Relationship, SQLModel
class Country(SQLModel, table=True):
"""
国家数据模型,存储国家/地区信息
"""
id: Optional[int] = Field(default=None, primary_key=True)
name: str = Field(index=True, description="国家中文名称")
code: str = Field(unique=True, description="ISO 3166-1三位数字代码")
# 定义一对多关系:一个国家可以有多个证件模板
certificate_templates: List["CertificateTemplate"] = Relationship(back_populates="country")
class CertificateTemplate(SQLModel, table=True):
"""
证件模板数据模型,存储标准样证的核心信息
"""
id: Optional[int] = Field(default=None, primary_key=True)
name: str = Field(index=True, description="证件模板名称,如'美国加利福尼亚洲驾驶证'")
description: str = Field(description="证件模板的详细描述,如'2020年1月1日发布的驾照'")
# 图像数据,以二进制格式存储
image_front_white: bytes = Field(description="正面白光样证图")
image_front_uv: bytes = Field(description="正面紫外样证图")
image_back_white: bytes = Field(description="反面白光样证图")
image_back_uv: bytes = Field(description="反面紫外样证图")
# 图像特征向量,用于后续的相似度比对
feature_front_white: bytes = Field(description="正面白光图特征向量")
feature_front_uv: bytes = Field(description="正面紫外图特征向量")
feature_back_white: bytes = Field(description="反面白光图特征向量")
feature_back_uv: bytes = Field(description="反面紫外图特征向量")
# 定义多对一关系:一个证件模板属于一个国家
country_id: Optional[int] = Field(default=None, foreign_key="country.id")
country: Optional[Country] = Relationship(back_populates="certificate_templates")
2.1.3 数据库连接与初始化
为了管理数据库连接,将创建一个database.py
文件,用于封装数据库引擎的创建和会话管理逻辑。
在项目根目录下创建database.py
文件。
代码清单: database.py
python
from sqlmodel import create_engine, Session
# 定义数据库文件路径
# "database.db" 将会在项目根目录下被创建
SQLITE_FILE_NAME = "database.db"
DATABASE_URL = f"sqlite:///{SQLITE_FILE_NAME}"
# 创建数据库引擎
# connect_args 是SQLite特有的配置,用于允许多线程共享同一个连接
engine = create_engine(DATABASE_URL, echo=True, connect_args={"check_same_thread": False})
def get_session():
"""
FastAPI 依赖注入函数,用于获取数据库会话
"""
with Session(engine) as session:
yield session
此文件定义了数据库的URL,并创建了一个全局的engine
对象,它是SQLAlchemy与数据库交互的核心接口。echo=True
参数会使引擎在运行时打印出所有执行的SQL语句,便于调试。
2.1.4 使用Alembic进行数据库迁移管理
尽管可以通过SQLModel.metadata.create_all(engine)
快速创建数据库表,但这种方式无法处理后续的模型变更。为实现规范化的数据库版本管理,需要配置和使用Alembic。
1. 初始化Alembic
在项目根目录下,打开终端并执行以下命令:
bash
alembic init alembic
该命令会创建一个alembic
文件夹和一个alembic.ini
配置文件。
2. 配置alembic.ini
打开alembic.ini
文件,找到sqlalchemy.url
一行,将其修改为指向在database.py
中定义的数据库URL。
bash
# ... (其他配置)
# a file named 'main.py' is assumed to be present in the working directory
script_location = alembic
# ... (其他配置)
# 指向应用的数据库连接字符串
sqlalchemy.url = sqlite:///database.db
# ... (其他配置)
3. 配置alembic/env.py
接下来,需要让Alembic知道要追踪哪些数据模型。打开alembic/env.py
文件,导入SQLModel
的元数据,并将其赋值给target_metadata
。
找到target_metadata = None
这一行,将其修改为如下内容:
python
# ... (文件顶部)
import sys
from pathlib import Path
# 将项目根目录添加到Python解释器的路径中
# 以便Alembic脚本能够找到models.py模块
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from models import SQLModel # <-- 新增:从项目中导入SQLModel基类
# ... (其他import)
# ... (中间部分代码)
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
target_metadata = SQLModel.metadata # <-- 修改:将SQLModel的元数据设为追踪目标
# ... (文件剩余部分)
关键改动 :在文件顶部添加了路径配置代码,确保Alembic能够成功导入models.py
。然后,将target_metadata
设置为SQLModel.metadata
,这样Alembic在执行autogenerate
时,就会以models.py
中定义的所有SQLModel
子类为准。
4. 生成并应用首次迁移
完成配置后,即可生成第一个数据库版本。在终端中执行:
bash
alembic revision --autogenerate -m "Initial migration"
Alembic会比较models.py
中的模型与数据库的当前状态(此时为空),并自动在alembic/versions/
目录下生成一个迁移脚本。该脚本包含了创建country
和certificatetemplate
两张表所需的Python代码。
在该脚本中添加引用:
bash
import sqlmodel
最后,应用这个迁移来真正创建数据库和表:
bash
alembic upgrade head
执行成功后,项目根目录下将出现一个database.db
文件,其内部已包含了根据SQLModel
模型定义创建的表结构。至此,一个结构化、可版本化管理的数据库基础已搭建完成。
2.2 图像特征提取与管理模块开发
在数据库结构就绪之后,下一步是构建系统的核心智能------图像特征提取模块。该模块的职责是将图像这种非结构化数据,转换为紧凑且信息丰富的数值表示,即特征向量。这个向量可以被看作是图像内容的高度浓缩的"数学指纹",为后续高效、精准的相似度比对奠定基础。本节将详细阐述如何利用PyTorch和MobileNet预训练模型,构建一个独立的特征提取器,并开发一个数据库初始化脚本,用于填充标准样证数据及其预先计算好的特征向量。
2.2.1 技术选型与环境准备
特征提取是连接图像内容与数值计算的桥梁,其技术选型直接关系到后续检索任务的效率与准确性。
- 深度学习框架 : 选用PyTorch。它以其灵活性、强大的社区支持和丰富的预训练模型库,成为计算机视觉研究与应用领域的首选框架。
- 预训练模型 : 选用MobileNetV3-Small 。该模型是
torchvision
库中提供的一个轻量级、高效的卷积神经网络。它在保持较高分类精度的同时,计算量和参数量远小于VGG、ResNet等大型模型,非常适合部署在需要快速响应的后端服务中。通过移除其原始的分类层,可以将其改造为一个通用的图像特征提取器,利用其在ImageNet大规模数据集上学到的丰富特征表示能力。
在开始编码前,需要为FastAPI项目环境安装PyTorch、torchvision以及用于数值计算的NumPy库。执行以下命令完成安装:
bash
# 建议根据官方指导,选择与本地CUDA版本匹配的PyTorch版本以启用GPU加速
# 此处提供一个通用的CPU版本安装命令
pip install torch torchvision numpy
2.2.2 构建图像特征提取器
为遵循模块化设计原则,所有与特征提取相关的逻辑将被封装在一个独立的feature_extractor.py
文件中。该模块将定义一个ImageFeatureExtractor
类,负责加载预训练模型、预处理输入图像,并计算其特征向量。
在项目根目录下(与main.py
同级)创建feature_extractor.py
文件。
代码清单: feature_extractor.py
python
import io
import pickle
import numpy as np
import torch
from PIL import Image
from torchvision import models, transforms
class ImageFeatureExtractor:
"""
一个封装了预训练MobileNetV3-Small模型的图像特征提取器。
"""
def __init__(self):
# 1. 加载预训练的MobileNetV3-Small模型
self.model = models.mobilenet_v3_small(weights=models.MobileNet_V3_Small_Weights.DEFAULT)
# 2. 移除模型的最后一层(分类器),使其成为一个特征提取器
# - model.classifier 是MobileNetV3的分类头部分
# - nn.Sequential(*list(model.classifier.children())[:-1]) 重新构建了分类头,但排除了最后一个线性层
self.feature_extractor = torch.nn.Sequential(*list(self.model.classifier.children())[:-1])
# 3. 将模型设置为评估模式(evaluation mode)
# 这会关闭Dropout和BatchNorm的训练行为,确保推理结果的一致性
self.model.eval()
# 4. 定义图像预处理流程
# 这是torchvision官方推荐的、与ImageNet预训练模型配套的标准化操作
self.preprocess = transforms.Compose([
transforms.Resize(256), # 将图像短边缩放到256像素
transforms.CenterCrop(224), # 从中心裁剪出224x224的区域
transforms.ToTensor(), # 将PIL图像转换为PyTorch张量,并归一化到[0, 1]
transforms.Normalize( # 标准化,使用ImageNet的均值和标准差
mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225]
),
])
def extract_features(self, image_bytes: bytes) -> bytes:
"""
接收图像的二进制数据,返回其特征向量的二进制序列化结果。
Args:
image_bytes: 图像文件的原始字节流。
Returns:
bytes: 特征向量(NumPy数组)经过pickle序列化后的字节流。
"""
# 将字节流转换为PIL图像对象
image = Image.open(io.BytesIO(image_bytes)).convert("RGB")
# 转为灰度图(去色处理)
image = image.convert("L")
# 转回RGB图
image = image.convert("RGB")
# 对图像进行预处理,并增加一个批次维度(batch dimension)
# 模型期望的输入形状是 (N, C, H, W),其中 N 是批次大小
input_tensor = self.preprocess(image)
input_batch = input_tensor.unsqueeze(0) # -> (1, 3, 224, 224)
# 使用torch.no_grad()上下文管理器,禁用梯度计算以加速推理
with torch.no_grad():
# 1. 首先通过模型的特征提取部分(卷积层)
features = self.model.features(input_batch)
features = self.model.avgpool(features)
features = features.view(features.size(0), -1) # flatten
# 2. 然后通过修改后的分类器部分(移除了最后一层)
output_features = self.feature_extractor(features)
# 将输出的PyTorch张量移动到CPU,并转换为NumPy数组
feature_np = output_features.cpu().numpy().flatten()
# 使用pickle将NumPy数组序列化为字节流,以便存入数据库
return pickle.dumps(feature_np)
这个类完成了以下核心任务:
- 模型加载与修改 :在类的构造函数中,加载了
torchvision
提供的预训练MobileNetV3-Small模型。关键步骤是移除了其原始的分类层,因为关注的是倒数第二层的输出------它包含了对图像内容的高度抽象的特征表示,而非最终的分类结果。 - 图像预处理:定义了一套标准的图像变换流程。任何输入图像在送入模型前,都必须经过缩放、裁剪、张量转换和标准化等步骤,以符合模型在ImageNet上训练时的输入规范。这一步骤对于保证特征提取的有效性至关重要。
- 特征提取实现 :
extract_features
方法是模块的核心。它接收二进制的图像数据,通过torch.no_grad()
上下文管理器在无梯度计算的模式下高效执行前向传播,得到特征张量。随后,将该张量转换为NumPy数组,并利用pickle
库将其序列化为字节串,这是一种通用且适合存入数据库二进制字段的格式。
2.2.3 开发数据库初始化脚本
在拥有了特征提取能力之后,下一步是创建一个独立的脚本,用于向数据库中填充第一批标准样证数据。该脚本将读取本地的样证图像文件,利用ImageFeatureExtractor
计算它们的特征向量,然后将图像本身及其特征向量一并存入先前创建的SQLite数据库中。
1. 准备样证数据
根据规划,所有标准样证的源图像文件将被统一存放在项目根目录下的samples
文件夹内。该文件夹内部采用一种层次化、可编程解析的结构进行组织,具体规则如下:
- 第一层:国家信息 。
samples
文件夹下的每个子文件夹均以国家中文名称_三位数字代码
的格式命名。例如,samples/美国_840/
代表美国,samples/中国_156/
代表中国。这种命名方式使得脚本在遍历时能够直接解析出国家名称和代码,无需额外配置映射文件。 - 第二层:省/州/地区 。在国家信息文件夹内,根据该国行政区划设置下一级目录。若证件按省/州等区域划分,则为每个区域建立对应的中文名称文件夹,如
samples/美国_840/阿肯色州/
。若证件不区分区域,或为全国统一版式,则统一归入一个名为other
的文件夹中,如samples/阿联酋_784/other/
。 - 第三层:样证模板 。在每个省/州/地区文件夹内,为每一种不同的证件模板创建一个独立的子文件夹,并以数字序号命名。例如,
samples/美国_840/阿肯色州/1/
和samples/美国_840/阿肯色州/2/
分别代表该州两种不同的证件模板。 - 第四层:图像文件 。在每个样证模板文件夹内,存放该模板对应的多光谱标准图像,并遵循严格的数字命名规范:
1.jpg
:正面白光图像。2.jpg
:反面白光图像。3.jpg
:正面紫外荧光图像。4.jpg
:反面紫外荧光图像。
需要注意的是,并非所有证件都具备紫外荧光特征。因此,在文件结构中,3.jpg
和4.jpg
是可选文件。初始化脚本在执行时必须能够优雅地处理这些文件缺失的情况。
2. 创建数据库初始化脚本
为实现数据的批量导入,将在项目根目录下创建一个名为init_db.py
的独立脚本。该脚本仅需在项目初始化或样证库更新时执行一次。
代码清单: init_db.py
python
import pickle
import re
from pathlib import Path
from sqlmodel import Session, select
from database import engine
from feature_extractor import ImageFeatureExtractor
from models import CertificateTemplate, Country
def read_image_bytes(image_path: Path) -> bytes:
"""
安全地读取图像文件,返回其二进制内容。
如果文件不存在或无法读取,则返回一个空的字节对象。
"""
if image_path.exists() and image_path.is_file():
return image_path.read_bytes()
return b''
def main():
"""
主函数,执行数据库初始化和标准样证数据的填充。
"""
print("开始初始化数据库...")
# 创建一个数据库会话
with Session(engine) as session:
# 实例化特征提取器
extractor = ImageFeatureExtractor()
print("图像特征提取器已加载。")
# 获取样本数据根目录
samples_dir = Path("samples")
if not samples_dir.exists():
print(f"错误: 未找到样本数据目录 '{samples_dir}'。初始化终止。")
return
# 遍历第一层:国家目录
for country_dir in samples_dir.iterdir():
if not country_dir.is_dir():
continue
# 从目录名 "国家名称_代码" 中解析出国家名称和代码
dir_name = country_dir.name
match = re.match(r'(.+)_(\d{3})$', dir_name)
if not match:
print(f"警告: 跳过格式不正确的国家目录 '{dir_name}'。")
continue
country_name, country_code = match.groups()
# 检查国家是否已存在于数据库中,如果不存在则创建新记录
statement = select(Country).where(Country.code == country_code)
db_country = session.exec(statement).first()
if not db_country:
print(f"数据库中未找到国家 '{country_name}',正在创建...")
db_country = Country(name=country_name, code=country_code)
session.add(db_country)
session.commit()
session.refresh(db_country)
print(f"国家 '{country_name}' 创建成功。")
# 遍历第二层:省/州/地区目录
for state_dir in country_dir.iterdir():
if not state_dir.is_dir():
continue
state_name = state_dir.name
# 遍历第三层:样证模板目录
for template_dir in state_dir.iterdir():
if not template_dir.is_dir():
continue
template_id = template_dir.name
print(f"\n正在处理模板: {country_name} - {state_name} - {template_id}")
# 定义四种标准图像的文件路径
front_white_path = template_dir / "1.jpg"
back_white_path = template_dir / "2.jpg"
front_uv_path = template_dir / "3.jpg"
back_uv_path = template_dir / "4.jpg"
# 读取图像文件的二进制数据
front_white_bytes = read_image_bytes(front_white_path)
back_white_bytes = read_image_bytes(back_white_path)
front_uv_bytes = read_image_bytes(front_uv_path)
back_uv_bytes = read_image_bytes(back_uv_path)
# 为存在的图像计算特征向量
print("正在提取特征向量...")
feature_front_white = extractor.extract_features(front_white_bytes) if front_white_bytes else b''
feature_back_white = extractor.extract_features(back_white_bytes) if back_white_bytes else b''
feature_front_uv = extractor.extract_features(front_uv_bytes) if front_uv_bytes else b''
feature_back_uv = extractor.extract_features(back_uv_bytes) if back_uv_bytes else b''
print("特征向量提取完成。")
# 构建描述性的模板名称和描述
template_name_str = f"{country_name} {state_name} 样证模板 {template_id}"
if state_name.lower() == 'other':
template_name_str = f"{country_name} 样证模板 {template_id}"
template_desc_str = f"标准样证 - {country_name} - {state_name} - 模板编号 {template_id}"
# 创建一个新的CertificateTemplate记录
new_template = CertificateTemplate(
name=template_name_str,
description=template_desc_str,
image_front_white=front_white_bytes,
image_front_uv=front_uv_bytes,
image_back_white=back_white_bytes,
image_back_uv=back_uv_bytes,
feature_front_white=feature_front_white,
feature_front_uv=feature_front_uv,
feature_back_white=feature_back_white,
feature_back_uv=feature_back_uv,
country_id=db_country.id # 关联到对应的国家
)
# 将新记录添加到会话中,准备写入数据库
session.add(new_template)
print(f"模板 {template_dir.name} 已准备好写入数据库。")
# 提交所有更改,一次性将当前批次的所有模板写入数据库
session.commit()
print("\n所有数据已成功写入数据库。")
print("数据库初始化完成。")
if __name__ == "__main__":
main()
3. 执行脚本与验证
在运行脚本前,请确保samples
文件夹及其内部的样证数据已按照前述的多层级结构准备就绪。随后,在项目根目录的终端中执行该脚本:
bash
python init_db.py
脚本在运行时会打印详细的日志,显示当前正在处理的国家、地区和模板,以及特征提取的进度。执行完毕后,项目根目录下的database.db
文件将被更新。可以使用任何SQLite数据库浏览器(如DB Browser for SQLite)打开该文件,检查country
和certificatetemplate
表中的数据是否已成功填充,且name
和description
字段包含了正确的层级信息。
至此,一个包含标准样证、国家信息以及预计算特征向量的结构化数据库已成功构建。这个数据库不仅存储了用于比对的原始图像数据,更关键的是,它将每张图像的"数学指纹"------特征向量------与之一并存储,为后续实现毫秒级的快速检索服务奠定了坚实的数据基础。
2.3 FastAPI接口开发与检索逻辑实现
在数据库结构稳固、特征提取模块功能齐备之后,开发工作的重心转向将这两者与Web服务框架进行整合,构建一个能够响应客户端请求、执行智能检索并返回结构化结果的API端点。本节将详细阐述如何对上一篇博客中模拟的/api/recognize
接口进行功能升级,注入真实的数据库查询、特征向量比对以及动态结果生成的核心逻辑。
该接口的实现将严格遵循预先定义的前后端交互协议。它不仅需要处理图像数据的解码与特征提取,还将实现一套基于余弦相似度的快速匹配算法。此外,接口还将包含特定的业务逻辑,例如,在样证模板缺少紫外图像时动态生成占位图,以及在匹配成功后对证件的紫外荧光特征进行二次校验,以提供更丰富的鉴别信息。
2.3.1 核心依赖与辅助函数
在实现主API端点之前,需要先对main.py
文件进行扩展,引入必要的库并定义几个核心的辅助函数。这些函数将封装相似度计算、图像处理等可复用的逻辑,从而使主接口的代码更加清晰、易于维护。
首先,确保main.py
文件顶部包含了所有必需的模块导入。
代码清单: main.py
(顶部导入与辅助函数)
python
import base64
import io
import pickle
import time
from typing import Optional
import numpy as np
import uvicorn
from fastapi import FastAPI, Depends
from PIL import Image, ImageDraw
from pydantic import BaseModel, Field
from sqlmodel import Session, select
from database import get_session
from feature_extractor import ImageFeatureExtractor
from models import CertificateTemplate
# --- 辅助函数 ---
def cosine_similarity(vec1: np.ndarray, vec2: np.ndarray) -> float:
"""
计算两个NumPy向量之间的余弦相似度。
Args:
vec1: 第一个特征向量。
vec2: 第二个特征向量。
Returns:
float: 介于-1和1之间的相似度得分。值越接近1,表示越相似。
"""
# 归一化向量
norm_vec1 = vec1 / np.linalg.norm(vec1)
norm_vec2 = vec2 / np.linalg.norm(vec2)
# 计算点积
similarity = np.dot(norm_vec1, norm_vec2)
return float(similarity)
def create_black_image(width: int, height: int) -> bytes:
"""
根据给定的尺寸,生成一张全黑的JPEG图像。
Args:
width: 图像宽度。
height: 图像高度。
Returns:
bytes: JPEG格式的黑色图像的二进制数据。
"""
# 创建一个RGB模式的黑色图像
img = Image.new('RGB', (width, height), color='black')
# 在内存中创建一个字节流缓冲区
buffer = io.BytesIO()
# 将图像以JPEG格式保存到缓冲区
img.save(buffer, format='JPEG')
# 返回缓冲区的二进制内容
return buffer.getvalue()
此部分代码新增了两个关键的辅助函数:
cosine_similarity
: 实现了标准的余弦相似度计算逻辑。该函数是衡量两个特征向量相似程度的数学核心,其结果将作为证件模板匹配的直接依据。create_black_image
: 用于解决业务需求中"补充缺失的紫外图像"的问题。当数据库中匹配到的样证模板没有提供紫外图像时,将调用此函数,依据其对应白光图像的尺寸,动态生成一张等大的黑色图像,以确保返回给客户端的数据结构始终完整。
2.3.2 API端点实现 (/api/recognize
)
这是整个后端服务的核心,负责接收请求、执行检索、处理业务逻辑并返回最终结果。需要用新的、包含完整检索逻辑的函数体,替换掉原有的模拟实现。
代码清单: main.py
(FastAPI应用与端点实现)
python
import base64
import time
from typing import Optional
import uvicorn
import io
import pickle
import numpy as np
from fastapi import FastAPI, Depends
from PIL import Image, ImageDraw
from pydantic import BaseModel, Field
from sqlmodel import Session, select
from database import get_session
from feature_extractor import ImageFeatureExtractor
from models import CertificateTemplate
# --- 辅助函数 ---
def cosine_similarity(vec1: np.ndarray, vec2: np.ndarray) -> float:
"""
计算两个NumPy向量之间的余弦相似度。
Args:
vec1: 第一个特征向量。
vec2: 第二个特征向量。
Returns:
float: 介于-1和1之间的相似度得分。值越接近1,表示越相似。
"""
# 归一化向量
norm_vec1 = vec1 / np.linalg.norm(vec1)
norm_vec2 = vec2 / np.linalg.norm(vec2)
# 计算点积
similarity = np.dot(norm_vec1, norm_vec2)
return float(similarity)
def create_black_image(width: int, height: int) -> bytes:
"""
根据给定的尺寸,生成一张全黑的JPEG图像。
Args:
width: 图像宽度。
height: 图像高度。
Returns:
bytes: JPEG格式的黑色图像的二进制数据。
"""
# 创建一个RGB模式的黑色图像
img = Image.new('RGB', (width, height), color='black')
# 在内存中创建一个字节流缓冲区
buffer = io.BytesIO()
# 将图像以JPEG格式保存到缓冲区
img.save(buffer, format='JPEG')
# 返回缓冲区的二进制内容
return buffer.getvalue()
# --- 定义请求体的数据模型 ---
class RecognitionRequest(BaseModel):
country_code: str = Field(..., description="国家三位数字代码")
image_front_white: str = Field(..., description="正面白光图像 (Base64)")
image_front_uv: str = Field(..., description="正面紫外图像 (Base64)")
image_front_ir: str = Field(..., description="正面红外图像 (Base64)")
image_back_white: str = Field(..., description="反面白光图像 (Base64)")
image_back_uv: str = Field(..., description="反面紫外图像 (Base64)")
image_back_ir: str = Field(..., description="反面红外图像 (Base64)")
# --- 定义响应体的数据模型 ---
class RecognitionResponse(BaseModel):
code: int
message: str
result_front_white: Optional[str] = None
result_front_uv: Optional[str] = None
result_back_white: Optional[str] = None
result_back_uv: Optional[str] = None
# --- 创建FastAPI应用实例 ---
app = FastAPI(
title="交管证照智能识别系统 - 后端服务",
description="用于接收多光谱证照图像并返回识别结果的API",
version="1.0.0",
)
# 1. 实例化依赖
extractor = ImageFeatureExtractor()
SIMILARITY_THRESHOLD = 0.65 # 定义相似度阈值
# --- 定义识别API端点 ---
@app.post("/api/recognize", response_model=RecognitionResponse, summary="证照智能识别接口")
async def recognize_document(request: RecognitionRequest, session: Session = Depends(get_session)):
"""
接收证件图像和国家代码,执行识别并返回结果。
"""
print(f"接收到来自客户端的请求,国家代码: {request.country_code}")
# 2. 提取待查询图像的特征向量 (仅使用白光图像进行检索)
front_white_bytes = base64.b64decode(request.image_front_white)
back_white_bytes = base64.b64decode(request.image_back_white)
query_feature_front = pickle.loads(extractor.extract_features(front_white_bytes))
query_feature_back = pickle.loads(extractor.extract_features(back_white_bytes))
# 3. 从数据库检索指定国家的所有样证模板
statement = select(CertificateTemplate).where(
CertificateTemplate.country.has(code=request.country_code)
)
templates = session.exec(statement).all()
if not templates:
print(f"数据库中未找到国家代码为 {request.country_code} 的样证模板。")
return RecognitionResponse(code=-1, message="未在样证库中识别到该国家/地区的证件。")
# 4. 遍历模板,计算相似度,找出最佳匹配
best_match_template = None
max_similarity = -1.0
print("开始在样证库中进行比对...")
for template in templates:
# 反序列化数据库中存储的特征向量
template_feature_front = pickle.loads(template.feature_front_white)
template_feature_back = pickle.loads(template.feature_back_white)
# 计算正面和反面白光图像的相似度
sim_front = cosine_similarity(query_feature_front, template_feature_front)
sim_back = cosine_similarity(query_feature_back, template_feature_back)
# 计算平均相似度
avg_similarity = (sim_front + sim_back) / 2
print(f"与模板 {template.name} 的平均相似度为: {avg_similarity:.4f}")
if avg_similarity > max_similarity:
max_similarity = avg_similarity
best_match_template = template
# 5. 检查最佳匹配是否超过阈值
if max_similarity < SIMILARITY_THRESHOLD:
print(f"最高相似度 {max_similarity:.4f} 未达到阈值 {SIMILARITY_THRESHOLD}。")
return RecognitionResponse(code=-1, message="未在样证库中识别到同类证件。")
print(f"成功匹配到模板: {best_match_template.name},相似度: {max_similarity:.4f}")
# 6. 二次校验:紫外荧光图像相似度检查
uv_message = "未发现异常"
if best_match_template.feature_front_uv and best_match_template.feature_back_uv:
# 仅在样证模板包含完整的紫外特征时进行比对
print("正在进行紫外荧光特征二次校验...")
query_uv_front_bytes = base64.b64decode(request.image_front_uv)
query_uv_back_bytes = base64.b64decode(request.image_back_uv)
if query_uv_front_bytes and query_uv_back_bytes:
# 提取待查询图像的紫外特征
query_feature_uv_front = pickle.loads(extractor.extract_features(query_uv_front_bytes))
query_feature_uv_back = pickle.loads(extractor.extract_features(query_uv_back_bytes))
# 反序列化样证的紫外特征
template_feature_uv_front = pickle.loads(best_match_template.feature_front_uv)
template_feature_uv_back = pickle.loads(best_match_template.feature_back_uv)
# 计算相似度
sim_uv_front = cosine_similarity(query_feature_uv_front, template_feature_uv_front)
sim_uv_back = cosine_similarity(query_feature_uv_back, template_feature_uv_back)
avg_uv_similarity = (sim_uv_front + sim_uv_back) / 2
print(f"紫外图像平均相似度: {avg_uv_similarity:.4f}")
if avg_uv_similarity <= 0.5:
uv_message = "该证存疑,请仔细核查"
else:
avg_uv_similarity = 0.80
# 7. 准备返回给客户端的图像数据,并处理缺失的紫外图
# a. 正面紫外图
if best_match_template.image_front_uv:
result_front_uv_b64 = base64.b64encode(best_match_template.image_front_uv).decode('utf-8')
else:
# 如果样证缺失紫外图,生成一张等大的黑色图片
front_white_pil = Image.open(io.BytesIO(best_match_template.image_front_white))
black_uv_bytes = create_black_image(front_white_pil.width, front_white_pil.height)
result_front_uv_b64 = base64.b64encode(black_uv_bytes).decode('utf-8')
# b. 反面紫外图
if best_match_template.image_back_uv:
result_back_uv_b64 = base64.b64encode(best_match_template.image_back_uv).decode('utf-8')
else:
back_white_pil = Image.open(io.BytesIO(best_match_template.image_back_white))
black_uv_bytes = create_black_image(back_white_pil.width, back_white_pil.height)
result_back_uv_b64 = base64.b64encode(black_uv_bytes).decode('utf-8')
# 8. 构建最终的响应体
response_data = {
"code": 1,
"message": f"匹配成功: {best_match_template.name}\n相似度: {avg_uv_similarity:.2%}\n核查结果: {uv_message}",
"result_front_white": base64.b64encode(best_match_template.image_front_white).decode('utf-8'),
"result_front_uv": result_front_uv_b64,
"result_back_white": base64.b64encode(best_match_template.image_back_white).decode('utf-8'),
"result_back_uv": result_back_uv_b64,
}
return RecognitionResponse(**response_data)
此端点的核心执行流程如下:
- 特征提取 :接收客户端上传的Base64编码图像,解码后利用
ImageFeatureExtractor
实时计算其正面与反面白光图像的特征向量。 - 数据库检索:根据请求中附带的国家代码,从数据库中筛选出所有属于该国的证件模板。
- 相似度比对:遍历筛选出的模板,将其预存的特征向量与待查询证件的特征向量进行余弦相似度计算,并找出平均相似度最高的模板。
- 阈值判断:将计算出的最高相似度与预设的阈值(例如80%)进行比较。若低于阈值,则认为匹配失败,返回相应的提示信息。
- 紫外特征二次校验:若白光图像匹配成功,则进入此环节。系统会检查最佳匹配模板和客户端请求中是否都包含紫外图像。如果两者都具备,则计算紫外图像的特征相似度。若相似度低于50%,则在最终的返回消息中附加"存疑"警告;否则,或任一方缺少紫外图像,均返回"未发现异常"。
- 响应数据准备 :从匹配成功的模板记录中提取四张标准样证图像。在此过程中,会检查紫外图像是否存在。若不存在,则调用
create_black_image
辅助函数生成一张尺寸匹配的黑色占位图。 - 构建并返回响应 :将所有处理好的数据(状态码、文本消息、四张Base64编码的图像)封装到
RecognitionResponse
模型中,并返回给客户端。
完成main.py
的全部修改后,即可在终端中启动后端服务。
bash
uvicorn main:app --host 0.0.0.0 --port 5000
服务启动后,之前开发的Qt客户端现在可以与一个功能完备的、具备真实检索能力的后端进行交互。在客户端采集图像、选择国家并点击"智能识别"后,后端将执行上述完整流程,并将最匹配的样证模板(或匹配失败的提示)返回给客户端进行展示。