wx-paper-parser 1.1.30__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- wx_paper_parser-1.1.30/PKG-INFO +77 -0
- wx_paper_parser-1.1.30/README.md +58 -0
- wx_paper_parser-1.1.30/pyproject.toml +33 -0
- wx_paper_parser-1.1.30/setup.cfg +4 -0
- wx_paper_parser-1.1.30/wx_paper_parser/__init__.py +27 -0
- wx_paper_parser-1.1.30/wx_paper_parser/_default_config.py +25 -0
- wx_paper_parser-1.1.30/wx_paper_parser/barcode_reader.py +217 -0
- wx_paper_parser-1.1.30/wx_paper_parser/bubble_classifier.py +94 -0
- wx_paper_parser-1.1.30/wx_paper_parser/composition_id_recognizer.py +8364 -0
- wx_paper_parser-1.1.30/wx_paper_parser/composition_id_recognizer_cnn.py +568 -0
- wx_paper_parser-1.1.30/wx_paper_parser/cv_tools.py +1276 -0
- wx_paper_parser-1.1.30/wx_paper_parser/detector.py +220 -0
- wx_paper_parser-1.1.30/wx_paper_parser/digit_ocr.py +114 -0
- wx_paper_parser-1.1.30/wx_paper_parser/enhance_id_extractor.py +422 -0
- wx_paper_parser-1.1.30/wx_paper_parser/filled_id_extractor.py +133 -0
- wx_paper_parser-1.1.30/wx_paper_parser/filled_zone_id_parser.py +881 -0
- wx_paper_parser-1.1.30/wx_paper_parser/function_test.py +64 -0
- wx_paper_parser-1.1.30/wx_paper_parser/mdl/bubble_cls.mdl +0 -0
- wx_paper_parser-1.1.30/wx_paper_parser/mdl/common_det/inference.mdl +0 -0
- wx_paper_parser-1.1.30/wx_paper_parser/mdl/common_det/inference.yml +53 -0
- wx_paper_parser-1.1.30/wx_paper_parser/mdl/hw_rec/inference.mdl +0 -0
- wx_paper_parser-1.1.30/wx_paper_parser/mdl/hw_rec/inference.yml +18426 -0
- wx_paper_parser-1.1.30/wx_paper_parser/mdl/id_det.mdl +0 -0
- wx_paper_parser-1.1.30/wx_paper_parser/mdl/paper_ori.mdl +0 -0
- wx_paper_parser-1.1.30/wx_paper_parser/mdl_config.yml +6 -0
- wx_paper_parser-1.1.30/wx_paper_parser/paper_orientation_checker.py +135 -0
- wx_paper_parser-1.1.30/wx_paper_parser/text_det.py +217 -0
- wx_paper_parser-1.1.30/wx_paper_parser.egg-info/PKG-INFO +77 -0
- wx_paper_parser-1.1.30/wx_paper_parser.egg-info/SOURCES.txt +30 -0
- wx_paper_parser-1.1.30/wx_paper_parser.egg-info/dependency_links.txt +1 -0
- wx_paper_parser-1.1.30/wx_paper_parser.egg-info/requires.txt +6 -0
- wx_paper_parser-1.1.30/wx_paper_parser.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wx_paper_parser
|
|
3
|
+
Version: 1.1.30
|
|
4
|
+
Summary: Answer sheet ID recognition
|
|
5
|
+
Author: asan
|
|
6
|
+
License: GPL-3.0
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
9
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: opencv-python>=4.0
|
|
14
|
+
Requires-Dist: numpy>=1.24
|
|
15
|
+
Requires-Dist: onnxruntime>=1.16
|
|
16
|
+
Requires-Dist: pyyaml>=6.0
|
|
17
|
+
Requires-Dist: zxing-cpp>=2.0
|
|
18
|
+
Requires-Dist: shapely>=2.0
|
|
19
|
+
|
|
20
|
+
# wx_paper_parser
|
|
21
|
+
|
|
22
|
+
> Answer-sheet student-ID recognition library — barcode / QR + handwritten bubble filling, with CNN OCR fallback.
|
|
23
|
+
|
|
24
|
+
答题卡学号识别库。从扫描或拍摄的答题卡图像中识别考号(条码 / 二维码 + 手写填涂)。
|
|
25
|
+
|
|
26
|
+
## 识别流程
|
|
27
|
+
|
|
28
|
+
入口 `EnhanceIdExtractor` 按速度优先依次 fallback:
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
条码 / 二维码 → CNN 检测 + OCR → 遗留识别器
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
- **条码 / 二维码**:最快,命中即返回
|
|
35
|
+
- **CNN 检测 + 手写 OCR**:定位学号区域并逐位识别
|
|
36
|
+
- **遗留识别器**:兼容旧版填涂式布局
|
|
37
|
+
|
|
38
|
+
## 安装
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install wx_paper_parser
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## 依赖
|
|
45
|
+
|
|
46
|
+
安装时自动拉取:`opencv-python`、`numpy`、`onnxruntime`、`pyyaml`、`zxing-cpp`、`shapely`。
|
|
47
|
+
|
|
48
|
+
要求 Python ≥ 3.10。
|
|
49
|
+
|
|
50
|
+
## 用法
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
import cv2
|
|
54
|
+
from wx_paper_parser import EnhanceIdExtractor
|
|
55
|
+
|
|
56
|
+
extractor = EnhanceIdExtractor() # 默认使用包内模型,懒加载
|
|
57
|
+
img = cv2.imread("answer_sheet.jpg")
|
|
58
|
+
|
|
59
|
+
angle, corrected = extractor.correct_direction(img) # 校正纸张方向
|
|
60
|
+
student_id = extractor.read_code(corrected) # 识别学号
|
|
61
|
+
print(student_id)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## 主要模块
|
|
65
|
+
|
|
66
|
+
| 名称 | 说明 |
|
|
67
|
+
| --- | --- |
|
|
68
|
+
| `EnhanceIdExtractor` | 识别入口,按速度优先 fallback |
|
|
69
|
+
| `read_barcode` | 条码 / 二维码读取 |
|
|
70
|
+
| `CNNIdRecognizer` | CNN 检测 + OCR 管线 |
|
|
71
|
+
| `CompositionIdRecognizer` | 遗留识别器 |
|
|
72
|
+
|
|
73
|
+
完整导出见 `wx_paper_parser/__init__.py`。
|
|
74
|
+
|
|
75
|
+
## License
|
|
76
|
+
|
|
77
|
+
GPL-3.0
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# wx_paper_parser
|
|
2
|
+
|
|
3
|
+
> Answer-sheet student-ID recognition library — barcode / QR + handwritten bubble filling, with CNN OCR fallback.
|
|
4
|
+
|
|
5
|
+
答题卡学号识别库。从扫描或拍摄的答题卡图像中识别考号(条码 / 二维码 + 手写填涂)。
|
|
6
|
+
|
|
7
|
+
## 识别流程
|
|
8
|
+
|
|
9
|
+
入口 `EnhanceIdExtractor` 按速度优先依次 fallback:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
条码 / 二维码 → CNN 检测 + OCR → 遗留识别器
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
- **条码 / 二维码**:最快,命中即返回
|
|
16
|
+
- **CNN 检测 + 手写 OCR**:定位学号区域并逐位识别
|
|
17
|
+
- **遗留识别器**:兼容旧版填涂式布局
|
|
18
|
+
|
|
19
|
+
## 安装
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install wx_paper_parser
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## 依赖
|
|
26
|
+
|
|
27
|
+
安装时自动拉取:`opencv-python`、`numpy`、`onnxruntime`、`pyyaml`、`zxing-cpp`、`shapely`。
|
|
28
|
+
|
|
29
|
+
要求 Python ≥ 3.10。
|
|
30
|
+
|
|
31
|
+
## 用法
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
import cv2
|
|
35
|
+
from wx_paper_parser import EnhanceIdExtractor
|
|
36
|
+
|
|
37
|
+
extractor = EnhanceIdExtractor() # 默认使用包内模型,懒加载
|
|
38
|
+
img = cv2.imread("answer_sheet.jpg")
|
|
39
|
+
|
|
40
|
+
angle, corrected = extractor.correct_direction(img) # 校正纸张方向
|
|
41
|
+
student_id = extractor.read_code(corrected) # 识别学号
|
|
42
|
+
print(student_id)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## 主要模块
|
|
46
|
+
|
|
47
|
+
| 名称 | 说明 |
|
|
48
|
+
| --- | --- |
|
|
49
|
+
| `EnhanceIdExtractor` | 识别入口,按速度优先 fallback |
|
|
50
|
+
| `read_barcode` | 条码 / 二维码读取 |
|
|
51
|
+
| `CNNIdRecognizer` | CNN 检测 + OCR 管线 |
|
|
52
|
+
| `CompositionIdRecognizer` | 遗留识别器 |
|
|
53
|
+
|
|
54
|
+
完整导出见 `wx_paper_parser/__init__.py`。
|
|
55
|
+
|
|
56
|
+
## License
|
|
57
|
+
|
|
58
|
+
GPL-3.0
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "wx_paper_parser"
|
|
7
|
+
version = "1.1.30"
|
|
8
|
+
description = "Answer sheet ID recognition"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [{name = "asan"}]
|
|
11
|
+
license = {text = "GPL-3.0"}
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Programming Language :: Python :: 3.10",
|
|
16
|
+
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"opencv-python>=4.0",
|
|
21
|
+
"numpy>=1.24",
|
|
22
|
+
"onnxruntime>=1.16",
|
|
23
|
+
"pyyaml>=6.0",
|
|
24
|
+
"zxing-cpp>=2.0",
|
|
25
|
+
"shapely>=2.0",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[tool.setuptools.packages.find]
|
|
29
|
+
where = ["."]
|
|
30
|
+
include = ["wx_paper_parser*"]
|
|
31
|
+
|
|
32
|
+
[tool.setuptools.package-data]
|
|
33
|
+
wx_paper_parser = ["mdl_config.yml", "mdl/**/*"]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from .composition_id_recognizer import CompositionIdRecognizer, PaperType, StatusCode, RecognitionResult, b64_to_mat, mat_to_b64, mat_to_md5
|
|
2
|
+
from .composition_id_recognizer_cnn import CNNIdRecognizer
|
|
3
|
+
from .enhance_id_extractor import EnhanceIdExtractor
|
|
4
|
+
from .digit_ocr import DigitOCR
|
|
5
|
+
from .detector import SNDetector
|
|
6
|
+
from .paper_orientation_checker import query_doc_orientation, get_detector
|
|
7
|
+
from .text_det import TextDet
|
|
8
|
+
from .barcode_reader import read_barcode
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
'CompositionIdRecognizer',
|
|
13
|
+
'CNNIdRecognizer',
|
|
14
|
+
'EnhanceIdExtractor',
|
|
15
|
+
'DigitOCR',
|
|
16
|
+
'SNDetector',
|
|
17
|
+
'query_doc_orientation',
|
|
18
|
+
'TextDet',
|
|
19
|
+
'get_detector',
|
|
20
|
+
'read_barcode',
|
|
21
|
+
'RecognitionResult',
|
|
22
|
+
'StatusCode',
|
|
23
|
+
'PaperType',
|
|
24
|
+
'b64_to_mat',
|
|
25
|
+
'mat_to_b64',
|
|
26
|
+
'mat_to_md5'
|
|
27
|
+
]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""包内默认模型路径解析工具"""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import yaml
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_default_model_path(key: str) -> str:
|
|
8
|
+
"""从包内 mdl_config.yml 读取指定 key 对应的模型路径(绝对路径)。
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
key: mdl_config.yml 中的配置 key,如 'id_det_mdl_path'
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
解析后的绝对路径字符串,找不到则返回空字符串
|
|
15
|
+
"""
|
|
16
|
+
config_path = Path(__file__).parent / "mdl_config.yml"
|
|
17
|
+
print(f"配置地址: {config_path}")
|
|
18
|
+
if not config_path.exists():
|
|
19
|
+
return ""
|
|
20
|
+
with open(config_path) as f:
|
|
21
|
+
cfg = yaml.safe_load(f) or {}
|
|
22
|
+
relative = cfg.get(key, "")
|
|
23
|
+
if not relative:
|
|
24
|
+
return ""
|
|
25
|
+
return str(config_path.parent / relative)
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""
|
|
2
|
+
通用条码/二维码读取工具
|
|
3
|
+
|
|
4
|
+
按速度优先的策略依次尝试(多二值化器 × 多缩放),命中即返回。
|
|
5
|
+
默认检测 Code128、Code39、Code93 条码 + QRCode 二维码。
|
|
6
|
+
|
|
7
|
+
使用方式:
|
|
8
|
+
from utils.barcode_reader import read_barcode
|
|
9
|
+
|
|
10
|
+
result = read_barcode(image)
|
|
11
|
+
if result:
|
|
12
|
+
content, points, code_type = result
|
|
13
|
+
# content: 识别到的文本
|
|
14
|
+
# points: 四角坐标 [[x,y], ...]
|
|
15
|
+
# code_type: 0=条形码, 1=二维码
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import logging
|
|
19
|
+
from typing import List, Optional, Tuple
|
|
20
|
+
import json
|
|
21
|
+
import cv2
|
|
22
|
+
from cv2.typing import MatLike
|
|
23
|
+
import numpy as np
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
# 缩放因子(从小到大,兼顾速度与覆盖率)
|
|
28
|
+
_SCALE_FACTORS = [1.0, 1.5, 2.0, 2.5, 3.0, 5.0]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _extract_points(position) -> List[List[int]]:
|
|
32
|
+
"""从 zxingcpp Position 提取四角坐标"""
|
|
33
|
+
points = []
|
|
34
|
+
for attr in ('top_left', 'top_right', 'bottom_right', 'bottom_left'):
|
|
35
|
+
pt = getattr(position, attr, None)
|
|
36
|
+
if pt is not None:
|
|
37
|
+
points.append([int(pt.x), int(pt.y)])
|
|
38
|
+
return points
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _max_channel_gray(image: MatLike) -> MatLike:
|
|
42
|
+
"""取 BGR 三通道最大值作为灰度图,消除彩色背景对条形码识别的干扰。
|
|
43
|
+
|
|
44
|
+
标准 BGR2GRAY 使用加权平均 (0.299R+0.587G+0.114B),纯红背景变成
|
|
45
|
+
gray=76,纯蓝变成 gray=29,在 zxingcpp FixedThreshold(127) 下与
|
|
46
|
+
黑色条码一起被二值化为黑色,对比度消失。
|
|
47
|
+
取三通道最大值后,任何饱和色都变成 255(白色),黑色条码保持 0,
|
|
48
|
+
恢复完整对比度。对标准灰度/白色背景图像无影响(B=G=R 时结果一致)。
|
|
49
|
+
"""
|
|
50
|
+
if len(image.shape) == 2:
|
|
51
|
+
return image.copy()
|
|
52
|
+
return np.max(image, axis=2).astype(np.uint8)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _is_json_structured(txt: str) -> bool:
|
|
56
|
+
try:
|
|
57
|
+
result = json.loads(txt)
|
|
58
|
+
return isinstance(result, (dict, list))
|
|
59
|
+
except (ValueError, TypeError):
|
|
60
|
+
return False
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def read_barcode(
|
|
64
|
+
image: MatLike,
|
|
65
|
+
crop_top_ratio: Optional[float] = None,
|
|
66
|
+
digit_only: bool = False,
|
|
67
|
+
include_qr: bool = True,
|
|
68
|
+
keys: Optional[List[str]] = None,
|
|
69
|
+
) -> List[Tuple[str, List[List[int]], int]]:
|
|
70
|
+
"""
|
|
71
|
+
读取条形码或二维码,命中即返回。
|
|
72
|
+
|
|
73
|
+
默认检测 Code128、Code39、Code93 条码 + QRCode 二维码。
|
|
74
|
+
策略顺序(尽早返回):
|
|
75
|
+
每个 scale 级别依次尝试三种 zxingcpp 内置二值化器:
|
|
76
|
+
FixedThreshold → LocalAverage → GlobalHistogram
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
image: BGR 图像
|
|
80
|
+
crop_top_ratio: 只检测顶部指定比例区域 (0~1), None 为整图
|
|
81
|
+
digit_only: 仅接受纯数字内容
|
|
82
|
+
include_qr: 是否同时检测二维码,默认 True
|
|
83
|
+
keys: 指定要检测的二维码内容关键词列表,None 表示不进行关键词过滤
|
|
84
|
+
Returns:
|
|
85
|
+
[(内容, [[x,y], ...], code_type)]
|
|
86
|
+
code_type: 0=条形码, 1=二维码
|
|
87
|
+
"""
|
|
88
|
+
try:
|
|
89
|
+
import zxingcpp
|
|
90
|
+
except ImportError:
|
|
91
|
+
logger.warning("[BarcodeReader] zxingcpp 未安装")
|
|
92
|
+
return []
|
|
93
|
+
|
|
94
|
+
# 检测格式:可靠的一维码 + 可选二维码
|
|
95
|
+
linear_formats = [zxingcpp.BarcodeFormat.Code128, zxingcpp.BarcodeFormat.Code39, zxingcpp.BarcodeFormat.Code93]
|
|
96
|
+
target_formats = linear_formats.copy()
|
|
97
|
+
if include_qr:
|
|
98
|
+
target_formats.append(zxingcpp.BarcodeFormat.QRCode)
|
|
99
|
+
|
|
100
|
+
h, w = image.shape[:2]
|
|
101
|
+
|
|
102
|
+
# 裁剪检测区域
|
|
103
|
+
if crop_top_ratio is not None and 0 < crop_top_ratio < 1:
|
|
104
|
+
roi = image[:int(h * crop_top_ratio), :]
|
|
105
|
+
else:
|
|
106
|
+
roi = image
|
|
107
|
+
|
|
108
|
+
roi_h, roi_w = roi.shape[:2]
|
|
109
|
+
is_color = len(roi.shape) == 3
|
|
110
|
+
gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY) if is_color else roi.copy()
|
|
111
|
+
|
|
112
|
+
# 三通道最大值灰度图:消除彩色边框/底色干扰
|
|
113
|
+
max_channel = _max_channel_gray(roi) if is_color else gray.copy()
|
|
114
|
+
|
|
115
|
+
# 预计算 CLAHE 对比度增强图(用于 fallback)
|
|
116
|
+
clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8, 8))
|
|
117
|
+
enhanced = clahe.apply(gray)
|
|
118
|
+
|
|
119
|
+
# 二值化器列表(按覆盖率排序:FixedThreshold 覆盖最广且最快)
|
|
120
|
+
binarizers = [
|
|
121
|
+
("Fixed", zxingcpp.Binarizer.FixedThreshold),
|
|
122
|
+
("Local", zxingcpp.Binarizer.LocalAverage),
|
|
123
|
+
("Global", zxingcpp.Binarizer.GlobalHistogram),
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
def _scan_image(scaled_img, scale_val):
|
|
127
|
+
"""在指定缩放的图像上扫描条码,返回匹配结果列表"""
|
|
128
|
+
scan_results = []
|
|
129
|
+
for bin_label, binarizer in binarizers:
|
|
130
|
+
codes = zxingcpp.read_barcodes(image=scaled_img, formats=target_formats, binarizer=binarizer)
|
|
131
|
+
|
|
132
|
+
for code in codes:
|
|
133
|
+
text = code.text
|
|
134
|
+
print(f"[BAR CODE] 读取条码: {text}, {code.format}")
|
|
135
|
+
if not text:
|
|
136
|
+
continue
|
|
137
|
+
if code.format != zxingcpp.BarcodeFormat.QRCode and digit_only and not text.isdigit():
|
|
138
|
+
continue
|
|
139
|
+
|
|
140
|
+
# 坐标映射回原始图像
|
|
141
|
+
points = _extract_points(code.position)
|
|
142
|
+
if scale_val != 1.0:
|
|
143
|
+
points = [[int(p[0] / scale_val), int(p[1] / scale_val)] for p in points]
|
|
144
|
+
|
|
145
|
+
code_type = 1 if code.format == zxingcpp.BarcodeFormat.QRCode else 0
|
|
146
|
+
if code_type == 1:
|
|
147
|
+
qr_info = {}
|
|
148
|
+
is_json = _is_json_structured(text)
|
|
149
|
+
if is_json:
|
|
150
|
+
try:
|
|
151
|
+
qr_info = json.loads(text)
|
|
152
|
+
except Exception:
|
|
153
|
+
pass
|
|
154
|
+
if is_json:
|
|
155
|
+
student_number = qr_info.get("studentNumber", "")
|
|
156
|
+
# 说明是维学专属答题卡二维码,且要求学号必须为纯数字
|
|
157
|
+
if not student_number or student_number.isdigit() == False:
|
|
158
|
+
print(f"[BarcodeReader] 二维码内容不符合学号要求: {text}")
|
|
159
|
+
continue
|
|
160
|
+
elif not is_json and digit_only and not text.isdigit():
|
|
161
|
+
continue
|
|
162
|
+
|
|
163
|
+
fmt_str = str(code.format)
|
|
164
|
+
|
|
165
|
+
print(f"[BarcodeReader] {bin_label} {scale_val}x: {text!r} ({fmt_str})")
|
|
166
|
+
scan_results.append((text, points, code_type))
|
|
167
|
+
|
|
168
|
+
if len(scan_results) > 0:
|
|
169
|
+
return scan_results
|
|
170
|
+
return scan_results
|
|
171
|
+
|
|
172
|
+
def _has_barcode(res):
|
|
173
|
+
return any(r[2] == 0 for r in res)
|
|
174
|
+
|
|
175
|
+
results = []
|
|
176
|
+
for scale in _SCALE_FACTORS:
|
|
177
|
+
new_w, new_h = int(roi_w * scale), int(roi_h * scale)
|
|
178
|
+
if scale == 1.0:
|
|
179
|
+
scaled = gray
|
|
180
|
+
scaled_enhanced = enhanced
|
|
181
|
+
scaled_maxch = max_channel
|
|
182
|
+
else:
|
|
183
|
+
scaled = cv2.resize(gray, (new_w, new_h), interpolation=cv2.INTER_CUBIC)
|
|
184
|
+
scaled_enhanced = cv2.resize(enhanced, (new_w, new_h), interpolation=cv2.INTER_CUBIC)
|
|
185
|
+
scaled_maxch = cv2.resize(max_channel, (new_w, new_h), interpolation=cv2.INTER_CUBIC)
|
|
186
|
+
|
|
187
|
+
# 原始图像扫描
|
|
188
|
+
results = _scan_image(scaled, scale)
|
|
189
|
+
has_barcode = _has_barcode(results)
|
|
190
|
+
|
|
191
|
+
# 同一 scale 内 CLAHE fallback(仅当尚未找到条形码时)
|
|
192
|
+
if not has_barcode:
|
|
193
|
+
clahe_results = _scan_image(scaled_enhanced, scale)
|
|
194
|
+
if clahe_results:
|
|
195
|
+
results.extend(clahe_results)
|
|
196
|
+
has_barcode = _has_barcode(results)
|
|
197
|
+
|
|
198
|
+
# 三通道最大值 fallback:消除彩色边框/底色干扰
|
|
199
|
+
# 即使已找到 QR 码,仍需检查是否存在被彩色遮挡的条形码
|
|
200
|
+
if not has_barcode:
|
|
201
|
+
maxch_results = _scan_image(scaled_maxch, scale)
|
|
202
|
+
if maxch_results:
|
|
203
|
+
results.extend(maxch_results)
|
|
204
|
+
has_barcode = _has_barcode(results)
|
|
205
|
+
|
|
206
|
+
# 已找到条形码(非 QR)即可返回
|
|
207
|
+
if has_barcode and len(results) > 0:
|
|
208
|
+
print(f"[BARCODE]1 识别结果: {results}")
|
|
209
|
+
return results
|
|
210
|
+
|
|
211
|
+
# 仅找到 QR 码时也返回(无条形码时不再继续尝试更大缩放)
|
|
212
|
+
if len(results) > 0:
|
|
213
|
+
print(f"[BARCODE]1 识别结果: {results}")
|
|
214
|
+
return results
|
|
215
|
+
|
|
216
|
+
print(f"[BARCODE]2 识别结果: {results}")
|
|
217
|
+
return results
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import cv2
|
|
2
|
+
import numpy as np
|
|
3
|
+
import onnxruntime as ort
|
|
4
|
+
from PIL import Image
|
|
5
|
+
if __package__:
|
|
6
|
+
from ._default_config import get_default_model_path
|
|
7
|
+
else:
|
|
8
|
+
from _default_config import get_default_model_path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BubbleClassifier:
|
|
12
|
+
"""填涂项二分类器,基于ONNX Runtime推理。
|
|
13
|
+
|
|
14
|
+
用法:
|
|
15
|
+
clf = BubbleClassifier("bubble_classifier.onnx")
|
|
16
|
+
result = clf.predict("image.jpg")
|
|
17
|
+
print(result) # {'label': 'filled', 'probability': 0.98, 'filled': True}
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, model_path: str | None = None, threshold=0.5, img_size=64):
|
|
21
|
+
model_path = model_path or get_default_model_path("bubble_cls_mdl_path")
|
|
22
|
+
self.session = ort.InferenceSession(model_path)
|
|
23
|
+
self.input_name = self.session.get_inputs()[0].name
|
|
24
|
+
self.threshold = threshold
|
|
25
|
+
self.img_size = img_size
|
|
26
|
+
|
|
27
|
+
def _preprocess(self, image):
|
|
28
|
+
if isinstance(image, str):
|
|
29
|
+
img = Image.open(image).convert("RGB")
|
|
30
|
+
img = img.resize((self.img_size, self.img_size), Image.BILINEAR)
|
|
31
|
+
arr = np.array(img, dtype=np.float32) / 255.0 # (H, W, 3)
|
|
32
|
+
else:
|
|
33
|
+
# OpenCV MatLike (BGR or Gray)
|
|
34
|
+
if image.ndim == 2:
|
|
35
|
+
arr = cv2.cvtColor(image, cv2.COLOR_GRAY2RGB).astype(np.float32) / 255.0
|
|
36
|
+
else:
|
|
37
|
+
arr = cv2.cvtColor(image, cv2.COLOR_BGR2RGB).astype(np.float32) / 255.0
|
|
38
|
+
arr = cv2.resize(arr, (self.img_size, self.img_size), interpolation=cv2.INTER_LINEAR)
|
|
39
|
+
arr = (arr - 0.8) / 0.2
|
|
40
|
+
arr = arr.transpose(2, 0, 1) # (3, H, W)
|
|
41
|
+
arr = arr[np.newaxis, :, :, :] # (1, 3, H, W)
|
|
42
|
+
return arr.astype(np.float32)
|
|
43
|
+
|
|
44
|
+
def predict(self, image):
|
|
45
|
+
"""预测单张图片是否为填涂项。
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
image: 图片路径(str) 或 OpenCV MatLike (BGR/Gray numpy数组)
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
dict: {'label': 'filled'/'unfilled', 'probability': float, 'filled': bool}
|
|
52
|
+
"""
|
|
53
|
+
blob = self._preprocess(image)
|
|
54
|
+
logit = self.session.run(None, {self.input_name: blob})[0]
|
|
55
|
+
prob = 1.0 / (1.0 + np.exp(-logit[0])) # sigmoid
|
|
56
|
+
filled = bool(prob >= self.threshold)
|
|
57
|
+
return {
|
|
58
|
+
"label": "filled" if filled else "unfilled",
|
|
59
|
+
"probability": float(prob),
|
|
60
|
+
"filled": filled,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
def predict_group(self, images):
|
|
64
|
+
"""对一组选项进行预测,通过组内相对比较判断哪个被填涂。
|
|
65
|
+
|
|
66
|
+
适用于同一道题的多个选项(如A/B/C/D),组内只选概率最高的作为filled。
|
|
67
|
+
如果组内所有选项概率都很低(低于绝对阈值),则判定为未作答。
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
images: 图片列表,每个元素为文件路径(str)或OpenCV MatLike
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
list[dict]: 每个选项的预测结果,额外包含 'group_filled' 字段表示
|
|
74
|
+
该选项是否为组内被选中的那个
|
|
75
|
+
"""
|
|
76
|
+
results = [self.predict(img) for img in images]
|
|
77
|
+
|
|
78
|
+
max_idx = max(range(len(results)), key=lambda i: results[i]["probability"])
|
|
79
|
+
max_prob = results[max_idx]["probability"]
|
|
80
|
+
|
|
81
|
+
for i, r in enumerate(results):
|
|
82
|
+
r["group_filled"] = (i == max_idx and max_prob >= self.threshold)
|
|
83
|
+
|
|
84
|
+
return results
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
_classifier: BubbleClassifier | None = None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def get_classifier(model_path: str | None = None, threshold=0.5, img_size=64) -> BubbleClassifier:
|
|
91
|
+
global _classifier
|
|
92
|
+
if _classifier is None:
|
|
93
|
+
_classifier = BubbleClassifier(model_path, threshold, img_size)
|
|
94
|
+
return _classifier
|