feat: integrate PP-OCRv6 model components and update configurations for detection and recognition

This commit is contained in:
myhloli
2026-06-15 18:59:14 +08:00
parent 8bbac9ca02
commit 2e4be2b261
13 changed files with 19600 additions and 70 deletions
+4 -3
View File
@@ -162,8 +162,9 @@ class PytorchPaddleOCR(TextSystem):
device = get_device()
if device == 'cpu':
if self.lang in ['ch', 'ch_server', 'japan', 'chinese_cht']:
# logger.warning("The current device in use is CPU. To ensure the speed of parsing, the language is automatically switched to ch_lite.")
# 显式指定 ch_server 时保留服务模型配置,避免 v6 medium rec 在 CPU 环境被静默降级。
if self.lang in ['ch', 'japan', 'chinese_cht']:
# logger.warning("CPU device switches this language to ch_lite for parsing speed.")
self.lang = 'ch_lite'
elif self.lang in ['seal']:
self.lang = 'seal_lite'
@@ -292,7 +293,7 @@ class PytorchPaddleOCR(TextSystem):
tqdm_progress_bar=None,
):
assert isinstance(img, (np.ndarray, list, str, bytes))
if isinstance(img, list) and det == True:
if isinstance(img, list) and det:
logger.error('When input a list of images, det must be false')
exit(0)
img = check_img(img)
+41 -3
View File
@@ -1,6 +1,9 @@
# Copyright (c) Opendatalab. All rights reserved.
import os
from pathlib import Path
import torch
from .modeling.architectures.base_model import BaseModel
class BaseOCRV20:
@@ -13,13 +16,47 @@ class BaseOCRV20:
def build_net(self, **kwargs):
self.net = BaseModel(self.config, **kwargs)
@staticmethod
def _is_safetensors_path(weights_path):
"""判断权重文件是否为 safetensors 格式。"""
return Path(weights_path).suffix == ".safetensors"
@staticmethod
def _load_weight_file(weights_path):
"""根据文件后缀选择 safetensors 或 torch 原生加载方式。"""
if BaseOCRV20._is_safetensors_path(weights_path):
from safetensors.torch import load_file
return load_file(str(weights_path), device="cpu")
try:
return torch.load(weights_path, map_location="cpu", weights_only=True)
except TypeError:
return torch.load(weights_path, map_location="cpu")
@staticmethod
def _normalize_ppocrv6_state_dict(weights, weights_path):
"""归一化 HF OCR safetensors 的外层 `model.` 前缀。"""
if not BaseOCRV20._is_safetensors_path(weights_path):
return weights
if not any(key.startswith("model.") for key in weights.keys()):
return weights
return {
key.removeprefix("model."): value
for key, value in weights.items()
}
def read_pytorch_weights(self, weights_path):
"""读取 PyTorch OCR 权重,并兼容 PP-OCRv6 safetensors。"""
if not os.path.exists(weights_path):
raise FileNotFoundError('{} is not existed.'.format(weights_path))
weights = torch.load(weights_path)
return weights
weights = self._load_weight_file(weights_path)
return self._normalize_ppocrv6_state_dict(weights, weights_path)
def get_out_channels(self, weights):
"""从权重结构推断识别输出通道数。"""
if "head.head.weight" in weights:
# PP-OCRv6 safetensors 的识别分类层固定命名为 head.head。
return weights["head.head.weight"].shape[0]
if list(weights.keys())[-1].endswith('.weight') and len(list(weights.values())[-1].shape) == 2:
out_channels = list(weights.values())[-1].numpy().shape[1]
else:
@@ -31,7 +68,8 @@ class BaseOCRV20:
# print('weights is loaded.')
def load_pytorch_weights(self, weights_path):
self.net.load_state_dict(torch.load(weights_path, weights_only=True))
"""加载 PyTorch OCR 权重,按后缀兼容 safetensors。"""
self.net.load_state_dict(self.read_pytorch_weights(weights_path))
# print('model is loaded: {}'.format(weights_path))
def inference(self, inputs):
@@ -20,39 +20,34 @@ def build_backbone(config, model_type):
from .det_mobilenet_v3 import MobileNetV3
from .rec_hgnet import PPHGNet_small
from .rec_lcnetv3 import PPLCNetV3
from .rec_lcnetv4 import PPLCNetV4
from .rec_pphgnetv2 import PPHGNetV2_B4
support_dict = [
"MobileNetV3",
"ResNet",
"ResNet_vd",
"ResNet_SAST",
"PPLCNetV3",
"PPHGNet_small",
'PPHGNetV2_B4',
]
support_dict = {
"MobileNetV3": MobileNetV3,
"PPLCNetV3": PPLCNetV3,
"PPLCNetV4": PPLCNetV4,
"PPHGNet_small": PPHGNet_small,
"PPHGNetV2_B4": PPHGNetV2_B4,
}
elif model_type == "rec" or model_type == "cls":
from .rec_hgnet import PPHGNet_small
from .rec_lcnetv3 import PPLCNetV3
from .rec_lcnetv4 import PPLCNetV4
from .rec_mobilenet_v3 import MobileNetV3
from .rec_svtrnet import SVTRNet
from .rec_mv1_enhance import MobileNetV1Enhance
from .rec_pphgnetv2 import PPHGNetV2_B4, PPHGNetV2_B6_Formula
support_dict = [
"MobileNetV1Enhance",
"MobileNetV3",
"ResNet",
"ResNetFPN",
"MTB",
"ResNet31",
"SVTRNet",
"ViTSTR",
"DenseNet",
"PPLCNetV3",
"PPHGNet_small",
"PPHGNetV2_B4",
"PPHGNetV2_B6_Formula"
]
support_dict = {
"MobileNetV1Enhance": MobileNetV1Enhance,
"MobileNetV3": MobileNetV3,
"SVTRNet": SVTRNet,
"PPLCNetV3": PPLCNetV3,
"PPLCNetV4": PPLCNetV4,
"PPHGNet_small": PPHGNet_small,
"PPHGNetV2_B4": PPHGNetV2_B4,
"PPHGNetV2_B6_Formula": PPHGNetV2_B6_Formula,
}
else:
raise NotImplementedError
@@ -62,5 +57,5 @@ def build_backbone(config, model_type):
model_type, support_dict
)
)
module_class = eval(module_name)(**config)
module_class = support_dict[module_name](**config)
return module_class
@@ -0,0 +1,311 @@
# Copyright (c) Opendatalab. All rights reserved.
import torch
import torch.nn.functional as F
from torch import nn
NET_CONFIG_DET = {
"small": {
"stem_channels": [3, 24, 48],
"block_configs": [
[[3, 48, 48, 1, True], [3, 48, 48, 1, False]],
[[3, 48, 96, 2, False], [3, 96, 96, 1, True], [3, 96, 96, 1, False]],
[
[3, 96, 192, 2, False],
[3, 192, 192, 1, True],
[3, 192, 192, 1, False],
[3, 192, 192, 1, True],
[3, 192, 192, 1, False],
],
[[3, 192, 384, 2, False], [3, 384, 384, 1, True], [3, 384, 384, 1, False]],
],
},
}
NET_CONFIG_REC = {
"small": {
"stem_channels": [3, 48, 96],
"block_configs": [
[[3, 96, 96, 1, True]],
[[3, 96, 96, 1, False], [3, 96, 96, 1, False]],
[
[3, 96, 192, (2, 1), False],
[3, 192, 192, 1, True],
[3, 192, 192, 1, False],
[3, 192, 192, 1, True],
[3, 192, 192, 1, False],
[3, 192, 192, 1, True],
[3, 192, 192, 1, False],
],
[[3, 192, 384, (2, 1), False], [3, 384, 384, 1, True], [3, 384, 384, 1, False]],
],
},
"medium": {
"stem_channels": [3, 64, 128],
"block_configs": [
[[3, 128, 128, 1, True]],
[[3, 128, 256, 1, False], [3, 256, 256, 1, False], [3, 256, 256, 1, True]],
[
[3, 256, 512, (2, 1), False],
[3, 512, 512, 1, True],
[3, 512, 512, 1, False],
[3, 512, 512, 1, True],
[3, 512, 512, 1, False],
[3, 512, 512, 1, True],
[3, 512, 512, 1, False],
],
[[3, 512, 768, (2, 1), False], [3, 768, 768, 1, True], [3, 768, 768, 1, False]],
],
},
}
def _build_activation(name):
"""按 PP-OCRv6 配置创建无参数激活层,便于和 safetensors 权重命名保持解耦。"""
if name is None:
return nn.Identity()
if name == "relu":
return nn.ReLU()
if name == "gelu":
return nn.GELU()
if name in {"silu", "swish"}:
return nn.SiLU()
if name == "hardsigmoid":
return nn.Hardsigmoid()
raise ValueError(f"Unsupported activation: {name}")
def _to_stride(stride):
"""把 Paddle/Transformers 配置里的 stride 统一成 PyTorch 可接受的格式。"""
if isinstance(stride, list):
return tuple(stride)
return stride
class PPLCNetV4ConvLayer(nn.Module):
"""PP-LCNetV4 的 Conv-BN-Act 基础层,属性名对齐 HF 权重。"""
def __init__(
self,
in_channels,
out_channels,
kernel_size,
stride=1,
groups=1,
activation="relu",
):
"""初始化卷积、归一化和激活层。"""
super().__init__()
self.convolution = nn.Conv2d(
in_channels,
out_channels,
kernel_size=kernel_size,
stride=_to_stride(stride),
groups=groups,
padding=(kernel_size - 1) // 2,
bias=False,
)
self.normalization = nn.BatchNorm2d(out_channels)
self.activation = _build_activation(activation)
def forward(self, hidden_states):
"""执行 Conv-BN-Act 前向计算。"""
hidden_states = self.convolution(hidden_states)
hidden_states = self.normalization(hidden_states)
hidden_states = self.activation(hidden_states)
return hidden_states
class PPLCNetV4SqueezeExcitationModule(nn.Module):
"""PP-LCNetV4 的 SE 模块,保留 `convolutions.0/2` 权重命名。"""
def __init__(self, channel, reduction=4):
"""初始化全局池化和两层 1x1 卷积。"""
super().__init__()
self.avg_pool = nn.AdaptiveAvgPool2d(1)
self.convolutions = nn.ModuleList(
[
nn.Conv2d(channel, channel // reduction, kernel_size=1, stride=1, padding=0, bias=True),
nn.ReLU(),
nn.Conv2d(channel // reduction, channel, kernel_size=1, stride=1, padding=0, bias=True),
nn.Hardsigmoid(),
]
)
def forward(self, hidden_states):
"""根据通道注意力缩放输入特征。"""
residual = hidden_states
hidden_states = self.avg_pool(hidden_states)
for layer in self.convolutions:
hidden_states = layer(hidden_states)
return residual * hidden_states
class PPLCNetV4LargeStem(nn.Module):
"""PP-LCNetV4 branch stem,属性名对齐 `encoder.convolution.stem*`。"""
def __init__(self, stem_channels):
"""初始化 v6 small/medium 使用的分支 stem。"""
super().__init__()
self.stem1 = PPLCNetV4ConvLayer(stem_channels[0], stem_channels[1], kernel_size=3, stride=2)
self.stem2a = PPLCNetV4ConvLayer(stem_channels[1], stem_channels[1] // 2, kernel_size=2, stride=1)
self.stem2b = PPLCNetV4ConvLayer(stem_channels[1] // 2, stem_channels[1], kernel_size=2, stride=1)
self.stem3 = PPLCNetV4ConvLayer(stem_channels[1] * 2, stem_channels[1], kernel_size=3, stride=2)
self.stem4 = PPLCNetV4ConvLayer(stem_channels[1], stem_channels[2], kernel_size=1, stride=1)
self.pool = nn.MaxPool2d(kernel_size=2, stride=1, ceil_mode=True)
def forward(self, pixel_values):
"""执行分支 stem 的 pad、pool 和 concat 流程。"""
embedding = self.stem1(pixel_values)
embedding = F.pad(embedding, (0, 1, 0, 1))
emb_stem_2a = self.stem2a(embedding)
emb_stem_2a = F.pad(emb_stem_2a, (0, 1, 0, 1))
emb_stem_2a = self.stem2b(emb_stem_2a)
pooled_emb = self.pool(embedding)
embedding = torch.cat([pooled_emb, emb_stem_2a], dim=1)
embedding = self.stem3(embedding)
embedding = self.stem4(embedding)
return embedding
class PPLCNetV4DepthwiseSeparableConvLayer(nn.Module):
"""PP-LCNetV4 block 中的 token mixer 和 channel mixer。"""
def __init__(
self,
in_channels,
out_channels,
stride,
kernel_size,
use_squeeze_excitation,
reduction=4,
):
"""按 v6 配置初始化深度卷积、SE 和两层 point-wise 卷积。"""
super().__init__()
self.has_residual = in_channels == out_channels and stride == 1
self.use_rep_dw = stride == 1 and in_channels == out_channels
if self.use_rep_dw:
self.token_conv = nn.Conv2d(
in_channels=in_channels,
out_channels=out_channels,
kernel_size=kernel_size,
stride=1,
padding=kernel_size // 2,
groups=in_channels,
bias=True,
)
else:
self.token_conv = PPLCNetV4ConvLayer(
in_channels=in_channels,
out_channels=in_channels,
kernel_size=kernel_size,
stride=stride,
groups=in_channels,
activation=None,
)
self.token_squeeze_excitation = (
PPLCNetV4SqueezeExcitationModule(in_channels, reduction) if use_squeeze_excitation else nn.Identity()
)
self.channel_conv1 = PPLCNetV4ConvLayer(
in_channels=in_channels,
out_channels=in_channels * 2,
kernel_size=1,
stride=1,
activation=None,
)
self.channel_act_fn = nn.GELU()
self.channel_conv2 = PPLCNetV4ConvLayer(
in_channels=in_channels * 2,
out_channels=out_channels,
kernel_size=1,
stride=1,
activation=None,
)
def forward(self, hidden_states):
"""执行 token mixing、channel mixing 和可选残差连接。"""
hidden_states = self.token_conv(hidden_states)
hidden_states = self.token_squeeze_excitation(hidden_states)
residual = hidden_states
hidden_states = self.channel_conv1(hidden_states)
hidden_states = self.channel_act_fn(hidden_states)
hidden_states = self.channel_conv2(hidden_states)
if self.has_residual:
hidden_states = residual + hidden_states
return hidden_states
class PPLCNetV4Block(nn.Module):
"""PP-LCNetV4 的一个 stage,内部包含多个 depthwise separable block。"""
def __init__(self, block_configs):
"""根据 stage 配置创建 block 列表。"""
super().__init__()
self.blocks = nn.ModuleList()
for kernel_size, in_channels, out_channels, stride, use_squeeze_excitation in block_configs:
self.blocks.append(
PPLCNetV4DepthwiseSeparableConvLayer(
in_channels=in_channels,
out_channels=out_channels,
kernel_size=kernel_size,
stride=_to_stride(stride),
use_squeeze_excitation=use_squeeze_excitation,
)
)
def forward(self, hidden_states):
"""顺序执行当前 stage 的所有 block。"""
for block in self.blocks:
hidden_states = block(hidden_states)
return hidden_states
class PPLCNetV4Encoder(nn.Module):
"""PP-LCNetV4 编码器,保留 `encoder.convolution` 和 `encoder.blocks` 命名。"""
def __init__(self, stem_channels, block_configs):
"""初始化 stem 和四个 stage。"""
super().__init__()
self.convolution = PPLCNetV4LargeStem(stem_channels)
self.blocks = nn.ModuleList([PPLCNetV4Block(stage_configs) for stage_configs in block_configs])
def forward(self, pixel_values):
"""返回四个 stage 的输出特征,供 det/rec 上层按需使用。"""
hidden_states = self.convolution(pixel_values)
feature_maps = []
for block in self.blocks:
hidden_states = block(hidden_states)
feature_maps.append(hidden_states)
return feature_maps
class PPLCNetV4(nn.Module):
"""PP-OCRv6 使用的 PPLCNetV4 backbone,支持 det small 和 rec small/medium。"""
def __init__(self, det=False, model_size="small", in_channels=3, **kwargs):
"""按 det/rec 模式选择 v6 的固定网络配置。"""
super().__init__()
self.det = det
if in_channels != 3:
raise ValueError(f"PPLCNetV4 only supports 3 input channels, got {in_channels}.")
config_dict = NET_CONFIG_DET if det else NET_CONFIG_REC
if model_size not in config_dict:
mode = "det" if det else "rec"
raise ValueError(f"PPLCNetV4 {mode} model_size must be one of {list(config_dict)}, got {model_size}.")
config = config_dict[model_size]
self.encoder = PPLCNetV4Encoder(config["stem_channels"], config["block_configs"])
stage_out_channels = [stage[-1][2] for stage in config["block_configs"]]
self.out_channels = stage_out_channels if det else stage_out_channels[-1]
def forward(self, x):
"""det 返回四级特征列表,rec 返回高度池化后的识别特征。"""
feature_maps = self.encoder(x)
if self.det:
return feature_maps
x = feature_maps[-1]
if self.training:
return F.adaptive_avg_pool2d(x, [1, 40])
if x.shape[2] < 3:
raise ValueError(f"Feature height {x.shape[2]} < pool kernel 3.")
return F.avg_pool2d(x, [3, 2])
@@ -49,6 +49,49 @@ class Head(nn.Module):
return x
class PPOCRV6DBConvBatchnormLayer(nn.Module):
"""PP-OCRv6 DBHead 使用的 Conv-BN-Act 基础层,命名对齐 safetensors。"""
def __init__(
self,
in_channels,
out_channels,
kernel_size,
stride=1,
padding=1,
activation="relu",
bias=False,
convolution_transpose=False,
):
"""初始化普通卷积或反卷积、BN 和激活层。"""
super().__init__()
if convolution_transpose:
self.convolution = nn.ConvTranspose2d(
in_channels=in_channels,
out_channels=out_channels,
kernel_size=kernel_size,
stride=stride,
)
else:
self.convolution = nn.Conv2d(
in_channels=in_channels,
out_channels=out_channels,
kernel_size=kernel_size,
stride=stride,
padding=padding,
bias=bias,
)
self.norm = nn.BatchNorm2d(out_channels)
self.act_fn = nn.ReLU() if activation == "relu" else nn.Identity()
def forward(self, hidden_states):
"""执行 DBHead v6 分支的卷积、BN 和激活。"""
hidden_states = self.convolution(hidden_states)
hidden_states = self.norm(hidden_states)
hidden_states = self.act_fn(hidden_states)
return hidden_states
class DBHead(nn.Module):
"""
Differentiable Binarization (DB) for text detection:
@@ -57,24 +100,51 @@ class DBHead(nn.Module):
params(dict): super parameters for build DB network
"""
def __init__(self, in_channels, k=50, **kwargs):
def __init__(self, in_channels, k=50, mode=None, kernel_list=None, fix_nan=False, **kwargs):
"""初始化 DBHeadv6 模式使用 safetensors 对齐的三层上采样 head。"""
super(DBHead, self).__init__()
self.k = k
binarize_name_list = [
'conv2d_56', 'batch_norm_47', 'conv2d_transpose_0', 'batch_norm_48',
'conv2d_transpose_1', 'binarize'
]
thresh_name_list = [
'conv2d_57', 'batch_norm_49', 'conv2d_transpose_2', 'batch_norm_50',
'conv2d_transpose_3', 'thresh'
]
self.binarize = Head(in_channels, **kwargs)# binarize_name_list)
self.thresh = Head(in_channels, **kwargs)#thresh_name_list)
self.mode = mode
self.fix_nan = fix_nan
if mode == "ppocrv6":
kernel_list = kernel_list or [3, 2, 2]
self.conv_down = PPOCRV6DBConvBatchnormLayer(
in_channels=in_channels,
out_channels=in_channels // 4,
kernel_size=kernel_list[0],
padding=int(kernel_list[0] // 2),
)
self.conv_up = PPOCRV6DBConvBatchnormLayer(
in_channels=in_channels // 4,
out_channels=in_channels // 4,
kernel_size=kernel_list[1],
stride=2,
convolution_transpose=True,
)
self.conv_final = nn.ConvTranspose2d(
in_channels=in_channels // 4,
out_channels=1,
kernel_size=kernel_list[2],
stride=2,
)
return
self.binarize = Head(in_channels, **kwargs)
self.thresh = Head(in_channels, **kwargs)
def step_function(self, x, y):
"""计算 DB 二值化近似阶跃函数。"""
return torch.reciprocal(1 + torch.exp(-self.k * (x - y)))
def forward(self, x):
"""推理时返回统一的 `maps` 字段,兼容现有 OCR-det 后处理。"""
if self.mode == "ppocrv6":
shrink_maps = self.conv_down(x)
shrink_maps = self.conv_up(shrink_maps)
shrink_maps = self.conv_final(shrink_maps)
shrink_maps = torch.sigmoid(shrink_maps)
if self.fix_nan:
shrink_maps = torch.nan_to_num(shrink_maps)
return {'maps': shrink_maps}
shrink_maps = self.binarize(x)
return {'maps': shrink_maps}
@@ -107,4 +177,4 @@ class PFHeadLocal(DBHead):
base_maps = shrink_maps
cbn_maps = self.cbn_layer(self.up_conv(f), shrink_maps, None)
cbn_maps = F.sigmoid(cbn_maps)
return {'maps': 0.5 * (base_maps + cbn_maps), 'cbn_maps': cbn_maps}
return {'maps': 0.5 * (base_maps + cbn_maps), 'cbn_maps': cbn_maps}
@@ -1,7 +1,8 @@
# Copyright (c) Opendatalab. All rights reserved.
import torch.nn.functional as F
from torch import nn
from ..necks.rnn import Im2Seq, SequenceEncoder
from ..necks.rnn import EncoderWithLightSVTR, Im2Seq, SequenceEncoder
from .rec_ctc_head import CTCHead
@@ -21,8 +22,10 @@ class FCTranspose(nn.Module):
class MultiHead(nn.Module):
def __init__(self, in_channels, out_channels_list, **kwargs):
"""初始化多头识别 Headv6 LightSVTR 分支使用 HF safetensors 命名。"""
super().__init__()
self.head_list = kwargs.pop("head_list")
self.use_light_svtr_head = False
self.gtc_head = "sar"
assert len(self.head_list) >= 2
@@ -38,22 +41,37 @@ class MultiHead(nn.Module):
self.encoder_reshape = Im2Seq(in_channels)
neck_args = self.head_list[idx][name]["Neck"]
encoder_type = neck_args.pop("name")
self.ctc_encoder = SequenceEncoder(
in_channels=in_channels, encoder_type=encoder_type, **neck_args
)
# ctc head
head_args = self.head_list[idx][name].get("Head", {})
if head_args is None:
head_args = {}
if encoder_type == "lightsvtr":
# v6 safetensors 中 CTC 分支直接命名为 head.encoder/head.head。
self.encoder = EncoderWithLightSVTR(in_channels=in_channels, **neck_args)
self.head = nn.Linear(self.encoder.out_channels, out_channels_list["CTCLabelDecode"], bias=True)
self.out_channels = out_channels_list["CTCLabelDecode"]
self.use_light_svtr_head = True
else:
self.ctc_encoder = SequenceEncoder(
in_channels=in_channels, encoder_type=encoder_type, **neck_args
)
# ctc head
head_args = self.head_list[idx][name].get("Head", {})
if head_args is None:
head_args = {}
self.ctc_head = CTCHead(
in_channels=self.ctc_encoder.out_channels,
out_channels=out_channels_list["CTCLabelDecode"],
**head_args,
)
self.ctc_head = CTCHead(
in_channels=self.ctc_encoder.out_channels,
out_channels=out_channels_list["CTCLabelDecode"],
**head_args,
)
else:
raise NotImplementedError(f"{name} is not supported in MultiHead yet")
def forward(self, x, data=None):
"""根据配置执行 v6 LightSVTR 或历史 CTC 分支。"""
if self.use_light_svtr_head:
ctc_encoder = self.encoder(x)
ctc_encoder = ctc_encoder.squeeze(dim=2).permute(0, 2, 1)
predicts = self.head(ctc_encoder)
if not self.training:
predicts = F.softmax(predicts, dim=2)
return predicts
ctc_encoder = self.ctc_encoder(x)
return self.ctc_head(ctc_encoder)
@@ -16,14 +16,20 @@ __all__ = ["build_neck"]
def build_neck(config):
from .db_fpn import DBFPN, LKPAN, RSEFPN
from .db_fpn import DBFPN, LKPAN, RSEFPN, RepLKFPN
from .rnn import SequenceEncoder
support_dict = ["DBFPN", "SequenceEncoder", "RSEFPN", "LKPAN"]
support_dict = {
"DBFPN": DBFPN,
"SequenceEncoder": SequenceEncoder,
"RSEFPN": RSEFPN,
"LKPAN": LKPAN,
"RepLKFPN": RepLKFPN,
}
module_name = config.pop("name")
assert module_name in support_dict, Exception(
"neck only support {}".format(support_dict)
)
module_class = eval(module_name)(**config)
module_class = support_dict[module_name](**config)
return module_class
@@ -25,7 +25,7 @@ class DSConv(nn.Module):
**kwargs
):
super(DSConv, self).__init__()
if groups == None:
if groups is None:
groups = in_channels
self.if_act = if_act
self.act = act
@@ -285,6 +285,136 @@ class RSEFPN(nn.Module):
return fuse
class RepLKFPNSqueezeExcitationModule(nn.Module):
"""PP-OCRv6 RepLKFPN 使用的轻量 SE 模块,命名对齐 safetensors。"""
def __init__(self, in_channels, reduction, activation="relu"):
"""初始化通道压缩与恢复卷积。"""
super().__init__()
self.avg_pool = nn.AdaptiveAvgPool2d(output_size=1)
self.conv1 = nn.Conv2d(in_channels, in_channels // reduction, kernel_size=1, stride=1, padding=0)
self.conv2 = nn.Conv2d(in_channels // reduction, in_channels, kernel_size=1, stride=1, padding=0)
if activation == "relu":
self.act_fn = nn.ReLU()
else:
raise ValueError(f"Unsupported RepLKFPN SE activation: {activation}")
def forward(self, hidden_states):
"""计算 SE 权重并缩放输入特征。"""
residual = hidden_states
hidden_states = self.avg_pool(hidden_states)
hidden_states = self.conv2(self.act_fn(self.conv1(hidden_states)))
hidden_states = torch.clamp(0.2 * hidden_states + 0.5, min=0.0, max=1.0)
return residual * hidden_states
class RepLKFPNDepthwiseSeparableConvLayer(nn.Module):
"""RepLKFPN 的大核深度卷积 + point-wise 压缩分支。"""
def __init__(self, in_channels, out_channels, kernel_size, reduction):
"""初始化 depthwise、pointwise 和 SE 子层。"""
super().__init__()
self.depthwise_convolution = nn.Conv2d(
in_channels=in_channels,
out_channels=out_channels,
kernel_size=kernel_size,
stride=1,
padding=kernel_size // 2,
groups=in_channels,
bias=True,
)
self.squeeze_excitation_module = RepLKFPNSqueezeExcitationModule(out_channels // 4, reduction)
self.pointwise_convolution = nn.Conv2d(
in_channels=out_channels,
out_channels=out_channels // 4,
kernel_size=1,
bias=False,
)
def forward(self, hidden_states):
"""执行大核 DW 卷积、PW 压缩和 SE 残差增强。"""
hidden_states = self.depthwise_convolution(hidden_states)
hidden_states = self.pointwise_convolution(hidden_states)
hidden_states = hidden_states + self.squeeze_excitation_module(hidden_states)
return hidden_states
class RepLKFPNResidualSqueezeExcitationLayer(nn.Module):
"""RepLKFPN 的输入投影层,属性名对齐 `insert_conv.*` 权重。"""
def __init__(self, in_channels, out_channels, kernel_size, reduction, shortcut=True):
"""初始化输入投影和 SE 分支。"""
super().__init__()
self.in_conv = nn.Conv2d(
in_channels=in_channels,
out_channels=out_channels,
kernel_size=kernel_size,
padding=int(kernel_size // 2),
bias=False,
)
self.squeeze_excitation_block = RepLKFPNSqueezeExcitationModule(out_channels, reduction)
self.shortcut = shortcut
def forward(self, hidden_states):
"""执行 1x1 投影并按配置叠加 SE 输出。"""
hidden_states = self.in_conv(hidden_states)
if self.shortcut:
return hidden_states + self.squeeze_excitation_block(hidden_states)
return self.squeeze_excitation_block(hidden_states)
class RepLKFPN(nn.Module):
"""PP-OCRv6 small det 使用的 RepLKFPN neck。"""
def __init__(self, in_channels, out_channels, shortcut=True, dilated_kernel_size=7, reduction=4, **kwargs):
"""按四级 backbone 通道创建 v6 FPN 投影和大核融合层。"""
super().__init__()
self.out_channels = out_channels
self.interpolate_mode = kwargs.get("interpolate_mode", "nearest")
self.insert_conv = nn.ModuleList()
self.input_conv = nn.ModuleList()
for channels in in_channels:
self.insert_conv.append(
RepLKFPNResidualSqueezeExcitationLayer(
in_channels=channels,
out_channels=out_channels,
kernel_size=1,
reduction=reduction,
shortcut=shortcut,
)
)
self.input_conv.append(
RepLKFPNDepthwiseSeparableConvLayer(
in_channels=out_channels,
out_channels=out_channels,
kernel_size=dilated_kernel_size,
reduction=reduction,
)
)
def forward(self, feature_maps):
"""融合四级特征并返回 DBHead 需要的单张特征图。"""
fused = []
for conv, feature in zip(self.insert_conv, feature_maps):
fused.append(conv(feature))
for idx in range(2, -1, -1):
fused[idx] = fused[idx] + F.interpolate(
fused[idx + 1],
scale_factor=2,
mode=self.interpolate_mode,
)
features = [conv(feat) for conv, feat in zip(self.input_conv, fused)]
processed = []
for feat, scale in zip(features, [1, 2, 4, 8]):
if scale == 1:
processed.append(feat)
else:
processed.append(F.interpolate(feat, scale_factor=scale, mode=self.interpolate_mode))
return torch.cat(processed[::-1], dim=1)
class LKPAN(nn.Module):
def __init__(self, in_channels, out_channels, mode="large", **kwargs):
super(LKPAN, self).__init__()
@@ -200,6 +200,185 @@ class EncoderWithSVTR(nn.Module):
return z
class LightSVTRConvLayer(nn.Module):
"""PP-OCRv6 LightSVTR 使用的 Conv-BN-SiLU 基础层。"""
def __init__(
self,
in_channels,
out_channels,
kernel_size=(3, 3),
activation="silu",
groups=1,
):
"""初始化与 safetensors key 对齐的卷积、归一化和激活层。"""
super().__init__()
if isinstance(kernel_size, int):
kernel_size = (kernel_size, kernel_size)
self.convolution = nn.Conv2d(
in_channels,
out_channels,
kernel_size=kernel_size,
stride=1,
padding=(kernel_size[0] // 2, kernel_size[1] // 2),
bias=False,
groups=groups,
)
self.normalization = nn.BatchNorm2d(out_channels)
self.activation = nn.SiLU() if activation in {"silu", "swish"} else nn.Identity()
def forward(self, hidden_states):
"""执行卷积、BN 和激活。"""
hidden_states = self.convolution(hidden_states)
hidden_states = self.normalization(hidden_states)
hidden_states = self.activation(hidden_states)
return hidden_states
class LightSVTRAttention(nn.Module):
"""LightSVTR 的多头自注意力,属性名对齐 `self_attn.*` 权重。"""
def __init__(self, hidden_size, num_heads=8, qkv_bias=True, attention_dropout=0.1):
"""初始化 qkv 投影和输出投影。"""
super().__init__()
if hidden_size % num_heads != 0:
raise ValueError(f"hidden_size {hidden_size} must be divisible by num_heads {num_heads}.")
self.hidden_size = hidden_size
self.num_heads = num_heads
self.head_dim = hidden_size // num_heads
self.scale = self.head_dim**-0.5
self.attention_dropout = attention_dropout
self.qkv = nn.Linear(hidden_size, 3 * hidden_size, bias=qkv_bias)
self.projection = nn.Linear(hidden_size, hidden_size)
def forward(self, hidden_states):
"""计算全局自注意力并返回投影后的序列特征。"""
batch_size, seq_len, embed_dim = hidden_states.shape
mixed_qkv = self.qkv(hidden_states)
mixed_qkv = mixed_qkv.reshape(batch_size, seq_len, 3, self.num_heads, embed_dim // self.num_heads)
mixed_qkv = mixed_qkv.permute(2, 0, 3, 1, 4)
query_states, key_states, value_states = mixed_qkv[0], mixed_qkv[1], mixed_qkv[2]
attn_weights = torch.matmul(query_states, key_states.transpose(-1, -2)) * self.scale
attn_weights = nn.functional.softmax(attn_weights, dim=-1)
attn_weights = nn.functional.dropout(attn_weights, p=self.attention_dropout, training=self.training)
attn_output = torch.matmul(attn_weights, value_states)
attn_output = attn_output.transpose(1, 2).reshape(batch_size, seq_len, embed_dim).contiguous()
return self.projection(attn_output)
class LightSVTRMLP(nn.Module):
"""LightSVTR block 内的前馈网络,属性名对齐 `mlp.fc*` 权重。"""
def __init__(self, hidden_size, mlp_ratio=4.0, drop_rate=0.1):
"""初始化两层线性层、SiLU 激活和 dropout。"""
super().__init__()
self.fc1 = nn.Linear(hidden_size, int(hidden_size * mlp_ratio))
self.activation = nn.SiLU()
self.fc2 = nn.Linear(int(hidden_size * mlp_ratio), hidden_size)
self.drop = nn.Dropout(drop_rate)
def forward(self, hidden_states):
"""执行 MLP 前向计算。"""
hidden_states = self.fc1(hidden_states)
hidden_states = self.activation(hidden_states)
hidden_states = self.drop(hidden_states)
hidden_states = self.fc2(hidden_states)
hidden_states = self.drop(hidden_states)
return hidden_states
class LightSVTRBlock(nn.Module):
"""LightSVTR 的 Transformer block,属性名对齐 v6 safetensors。"""
def __init__(
self,
hidden_size,
num_heads=8,
qkv_bias=True,
mlp_ratio=4.0,
drop_rate=0.1,
attn_drop_rate=0.1,
layer_norm_eps=1e-6,
):
"""初始化注意力、MLP 和两层 LayerNorm。"""
super().__init__()
self.self_attn = LightSVTRAttention(hidden_size, num_heads, qkv_bias, attn_drop_rate)
self.layer_norm1 = nn.LayerNorm(hidden_size, eps=layer_norm_eps)
self.mlp = LightSVTRMLP(hidden_size, mlp_ratio, drop_rate)
self.layer_norm2 = nn.LayerNorm(hidden_size, eps=layer_norm_eps)
def forward(self, hidden_states):
"""执行 pre-norm attention 和 pre-norm MLP 残差结构。"""
residual = hidden_states
hidden_states = self.layer_norm1(hidden_states)
hidden_states = residual + self.self_attn(hidden_states)
residual = hidden_states
hidden_states = self.layer_norm2(hidden_states)
hidden_states = residual + self.mlp(hidden_states)
return hidden_states
class EncoderWithLightSVTR(nn.Module):
"""PP-OCRv6 使用的 LightSVTR neck,输出仍保持 4D 特征。"""
def __init__(
self,
in_channels,
dims=64,
depth=1,
num_heads=8,
qkv_bias=True,
mlp_ratio=4.0,
drop_rate=0.1,
attn_drop_rate=0.1,
drop_path=0.0,
qk_scale=None,
local_kernel=7,
use_guide=False,
**kwargs,
):
"""初始化 skip/reduce/local conv、LightSVTR block 和归一化层。"""
super().__init__()
self.use_guide = use_guide
self.conv_block = nn.ModuleList(
[
LightSVTRConvLayer(in_channels, dims, kernel_size=(1, 1), activation="silu"),
LightSVTRConvLayer(in_channels, dims, kernel_size=(1, 1), activation="silu"),
LightSVTRConvLayer(dims, dims, kernel_size=(1, local_kernel), activation="silu", groups=dims),
]
)
self.svtr_block = nn.ModuleList(
[
LightSVTRBlock(
hidden_size=dims,
num_heads=num_heads,
qkv_bias=qkv_bias,
mlp_ratio=mlp_ratio,
drop_rate=drop_rate,
attn_drop_rate=attn_drop_rate,
)
for _ in range(depth)
]
)
self.norm = nn.LayerNorm(dims, eps=1e-6)
self.out_channels = dims
def forward(self, x):
"""执行轻量局部卷积增强、全局注意力和 skip 残差融合。"""
if self.use_guide:
x = x.detach()
residual = self.conv_block[0](x)
hidden_states = self.conv_block[1](x)
hidden_states = hidden_states + self.conv_block[2](hidden_states)
batch_size, channels, height, width = hidden_states.shape
hidden_states = hidden_states.flatten(2).permute(0, 2, 1)
for block in self.svtr_block:
hidden_states = block(hidden_states)
hidden_states = self.norm(hidden_states)
hidden_states = hidden_states.reshape(batch_size, height, width, channels).permute(0, 3, 1, 2)
return hidden_states + residual
class SequenceEncoder(nn.Module):
def __init__(self, in_channels, encoder_type, hidden_size=48, **kwargs):
super(SequenceEncoder, self).__init__()
@@ -214,12 +393,13 @@ class SequenceEncoder(nn.Module):
"fc": EncoderWithFC,
"rnn": EncoderWithRNN,
"svtr": EncoderWithSVTR,
"lightsvtr": EncoderWithLightSVTR,
}
assert encoder_type in support_encoder_dict, "{} must in {}".format(
encoder_type, support_encoder_dict.keys()
)
if encoder_type == "svtr":
if encoder_type in ("svtr", "lightsvtr"):
self.encoder = support_encoder_dict[encoder_type](
self.encoder_reshape.out_channels, **kwargs
)
@@ -231,7 +411,7 @@ class SequenceEncoder(nn.Module):
self.only_reshape = False
def forward(self, x):
if self.encoder_type != "svtr":
if self.encoder_type not in ("svtr", "lightsvtr"):
x = self.encoder_reshape(x)
if not self.only_reshape:
x = self.encoder(x)
@@ -92,6 +92,27 @@ ch_PP-OCRv5_det_server_infer:
k: 50
mode: "large"
ch_PP-OCRv6_small_det_infer:
model_type: det
algorithm: DB
Transform: null
Backbone:
name: PPLCNetV4
det: True
model_size: small
Neck:
name: RepLKFPN
out_channels: 96
dilated_kernel_size: 7
shortcut: True
Head:
name: DBHead
k: 50
mode: ppocrv6
fix_nan: True
aux_in_channels: 96
kernel_list: [3, 2, 2]
ch_PP-OCRv4_det_server_infer:
model_type: det
algorithm: DB
@@ -236,6 +257,57 @@ ch_PP-OCRv5_rec_infer:
nrtr_dim: 384
max_text_length: 25
ch_PP-OCRv6_small_rec_infer:
model_type: rec
algorithm: SVTR_LCNet
Transform:
Backbone:
name: PPLCNetV4
model_size: small
Head:
name: MultiHead
out_channels_list:
CTCLabelDecode: 18710
head_list:
- CTCHead:
Neck:
name: lightsvtr
dims: 120
depth: 2
mlp_ratio: 2.0
local_kernel: 7
Head:
fc_decay: 0.00001
- NRTRHead:
nrtr_dim: 384
max_text_length: 25
ch_PP-OCRv6_medium_rec_infer:
model_type: rec
algorithm: SVTR_LCNet
Transform:
Backbone:
name: PPLCNetV4
model_size: medium
Head:
name: MultiHead
out_channels_list:
CTCLabelDecode: 18710
head_list:
- CTCHead:
Neck:
name: lightsvtr
dims: 192
depth: 2
mlp_ratio: 4.0
local_kernel: 7
use_guide: False
Head:
fc_decay: 0.00001
- NRTRHead:
nrtr_dim: 512
max_text_length: 25
ka_PP-OCRv3_rec_infer:
model_type: rec
algorithm: SVTR
@@ -541,4 +613,4 @@ te_PP-OCRv5_rec_infer:
fc_decay: 0.00001
- NRTRHead:
nrtr_dim: 384
max_text_length: 25
max_text_length: 25
File diff suppressed because it is too large Load Diff
@@ -1,12 +1,12 @@
lang:
ch_lite:
det: ch_PP-OCRv5_det_infer.pth
rec: ch_PP-OCRv5_rec_infer.pth
dict: ppocrv5_dict.txt
det: ch_PP-OCRv6_small_det_infer.safetensors
rec: ch_PP-OCRv6_small_rec_infer.safetensors
dict: ppocrv6_dict.txt
ch_server:
det: ch_PP-OCRv5_det_infer.pth
rec: ch_PP-OCRv5_rec_server_infer.pth
dict: ppocrv5_dict.txt
det: ch_PP-OCRv6_small_det_infer.safetensors
rec: ch_PP-OCRv6_medium_rec_infer.safetensors
dict: ppocrv6_dict.txt
ch:
det: ch_PP-OCRv5_det_infer.pth
rec: ch_PP-OCRv4_rec_server_doc_infer.pth
@@ -74,4 +74,4 @@ lang:
seal_lite:
det: seal_PP-OCRv4_det_infer.pth
rec: ch_PP-OCRv4_rec_infer.pth
dict: ppocr_keys_v1.txt
dict: ppocr_keys_v1.txt
+1
View File
@@ -98,6 +98,7 @@ pipeline = [
"torch>=2.6.0,<3",
"torchvision",
"transformers>=4.57.3,<5.0.0",
"safetensors>=0.4.0,<1",
"onnxruntime>1.17.0",
]
gradio = [