项目实践11—全球证件智能识别系统(切换为PostgreSQL数据库)

目录

  • 一、任务概述
  • [二、数据库升级:从 SQLite 到 PostgreSQL](#二、数据库升级:从 SQLite 到 PostgreSQL)
    • [2.1 为什么要换 PostgreSQL?](#2.1 为什么要换 PostgreSQL?)
    • [2.2 在 Ubuntu 上安装 PostgreSQL](#2.2 在 Ubuntu 上安装 PostgreSQL)
    • [2.3 安装适配FastAPI的PostgreSQL驱动库](#2.3 安装适配FastAPI的PostgreSQL驱动库)
    • [2.4 深度改造:PostgreSQL 环境搭建与向量存储优化](#2.4 深度改造:PostgreSQL 环境搭建与向量存储优化)
      • [2.4.1 代码重构:原生向量存储](#2.4.1 代码重构:原生向量存储)
        • [1. 修改 `models.py`:使用 `ARRAY(Float)` 类型](#1. 修改 models.py:使用 ARRAY(Float) 类型)
        • [2. 修改 `feature_extractor.py`:返回 List[float]](#2. 修改 feature_extractor.py:返回 List[float])
        • [3. 修改 `init_db.py`:适配新类型](#3. 修改 init_db.py:适配新类型)
        • [4. 修改 `main.py`:移除 pickle 反序列化](#4. 修改 main.py:移除 pickle 反序列化)
      • [2.4.2 重置迁移与数据初始化](#2.4.2 重置迁移与数据初始化)
      • [2.4.3 全链路测试](#2.4.3 全链路测试)

一、任务概述

在前序的十篇博客中,我们一步步从零构建了一个包含完整前后端的"全球证件智能识别系统"。目前,我们的后端服务运行在开发环境中,使用SQLite这种轻量级的文件数据库,本篇博客将完成数据库的迁移,完成从SQLite到PostgreSQL的过度。

二、数据库升级:从 SQLite 到 PostgreSQL

2.1 为什么要换 PostgreSQL?

虽然SQLite对原型开发很友好,但PostgreSQL在生产环境具有压倒性优势:

  • 高并发支持:采用多进程架构,支持大量客户端同时读写。
  • 数据类型丰富:原生支持数组(Array)和 JSONB,非常适合存储我们的特征向量(无需 pickle 序列化为 bytes,可以直接存为浮点数数组)和结构化 OCR 结果。
  • 生态强大 :配合 pgvector 插件,可以直接在数据库层面进行向量相似度搜索(虽然本项目目前在内存中计算,但未来扩展性极佳)。

2.2 在 Ubuntu 上安装 PostgreSQL

首先,我们需要在开发机(Ubuntu 22.04)上安装 PostgreSQL 数据库服务。

  1. 更新包列表并安装

    bash 复制代码
    sudo apt update
    sudo apt install postgresql postgresql-contrib
  2. 启动服务并设置开机自启

    bash 复制代码
    sudo systemctl start postgresql
    sudo systemctl enable postgresql
  3. 配置用户和数据库

    PostgreSQL 安装后默认会创建一个名为 postgres 的系统用户。我们需要切换到该用户并进入数据库控制台。

    bash 复制代码
    # 切换到 postgres 用户
    sudo -i -u postgres
    
    # 进入数据库控制台
    psql

    postgres=# 提示符下,执行 SQL 命令来设置密码并创建数据库:

    sql 复制代码
    -- 1. 修改默认用户 postgres 的密码 (请将 'mysecretpassword' 替换为你的强密码)
    ALTER USER postgres PASSWORD 'mysecretpassword';
    
    -- 2. 创建项目专用数据库
    CREATE DATABASE card_db;
    
    -- 3. 退出控制台
    \q

    退出 postgres 用户身份:

    bash 复制代码
    exit
  4. 更新环境变量

    最后,为了让代码连接到这个本地数据库,我们需要设置 DATABASE_URL 环境变量。
    在当前终端执行(或添加到 ~/.bashrc):

    bash 复制代码
    export DATABASE_URL="postgresql://postgres:mysecretpassword@localhost:5432/card_db"

2.3 安装适配FastAPI的PostgreSQL驱动库

在 FastAPI 项目中连接 PostgreSQL,需要安装 psycopg2 驱动。

bash 复制代码
pip install psycopg2-binary

接下来需要修改 database.py,使其能够根据环境变量动态切换连接地址。这样既保留了本地开发的灵活性(连本地库),又适应了 Docker 部署(连容器库)。

代码清单:database.py

python 复制代码
import os
from sqlmodel import create_engine, Session

# 从环境变量中读取数据库连接 URL
# 如果环境变量未设置,默认使用 PostgreSQL 的本地连接字符串(开发环境)
# 格式: postgresql://user:password@host:port/dbname
DATABASE_URL = os.getenv(
    "DATABASE_URL", 
    "postgresql://postgres:mysecretpassword@localhost:5432/card_db"
)

# 创建数据库引擎
# 注意:PostgreSQL 不需要 check_same_thread 参数,那是 SQLite 特有的
engine = create_engine(DATABASE_URL, echo=False)

def get_session():
    """
    FastAPI 依赖注入函数,用于获取数据库会话
    """
    with Session(engine) as session:
        yield session

最后,修改Alembic 的配置文件 alembic.ini ,该文件通常包含硬编码的数据库 URL。为了让它也能读取环境变量,我们需要修改 alembic/env.py 文件。

代码清单:alembic/env.py

找到 run_migrations_online 函数,修改 connectable 的获取方式:

python 复制代码
# ... (原有导入)
import os
from database import DATABASE_URL # <-- 导入我们在database.py中定义的URL

# ...

def run_migrations_online() -> None:
    """Run migrations in 'online' mode."""
    
    # --- 修改开始:使用代码中配置的 URL 覆盖 alembic.ini 中的配置 ---
    configuration = config.get_section(config.config_ini_section)
    configuration["sqlalchemy.url"] = DATABASE_URL
    
    connectable = engine_from_config(
        configuration,
        prefix="sqlalchemy.",
        poolclass=pool.NullPool,
    )
    # --- 修改结束 ---

    with connectable.connect() as connection:
        context.configure(
            connection=connection, target_metadata=target_metadata
        )

        with context.begin_transaction():
            context.run_migrations()

这样,无论在什么环境下运行 alembic upgrade head,它都会使用 database.py 中逻辑确定的数据库地址。

2.4 深度改造:PostgreSQL 环境搭建与向量存储优化

在将系统容器化之前,我们需要在本地开发环境(Ubuntu)中基于PostgreSQL,对代码进行一次深度重构:弃用 Python 的 pickle 序列化方式,改用 PostgreSQL 原生的 ARRAY 类型来存储图像特征向量。这将显著提升数据的透明度,并减少编解码开销。

2.4.1 代码重构:原生向量存储

这是本次改造的核心。我们将修改数据模型、特征提取器和检索逻辑,彻底移除 pickle,直接以浮点数数组(Float Array)的形式处理特征向量。

1. 修改 models.py:使用 ARRAY(Float) 类型

我们需要引入 SQLAlchemy 的 PostgreSQL 方言来定义数组列。

代码清单:models.py

python 复制代码
from typing import List, Optional
from sqlmodel import Field, Relationship, SQLModel
# 引入 PostgreSQL 特有的数组类型和 Float 类型
from sqlalchemy import Column, Float
from sqlalchemy.dialects.postgresql import ARRAY

class Country(SQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    name: str = Field(index=True)
    code: str = Field(unique=True)
    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: str = Field()

    # 图像数据保持为 bytes
    image_front_white: bytes = Field(description="正面白光样证图")
    image_front_uv: bytes = Field(description="正面紫外样证图")
    image_back_white: bytes = Field(description="反面白光样证图")
    image_back_uv: bytes = Field(description="反面紫外样证图")

    # --- 核心修改:特征向量改为浮点数数组 ---
    # sa_column=Column(ARRAY(Float)) 告诉数据库这是一个浮点数数组列
    # Python 侧对应的数据类型是 List[float]
    feature_front_white: List[float] = Field(sa_column=Column(ARRAY(Float)))
    feature_front_uv: List[float] = Field(sa_column=Column(ARRAY(Float)))
    feature_back_white: List[float] = Field(sa_column=Column(ARRAY(Float)))
    feature_back_uv: List[float] = Field(sa_column=Column(ARRAY(Float)))

    country_id: Optional[int] = Field(default=None, foreign_key="country.id")
    country: Optional[Country] = Relationship(back_populates="certificate_templates")
2. 修改 feature_extractor.py:返回 List[float]

移除 pickle 序列化,直接返回 Python 列表。

代码清单:feature_extractor.py (仅展示修改的方法)

python 复制代码
# ... (保留 Imports, 注意移除 pickle) ...
# import pickle  <-- 删除此行

class ImageFeatureExtractor:
    # ... (__init__ 保持不变) ...

    def extract_features(self, image_bytes: bytes) -> list[float]:
        """
        接收图像二进制数据,返回浮点数特征列表。
        """
        # "去色"处理等逻辑保持不变
        image = Image.open(io.BytesIO(image_bytes)).convert("L").convert("RGB")
        input_tensor = self.preprocess(image)
        input_batch = input_tensor.unsqueeze(0).to(self.device)

        with torch.no_grad():
            output_features = self.model(input_batch)

        feature_np = output_features.cpu().numpy().flatten()
        
        # --- 修改:直接转换为 Python List 返回 ---
        return feature_np.tolist()
3. 修改 init_db.py:适配新类型

由于 init_db.py 只是负责数据搬运,且 extractor 返回类型已变更为 List[float]models.py 也接受 List[float],因此该文件几乎不需要修改逻辑

注意 :请确保 init_db.py 顶部的导入中不再包含 pickle ,且在 extract_features 返回空值时(例如文件不存在),应赋予默认空列表 [] 而不是 b''

代码片段修正建议:

python 复制代码
# ...
print("      - 正在提取特征向量...")
# 如果图像存在则提取,否则返回空列表 []
feature_front_white = extractor.extract_features(front_white_bytes) if front_white_bytes else []
# ... (其他字段同理)
4. 修改 main.py:移除 pickle 反序列化

在检索逻辑中,从数据库取出的数据已经是 List,我们只需要将其转回 Numpy Array 即可进行余弦相似度计算。

代码清单:main.py (修改 recognize_document 部分)

python 复制代码
# ... (保留 Imports, 注意移除 pickle) ...
# import pickle <-- 删除此行

# ...

@app.post("/api/recognize", ...)
async def recognize_document(...):
    # ...
    
    # 1. 提取待查询图像的特征 (extractor 现在返回 List[float])
    # 我们需要将其转换为 numpy array 以进行数学计算
    query_feature_front = np.array(extractor.extract_features(front_white_bytes))
    query_feature_back = np.array(extractor.extract_features(back_white_bytes))

    # ... (数据库检索逻辑不变) ...

    for template in templates:
        # --- 修改:直接从对象属性获取 List,并转为 Numpy Array ---
        # 数据库已经帮我们把 ARRAY(Float) 转回了 Python List
        template_feature_front = np.array(template.feature_front_white)
        template_feature_back = np.array(template.feature_back_white)

        # 相似度计算逻辑保持不变 (cosine_similarity 接收 numpy array)
        sim_front = cosine_similarity(query_feature_front, template_feature_front)
        sim_back = cosine_similarity(query_feature_back, template_feature_back)
        
        # ... (后续逻辑不变)
    # 5. 二次校验:紫外荧光图像相似度检查
    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:
            # 提取待查询图像的紫外特征
            # --- 修改:直接转为 Numpy Array ---
            query_feature_uv_front = np.array(extractor.extract_features(query_uv_front_bytes))
            query_feature_uv_back = np.array(extractor.extract_features(query_uv_back_bytes))
            
            # 反序列化样证的紫外特征
            # --- 修改:直接转为 Numpy Array ---
            template_feature_uv_front = np.array(best_match_template.feature_front_uv)
            template_feature_uv_back = np.array(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.80:  # 境外证真伪相似度阈值
                uv_message = "该证存疑,请仔细核查"     
        else:
            avg_uv_similarity = 0.85

2.4.2 重置迁移与数据初始化

由于我们彻底改变了底层数据类型(从 Bytes 变为 Array),且数据库引擎从 SQLite 换成了 PostgreSQL,建议重新生成迁移文件以避免兼容性问题。

  1. 清理旧的迁移记录

    删除项目目录下的 alembic/versions 文件夹内的所有 .py 文件。

    删除旧的 database.db 文件(如果存在)。

  2. 生成新的迁移脚本

    bash 复制代码
    alembic revision --autogenerate -m "init_postgres_array"

    然后在生成的py文件头部添加代码:

    bash 复制代码
    import sqlmodel
  3. 应用迁移到 PostgreSQL

    bash 复制代码
    alembic upgrade head

    此时,Alembic 会在 card_db 中创建表,且特征列的类型为 double precision[]

  4. 初始化数据

    运行脚本,将特征向量计算并以数组形式写入 PostgreSQL。

    bash 复制代码
    python init_db.py

2.4.3 全链路测试

一切就绪,让我们验证这个更先进的数据库架构。

  1. 启动后端

    bash 复制代码
    uvicorn main:app --host 0.0.0.0 --port 8001
  2. 启动客户端并测试

    打开 Qt 客户端,选择一个国外证件(例如"韩国驾照"),点击识别。

观察点

  • 后端日志:不应出现 pickle 相关的错误。

  • 识别速度:理论上与之前持平,但省去了序列化步骤,CPU 开销略有降低。

  • 数据库检查 :使用 psql 或 DBeaver 查看数据库:
    参照前面的方法切换到当前的 postgres=# 提示符下,输入以下命令并回车:
    \c card_db,然后使用\dt命令可以查看所有的表。
    最后输入下面的命令:

    sql 复制代码
    SELECT feature_front_white FROM certificatetemplate LIMIT 1;

    应该能看到清晰的浮点数数组 {0.123, -0.456, ...},而不是乱码般的二进制数据。

至此,我们成功完成了后端架构向生产级数据库 PostgreSQL 的迁移,并实现了特征向量的原生存储。


相关推荐
SmartRadio14 小时前
在CH585M代码中如何精细化配置PMU(电源管理单元)和RAM保留
linux·c语言·开发语言·人工智能·单片机·嵌入式硬件·lora
旦莫14 小时前
Pytest教程:Pytest与主流测试框架对比
人工智能·python·pytest
●VON14 小时前
从模型到价值:MLOps 工程体系全景解析
人工智能·学习·制造·von
智慧地球(AI·Earth)14 小时前
Codex配置问题解析:wire_api格式不匹配导致的“Reconnecting...”循环
开发语言·人工智能·vscode·codex·claude code
GISer_Jing14 小时前
AI:多智能体协作与记忆管理
人工智能·设计模式·aigc
kupeThinkPoem14 小时前
QJsonObject能否嵌套查找?
qt·json
qq_4112624214 小时前
纯图像传感器(只出像素),还是 Himax WiseEye/WE1/WE-I Plus 这类带处理器、能在端侧跑模型并输出“metadata”的模块
人工智能·嵌入式硬件·esp32·四博智联
InfiSight智睿视界15 小时前
门店智能体技术如何破解美容美发连锁的“标准执行困境”
大数据·运维·人工智能
Toky丶15 小时前
【文献阅读】BitNet Distillation
人工智能
LaughingZhu15 小时前
Product Hunt 每日热榜 | 2026-01-09
人工智能·经验分享·神经网络·搜索引擎·产品运营