From e044c3fa5e6610d67e1db48ee12691f1085ec3a6 Mon Sep 17 00:00:00 2001 From: SWHL Date: Mon, 17 Feb 2025 22:25:21 +0800 Subject: [PATCH] chore: update files --- python/demo.py | 2 +- python/rapidocr/inference_engine/torch.py | 39 +- python/rapidocr/networks/__init__.py | 0 python/rapidocr/networks/arch_config.yaml | 98 +++ .../networks/architectures/__init__.py | 25 + .../networks/architectures/base_model.py | 105 +++ .../rapidocr/networks/backbones/__init__.py | 61 ++ .../networks/backbones/det_mobilenet_v3.py | 269 ++++++++ .../rapidocr/networks/backbones/rec_hgnet.py | 290 ++++++++ .../networks/backbones/rec_lcnetv3.py | 516 ++++++++++++++ .../networks/backbones/rec_mobilenet_v3.py | 136 ++++ .../networks/backbones/rec_svtrnet.py | 638 ++++++++++++++++++ python/rapidocr/networks/common.py | 76 +++ python/rapidocr/networks/heads/__init__.py | 43 ++ python/rapidocr/networks/heads/cls_head.py | 23 + python/rapidocr/networks/heads/det_db_head.py | 131 ++++ .../rapidocr/networks/heads/rec_ctc_head.py | 54 ++ .../rapidocr/networks/heads/rec_multi_head.py | 58 ++ python/rapidocr/networks/necks/__init__.py | 28 + python/rapidocr/networks/necks/db_fpn.py | 456 +++++++++++++ python/rapidocr/networks/necks/intracl.py | 117 ++++ python/rapidocr/networks/necks/rnn.py | 228 +++++++ python/rapidocr/utils/parse_parameters.py | 4 + python/rapidocr_torch/modeling/common.py | 33 +- .../rapidocr_torch/modeling/necks/intracl.py | 43 +- python/tests/test_files/config.yaml | 62 -- python/tests/test_files/en.jpg | Bin 0 -> 40594 bytes python/tests/test_main.py | 11 +- 28 files changed, 3422 insertions(+), 124 deletions(-) create mode 100644 python/rapidocr/networks/__init__.py create mode 100644 python/rapidocr/networks/arch_config.yaml create mode 100644 python/rapidocr/networks/architectures/__init__.py create mode 100644 python/rapidocr/networks/architectures/base_model.py create mode 100644 python/rapidocr/networks/backbones/__init__.py create mode 100644 python/rapidocr/networks/backbones/det_mobilenet_v3.py create mode 100644 python/rapidocr/networks/backbones/rec_hgnet.py create mode 100644 python/rapidocr/networks/backbones/rec_lcnetv3.py create mode 100644 python/rapidocr/networks/backbones/rec_mobilenet_v3.py create mode 100644 python/rapidocr/networks/backbones/rec_svtrnet.py create mode 100644 python/rapidocr/networks/common.py create mode 100644 python/rapidocr/networks/heads/__init__.py create mode 100644 python/rapidocr/networks/heads/cls_head.py create mode 100644 python/rapidocr/networks/heads/det_db_head.py create mode 100644 python/rapidocr/networks/heads/rec_ctc_head.py create mode 100644 python/rapidocr/networks/heads/rec_multi_head.py create mode 100644 python/rapidocr/networks/necks/__init__.py create mode 100644 python/rapidocr/networks/necks/db_fpn.py create mode 100644 python/rapidocr/networks/necks/intracl.py create mode 100644 python/rapidocr/networks/necks/rnn.py delete mode 100644 python/tests/test_files/config.yaml create mode 100644 python/tests/test_files/en.jpg diff --git a/python/demo.py b/python/demo.py index c4cbda4..ab4fdd7 100644 --- a/python/demo.py +++ b/python/demo.py @@ -18,7 +18,7 @@ from rapidocr import RapidOCR, VisRes # engine = engine = RapidOCR( # params={"Global.with_onnx": True, "EngineConfig.onnxruntime.use_cuda": True} # ) -engine = RapidOCR(params={"Global.with_openvino": True, "Global.lang": "ch"}) +engine = RapidOCR(params={"Global.with_torch": True, "Global.lang": "ch"}) vis = VisRes() image_path = "tests/test_files/ch_en_num.jpg" diff --git a/python/rapidocr/inference_engine/torch.py b/python/rapidocr/inference_engine/torch.py index 500de16..bd5756f 100644 --- a/python/rapidocr/inference_engine/torch.py +++ b/python/rapidocr/inference_engine/torch.py @@ -2,44 +2,39 @@ # @Author: SWHL # @Contact: liekkaskono@163.com from pathlib import Path -from typing import Dict, Optional, Union +from typing import Optional import numpy as np import torch -import yaml +from omegaconf import OmegaConf + +from ..networks.architectures.base_model import BaseModel +from ..utils.logger import Logger +from .base import InferSession root_dir = Path(__file__).resolve().parent.parent -DEFAULT_CFG_PATH = root_dir / "arch_config.yaml" +DEFAULT_CFG_PATH = root_dir / "networks" / "arch_config.yaml" -def read_yaml(yaml_path: Union[str, Path]) -> Dict[str, Dict]: - with open(yaml_path, "rb") as f: - data = yaml.load(f, Loader=yaml.Loader) - return data - - -from rapidocr_torch.modeling.architectures.base_model import BaseModel - -from ..utils.logger import Logger - - -class TorchInferSession: +class TorchInferSession(InferSession): def __init__(self, config, mode: Optional[str] = None) -> None: self.logger = Logger(logger_name=__name__).get_log() - all_arch_config = read_yaml(DEFAULT_CFG_PATH) + all_arch_config = OmegaConf.load(DEFAULT_CFG_PATH) self.mode = mode model_path = Path(config["model_path"]) self._verify_model(model_path) + file_name = model_path.stem if file_name not in all_arch_config: - raise ValueError(f"architecture {file_name} is not in config.yaml") + raise ValueError(f"architecture {file_name} is not in arch_config.yaml") arch_config = all_arch_config[file_name] self.predictor = BaseModel(arch_config) self.predictor.load_state_dict(torch.load(model_path, weights_only=True)) self.predictor.eval() + self.use_gpu = False if config["use_cuda"]: self.predictor.cuda() @@ -50,17 +45,13 @@ class TorchInferSession: inp = torch.from_numpy(img) if self.use_gpu: inp = inp.cuda() + # 适配跟onnx对齐取值逻辑 outputs = self.predictor(inp).unsqueeze(0) return outputs.cpu().numpy() - @staticmethod - def _verify_model(model_path): - model_path = Path(model_path) - if not model_path.exists(): - raise FileNotFoundError(f"{model_path} does not exists.") - if not model_path.is_file(): - raise FileExistsError(f"{model_path} is not a file.") + def have_key(self, key: str = "character") -> bool: + return False class TorchInferError(Exception): diff --git a/python/rapidocr/networks/__init__.py b/python/rapidocr/networks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/rapidocr/networks/arch_config.yaml b/python/rapidocr/networks/arch_config.yaml new file mode 100644 index 0000000..0879157 --- /dev/null +++ b/python/rapidocr/networks/arch_config.yaml @@ -0,0 +1,98 @@ +ch_ptocr_mobile_v2.0_cls_infer: + model_type: cls + algorithm: CLS + Transform: + Backbone: + name: MobileNetV3 + scale: 0.35 + model_name: small + Neck: + Head: + name: ClsHead + class_dim: 2 + +ch_PP-OCRv4_det_infer: + model_type: det + algorithm: DB + Transform: null + Backbone: + name: PPLCNetV3 + scale: 0.75 + det: True + Neck: + name: RSEFPN + out_channels: 96 + shortcut: True + Head: + name: DBHead + k: 50 + + +ch_PP-OCRv4_det_server_infer: + model_type: det + algorithm: DB + Transform: null + Backbone: + name: PPHGNet_small + det: True + Neck: + name: LKPAN + out_channels: 256 + intracl: true + Head: + name: PFHeadLocal + k: 50 + mode: "large" + + +ch_PP-OCRv4_rec_infer: + model_type: rec + algorithm: SVTR_LCNet + Transform: + Backbone: + name: PPLCNetV3 + scale: 0.95 + Head: + name: MultiHead + out_channels_list: + CTCLabelDecode: 6625 #'blank' + ...(6623) + ' ' + head_list: + - CTCHead: + Neck: + name: svtr + dims: 120 + depth: 2 + hidden_dims: 120 + kernel_size: [ 1, 3 ] + use_guide: True + Head: + fc_decay: 0.00001 + - NRTRHead: + nrtr_dim: 384 + max_text_length: 25 + + +ch_PP-OCRv4_rec_server_infer: + model_type: rec + algorithm: SVTR_HGNet + Transform: + Backbone: + name: PPHGNet_small + Head: + name: MultiHead + out_channels_list: + CTCLabelDecode: 6625 #'blank' + ...(6623) + ' ' + head_list: + - CTCHead: + Neck: + name: svtr + dims: 120 + depth: 2 + hidden_dims: 120 + kernel_size: [ 1, 3 ] + use_guide: True + Head: + fc_decay: 0.00001 + - NRTRHead: + nrtr_dim: 384 + max_text_length: 25 \ No newline at end of file diff --git a/python/rapidocr/networks/architectures/__init__.py b/python/rapidocr/networks/architectures/__init__.py new file mode 100644 index 0000000..7ad5eb4 --- /dev/null +++ b/python/rapidocr/networks/architectures/__init__.py @@ -0,0 +1,25 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import copy + +__all__ = ["build_model"] + + +def build_model(config, **kwargs): + from .base_model import BaseModel + + config = copy.deepcopy(config) + module_class = BaseModel(config, **kwargs) + return module_class diff --git a/python/rapidocr/networks/architectures/base_model.py b/python/rapidocr/networks/architectures/base_model.py new file mode 100644 index 0000000..e7f7ce4 --- /dev/null +++ b/python/rapidocr/networks/architectures/base_model.py @@ -0,0 +1,105 @@ +from torch import nn + +from ..backbones import build_backbone +from ..heads import build_head +from ..necks import build_neck + + +class BaseModel(nn.Module): + def __init__(self, config, **kwargs): + """ + the module for OCR. + args: + config (dict): the super parameters for module. + """ + super(BaseModel, self).__init__() + + in_channels = config.get("in_channels", 3) + model_type = config["model_type"] + # build backbone, backbone is need for del, rec and cls + if "Backbone" not in config or config["Backbone"] is None: + self.use_backbone = False + else: + self.use_backbone = True + config["Backbone"]["in_channels"] = in_channels + self.backbone = build_backbone(config["Backbone"], model_type) + in_channels = self.backbone.out_channels + + # build neck + # for rec, neck can be cnn,rnn or reshape(None) + # for det, neck can be FPN, BIFPN and so on. + # for cls, neck should be none + if "Neck" not in config or config["Neck"] is None: + self.use_neck = False + else: + self.use_neck = True + config["Neck"]["in_channels"] = in_channels + self.neck = build_neck(config["Neck"]) + in_channels = self.neck.out_channels + + # # build head, head is need for det, rec and cls + if "Head" not in config or config["Head"] is None: + self.use_head = False + else: + self.use_head = True + config["Head"]["in_channels"] = in_channels + self.head = build_head(config["Head"], **kwargs) + + self.return_all_feats = config.get("return_all_feats", False) + + self._initialize_weights() + + def _initialize_weights(self): + # weight initialization + for m in self.modules(): + if isinstance(m, nn.Conv2d): + nn.init.kaiming_normal_(m.weight, mode="fan_out") + if m.bias is not None: + nn.init.zeros_(m.bias) + elif isinstance(m, nn.BatchNorm2d): + nn.init.ones_(m.weight) + nn.init.zeros_(m.bias) + elif isinstance(m, nn.Linear): + nn.init.normal_(m.weight, 0, 0.01) + if m.bias is not None: + nn.init.zeros_(m.bias) + elif isinstance(m, nn.ConvTranspose2d): + nn.init.kaiming_normal_(m.weight, mode="fan_out") + if m.bias is not None: + nn.init.zeros_(m.bias) + + def forward(self, x): + y = dict() + if self.use_backbone: + x = self.backbone(x) + if isinstance(x, dict): + y.update(x) + else: + y["backbone_out"] = x + final_name = "backbone_out" + if self.use_neck: + x = self.neck(x) + if isinstance(x, dict): + y.update(x) + else: + y["neck_out"] = x + final_name = "neck_out" + if self.use_head: + x = self.head(x) + # for multi head, save ctc neck out for udml + if isinstance(x, dict) and "ctc_nect" in x.keys(): + y["neck_out"] = x["ctc_neck"] + y["head_out"] = x + elif isinstance(x, dict): + y.update(x) + else: + y["head_out"] = x + if self.return_all_feats: + if self.training: + return y + elif isinstance(x, dict): + return x + else: + return {final_name: x} + else: + return x diff --git a/python/rapidocr/networks/backbones/__init__.py b/python/rapidocr/networks/backbones/__init__.py new file mode 100644 index 0000000..8e7c5b3 --- /dev/null +++ b/python/rapidocr/networks/backbones/__init__.py @@ -0,0 +1,61 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__all__ = ["build_backbone"] + + +def build_backbone(config, model_type): + if model_type == "det": + from .det_mobilenet_v3 import MobileNetV3 + from .rec_hgnet import PPHGNet_small + from .rec_lcnetv3 import PPLCNetV3 + + support_dict = [ + "MobileNetV3", + "ResNet", + "ResNet_vd", + "ResNet_SAST", + "PPLCNetV3", + "PPHGNet_small", + ] + elif model_type == "rec" or model_type == "cls": + from .rec_hgnet import PPHGNet_small + from .rec_lcnetv3 import PPLCNetV3 + from .rec_mobilenet_v3 import MobileNetV3 + from .rec_svtrnet import SVTRNet + + support_dict = [ + "MobileNetV1Enhance", + "MobileNetV3", + "ResNet", + "ResNetFPN", + "MTB", + "ResNet31", + "SVTRNet", + "ViTSTR", + "DenseNet", + "PPLCNetV3", + "PPHGNet_small", + ] + else: + raise NotImplementedError + + module_name = config.pop("name") + assert module_name in support_dict, Exception( + "when model typs is {}, backbone only support {}".format( + model_type, support_dict + ) + ) + module_class = eval(module_name)(**config) + return module_class diff --git a/python/rapidocr/networks/backbones/det_mobilenet_v3.py b/python/rapidocr/networks/backbones/det_mobilenet_v3.py new file mode 100644 index 0000000..0351159 --- /dev/null +++ b/python/rapidocr/networks/backbones/det_mobilenet_v3.py @@ -0,0 +1,269 @@ +from torch import nn + +from ..common import Activation + + +def make_divisible(v, divisor=8, min_value=None): + if min_value is None: + min_value = divisor + new_v = max(min_value, int(v + divisor / 2) // divisor * divisor) + if new_v < 0.9 * v: + new_v += divisor + return new_v + + +class ConvBNLayer(nn.Module): + def __init__( + self, + in_channels, + out_channels, + kernel_size, + stride, + padding, + groups=1, + if_act=True, + act=None, + name=None, + ): + super(ConvBNLayer, self).__init__() + self.if_act = if_act + self.conv = nn.Conv2d( + in_channels=in_channels, + out_channels=out_channels, + kernel_size=kernel_size, + stride=stride, + padding=padding, + groups=groups, + bias=False, + ) + + self.bn = nn.BatchNorm2d( + out_channels, + ) + if self.if_act: + self.act = Activation(act_type=act, inplace=True) + + def forward(self, x): + x = self.conv(x) + x = self.bn(x) + if self.if_act: + x = self.act(x) + return x + + +class SEModule(nn.Module): + def __init__(self, in_channels, reduction=4, name=""): + super(SEModule, self).__init__() + self.avg_pool = nn.AdaptiveAvgPool2d(1) + self.conv1 = nn.Conv2d( + in_channels=in_channels, + out_channels=in_channels // reduction, + kernel_size=1, + stride=1, + padding=0, + bias=True, + ) + self.relu1 = Activation(act_type="relu", inplace=True) + self.conv2 = nn.Conv2d( + in_channels=in_channels // reduction, + out_channels=in_channels, + kernel_size=1, + stride=1, + padding=0, + bias=True, + ) + self.hard_sigmoid = Activation(act_type="hard_sigmoid", inplace=True) + + def forward(self, inputs): + outputs = self.avg_pool(inputs) + outputs = self.conv1(outputs) + outputs = self.relu1(outputs) + outputs = self.conv2(outputs) + outputs = self.hard_sigmoid(outputs) + outputs = inputs * outputs + return outputs + + +class ResidualUnit(nn.Module): + def __init__( + self, + in_channels, + mid_channels, + out_channels, + kernel_size, + stride, + use_se, + act=None, + name="", + ): + super(ResidualUnit, self).__init__() + self.if_shortcut = stride == 1 and in_channels == out_channels + self.if_se = use_se + + self.expand_conv = ConvBNLayer( + in_channels=in_channels, + out_channels=mid_channels, + kernel_size=1, + stride=1, + padding=0, + if_act=True, + act=act, + name=name + "_expand", + ) + self.bottleneck_conv = ConvBNLayer( + in_channels=mid_channels, + out_channels=mid_channels, + kernel_size=kernel_size, + stride=stride, + padding=int((kernel_size - 1) // 2), + groups=mid_channels, + if_act=True, + act=act, + name=name + "_depthwise", + ) + if self.if_se: + self.mid_se = SEModule(mid_channels, name=name + "_se") + self.linear_conv = ConvBNLayer( + in_channels=mid_channels, + out_channels=out_channels, + kernel_size=1, + stride=1, + padding=0, + if_act=False, + act=None, + name=name + "_linear", + ) + + def forward(self, inputs): + x = self.expand_conv(inputs) + x = self.bottleneck_conv(x) + if self.if_se: + x = self.mid_se(x) + x = self.linear_conv(x) + if self.if_shortcut: + x = inputs + x + return x + + +class MobileNetV3(nn.Module): + def __init__( + self, in_channels=3, model_name="large", scale=0.5, disable_se=False, **kwargs + ): + """ + the MobilenetV3 backbone network for detection module. + Args: + params(dict): the super parameters for build network + """ + super(MobileNetV3, self).__init__() + + self.disable_se = disable_se + + if model_name == "large": + cfg = [ + # k, exp, c, se, nl, s, + [3, 16, 16, False, "relu", 1], + [3, 64, 24, False, "relu", 2], + [3, 72, 24, False, "relu", 1], + [5, 72, 40, True, "relu", 2], + [5, 120, 40, True, "relu", 1], + [5, 120, 40, True, "relu", 1], + [3, 240, 80, False, "hard_swish", 2], + [3, 200, 80, False, "hard_swish", 1], + [3, 184, 80, False, "hard_swish", 1], + [3, 184, 80, False, "hard_swish", 1], + [3, 480, 112, True, "hard_swish", 1], + [3, 672, 112, True, "hard_swish", 1], + [5, 672, 160, True, "hard_swish", 2], + [5, 960, 160, True, "hard_swish", 1], + [5, 960, 160, True, "hard_swish", 1], + ] + cls_ch_squeeze = 960 + elif model_name == "small": + cfg = [ + # k, exp, c, se, nl, s, + [3, 16, 16, True, "relu", 2], + [3, 72, 24, False, "relu", 2], + [3, 88, 24, False, "relu", 1], + [5, 96, 40, True, "hard_swish", 2], + [5, 240, 40, True, "hard_swish", 1], + [5, 240, 40, True, "hard_swish", 1], + [5, 120, 48, True, "hard_swish", 1], + [5, 144, 48, True, "hard_swish", 1], + [5, 288, 96, True, "hard_swish", 2], + [5, 576, 96, True, "hard_swish", 1], + [5, 576, 96, True, "hard_swish", 1], + ] + cls_ch_squeeze = 576 + else: + raise NotImplementedError( + "mode[" + model_name + "_model] is not implemented!" + ) + + supported_scale = [0.35, 0.5, 0.75, 1.0, 1.25] + assert ( + scale in supported_scale + ), "supported scale are {} but input scale is {}".format(supported_scale, scale) + inplanes = 16 + # conv1 + self.conv = ConvBNLayer( + in_channels=in_channels, + out_channels=make_divisible(inplanes * scale), + kernel_size=3, + stride=2, + padding=1, + groups=1, + if_act=True, + act="hard_swish", + name="conv1", + ) + + self.stages = nn.ModuleList() + self.out_channels = [] + block_list = [] + i = 0 + inplanes = make_divisible(inplanes * scale) + for k, exp, c, se, nl, s in cfg: + se = se and not self.disable_se + if s == 2 and i > 2: + self.out_channels.append(inplanes) + self.stages.append(nn.Sequential(*block_list)) + block_list = [] + block_list.append( + ResidualUnit( + in_channels=inplanes, + mid_channels=make_divisible(scale * exp), + out_channels=make_divisible(scale * c), + kernel_size=k, + stride=s, + use_se=se, + act=nl, + name="conv" + str(i + 2), + ) + ) + inplanes = make_divisible(scale * c) + i += 1 + block_list.append( + ConvBNLayer( + in_channels=inplanes, + out_channels=make_divisible(scale * cls_ch_squeeze), + kernel_size=1, + stride=1, + padding=0, + groups=1, + if_act=True, + act="hard_swish", + name="conv_last", + ) + ) + self.stages.append(nn.Sequential(*block_list)) + self.out_channels.append(make_divisible(scale * cls_ch_squeeze)) + # for i, stage in enumerate(self.stages): + # self.add_sublayer(sublayer=stage, name="stage{}".format(i)) + + def forward(self, x): + x = self.conv(x) + out_list = [] + for stage in self.stages: + x = stage(x) + out_list.append(x) + return out_list diff --git a/python/rapidocr/networks/backbones/rec_hgnet.py b/python/rapidocr/networks/backbones/rec_hgnet.py new file mode 100644 index 0000000..c1515a7 --- /dev/null +++ b/python/rapidocr/networks/backbones/rec_hgnet.py @@ -0,0 +1,290 @@ +import torch +import torch.nn.functional as F +from torch import nn + + +class ConvBNAct(nn.Module): + def __init__( + self, in_channels, out_channels, kernel_size, stride, groups=1, use_act=True + ): + super().__init__() + self.use_act = use_act + self.conv = nn.Conv2d( + in_channels, + out_channels, + kernel_size, + stride, + padding=(kernel_size - 1) // 2, + groups=groups, + bias=False, + ) + self.bn = nn.BatchNorm2d(out_channels) + if self.use_act: + self.act = nn.ReLU() + + def forward(self, x): + x = self.conv(x) + x = self.bn(x) + if self.use_act: + x = self.act(x) + return x + + +class ESEModule(nn.Module): + def __init__(self, channels): + super().__init__() + self.avg_pool = nn.AdaptiveAvgPool2d(1) + self.conv = nn.Conv2d( + in_channels=channels, + out_channels=channels, + kernel_size=1, + stride=1, + padding=0, + ) + self.sigmoid = nn.Sigmoid() + + def forward(self, x): + identity = x + x = self.avg_pool(x) + x = self.conv(x) + x = self.sigmoid(x) + return x * identity + + +class HG_Block(nn.Module): + def __init__( + self, + in_channels, + mid_channels, + out_channels, + layer_num, + identity=False, + ): + super().__init__() + self.identity = identity + + self.layers = nn.ModuleList() + self.layers.append( + ConvBNAct( + in_channels=in_channels, + out_channels=mid_channels, + kernel_size=3, + stride=1, + ) + ) + for _ in range(layer_num - 1): + self.layers.append( + ConvBNAct( + in_channels=mid_channels, + out_channels=mid_channels, + kernel_size=3, + stride=1, + ) + ) + + # feature aggregation + total_channels = in_channels + layer_num * mid_channels + self.aggregation_conv = ConvBNAct( + in_channels=total_channels, + out_channels=out_channels, + kernel_size=1, + stride=1, + ) + self.att = ESEModule(out_channels) + + def forward(self, x): + identity = x + output = [] + output.append(x) + for layer in self.layers: + x = layer(x) + output.append(x) + x = torch.cat(output, dim=1) + x = self.aggregation_conv(x) + x = self.att(x) + if self.identity: + x += identity + return x + + +class HG_Stage(nn.Module): + def __init__( + self, + in_channels, + mid_channels, + out_channels, + block_num, + layer_num, + downsample=True, + stride=[2, 1], + ): + super().__init__() + self.downsample = downsample + if downsample: + self.downsample = ConvBNAct( + in_channels=in_channels, + out_channels=in_channels, + kernel_size=3, + stride=stride, + groups=in_channels, + use_act=False, + ) + + blocks_list = [] + blocks_list.append( + HG_Block(in_channels, mid_channels, out_channels, layer_num, identity=False) + ) + for _ in range(block_num - 1): + blocks_list.append( + HG_Block( + out_channels, mid_channels, out_channels, layer_num, identity=True + ) + ) + self.blocks = nn.Sequential(*blocks_list) + + def forward(self, x): + if self.downsample: + x = self.downsample(x) + x = self.blocks(x) + return x + + +class PPHGNet(nn.Module): + """ + PPHGNet + Args: + stem_channels: list. Stem channel list of PPHGNet. + stage_config: dict. The configuration of each stage of PPHGNet. such as the number of channels, stride, etc. + layer_num: int. Number of layers of HG_Block. + use_last_conv: boolean. Whether to use a 1x1 convolutional layer before the classification layer. + class_expand: int=2048. Number of channels for the last 1x1 convolutional layer. + dropout_prob: float. Parameters of dropout, 0.0 means dropout is not used. + class_num: int=1000. The number of classes. + Returns: + model: nn.Layer. Specific PPHGNet model depends on args. + """ + + def __init__( + self, + stem_channels, + stage_config, + layer_num, + in_channels=3, + det=False, + out_indices=None, + ): + super().__init__() + self.det = det + self.out_indices = out_indices if out_indices is not None else [0, 1, 2, 3] + + # stem + stem_channels.insert(0, in_channels) + self.stem = nn.Sequential( + *[ + ConvBNAct( + in_channels=stem_channels[i], + out_channels=stem_channels[i + 1], + kernel_size=3, + stride=2 if i == 0 else 1, + ) + for i in range(len(stem_channels) - 1) + ] + ) + + if self.det: + self.pool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1) + # stages + self.stages = nn.ModuleList() + self.out_channels = [] + for block_id, k in enumerate(stage_config): + ( + in_channels, + mid_channels, + out_channels, + block_num, + downsample, + stride, + ) = stage_config[k] + self.stages.append( + HG_Stage( + in_channels, + mid_channels, + out_channels, + block_num, + layer_num, + downsample, + stride, + ) + ) + if block_id in self.out_indices: + self.out_channels.append(out_channels) + + if not self.det: + self.out_channels = stage_config["stage4"][2] + + self._init_weights() + + def _init_weights(self): + for m in self.modules(): + if isinstance(m, nn.Conv2d): + nn.init.kaiming_normal_(m.weight) + elif isinstance(m, nn.BatchNorm2d): + nn.init.ones_(m.weight) + nn.init.zeros_(m.bias) + elif isinstance(m, nn.Linear): + nn.init.zeros_(m.bias) + + def forward(self, x): + x = self.stem(x) + if self.det: + x = self.pool(x) + + out = [] + for i, stage in enumerate(self.stages): + x = stage(x) + if self.det and i in self.out_indices: + out.append(x) + if self.det: + return out + + if self.training: + x = F.adaptive_avg_pool2d(x, [1, 40]) + else: + x = F.avg_pool2d(x, [3, 2]) + return x + + +def PPHGNet_small(pretrained=False, use_ssld=False, det=False, **kwargs): + """ + PPHGNet_small + Args: + pretrained: bool=False or str. If `True` load pretrained parameters, `False` otherwise. + If str, means the path of the pretrained model. + use_ssld: bool=False. Whether using distillation pretrained model when pretrained=True. + Returns: + model: nn.Layer. Specific `PPHGNet_small` model depends on args. + """ + stage_config_det = { + # in_channels, mid_channels, out_channels, blocks, downsample + "stage1": [128, 128, 256, 1, False, 2], + "stage2": [256, 160, 512, 1, True, 2], + "stage3": [512, 192, 768, 2, True, 2], + "stage4": [768, 224, 1024, 1, True, 2], + } + + stage_config_rec = { + # in_channels, mid_channels, out_channels, blocks, downsample + "stage1": [128, 128, 256, 1, True, [2, 1]], + "stage2": [256, 160, 512, 1, True, [1, 2]], + "stage3": [512, 192, 768, 2, True, [2, 1]], + "stage4": [768, 224, 1024, 1, True, [2, 1]], + } + + model = PPHGNet( + stem_channels=[64, 64, 128], + stage_config=stage_config_det if det else stage_config_rec, + layer_num=6, + det=det, + **kwargs + ) + return model diff --git a/python/rapidocr/networks/backbones/rec_lcnetv3.py b/python/rapidocr/networks/backbones/rec_lcnetv3.py new file mode 100644 index 0000000..e2bd457 --- /dev/null +++ b/python/rapidocr/networks/backbones/rec_lcnetv3.py @@ -0,0 +1,516 @@ +# copyright (c) 2021 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import, division, print_function + +import torch +import torch.nn.functional as F +from torch import nn + +from ..common import Activation + +NET_CONFIG_det = { + "blocks2": + # k, in_c, out_c, s, use_se + [[3, 16, 32, 1, False]], + "blocks3": [[3, 32, 64, 2, False], [3, 64, 64, 1, False]], + "blocks4": [[3, 64, 128, 2, False], [3, 128, 128, 1, False]], + "blocks5": [ + [3, 128, 256, 2, False], + [5, 256, 256, 1, False], + [5, 256, 256, 1, False], + [5, 256, 256, 1, False], + [5, 256, 256, 1, False], + ], + "blocks6": [ + [5, 256, 512, 2, True], + [5, 512, 512, 1, True], + [5, 512, 512, 1, False], + [5, 512, 512, 1, False], + ], +} + +NET_CONFIG_rec = { + "blocks2": + # k, in_c, out_c, s, use_se + [[3, 16, 32, 1, False]], + "blocks3": [[3, 32, 64, 1, False], [3, 64, 64, 1, False]], + "blocks4": [[3, 64, 128, (2, 1), False], [3, 128, 128, 1, False]], + "blocks5": [ + [3, 128, 256, (1, 2), False], + [5, 256, 256, 1, False], + [5, 256, 256, 1, False], + [5, 256, 256, 1, False], + [5, 256, 256, 1, False], + ], + "blocks6": [ + [5, 256, 512, (2, 1), True], + [5, 512, 512, 1, True], + [5, 512, 512, (2, 1), False], + [5, 512, 512, 1, False], + ], +} + + +def make_divisible(v, divisor=16, min_value=None): + if min_value is None: + min_value = divisor + new_v = max(min_value, int(v + divisor / 2) // divisor * divisor) + if new_v < 0.9 * v: + new_v += divisor + return new_v + + +class LearnableAffineBlock(nn.Module): + def __init__(self, scale_value=1.0, bias_value=0.0, lr_mult=1.0, lab_lr=0.1): + super().__init__() + self.scale = nn.Parameter(torch.Tensor([scale_value])) + self.bias = nn.Parameter(torch.Tensor([bias_value])) + + def forward(self, x): + return self.scale * x + self.bias + + +class ConvBNLayer(nn.Module): + def __init__( + self, in_channels, out_channels, kernel_size, stride, groups=1, lr_mult=1.0 + ): + super().__init__() + self.conv = nn.Conv2d( + in_channels=in_channels, + out_channels=out_channels, + kernel_size=kernel_size, + stride=stride, + padding=(kernel_size - 1) // 2, + groups=groups, + bias=False, + ) + + self.bn = nn.BatchNorm2d( + out_channels, + ) + + def forward(self, x): + x = self.conv(x) + x = self.bn(x) + return x + + +class Act(nn.Module): + def __init__(self, act="hswish", lr_mult=1.0, lab_lr=0.1): + super().__init__() + if act == "hswish": + self.act = nn.Hardswish(inplace=True) + else: + assert act == "relu" + self.act = Activation(act) + self.lab = LearnableAffineBlock(lr_mult=lr_mult, lab_lr=lab_lr) + + def forward(self, x): + return self.lab(self.act(x)) + + +class LearnableRepLayer(nn.Module): + def __init__( + self, + in_channels, + out_channels, + kernel_size, + stride=1, + groups=1, + num_conv_branches=1, + lr_mult=1.0, + lab_lr=0.1, + ): + super().__init__() + self.is_repped = False + self.groups = groups + self.stride = stride + self.kernel_size = kernel_size + self.in_channels = in_channels + self.out_channels = out_channels + self.num_conv_branches = num_conv_branches + self.padding = (kernel_size - 1) // 2 + + self.identity = ( + nn.BatchNorm2d( + num_features=in_channels, + ) + if out_channels == in_channels and stride == 1 + else None + ) + + self.conv_kxk = nn.ModuleList( + [ + ConvBNLayer( + in_channels, + out_channels, + kernel_size, + stride, + groups=groups, + lr_mult=lr_mult, + ) + for _ in range(self.num_conv_branches) + ] + ) + + self.conv_1x1 = ( + ConvBNLayer( + in_channels, out_channels, 1, stride, groups=groups, lr_mult=lr_mult + ) + if kernel_size > 1 + else None + ) + + self.lab = LearnableAffineBlock(lr_mult=lr_mult, lab_lr=lab_lr) + self.act = Act(lr_mult=lr_mult, lab_lr=lab_lr) + + def forward(self, x): + # for export + if self.is_repped: + out = self.lab(self.reparam_conv(x)) + if self.stride != 2: + out = self.act(out) + return out + + out = 0 + if self.identity is not None: + out += self.identity(x) + + if self.conv_1x1 is not None: + out += self.conv_1x1(x) + + for conv in self.conv_kxk: + out += conv(x) + + out = self.lab(out) + if self.stride != 2: + out = self.act(out) + return out + + def rep(self): + if self.is_repped: + return + kernel, bias = self._get_kernel_bias() + self.reparam_conv = nn.Conv2d( + in_channels=self.in_channels, + out_channels=self.out_channels, + kernel_size=self.kernel_size, + stride=self.stride, + padding=self.padding, + groups=self.groups, + ) + self.reparam_conv.weight.data = kernel + self.reparam_conv.bias.data = bias + self.is_repped = True + + def _pad_kernel_1x1_to_kxk(self, kernel1x1, pad): + if not isinstance(kernel1x1, torch.Tensor): + return 0 + else: + return nn.functional.pad(kernel1x1, [pad, pad, pad, pad]) + + def _get_kernel_bias(self): + kernel_conv_1x1, bias_conv_1x1 = self._fuse_bn_tensor(self.conv_1x1) + kernel_conv_1x1 = self._pad_kernel_1x1_to_kxk( + kernel_conv_1x1, self.kernel_size // 2 + ) + + kernel_identity, bias_identity = self._fuse_bn_tensor(self.identity) + + kernel_conv_kxk = 0 + bias_conv_kxk = 0 + for conv in self.conv_kxk: + kernel, bias = self._fuse_bn_tensor(conv) + kernel_conv_kxk += kernel + bias_conv_kxk += bias + + kernel_reparam = kernel_conv_kxk + kernel_conv_1x1 + kernel_identity + bias_reparam = bias_conv_kxk + bias_conv_1x1 + bias_identity + return kernel_reparam, bias_reparam + + def _fuse_bn_tensor(self, branch): + if not branch: + return 0, 0 + elif isinstance(branch, ConvBNLayer): + kernel = branch.conv.weight + running_mean = branch.bn._mean + running_var = branch.bn._variance + gamma = branch.bn.weight + beta = branch.bn.bias + eps = branch.bn._epsilon + else: + assert isinstance(branch, nn.BatchNorm2d) + if not hasattr(self, "id_tensor"): + input_dim = self.in_channels // self.groups + kernel_value = torch.zeros( + (self.in_channels, input_dim, self.kernel_size, self.kernel_size), + dtype=branch.weight.dtype, + ) + for i in range(self.in_channels): + kernel_value[ + i, i % input_dim, self.kernel_size // 2, self.kernel_size // 2 + ] = 1 + self.id_tensor = kernel_value + kernel = self.id_tensor + running_mean = branch._mean + running_var = branch._variance + gamma = branch.weight + beta = branch.bias + eps = branch._epsilon + std = (running_var + eps).sqrt() + t = (gamma / std).reshape((-1, 1, 1, 1)) + return kernel * t, beta - running_mean * gamma / std + + +class SELayer(nn.Module): + def __init__(self, channel, reduction=4, lr_mult=1.0): + super().__init__() + self.avg_pool = nn.AdaptiveAvgPool2d(1) + self.conv1 = nn.Conv2d( + in_channels=channel, + out_channels=channel // reduction, + kernel_size=1, + stride=1, + padding=0, + ) + self.relu = nn.ReLU() + self.conv2 = nn.Conv2d( + in_channels=channel // reduction, + out_channels=channel, + kernel_size=1, + stride=1, + padding=0, + ) + self.hardsigmoid = nn.Hardsigmoid(inplace=True) + + def forward(self, x): + identity = x + x = self.avg_pool(x) + x = self.conv1(x) + x = self.relu(x) + x = self.conv2(x) + x = self.hardsigmoid(x) + x = identity * x + return x + + +class LCNetV3Block(nn.Module): + def __init__( + self, + in_channels, + out_channels, + stride, + dw_size, + use_se=False, + conv_kxk_num=4, + lr_mult=1.0, + lab_lr=0.1, + ): + super().__init__() + self.use_se = use_se + self.dw_conv = LearnableRepLayer( + in_channels=in_channels, + out_channels=in_channels, + kernel_size=dw_size, + stride=stride, + groups=in_channels, + num_conv_branches=conv_kxk_num, + lr_mult=lr_mult, + lab_lr=lab_lr, + ) + if use_se: + self.se = SELayer(in_channels, lr_mult=lr_mult) + self.pw_conv = LearnableRepLayer( + in_channels=in_channels, + out_channels=out_channels, + kernel_size=1, + stride=1, + num_conv_branches=conv_kxk_num, + lr_mult=lr_mult, + lab_lr=lab_lr, + ) + + def forward(self, x): + x = self.dw_conv(x) + if self.use_se: + x = self.se(x) + x = self.pw_conv(x) + return x + + +class PPLCNetV3(nn.Module): + def __init__( + self, + scale=1.0, + conv_kxk_num=4, + lr_mult_list=[1.0, 1.0, 1.0, 1.0, 1.0, 1.0], + lab_lr=0.1, + det=False, + **kwargs + ): + super().__init__() + self.scale = scale + self.lr_mult_list = lr_mult_list + self.det = det + + self.net_config = NET_CONFIG_det if self.det else NET_CONFIG_rec + + assert isinstance( + self.lr_mult_list, (list, tuple) + ), "lr_mult_list should be in (list, tuple) but got {}".format( + type(self.lr_mult_list) + ) + assert ( + len(self.lr_mult_list) == 6 + ), "lr_mult_list length should be 6 but got {}".format(len(self.lr_mult_list)) + + self.conv1 = ConvBNLayer( + in_channels=3, + out_channels=make_divisible(16 * scale), + kernel_size=3, + stride=2, + lr_mult=self.lr_mult_list[0], + ) + + self.blocks2 = nn.Sequential( + *[ + LCNetV3Block( + in_channels=make_divisible(in_c * scale), + out_channels=make_divisible(out_c * scale), + dw_size=k, + stride=s, + use_se=se, + conv_kxk_num=conv_kxk_num, + lr_mult=self.lr_mult_list[1], + lab_lr=lab_lr, + ) + for i, (k, in_c, out_c, s, se) in enumerate(self.net_config["blocks2"]) + ] + ) + + self.blocks3 = nn.Sequential( + *[ + LCNetV3Block( + in_channels=make_divisible(in_c * scale), + out_channels=make_divisible(out_c * scale), + dw_size=k, + stride=s, + use_se=se, + conv_kxk_num=conv_kxk_num, + lr_mult=self.lr_mult_list[2], + lab_lr=lab_lr, + ) + for i, (k, in_c, out_c, s, se) in enumerate(self.net_config["blocks3"]) + ] + ) + + self.blocks4 = nn.Sequential( + *[ + LCNetV3Block( + in_channels=make_divisible(in_c * scale), + out_channels=make_divisible(out_c * scale), + dw_size=k, + stride=s, + use_se=se, + conv_kxk_num=conv_kxk_num, + lr_mult=self.lr_mult_list[3], + lab_lr=lab_lr, + ) + for i, (k, in_c, out_c, s, se) in enumerate(self.net_config["blocks4"]) + ] + ) + + self.blocks5 = nn.Sequential( + *[ + LCNetV3Block( + in_channels=make_divisible(in_c * scale), + out_channels=make_divisible(out_c * scale), + dw_size=k, + stride=s, + use_se=se, + conv_kxk_num=conv_kxk_num, + lr_mult=self.lr_mult_list[4], + lab_lr=lab_lr, + ) + for i, (k, in_c, out_c, s, se) in enumerate(self.net_config["blocks5"]) + ] + ) + + self.blocks6 = nn.Sequential( + *[ + LCNetV3Block( + in_channels=make_divisible(in_c * scale), + out_channels=make_divisible(out_c * scale), + dw_size=k, + stride=s, + use_se=se, + conv_kxk_num=conv_kxk_num, + lr_mult=self.lr_mult_list[5], + lab_lr=lab_lr, + ) + for i, (k, in_c, out_c, s, se) in enumerate(self.net_config["blocks6"]) + ] + ) + self.out_channels = make_divisible(512 * scale) + + if self.det: + mv_c = [16, 24, 56, 480] + self.out_channels = [ + make_divisible(self.net_config["blocks3"][-1][2] * scale), + make_divisible(self.net_config["blocks4"][-1][2] * scale), + make_divisible(self.net_config["blocks5"][-1][2] * scale), + make_divisible(self.net_config["blocks6"][-1][2] * scale), + ] + + self.layer_list = nn.ModuleList( + [ + nn.Conv2d(self.out_channels[0], int(mv_c[0] * scale), 1, 1, 0), + nn.Conv2d(self.out_channels[1], int(mv_c[1] * scale), 1, 1, 0), + nn.Conv2d(self.out_channels[2], int(mv_c[2] * scale), 1, 1, 0), + nn.Conv2d(self.out_channels[3], int(mv_c[3] * scale), 1, 1, 0), + ] + ) + self.out_channels = [ + int(mv_c[0] * scale), + int(mv_c[1] * scale), + int(mv_c[2] * scale), + int(mv_c[3] * scale), + ] + + def forward(self, x): + out_list = [] + x = self.conv1(x) + x = self.blocks2(x) + x = self.blocks3(x) + out_list.append(x) + x = self.blocks4(x) + out_list.append(x) + x = self.blocks5(x) + out_list.append(x) + x = self.blocks6(x) + out_list.append(x) + + if self.det: + out_list[0] = self.layer_list[0](out_list[0]) + out_list[1] = self.layer_list[1](out_list[1]) + out_list[2] = self.layer_list[2](out_list[2]) + out_list[3] = self.layer_list[3](out_list[3]) + return out_list + + if self.training: + x = F.adaptive_avg_pool2d(x, [1, 40]) + else: + x = F.avg_pool2d(x, [3, 2]) + return x diff --git a/python/rapidocr/networks/backbones/rec_mobilenet_v3.py b/python/rapidocr/networks/backbones/rec_mobilenet_v3.py new file mode 100644 index 0000000..d284a6d --- /dev/null +++ b/python/rapidocr/networks/backbones/rec_mobilenet_v3.py @@ -0,0 +1,136 @@ +from torch import nn + +from .det_mobilenet_v3 import ConvBNLayer, ResidualUnit, make_divisible + + +class MobileNetV3(nn.Module): + def __init__( + self, + in_channels=3, + model_name="small", + scale=0.5, + large_stride=None, + small_stride=None, + **kwargs + ): + super(MobileNetV3, self).__init__() + if small_stride is None: + small_stride = [2, 2, 2, 2] + if large_stride is None: + large_stride = [1, 2, 2, 2] + + assert isinstance( + large_stride, list + ), "large_stride type must " "be list but got {}".format(type(large_stride)) + assert isinstance( + small_stride, list + ), "small_stride type must " "be list but got {}".format(type(small_stride)) + assert ( + len(large_stride) == 4 + ), "large_stride length must be " "4 but got {}".format(len(large_stride)) + assert ( + len(small_stride) == 4 + ), "small_stride length must be " "4 but got {}".format(len(small_stride)) + + if model_name == "large": + cfg = [ + # k, exp, c, se, nl, s, + [3, 16, 16, False, "relu", large_stride[0]], + [3, 64, 24, False, "relu", (large_stride[1], 1)], + [3, 72, 24, False, "relu", 1], + [5, 72, 40, True, "relu", (large_stride[2], 1)], + [5, 120, 40, True, "relu", 1], + [5, 120, 40, True, "relu", 1], + [3, 240, 80, False, "hard_swish", 1], + [3, 200, 80, False, "hard_swish", 1], + [3, 184, 80, False, "hard_swish", 1], + [3, 184, 80, False, "hard_swish", 1], + [3, 480, 112, True, "hard_swish", 1], + [3, 672, 112, True, "hard_swish", 1], + [5, 672, 160, True, "hard_swish", (large_stride[3], 1)], + [5, 960, 160, True, "hard_swish", 1], + [5, 960, 160, True, "hard_swish", 1], + ] + cls_ch_squeeze = 960 + elif model_name == "small": + cfg = [ + # k, exp, c, se, nl, s, + [3, 16, 16, True, "relu", (small_stride[0], 1)], + [3, 72, 24, False, "relu", (small_stride[1], 1)], + [3, 88, 24, False, "relu", 1], + [5, 96, 40, True, "hard_swish", (small_stride[2], 1)], + [5, 240, 40, True, "hard_swish", 1], + [5, 240, 40, True, "hard_swish", 1], + [5, 120, 48, True, "hard_swish", 1], + [5, 144, 48, True, "hard_swish", 1], + [5, 288, 96, True, "hard_swish", (small_stride[3], 1)], + [5, 576, 96, True, "hard_swish", 1], + [5, 576, 96, True, "hard_swish", 1], + ] + cls_ch_squeeze = 576 + else: + raise NotImplementedError( + "mode[" + model_name + "_model] is not implemented!" + ) + + supported_scale = [0.35, 0.5, 0.75, 1.0, 1.25] + assert ( + scale in supported_scale + ), "supported scales are {} but input scale is {}".format( + supported_scale, scale + ) + + inplanes = 16 + # conv1 + self.conv1 = ConvBNLayer( + in_channels=in_channels, + out_channels=make_divisible(inplanes * scale), + kernel_size=3, + stride=2, + padding=1, + groups=1, + if_act=True, + act="hard_swish", + name="conv1", + ) + i = 0 + block_list = [] + inplanes = make_divisible(inplanes * scale) + for k, exp, c, se, nl, s in cfg: + block_list.append( + ResidualUnit( + in_channels=inplanes, + mid_channels=make_divisible(scale * exp), + out_channels=make_divisible(scale * c), + kernel_size=k, + stride=s, + use_se=se, + act=nl, + name="conv" + str(i + 2), + ) + ) + inplanes = make_divisible(scale * c) + i += 1 + self.blocks = nn.Sequential(*block_list) + + self.conv2 = ConvBNLayer( + in_channels=inplanes, + out_channels=make_divisible(scale * cls_ch_squeeze), + kernel_size=1, + stride=1, + padding=0, + groups=1, + if_act=True, + act="hard_swish", + name="conv_last", + ) + + self.pool = nn.MaxPool2d(kernel_size=2, stride=2, padding=0) + self.out_channels = make_divisible(scale * cls_ch_squeeze) + + def forward(self, x): + x = self.conv1(x) + x = self.blocks(x) + x = self.conv2(x) + x = self.pool(x) + return x diff --git a/python/rapidocr/networks/backbones/rec_svtrnet.py b/python/rapidocr/networks/backbones/rec_svtrnet.py new file mode 100644 index 0000000..3a11773 --- /dev/null +++ b/python/rapidocr/networks/backbones/rec_svtrnet.py @@ -0,0 +1,638 @@ +import numpy as np +import torch +from torch import nn + +from ..common import Activation + + +def drop_path(x, drop_prob=0.0, training=False): + """Drop paths (Stochastic Depth) per sample (when applied in main path of residual blocks). + the original name is misleading as 'Drop Connect' is a different form of dropout in a separate paper... + See discussion: https://github.com/tensorflow/tpu/issues/494#issuecomment-532968956 ... + """ + if drop_prob == 0.0 or not training: + return x + keep_prob = torch.as_tensor(1 - drop_prob) + shape = (x.shape[0],) + (1,) * (x.ndim - 1) + random_tensor = keep_prob + torch.rand(shape, dtype=x.dtype) + random_tensor = torch.floor(random_tensor) # binarize + output = x.divide(keep_prob) * random_tensor + return output + + +class ConvBNLayer(nn.Module): + def __init__( + self, + in_channels, + out_channels, + kernel_size=3, + stride=1, + padding=0, + bias_attr=False, + groups=1, + act="gelu", + ): + super().__init__() + self.conv = nn.Conv2d( + in_channels=in_channels, + out_channels=out_channels, + kernel_size=kernel_size, + stride=stride, + padding=padding, + groups=groups, + bias=bias_attr, + ) + self.norm = nn.BatchNorm2d(out_channels) + self.act = Activation(act_type=act, inplace=True) + + def forward(self, inputs): + out = self.conv(inputs) + out = self.norm(out) + out = self.act(out) + return out + + +class DropPath(nn.Module): + """Drop paths (Stochastic Depth) per sample (when applied in main path of residual blocks).""" + + def __init__(self, drop_prob=None): + super(DropPath, self).__init__() + self.drop_prob = drop_prob + + def forward(self, x): + return drop_path(x, self.drop_prob, self.training) + + +class Identity(nn.Module): + def __init__(self): + super(Identity, self).__init__() + + def forward(self, input): + return input + + +class Mlp(nn.Module): + def __init__( + self, + in_features, + hidden_features=None, + out_features=None, + act_layer="gelu", + drop=0.0, + ): + super().__init__() + out_features = out_features or in_features + hidden_features = hidden_features or in_features + self.fc1 = nn.Linear(in_features, hidden_features) + self.act = Activation(act_type=act_layer, inplace=True) + self.fc2 = nn.Linear(hidden_features, out_features) + self.drop = nn.Dropout(drop) + + def forward(self, x): + x = self.fc1(x) + x = self.act(x) + x = self.drop(x) + x = self.fc2(x) + x = self.drop(x) + return x + + +class ConvMixer(nn.Module): + def __init__( + self, + dim, + num_heads=8, + HW=[8, 25], + local_k=[3, 3], + ): + super().__init__() + self.HW = HW + self.dim = dim + self.local_mixer = nn.Conv2d( + dim, + dim, + local_k, + 1, + [local_k[0] // 2, local_k[1] // 2], + groups=num_heads, + ) + + def forward(self, x): + h = self.HW[0] + w = self.HW[1] + x = x.transpose([0, 2, 1]).reshape([0, self.dim, h, w]) + x = self.local_mixer(x) + x = x.flatten(2).permute(0, 2, 1) + return x + + +class Attention(nn.Module): + def __init__( + self, + dim, + num_heads=8, + mixer="Global", + HW=[8, 25], + local_k=[7, 11], + qkv_bias=False, + qk_scale=None, + attn_drop=0.0, + proj_drop=0.0, + ): + super().__init__() + self.num_heads = num_heads + head_dim = dim // num_heads + self.scale = qk_scale or head_dim**-0.5 + self.qkv = nn.Linear(dim, dim * 3, bias=qkv_bias) + self.attn_drop = nn.Dropout(attn_drop) + self.proj = nn.Linear(dim, dim) + self.proj_drop = nn.Dropout(proj_drop) + self.HW = HW + if HW is not None: + H = HW[0] + W = HW[1] + self.N = H * W + self.C = dim + if mixer == "Local" and HW is not None: + hk = local_k[0] + wk = local_k[1] + mask = torch.ones(H * W, H + hk - 1, W + wk - 1, dtype=torch.float32) + for h in range(0, H): + for w in range(0, W): + mask[h * W + w, h : h + hk, w : w + wk] = 0.0 + mask_paddle = mask[:, hk // 2 : H + hk // 2, wk // 2 : W + wk // 2].flatten( + 1 + ) + mask_inf = torch.full( + [H * W, H * W], fill_value=float("-Inf"), dtype=torch.float32 + ) + mask = torch.where(mask_paddle < 1, mask_paddle, mask_inf) + self.mask = mask.unsqueeze(0).unsqueeze(1) + # self.mask = mask[None, None, :] + self.mixer = mixer + + def forward(self, x): + if self.HW is not None: + N = self.N + C = self.C + else: + _, N, C = x.shape + qkv = self.qkv(x) + qkv = qkv.reshape((-1, N, 3, self.num_heads, C // self.num_heads)).permute( + 2, 0, 3, 1, 4 + ) + q, k, v = qkv[0] * self.scale, qkv[1], qkv[2] + + attn = q.matmul(k.permute(0, 1, 3, 2)) + if self.mixer == "Local": + attn += self.mask + attn = nn.functional.softmax(attn, dim=-1) + attn = self.attn_drop(attn) + + x = (attn.matmul(v)).permute(0, 2, 1, 3).reshape((-1, N, C)) + x = self.proj(x) + x = self.proj_drop(x) + return x + + +class Block(nn.Module): + def __init__( + self, + dim, + num_heads, + mixer="Global", + local_mixer=[7, 11], + HW=None, + mlp_ratio=4.0, + qkv_bias=False, + qk_scale=None, + drop=0.0, + attn_drop=0.0, + drop_path=0.0, + act_layer="gelu", + norm_layer="nn.LayerNorm", + epsilon=1e-6, + prenorm=True, + ): + super().__init__() + if isinstance(norm_layer, str): + self.norm1 = eval(norm_layer)(dim, eps=epsilon) + else: + self.norm1 = norm_layer(dim) + if mixer == "Global" or mixer == "Local": + self.mixer = Attention( + dim, + num_heads=num_heads, + mixer=mixer, + HW=HW, + local_k=local_mixer, + qkv_bias=qkv_bias, + qk_scale=qk_scale, + attn_drop=attn_drop, + proj_drop=drop, + ) + elif mixer == "Conv": + self.mixer = ConvMixer(dim, num_heads=num_heads, HW=HW, local_k=local_mixer) + else: + raise TypeError("The mixer must be one of [Global, Local, Conv]") + + self.drop_path = DropPath(drop_path) if drop_path > 0.0 else Identity() + if isinstance(norm_layer, str): + self.norm2 = eval(norm_layer)(dim, eps=epsilon) + else: + self.norm2 = norm_layer(dim) + mlp_hidden_dim = int(dim * mlp_ratio) + self.mlp_ratio = mlp_ratio + self.mlp = Mlp( + in_features=dim, + hidden_features=mlp_hidden_dim, + act_layer=act_layer, + drop=drop, + ) + self.prenorm = prenorm + + def forward(self, x): + if self.prenorm: + x = self.norm1(x + self.drop_path(self.mixer(x))) + x = self.norm2(x + self.drop_path(self.mlp(x))) + else: + x = x + self.drop_path(self.mixer(self.norm1(x))) + x = x + self.drop_path(self.mlp(self.norm2(x))) + return x + + +class PatchEmbed(nn.Module): + """Image to Patch Embedding""" + + def __init__( + self, + img_size=[32, 100], + in_channels=3, + embed_dim=768, + sub_num=2, + patch_size=[4, 4], + mode="pope", + ): + super().__init__() + num_patches = (img_size[1] // (2**sub_num)) * (img_size[0] // (2**sub_num)) + self.img_size = img_size + self.num_patches = num_patches + self.embed_dim = embed_dim + self.norm = None + if mode == "pope": + if sub_num == 2: + self.proj = nn.Sequential( + ConvBNLayer( + in_channels=in_channels, + out_channels=embed_dim // 2, + kernel_size=3, + stride=2, + padding=1, + act="gelu", + bias_attr=True, + ), + ConvBNLayer( + in_channels=embed_dim // 2, + out_channels=embed_dim, + kernel_size=3, + stride=2, + padding=1, + act="gelu", + bias_attr=True, + ), + ) + if sub_num == 3: + self.proj = nn.Sequential( + ConvBNLayer( + in_channels=in_channels, + out_channels=embed_dim // 4, + kernel_size=3, + stride=2, + padding=1, + act="gelu", + bias_attr=True, + ), + ConvBNLayer( + in_channels=embed_dim // 4, + out_channels=embed_dim // 2, + kernel_size=3, + stride=2, + padding=1, + act="gelu", + bias_attr=True, + ), + ConvBNLayer( + in_channels=embed_dim // 2, + out_channels=embed_dim, + kernel_size=3, + stride=2, + padding=1, + act="gelu", + bias_attr=True, + ), + ) + elif mode == "linear": + self.proj = nn.Conv2d( + 1, embed_dim, kernel_size=patch_size, stride=patch_size + ) + self.num_patches = ( + img_size[0] // patch_size[0] * img_size[1] // patch_size[1] + ) + + def forward(self, x): + B, C, H, W = x.shape + assert ( + H == self.img_size[0] and W == self.img_size[1] + ), "Input image size ({}*{}) doesn't match model ({}*{}).".format( + H, W, self.img_size[0], self.img_size[1] + ) + x = self.proj(x).flatten(2).permute(0, 2, 1) + return x + + +class SubSample(nn.Module): + def __init__( + self, + in_channels, + out_channels, + types="Pool", + stride=[2, 1], + sub_norm="nn.LayerNorm", + act=None, + ): + super().__init__() + self.types = types + if types == "Pool": + self.avgpool = nn.AvgPool2d( + kernel_size=[3, 5], stride=stride, padding=[1, 2] + ) + self.maxpool = nn.MaxPool2d( + kernel_size=[3, 5], stride=stride, padding=[1, 2] + ) + self.proj = nn.Linear(in_channels, out_channels) + else: + self.conv = nn.Conv2d( + in_channels, + out_channels, + kernel_size=3, + stride=stride, + padding=1, + ) + self.norm = eval(sub_norm)(out_channels) + if act is not None: + self.act = act() + else: + self.act = None + + def forward(self, x): + if self.types == "Pool": + x1 = self.avgpool(x) + x2 = self.maxpool(x) + x = (x1 + x2) * 0.5 + out = self.proj(x.flatten(2).permute(0, 2, 1)) + else: + x = self.conv(x) + out = x.flatten(2).permute(0, 2, 1) + out = self.norm(out) + if self.act is not None: + out = self.act(out) + + return out + + +class SVTRNet(nn.Module): + def __init__( + self, + img_size=[32, 100], + in_channels=3, + embed_dim=[64, 128, 256], + depth=[3, 6, 3], + num_heads=[2, 4, 8], + mixer=["Local"] * 6 + ["Global"] * 6, # Local atten, Global atten, Conv + local_mixer=[[7, 11], [7, 11], [7, 11]], + patch_merging="Conv", # Conv, Pool, None + mlp_ratio=4, + qkv_bias=True, + qk_scale=None, + drop_rate=0.0, + last_drop=0.0, + attn_drop_rate=0.0, + drop_path_rate=0.1, + norm_layer="nn.LayerNorm", + sub_norm="nn.LayerNorm", + epsilon=1e-6, + out_channels=192, + out_char_num=25, + block_unit="Block", + act="gelu", + last_stage=True, + sub_num=2, + prenorm=True, + use_lenhead=False, + **kwargs + ): + super().__init__() + self.img_size = img_size + self.embed_dim = embed_dim + self.out_channels = out_channels + self.prenorm = prenorm + patch_merging = ( + None + if patch_merging != "Conv" and patch_merging != "Pool" + else patch_merging + ) + self.patch_embed = PatchEmbed( + img_size=img_size, + in_channels=in_channels, + embed_dim=embed_dim[0], + sub_num=sub_num, + ) + num_patches = self.patch_embed.num_patches + self.HW = [img_size[0] // (2**sub_num), img_size[1] // (2**sub_num)] + self.pos_embed = nn.Parameter(torch.zeros(1, num_patches, embed_dim[0])) + self.pos_drop = nn.Dropout(p=drop_rate) + Block_unit = eval(block_unit) + + dpr = np.linspace(0, drop_path_rate, sum(depth)) + self.blocks1 = nn.ModuleList( + [ + Block_unit( + dim=embed_dim[0], + num_heads=num_heads[0], + mixer=mixer[0 : depth[0]][i], + HW=self.HW, + local_mixer=local_mixer[0], + mlp_ratio=mlp_ratio, + qkv_bias=qkv_bias, + qk_scale=qk_scale, + drop=drop_rate, + act_layer=act, + attn_drop=attn_drop_rate, + drop_path=dpr[0 : depth[0]][i], + norm_layer=norm_layer, + epsilon=epsilon, + prenorm=prenorm, + ) + for i in range(depth[0]) + ] + ) + if patch_merging is not None: + self.sub_sample1 = SubSample( + embed_dim[0], + embed_dim[1], + sub_norm=sub_norm, + stride=[2, 1], + types=patch_merging, + ) + HW = [self.HW[0] // 2, self.HW[1]] + else: + HW = self.HW + self.patch_merging = patch_merging + self.blocks2 = nn.ModuleList( + [ + Block_unit( + dim=embed_dim[1], + num_heads=num_heads[1], + mixer=mixer[depth[0] : depth[0] + depth[1]][i], + HW=HW, + local_mixer=local_mixer[1], + mlp_ratio=mlp_ratio, + qkv_bias=qkv_bias, + qk_scale=qk_scale, + drop=drop_rate, + act_layer=act, + attn_drop=attn_drop_rate, + drop_path=dpr[depth[0] : depth[0] + depth[1]][i], + norm_layer=norm_layer, + epsilon=epsilon, + prenorm=prenorm, + ) + for i in range(depth[1]) + ] + ) + if patch_merging is not None: + self.sub_sample2 = SubSample( + embed_dim[1], + embed_dim[2], + sub_norm=sub_norm, + stride=[2, 1], + types=patch_merging, + ) + HW = [self.HW[0] // 4, self.HW[1]] + else: + HW = self.HW + self.blocks3 = nn.ModuleList( + [ + Block_unit( + dim=embed_dim[2], + num_heads=num_heads[2], + mixer=mixer[depth[0] + depth[1] :][i], + HW=HW, + local_mixer=local_mixer[2], + mlp_ratio=mlp_ratio, + qkv_bias=qkv_bias, + qk_scale=qk_scale, + drop=drop_rate, + act_layer=act, + attn_drop=attn_drop_rate, + drop_path=dpr[depth[0] + depth[1] :][i], + norm_layer=norm_layer, + epsilon=epsilon, + prenorm=prenorm, + ) + for i in range(depth[2]) + ] + ) + self.last_stage = last_stage + if last_stage: + self.avg_pool = nn.AdaptiveAvgPool2d([1, out_char_num]) + self.last_conv = nn.Conv2d( + in_channels=embed_dim[2], + out_channels=self.out_channels, + kernel_size=1, + stride=1, + padding=0, + bias=False, + ) + self.hardswish = Activation("hard_swish", inplace=True) # nn.Hardswish() + # self.dropout = nn.Dropout(p=last_drop, mode="downscale_in_infer") + self.dropout = nn.Dropout(p=last_drop) + if not prenorm: + self.norm = eval(norm_layer)(embed_dim[-1], eps=epsilon) + self.use_lenhead = use_lenhead + if use_lenhead: + self.len_conv = nn.Linear(embed_dim[2], self.out_channels) + self.hardswish_len = Activation( + "hard_swish", inplace=True + ) # nn.Hardswish() + self.dropout_len = nn.Dropout(p=last_drop) + + torch.nn.init.xavier_normal_(self.pos_embed) + self.apply(self._init_weights) + + def _init_weights(self, m): + # weight initialization + if isinstance(m, nn.Conv2d): + nn.init.kaiming_normal_(m.weight, mode="fan_out") + if m.bias is not None: + nn.init.zeros_(m.bias) + elif isinstance(m, nn.BatchNorm2d): + nn.init.ones_(m.weight) + nn.init.zeros_(m.bias) + elif isinstance(m, nn.Linear): + nn.init.normal_(m.weight, 0, 0.01) + if m.bias is not None: + nn.init.zeros_(m.bias) + elif isinstance(m, nn.ConvTranspose2d): + nn.init.kaiming_normal_(m.weight, mode="fan_out") + if m.bias is not None: + nn.init.zeros_(m.bias) + elif isinstance(m, nn.LayerNorm): + nn.init.ones_(m.weight) + nn.init.zeros_(m.bias) + + def forward_features(self, x): + x = self.patch_embed(x) + x = x + self.pos_embed + x = self.pos_drop(x) + for blk in self.blocks1: + x = blk(x) + if self.patch_merging is not None: + x = self.sub_sample1( + x.permute(0, 2, 1).reshape( + [-1, self.embed_dim[0], self.HW[0], self.HW[1]] + ) + ) + for blk in self.blocks2: + x = blk(x) + if self.patch_merging is not None: + x = self.sub_sample2( + x.permute(0, 2, 1).reshape( + [-1, self.embed_dim[1], self.HW[0] // 2, self.HW[1]] + ) + ) + for blk in self.blocks3: + x = blk(x) + if not self.prenorm: + x = self.norm(x) + return x + + def forward(self, x): + x = self.forward_features(x) + if self.use_lenhead: + len_x = self.len_conv(x.mean(1)) + len_x = self.dropout_len(self.hardswish_len(len_x)) + if self.last_stage: + if self.patch_merging is not None: + h = self.HW[0] // 4 + else: + h = self.HW[0] + x = self.avg_pool( + x.permute(0, 2, 1).reshape([-1, self.embed_dim[2], h, self.HW[1]]) + ) + x = self.last_conv(x) + x = self.hardswish(x) + x = self.dropout(x) + if self.use_lenhead: + return x, len_x + return x diff --git a/python/rapidocr/networks/common.py b/python/rapidocr/networks/common.py new file mode 100644 index 0000000..ec1b30c --- /dev/null +++ b/python/rapidocr/networks/common.py @@ -0,0 +1,76 @@ +import torch +import torch.nn.functional as F +from torch import nn + + +class Hswish(nn.Module): + def __init__(self, inplace=True): + super(Hswish, self).__init__() + self.inplace = inplace + + def forward(self, x): + return x * F.relu6(x + 3.0, inplace=self.inplace) / 6.0 + + +# out = max(0, min(1, slop*x+offset)) +# paddle.fluid.layers.hard_sigmoid(x, slope=0.2, offset=0.5, name=None) +class Hsigmoid(nn.Module): + def __init__(self, inplace=True): + super(Hsigmoid, self).__init__() + self.inplace = inplace + + def forward(self, x): + # torch: F.relu6(x + 3., inplace=self.inplace) / 6. + # paddle: F.relu6(1.2 * x + 3., inplace=self.inplace) / 6. + return F.relu6(1.2 * x + 3.0, inplace=self.inplace) / 6.0 + + +class GELU(nn.Module): + def __init__(self, inplace=True): + super(GELU, self).__init__() + self.inplace = inplace + + def forward(self, x): + return torch.nn.functional.gelu(x) + + +class Swish(nn.Module): + def __init__(self, inplace=True): + super(Swish, self).__init__() + self.inplace = inplace + + def forward(self, x): + if self.inplace: + x.mul_(torch.sigmoid(x)) + return x + else: + return x * torch.sigmoid(x) + + +class Activation(nn.Module): + def __init__(self, act_type, inplace=True): + super(Activation, self).__init__() + act_type = act_type.lower() + if act_type == "relu": + self.act = nn.ReLU(inplace=inplace) + elif act_type == "relu6": + self.act = nn.ReLU6(inplace=inplace) + elif act_type == "sigmoid": + raise NotImplementedError + elif act_type == "hard_sigmoid": + self.act = Hsigmoid( + inplace + ) # nn.Hardsigmoid(inplace=inplace)#Hsigmoid(inplace)# + elif act_type == "hard_swish" or act_type == "hswish": + self.act = Hswish(inplace=inplace) + elif act_type == "leakyrelu": + self.act = nn.LeakyReLU(inplace=inplace) + elif act_type == "gelu": + self.act = GELU(inplace=inplace) + elif act_type == "swish": + self.act = Swish(inplace=inplace) + else: + raise NotImplementedError + + def forward(self, inputs): + return self.act(inputs) diff --git a/python/rapidocr/networks/heads/__init__.py b/python/rapidocr/networks/heads/__init__.py new file mode 100644 index 0000000..00428c4 --- /dev/null +++ b/python/rapidocr/networks/heads/__init__.py @@ -0,0 +1,43 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__all__ = ["build_head"] + + +def build_head(config, **kwargs): + # det head + from .det_db_head import DBHead, PFHeadLocal + + # rec head + from .rec_ctc_head import CTCHead + from .rec_multi_head import MultiHead + + # cls head + from .cls_head import ClsHead + + support_dict = [ + "DBHead", + "CTCHead", + "ClsHead", + "MultiHead", + "PFHeadLocal", + ] + + module_name = config.pop("name") + char_num = config.pop("char_num", 6625) + assert module_name in support_dict, Exception( + "head only support {}".format(support_dict) + ) + module_class = eval(module_name)(**config, **kwargs) + return module_class diff --git a/python/rapidocr/networks/heads/cls_head.py b/python/rapidocr/networks/heads/cls_head.py new file mode 100644 index 0000000..9353b9e --- /dev/null +++ b/python/rapidocr/networks/heads/cls_head.py @@ -0,0 +1,23 @@ +import torch +import torch.nn.functional as F +from torch import nn + + +class ClsHead(nn.Module): + """ + Class orientation + Args: + params(dict): super parameters for build Class network + """ + + def __init__(self, in_channels, class_dim, **kwargs): + super(ClsHead, self).__init__() + self.pool = nn.AdaptiveAvgPool2d(1) + self.fc = nn.Linear(in_channels, class_dim, bias=True) + + def forward(self, x): + x = self.pool(x) + x = torch.reshape(x, shape=[x.shape[0], x.shape[1]]) + x = self.fc(x) + x = F.softmax(x, dim=1) + return x diff --git a/python/rapidocr/networks/heads/det_db_head.py b/python/rapidocr/networks/heads/det_db_head.py new file mode 100644 index 0000000..2de9982 --- /dev/null +++ b/python/rapidocr/networks/heads/det_db_head.py @@ -0,0 +1,131 @@ +import torch +import torch.nn.functional as F +from torch import nn + +from ..backbones.det_mobilenet_v3 import ConvBNLayer +from ..common import Activation + + +class Head(nn.Module): + def __init__(self, in_channels, **kwargs): + super(Head, self).__init__() + self.conv1 = nn.Conv2d( + in_channels=in_channels, + out_channels=in_channels // 4, + kernel_size=3, + padding=1, + bias=False, + ) + self.conv_bn1 = nn.BatchNorm2d(in_channels // 4) + self.relu1 = Activation(act_type="relu") + + self.conv2 = nn.ConvTranspose2d( + in_channels=in_channels // 4, + out_channels=in_channels // 4, + kernel_size=2, + stride=2, + ) + self.conv_bn2 = nn.BatchNorm2d(in_channels // 4) + self.relu2 = Activation(act_type="relu") + + self.conv3 = nn.ConvTranspose2d( + in_channels=in_channels // 4, out_channels=1, kernel_size=2, stride=2 + ) + + def forward(self, x, return_f=False): + x = self.conv1(x) + x = self.conv_bn1(x) + x = self.relu1(x) + x = self.conv2(x) + x = self.conv_bn2(x) + x = self.relu2(x) + if return_f is True: + f = x + x = self.conv3(x) + x = torch.sigmoid(x) + if return_f is True: + return x, f + return x + + +class DBHead(nn.Module): + """ + Differentiable Binarization (DB) for text detection: + see https://arxiv.org/abs/1911.08947 + args: + params(dict): super parameters for build DB network + """ + + def __init__(self, in_channels, k=50, **kwargs): + 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) + + def step_function(self, x, y): + return torch.reciprocal(1 + torch.exp(-self.k * (x - y))) + + def forward(self, x): + # shrink_maps = self.binarize(x) + return self.binarize(x) + # if not self.training: + # return {'maps': shrink_maps} + + # threshold_maps = self.thresh(x) + # binary_maps = self.step_function(shrink_maps, threshold_maps) + # y = torch.cat([shrink_maps, threshold_maps, binary_maps], dim=1) + # return y + + +class LocalModule(nn.Module): + def __init__(self, in_c, mid_c, use_distance=True): + super(self.__class__, self).__init__() + self.last_3 = ConvBNLayer(in_c + 1, mid_c, 3, 1, 1, act="relu") + self.last_1 = nn.Conv2d(mid_c, 1, 1, 1, 0) + + def forward(self, x, init_map, distance_map): + outf = torch.cat([init_map, x], dim=1) + # last Conv + out = self.last_1(self.last_3(outf)) + return out + + +class PFHeadLocal(DBHead): + def __init__(self, in_channels, k=50, mode="small", **kwargs): + super(PFHeadLocal, self).__init__(in_channels, k, **kwargs) + self.mode = mode + + self.up_conv = nn.Upsample(scale_factor=2, mode="nearest") + if self.mode == "large": + self.cbn_layer = LocalModule(in_channels // 4, in_channels // 4) + elif self.mode == "small": + self.cbn_layer = LocalModule(in_channels // 4, in_channels // 8) + + def forward(self, x, targets=None): + shrink_maps, f = self.binarize(x, return_f=True) + base_maps = shrink_maps + cbn_maps = self.cbn_layer(self.up_conv(f), shrink_maps, None) + cbn_maps = F.sigmoid(cbn_maps) + return 0.5 * (base_maps + cbn_maps) + # return {'maps': 0.5 * (base_maps + cbn_maps), 'cbn_maps': cbn_maps} + + # threshold_maps = self.thresh(x) + # binary_maps = self.step_function(shrink_maps, threshold_maps) + # y = torch.cat([cbn_maps, threshold_maps, binary_maps], dim=1) + # return y diff --git a/python/rapidocr/networks/heads/rec_ctc_head.py b/python/rapidocr/networks/heads/rec_ctc_head.py new file mode 100644 index 0000000..42e2fab --- /dev/null +++ b/python/rapidocr/networks/heads/rec_ctc_head.py @@ -0,0 +1,54 @@ +import torch.nn.functional as F +from torch import nn + + +class CTCHead(nn.Module): + def __init__( + self, + in_channels, + out_channels=6625, + fc_decay=0.0004, + mid_channels=None, + return_feats=False, + **kwargs + ): + super(CTCHead, self).__init__() + if mid_channels is None: + self.fc = nn.Linear( + in_channels, + out_channels, + bias=True, + ) + else: + self.fc1 = nn.Linear( + in_channels, + mid_channels, + bias=True, + ) + self.fc2 = nn.Linear( + mid_channels, + out_channels, + bias=True, + ) + + self.out_channels = out_channels + self.mid_channels = mid_channels + self.return_feats = return_feats + + def forward(self, x, labels=None): + if self.mid_channels is None: + predicts = self.fc(x) + else: + x = self.fc1(x) + predicts = self.fc2(x) + + if self.return_feats: + result = (x, predicts) + else: + result = predicts + + if not self.training: + predicts = F.softmax(predicts, dim=2) + result = predicts + + return result diff --git a/python/rapidocr/networks/heads/rec_multi_head.py b/python/rapidocr/networks/heads/rec_multi_head.py new file mode 100644 index 0000000..b69371b --- /dev/null +++ b/python/rapidocr/networks/heads/rec_multi_head.py @@ -0,0 +1,58 @@ +from torch import nn + +from ..necks.rnn import Im2Seq, SequenceEncoder + + +class FCTranspose(nn.Module): + def __init__(self, in_channels, out_channels, only_transpose=False): + super().__init__() + self.only_transpose = only_transpose + if not self.only_transpose: + self.fc = nn.Linear(in_channels, out_channels, bias=False) + + def forward(self, x): + if self.only_transpose: + return x.permute([0, 2, 1]) + else: + return self.fc(x.permute([0, 2, 1])) + + +class MultiHead(nn.Module): + def __init__(self, in_channels, out_channels_list, **kwargs): + super().__init__() + self.head_list = kwargs.pop("head_list") + + self.gtc_head = "sar" + assert len(self.head_list) >= 2 + for idx, head_name in enumerate(self.head_list): + name = list(head_name)[0] + if name == "SARHead": + pass + + elif name == "NRTRHead": + pass + elif name == "CTCHead": + # ctc neck + 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 = {} + self.ctc_head = eval(name)( + in_channels=self.ctc_encoder.out_channels, + out_channels=out_channels_list["CTCLabelDecode"], + **head_args + ) + else: + raise NotImplementedError( + "{} is not supported in MultiHead yet".format(name) + ) + + def forward(self, x, data=None): + ctc_encoder = self.ctc_encoder(x) + return self.ctc_head(ctc_encoder) diff --git a/python/rapidocr/networks/necks/__init__.py b/python/rapidocr/networks/necks/__init__.py new file mode 100644 index 0000000..178ba76 --- /dev/null +++ b/python/rapidocr/networks/necks/__init__.py @@ -0,0 +1,28 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__all__ = ["build_neck"] + + +def build_neck(config): + from .db_fpn import DBFPN, LKPAN, RSEFPN + + support_dict = ["DBFPN", "SequenceEncoder", "RSEFPN", "LKPAN"] + + module_name = config.pop("name") + assert module_name in support_dict, Exception( + "neck only support {}".format(support_dict) + ) + module_class = eval(module_name)(**config) + return module_class diff --git a/python/rapidocr/networks/necks/db_fpn.py b/python/rapidocr/networks/necks/db_fpn.py new file mode 100644 index 0000000..9c8460a --- /dev/null +++ b/python/rapidocr/networks/necks/db_fpn.py @@ -0,0 +1,456 @@ +import torch +import torch.nn.functional as F +from torch import nn + +from ..backbones.det_mobilenet_v3 import SEModule +from ..necks.intracl import IntraCLBlock + + +def hard_swish(x, inplace=True): + return x * F.relu6(x + 3.0, inplace=inplace) / 6.0 + + +class DSConv(nn.Module): + def __init__( + self, + in_channels, + out_channels, + kernel_size, + padding, + stride=1, + groups=None, + if_act=True, + act="relu", + **kwargs + ): + super(DSConv, self).__init__() + if groups == None: + groups = in_channels + self.if_act = if_act + self.act = act + self.conv1 = nn.Conv2d( + in_channels=in_channels, + out_channels=in_channels, + kernel_size=kernel_size, + stride=stride, + padding=padding, + groups=groups, + bias=False, + ) + + self.bn1 = nn.BatchNorm2d(in_channels) + + self.conv2 = nn.Conv2d( + in_channels=in_channels, + out_channels=int(in_channels * 4), + kernel_size=1, + stride=1, + bias=False, + ) + + self.bn2 = nn.BatchNorm2d(int(in_channels * 4)) + + self.conv3 = nn.Conv2d( + in_channels=int(in_channels * 4), + out_channels=out_channels, + kernel_size=1, + stride=1, + bias=False, + ) + self._c = [in_channels, out_channels] + if in_channels != out_channels: + self.conv_end = nn.Conv2d( + in_channels=in_channels, + out_channels=out_channels, + kernel_size=1, + stride=1, + bias=False, + ) + + def forward(self, inputs): + x = self.conv1(inputs) + x = self.bn1(x) + + x = self.conv2(x) + x = self.bn2(x) + if self.if_act: + if self.act == "relu": + x = F.relu(x) + elif self.act == "hardswish": + x = hard_swish(x) + else: + print( + "The activation function({}) is selected incorrectly.".format( + self.act + ) + ) + exit() + + x = self.conv3(x) + if self._c[0] != self._c[1]: + x = x + self.conv_end(inputs) + return x + + +class DBFPN(nn.Module): + def __init__(self, in_channels, out_channels, use_asf=False, **kwargs): + super(DBFPN, self).__init__() + self.out_channels = out_channels + self.use_asf = use_asf + + self.in2_conv = nn.Conv2d( + in_channels=in_channels[0], + out_channels=self.out_channels, + kernel_size=1, + bias=False, + ) + self.in3_conv = nn.Conv2d( + in_channels=in_channels[1], + out_channels=self.out_channels, + kernel_size=1, + bias=False, + ) + self.in4_conv = nn.Conv2d( + in_channels=in_channels[2], + out_channels=self.out_channels, + kernel_size=1, + bias=False, + ) + self.in5_conv = nn.Conv2d( + in_channels=in_channels[3], + out_channels=self.out_channels, + kernel_size=1, + bias=False, + ) + self.p5_conv = nn.Conv2d( + in_channels=self.out_channels, + out_channels=self.out_channels // 4, + kernel_size=3, + padding=1, + bias=False, + ) + self.p4_conv = nn.Conv2d( + in_channels=self.out_channels, + out_channels=self.out_channels // 4, + kernel_size=3, + padding=1, + bias=False, + ) + self.p3_conv = nn.Conv2d( + in_channels=self.out_channels, + out_channels=self.out_channels // 4, + kernel_size=3, + padding=1, + bias=False, + ) + self.p2_conv = nn.Conv2d( + in_channels=self.out_channels, + out_channels=self.out_channels // 4, + kernel_size=3, + padding=1, + bias=False, + ) + + if self.use_asf is True: + self.asf = ASFBlock(self.out_channels, self.out_channels // 4) + + def forward(self, x): + c2, c3, c4, c5 = x + + in5 = self.in5_conv(c5) + in4 = self.in4_conv(c4) + in3 = self.in3_conv(c3) + in2 = self.in2_conv(c2) + + out4 = in4 + F.interpolate( + in5, + scale_factor=2, + mode="nearest", + ) # align_mode=1) # 1/16 + out3 = in3 + F.interpolate( + out4, + scale_factor=2, + mode="nearest", + ) # align_mode=1) # 1/8 + out2 = in2 + F.interpolate( + out3, + scale_factor=2, + mode="nearest", + ) # align_mode=1) # 1/4 + + p5 = self.p5_conv(in5) + p4 = self.p4_conv(out4) + p3 = self.p3_conv(out3) + p2 = self.p2_conv(out2) + p5 = F.interpolate( + p5, + scale_factor=8, + mode="nearest", + ) # align_mode=1) + p4 = F.interpolate( + p4, + scale_factor=4, + mode="nearest", + ) # align_mode=1) + p3 = F.interpolate( + p3, + scale_factor=2, + mode="nearest", + ) # align_mode=1) + + fuse = torch.cat([p5, p4, p3, p2], dim=1) + + if self.use_asf is True: + fuse = self.asf(fuse, [p5, p4, p3, p2]) + + return fuse + + +class RSELayer(nn.Module): + def __init__(self, in_channels, out_channels, kernel_size, shortcut=True): + super(RSELayer, self).__init__() + self.out_channels = out_channels + self.in_conv = nn.Conv2d( + in_channels=in_channels, + out_channels=self.out_channels, + kernel_size=kernel_size, + padding=int(kernel_size // 2), + bias=False, + ) + self.se_block = SEModule(self.out_channels) + self.shortcut = shortcut + + def forward(self, ins): + x = self.in_conv(ins) + if self.shortcut: + out = x + self.se_block(x) + else: + out = self.se_block(x) + return out + + +class RSEFPN(nn.Module): + def __init__(self, in_channels, out_channels, shortcut=True, **kwargs): + super(RSEFPN, self).__init__() + self.out_channels = out_channels + self.ins_conv = nn.ModuleList() + self.inp_conv = nn.ModuleList() + self.intracl = False + if "intracl" in kwargs.keys() and kwargs["intracl"] is True: + self.intracl = kwargs["intracl"] + self.incl1 = IntraCLBlock(self.out_channels // 4, reduce_factor=2) + self.incl2 = IntraCLBlock(self.out_channels // 4, reduce_factor=2) + self.incl3 = IntraCLBlock(self.out_channels // 4, reduce_factor=2) + self.incl4 = IntraCLBlock(self.out_channels // 4, reduce_factor=2) + + for i in range(len(in_channels)): + self.ins_conv.append( + RSELayer(in_channels[i], out_channels, kernel_size=1, shortcut=shortcut) + ) + self.inp_conv.append( + RSELayer( + out_channels, out_channels // 4, kernel_size=3, shortcut=shortcut + ) + ) + + def forward(self, x): + c2, c3, c4, c5 = x + + in5 = self.ins_conv[3](c5) + in4 = self.ins_conv[2](c4) + in3 = self.ins_conv[1](c3) + in2 = self.ins_conv[0](c2) + + out4 = in4 + F.interpolate(in5, scale_factor=2, mode="nearest") # 1/16 + out3 = in3 + F.interpolate(out4, scale_factor=2, mode="nearest") # 1/8 + out2 = in2 + F.interpolate(out3, scale_factor=2, mode="nearest") # 1/4 + + p5 = self.inp_conv[3](in5) + p4 = self.inp_conv[2](out4) + p3 = self.inp_conv[1](out3) + p2 = self.inp_conv[0](out2) + + if self.intracl is True: + p5 = self.incl4(p5) + p4 = self.incl3(p4) + p3 = self.incl2(p3) + p2 = self.incl1(p2) + + p5 = F.interpolate(p5, scale_factor=8, mode="nearest") + p4 = F.interpolate(p4, scale_factor=4, mode="nearest") + p3 = F.interpolate(p3, scale_factor=2, mode="nearest") + + fuse = torch.cat([p5, p4, p3, p2], dim=1) + return fuse + + +class LKPAN(nn.Module): + def __init__(self, in_channels, out_channels, mode="large", **kwargs): + super(LKPAN, self).__init__() + self.out_channels = out_channels + + self.ins_conv = nn.ModuleList() + self.inp_conv = nn.ModuleList() + # pan head + self.pan_head_conv = nn.ModuleList() + self.pan_lat_conv = nn.ModuleList() + + if mode.lower() == "lite": + p_layer = DSConv + elif mode.lower() == "large": + p_layer = nn.Conv2d + else: + raise ValueError( + "mode can only be one of ['lite', 'large'], but received {}".format( + mode + ) + ) + + for i in range(len(in_channels)): + self.ins_conv.append( + nn.Conv2d( + in_channels=in_channels[i], + out_channels=self.out_channels, + kernel_size=1, + bias=False, + ) + ) + + self.inp_conv.append( + p_layer( + in_channels=self.out_channels, + out_channels=self.out_channels // 4, + kernel_size=9, + padding=4, + bias=False, + ) + ) + + if i > 0: + self.pan_head_conv.append( + nn.Conv2d( + in_channels=self.out_channels // 4, + out_channels=self.out_channels // 4, + kernel_size=3, + padding=1, + stride=2, + bias=False, + ) + ) + self.pan_lat_conv.append( + p_layer( + in_channels=self.out_channels // 4, + out_channels=self.out_channels // 4, + kernel_size=9, + padding=4, + bias=False, + ) + ) + self.intracl = False + if "intracl" in kwargs.keys() and kwargs["intracl"] is True: + self.intracl = kwargs["intracl"] + self.incl1 = IntraCLBlock(self.out_channels // 4, reduce_factor=2) + self.incl2 = IntraCLBlock(self.out_channels // 4, reduce_factor=2) + self.incl3 = IntraCLBlock(self.out_channels // 4, reduce_factor=2) + self.incl4 = IntraCLBlock(self.out_channels // 4, reduce_factor=2) + + def forward(self, x): + c2, c3, c4, c5 = x + + in5 = self.ins_conv[3](c5) + in4 = self.ins_conv[2](c4) + in3 = self.ins_conv[1](c3) + in2 = self.ins_conv[0](c2) + + out4 = in4 + F.interpolate(in5, scale_factor=2, mode="nearest") # 1/16 + out3 = in3 + F.interpolate(out4, scale_factor=2, mode="nearest") # 1/8 + out2 = in2 + F.interpolate(out3, scale_factor=2, mode="nearest") # 1/4 + + f5 = self.inp_conv[3](in5) + f4 = self.inp_conv[2](out4) + f3 = self.inp_conv[1](out3) + f2 = self.inp_conv[0](out2) + + pan3 = f3 + self.pan_head_conv[0](f2) + pan4 = f4 + self.pan_head_conv[1](pan3) + pan5 = f5 + self.pan_head_conv[2](pan4) + + p2 = self.pan_lat_conv[0](f2) + p3 = self.pan_lat_conv[1](pan3) + p4 = self.pan_lat_conv[2](pan4) + p5 = self.pan_lat_conv[3](pan5) + + if self.intracl is True: + p5 = self.incl4(p5) + p4 = self.incl3(p4) + p3 = self.incl2(p3) + p2 = self.incl1(p2) + + p5 = F.interpolate(p5, scale_factor=8, mode="nearest") + p4 = F.interpolate(p4, scale_factor=4, mode="nearest") + p3 = F.interpolate(p3, scale_factor=2, mode="nearest") + + fuse = torch.cat([p5, p4, p3, p2], dim=1) + return fuse + + +class ASFBlock(nn.Module): + """ + This code is refered from: + https://github.com/MhLiao/DB/blob/master/decoders/feature_attention.py + """ + + def __init__(self, in_channels, inter_channels, out_features_num=4): + """ + Adaptive Scale Fusion (ASF) block of DBNet++ + Args: + in_channels: the number of channels in the input data + inter_channels: the number of middle channels + out_features_num: the number of fused stages + """ + super(ASFBlock, self).__init__() + self.in_channels = in_channels + self.inter_channels = inter_channels + self.out_features_num = out_features_num + self.conv = nn.Conv2d(in_channels, inter_channels, 3, padding=1) + + self.spatial_scale = nn.Sequential( + # Nx1xHxW + nn.Conv2d( + in_channels=1, + out_channels=1, + kernel_size=3, + bias=False, + padding=1, + ), + nn.ReLU(), + nn.Conv2d( + in_channels=1, + out_channels=1, + kernel_size=1, + bias=False, + ), + nn.Sigmoid(), + ) + + self.channel_scale = nn.Sequential( + nn.Conv2d( + in_channels=inter_channels, + out_channels=out_features_num, + kernel_size=1, + bias=False, + ), + nn.Sigmoid(), + ) + + def forward(self, fuse_features, features_list): + fuse_features = self.conv(fuse_features) + spatial_x = torch.mean(fuse_features, dim=1, keepdim=True) + attention_scores = self.spatial_scale(spatial_x) + fuse_features + attention_scores = self.channel_scale(attention_scores) + assert len(features_list) == self.out_features_num + + out_list = [] + for i in range(self.out_features_num): + out_list.append(attention_scores[:, i : i + 1] * features_list[i]) + return torch.cat(out_list, dim=1) diff --git a/python/rapidocr/networks/necks/intracl.py b/python/rapidocr/networks/necks/intracl.py new file mode 100644 index 0000000..0ba85fa --- /dev/null +++ b/python/rapidocr/networks/necks/intracl.py @@ -0,0 +1,117 @@ +from torch import nn + + +class IntraCLBlock(nn.Module): + def __init__(self, in_channels=96, reduce_factor=4): + super(IntraCLBlock, self).__init__() + self.channels = in_channels + self.rf = reduce_factor + self.conv1x1_reduce_channel = nn.Conv2d( + self.channels, self.channels // self.rf, kernel_size=1, stride=1, padding=0 + ) + self.conv1x1_return_channel = nn.Conv2d( + self.channels // self.rf, self.channels, kernel_size=1, stride=1, padding=0 + ) + + self.v_layer_7x1 = nn.Conv2d( + self.channels // self.rf, + self.channels // self.rf, + kernel_size=(7, 1), + stride=(1, 1), + padding=(3, 0), + ) + self.v_layer_5x1 = nn.Conv2d( + self.channels // self.rf, + self.channels // self.rf, + kernel_size=(5, 1), + stride=(1, 1), + padding=(2, 0), + ) + self.v_layer_3x1 = nn.Conv2d( + self.channels // self.rf, + self.channels // self.rf, + kernel_size=(3, 1), + stride=(1, 1), + padding=(1, 0), + ) + + self.q_layer_1x7 = nn.Conv2d( + self.channels // self.rf, + self.channels // self.rf, + kernel_size=(1, 7), + stride=(1, 1), + padding=(0, 3), + ) + self.q_layer_1x5 = nn.Conv2d( + self.channels // self.rf, + self.channels // self.rf, + kernel_size=(1, 5), + stride=(1, 1), + padding=(0, 2), + ) + self.q_layer_1x3 = nn.Conv2d( + self.channels // self.rf, + self.channels // self.rf, + kernel_size=(1, 3), + stride=(1, 1), + padding=(0, 1), + ) + + # base + self.c_layer_7x7 = nn.Conv2d( + self.channels // self.rf, + self.channels // self.rf, + kernel_size=(7, 7), + stride=(1, 1), + padding=(3, 3), + ) + self.c_layer_5x5 = nn.Conv2d( + self.channels // self.rf, + self.channels // self.rf, + kernel_size=(5, 5), + stride=(1, 1), + padding=(2, 2), + ) + self.c_layer_3x3 = nn.Conv2d( + self.channels // self.rf, + self.channels // self.rf, + kernel_size=(3, 3), + stride=(1, 1), + padding=(1, 1), + ) + + self.bn = nn.BatchNorm2d(self.channels) + self.relu = nn.ReLU() + + def forward(self, x): + x_new = self.conv1x1_reduce_channel(x) + + x_7_c = self.c_layer_7x7(x_new) + x_7_v = self.v_layer_7x1(x_new) + x_7_q = self.q_layer_1x7(x_new) + x_7 = x_7_c + x_7_v + x_7_q + + x_5_c = self.c_layer_5x5(x_7) + x_5_v = self.v_layer_5x1(x_7) + x_5_q = self.q_layer_1x5(x_7) + x_5 = x_5_c + x_5_v + x_5_q + + x_3_c = self.c_layer_3x3(x_5) + x_3_v = self.v_layer_3x1(x_5) + x_3_q = self.q_layer_1x3(x_5) + x_3 = x_3_c + x_3_v + x_3_q + + x_relation = self.conv1x1_return_channel(x_3) + + x_relation = self.bn(x_relation) + x_relation = self.relu(x_relation) + + return x + x_relation + + +def build_intraclblock_list(num_block): + IntraCLBlock_list = nn.ModuleList() + for i in range(num_block): + IntraCLBlock_list.append(IntraCLBlock()) + + return IntraCLBlock_list diff --git a/python/rapidocr/networks/necks/rnn.py b/python/rapidocr/networks/necks/rnn.py new file mode 100644 index 0000000..244ea44 --- /dev/null +++ b/python/rapidocr/networks/necks/rnn.py @@ -0,0 +1,228 @@ +import torch +from torch import nn + +from ..backbones.rec_svtrnet import Block, ConvBNLayer + + +class Im2Seq(nn.Module): + def __init__(self, in_channels, **kwargs): + super().__init__() + self.out_channels = in_channels + + def forward(self, x): + B, C, H, W = x.shape + # assert H == 1 + x = x.squeeze(dim=2) + # x = x.transpose([0, 2, 1]) # paddle (NTC)(batch, width, channels) + x = x.permute(0, 2, 1) + return x + + +class EncoderWithRNN_(nn.Module): + def __init__(self, in_channels, hidden_size): + super(EncoderWithRNN_, self).__init__() + self.out_channels = hidden_size * 2 + self.rnn1 = nn.LSTM( + in_channels, + hidden_size, + bidirectional=False, + batch_first=True, + num_layers=2, + ) + self.rnn2 = nn.LSTM( + in_channels, + hidden_size, + bidirectional=False, + batch_first=True, + num_layers=2, + ) + + def forward(self, x): + self.rnn1.flatten_parameters() + self.rnn2.flatten_parameters() + out1, h1 = self.rnn1(x) + out2, h2 = self.rnn2(torch.flip(x, [1])) + return torch.cat([out1, torch.flip(out2, [1])], 2) + + +class EncoderWithRNN(nn.Module): + def __init__(self, in_channels, hidden_size): + super(EncoderWithRNN, self).__init__() + self.out_channels = hidden_size * 2 + self.lstm = nn.LSTM( + in_channels, hidden_size, num_layers=2, batch_first=True, bidirectional=True + ) # batch_first:=True + + def forward(self, x): + x, _ = self.lstm(x) + return x + + +class EncoderWithFC(nn.Module): + def __init__(self, in_channels, hidden_size): + super(EncoderWithFC, self).__init__() + self.out_channels = hidden_size + self.fc = nn.Linear( + in_channels, + hidden_size, + bias=True, + ) + + def forward(self, x): + x = self.fc(x) + return x + + +class EncoderWithSVTR(nn.Module): + def __init__( + self, + in_channels, + dims=64, # XS + depth=2, + hidden_dims=120, + use_guide=False, + num_heads=8, + qkv_bias=True, + mlp_ratio=2.0, + drop_rate=0.1, + kernel_size=[3, 3], + attn_drop_rate=0.1, + drop_path=0.0, + qk_scale=None, + ): + super(EncoderWithSVTR, self).__init__() + self.depth = depth + self.use_guide = use_guide + self.conv1 = ConvBNLayer( + in_channels, + in_channels // 8, + kernel_size=kernel_size, + padding=[kernel_size[0] // 2, kernel_size[1] // 2], + act="swish", + ) + self.conv2 = ConvBNLayer( + in_channels // 8, hidden_dims, kernel_size=1, act="swish" + ) + + self.svtr_block = nn.ModuleList( + [ + Block( + dim=hidden_dims, + num_heads=num_heads, + mixer="Global", + HW=None, + mlp_ratio=mlp_ratio, + qkv_bias=qkv_bias, + qk_scale=qk_scale, + drop=drop_rate, + act_layer="swish", + attn_drop=attn_drop_rate, + drop_path=drop_path, + norm_layer="nn.LayerNorm", + epsilon=1e-05, + prenorm=False, + ) + for i in range(depth) + ] + ) + self.norm = nn.LayerNorm(hidden_dims, eps=1e-6) + self.conv3 = ConvBNLayer(hidden_dims, in_channels, kernel_size=1, act="swish") + # last conv-nxn, the input is concat of input tensor and conv3 output tensor + self.conv4 = ConvBNLayer( + 2 * in_channels, in_channels // 8, padding=1, act="swish" + ) + + self.conv1x1 = ConvBNLayer(in_channels // 8, dims, kernel_size=1, act="swish") + self.out_channels = dims + self.apply(self._init_weights) + + def _init_weights(self, m): + # weight initialization + if isinstance(m, nn.Conv2d): + nn.init.kaiming_normal_(m.weight, mode="fan_out") + if m.bias is not None: + nn.init.zeros_(m.bias) + elif isinstance(m, nn.BatchNorm2d): + nn.init.ones_(m.weight) + nn.init.zeros_(m.bias) + elif isinstance(m, nn.Linear): + nn.init.normal_(m.weight, 0, 0.01) + if m.bias is not None: + nn.init.zeros_(m.bias) + elif isinstance(m, nn.ConvTranspose2d): + nn.init.kaiming_normal_(m.weight, mode="fan_out") + if m.bias is not None: + nn.init.zeros_(m.bias) + elif isinstance(m, nn.LayerNorm): + nn.init.ones_(m.weight) + nn.init.zeros_(m.bias) + + def forward(self, x): + # for use guide + if self.use_guide: + z = x.clone() + z.stop_gradient = True + else: + z = x + # for short cut + h = z + # reduce dim + z = self.conv1(z) + z = self.conv2(z) + # SVTR global block + B, C, H, W = z.shape + z = z.flatten(2).permute(0, 2, 1) + + for blk in self.svtr_block: + z = blk(z) + + z = self.norm(z) + # last stage + z = z.reshape([-1, H, W, C]).permute(0, 3, 1, 2) + z = self.conv3(z) + z = torch.cat((h, z), dim=1) + z = self.conv1x1(self.conv4(z)) + + return z + + +class SequenceEncoder(nn.Module): + def __init__(self, in_channels, encoder_type, hidden_size=48, **kwargs): + super(SequenceEncoder, self).__init__() + self.encoder_reshape = Im2Seq(in_channels) + self.out_channels = self.encoder_reshape.out_channels + self.encoder_type = encoder_type + if encoder_type == "reshape": + self.only_reshape = True + else: + support_encoder_dict = { + "reshape": Im2Seq, + "fc": EncoderWithFC, + "rnn": EncoderWithRNN, + "svtr": EncoderWithSVTR, + } + assert encoder_type in support_encoder_dict, "{} must in {}".format( + encoder_type, support_encoder_dict.keys() + ) + + if encoder_type == "svtr": + self.encoder = support_encoder_dict[encoder_type]( + self.encoder_reshape.out_channels, **kwargs + ) + else: + self.encoder = support_encoder_dict[encoder_type]( + self.encoder_reshape.out_channels, hidden_size + ) + self.out_channels = self.encoder.out_channels + self.only_reshape = False + + def forward(self, x): + if self.encoder_type != "svtr": + x = self.encoder_reshape(x) + if not self.only_reshape: + x = self.encoder(x) + return x + else: + x = self.encoder(x) + x = self.encoder_reshape(x) + return x diff --git a/python/rapidocr/utils/parse_parameters.py b/python/rapidocr/utils/parse_parameters.py index b946fed..f9c0064 100644 --- a/python/rapidocr/utils/parse_parameters.py +++ b/python/rapidocr/utils/parse_parameters.py @@ -100,6 +100,10 @@ def parse_lang(lang): lang = "cyrillic" elif lang in devanagari_lang: lang = "devanagari" + elif lang in ["ch", "en"]: + pass + else: + raise ValueError(f"lang: {lang} is not in the supported list.") if lang == "ch": det_lang = "ch" diff --git a/python/rapidocr_torch/modeling/common.py b/python/rapidocr_torch/modeling/common.py index 2d6e3d9..7378093 100644 --- a/python/rapidocr_torch/modeling/common.py +++ b/python/rapidocr_torch/modeling/common.py @@ -1,16 +1,16 @@ - - import torch import torch.nn as nn import torch.nn.functional as F + class Hswish(nn.Module): def __init__(self, inplace=True): super(Hswish, self).__init__() self.inplace = inplace def forward(self, x): - return x * F.relu6(x + 3., inplace=self.inplace) / 6. + return x * F.relu6(x + 3.0, inplace=self.inplace) / 6.0 + # out = max(0, min(1, slop*x+offset)) # paddle.fluid.layers.hard_sigmoid(x, slope=0.2, offset=0.5, name=None) @@ -22,7 +22,8 @@ class Hsigmoid(nn.Module): def forward(self, x): # torch: F.relu6(x + 3., inplace=self.inplace) / 6. # paddle: F.relu6(1.2 * x + 3., inplace=self.inplace) / 6. - return F.relu6(1.2 * x + 3., inplace=self.inplace) / 6. + return F.relu6(1.2 * x + 3.0, inplace=self.inplace) / 6.0 + class GELU(nn.Module): def __init__(self, inplace=True): @@ -43,31 +44,33 @@ class Swish(nn.Module): x.mul_(torch.sigmoid(x)) return x else: - return x*torch.sigmoid(x) + return x * torch.sigmoid(x) class Activation(nn.Module): def __init__(self, act_type, inplace=True): super(Activation, self).__init__() act_type = act_type.lower() - if act_type == 'relu': + if act_type == "relu": self.act = nn.ReLU(inplace=inplace) - elif act_type == 'relu6': + elif act_type == "relu6": self.act = nn.ReLU6(inplace=inplace) - elif act_type == 'sigmoid': + elif act_type == "sigmoid": raise NotImplementedError - elif act_type == 'hard_sigmoid': - self.act = Hsigmoid(inplace)#nn.Hardsigmoid(inplace=inplace)#Hsigmoid(inplace)# - elif act_type == 'hard_swish' or act_type == 'hswish': + elif act_type == "hard_sigmoid": + self.act = Hsigmoid( + inplace + ) # nn.Hardsigmoid(inplace=inplace)#Hsigmoid(inplace)# + elif act_type == "hard_swish" or act_type == "hswish": self.act = Hswish(inplace=inplace) - elif act_type == 'leakyrelu': + elif act_type == "leakyrelu": self.act = nn.LeakyReLU(inplace=inplace) - elif act_type == 'gelu': + elif act_type == "gelu": self.act = GELU(inplace=inplace) - elif act_type == 'swish': + elif act_type == "swish": self.act = Swish(inplace=inplace) else: raise NotImplementedError def forward(self, inputs): - return self.act(inputs) \ No newline at end of file + return self.act(inputs) diff --git a/python/rapidocr_torch/modeling/necks/intracl.py b/python/rapidocr_torch/modeling/necks/intracl.py index 414b723..0ba85fa 100644 --- a/python/rapidocr_torch/modeling/necks/intracl.py +++ b/python/rapidocr_torch/modeling/necks/intracl.py @@ -7,55 +7,55 @@ class IntraCLBlock(nn.Module): self.channels = in_channels self.rf = reduce_factor self.conv1x1_reduce_channel = nn.Conv2d( - self.channels, - self.channels // self.rf, - kernel_size=1, - stride=1, - padding=0) + self.channels, self.channels // self.rf, kernel_size=1, stride=1, padding=0 + ) self.conv1x1_return_channel = nn.Conv2d( - self.channels // self.rf, - self.channels, - kernel_size=1, - stride=1, - padding=0) + self.channels // self.rf, self.channels, kernel_size=1, stride=1, padding=0 + ) self.v_layer_7x1 = nn.Conv2d( self.channels // self.rf, self.channels // self.rf, kernel_size=(7, 1), stride=(1, 1), - padding=(3, 0)) + padding=(3, 0), + ) self.v_layer_5x1 = nn.Conv2d( self.channels // self.rf, self.channels // self.rf, kernel_size=(5, 1), stride=(1, 1), - padding=(2, 0)) + padding=(2, 0), + ) self.v_layer_3x1 = nn.Conv2d( self.channels // self.rf, self.channels // self.rf, kernel_size=(3, 1), stride=(1, 1), - padding=(1, 0)) + padding=(1, 0), + ) self.q_layer_1x7 = nn.Conv2d( self.channels // self.rf, self.channels // self.rf, kernel_size=(1, 7), stride=(1, 1), - padding=(0, 3)) + padding=(0, 3), + ) self.q_layer_1x5 = nn.Conv2d( self.channels // self.rf, self.channels // self.rf, kernel_size=(1, 5), stride=(1, 1), - padding=(0, 2)) + padding=(0, 2), + ) self.q_layer_1x3 = nn.Conv2d( self.channels // self.rf, self.channels // self.rf, kernel_size=(1, 3), stride=(1, 1), - padding=(0, 1)) + padding=(0, 1), + ) # base self.c_layer_7x7 = nn.Conv2d( @@ -63,19 +63,22 @@ class IntraCLBlock(nn.Module): self.channels // self.rf, kernel_size=(7, 7), stride=(1, 1), - padding=(3, 3)) + padding=(3, 3), + ) self.c_layer_5x5 = nn.Conv2d( self.channels // self.rf, self.channels // self.rf, kernel_size=(5, 5), stride=(1, 1), - padding=(2, 2)) + padding=(2, 2), + ) self.c_layer_3x3 = nn.Conv2d( self.channels // self.rf, self.channels // self.rf, kernel_size=(3, 3), stride=(1, 1), - padding=(1, 1)) + padding=(1, 1), + ) self.bn = nn.BatchNorm2d(self.channels) self.relu = nn.ReLU() @@ -111,4 +114,4 @@ def build_intraclblock_list(num_block): for i in range(num_block): IntraCLBlock_list.append(IntraCLBlock()) - return IntraCLBlock_list \ No newline at end of file + return IntraCLBlock_list diff --git a/python/tests/test_files/config.yaml b/python/tests/test_files/config.yaml deleted file mode 100644 index b4f114b..0000000 --- a/python/tests/test_files/config.yaml +++ /dev/null @@ -1,62 +0,0 @@ -Global: - lang: "ch" - text_score: 0.5 - use_det: true - use_cls: true - use_rec: true - - print_verbose: false - min_height: 30 - width_height_ratio: 8 - max_side_len: 2000 - min_side_len: 30 - return_word_box: false - - with_onnx: false - with_openvino: false - with_paddle: false - with_torch: false - -EngineConfig: - onnxruntime: - intra_op_num_threads: -1 - inter_op_num_threads: -1 - use_cuda: false - use_dml: false - - paddlepaddle: - cpu_math_library_num_threads: -1 - gpu_id: 0 - gpu_mem: 500 - -Det: - model_path: models/ch_PP-OCRv4_det_infer.onnx - paddle_model_dir: models/paddle/ch_PP-OCRv4_det_infer - - limit_side_len: 736 - limit_type: min - std: [ 0.5, 0.5, 0.5 ] - mean: [ 0.5, 0.5, 0.5 ] - - thresh: 0.3 - box_thresh: 0.5 - max_candidates: 1000 - unclip_ratio: 1.6 - use_dilation: true - score_mode: fast - -Cls: - model_path: models/ch_ppocr_mobile_v2.0_cls_infer.onnx - paddle_model_dir: models/paddle/ch_ppocr_mobile_v2_cls_infer - - cls_image_shape: [3, 48, 192] - cls_batch_num: 6 - cls_thresh: 0.9 - label_list: ['0', '180'] - -Rec: - model_path: models/ch_PP-OCRv4_rec_infer.onnx - paddle_model_dir: models/paddle/ch_PP-OCRv4_rec_infer - - rec_img_shape: [3, 48, 320] - rec_batch_num: 6 diff --git a/python/tests/test_files/en.jpg b/python/tests/test_files/en.jpg new file mode 100644 index 0000000000000000000000000000000000000000..bcfe003c17ded6c256010ffb6d0ed214a3597df4 GIT binary patch literal 40594 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 literal 0 HcmV?d00001 diff --git a/python/tests/test_main.py b/python/tests/test_main.py index 6de89f8..2fe3f7e 100644 --- a/python/tests/test_main.py +++ b/python/tests/test_main.py @@ -34,11 +34,18 @@ def get_engine(params: Optional[Dict[str, Any]] = None): return engine +def test_error_lang(): + with pytest.raises(ValueError) as exc_info: + engine = get_engine(params={"Global.lang": "eh"}) + assert exc_info.type is ValueError + + def test_lang(): - engine = get_engine(params={"Global.lang": "ch", "Global.with_openvino": True}) + engine = get_engine(params={"Global.lang": "en"}) + img_path = tests_dir / "en.jpg" result = engine(img_path) assert result.txts is not None - assert result.txts[0] == "正品促销" + assert result.txts[0] == "MovieShots Dataset" def test_engine_openvino():