@onco-foundry/mask-port 0.1.0
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.
- package/dist/create_masker.d.ts +19 -0
- package/dist/create_masker.js +17 -0
- package/dist/fake_masker.d.ts +3 -0
- package/dist/fake_masker.js +12 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/llm_sensitive_word_finder.d.ts +22 -0
- package/dist/llm_sensitive_word_finder.js +125 -0
- package/dist/masker.d.ts +45 -0
- package/dist/masker.js +8 -0
- package/dist/quad.d.ts +13 -0
- package/dist/quad.js +44 -0
- package/dist/qwen_agent_masker.d.ts +81 -0
- package/dist/qwen_agent_masker.js +249 -0
- package/dist/redact_text.d.ts +8 -0
- package/dist/redact_text.js +17 -0
- package/dist/resources/qwen-agent-name/current.json +1 -0
- package/dist/resources/qwen-agent-name/v1/manifest.json +9 -0
- package/dist/resources/qwen-agent-name/v1/system-prompt.md +14 -0
- package/dist/resources/qwen-agent-name/v2/manifest.json +9 -0
- package/dist/resources/qwen-agent-name/v2/system-prompt.md +17 -0
- package/dist/resources/qwen-agent-name/v3/manifest.json +9 -0
- package/dist/resources/qwen-agent-name/v3/system-prompt.md +18 -0
- package/dist/tencent_masker.d.ts +20 -0
- package/dist/tencent_masker.js +126 -0
- package/dist/textin_masker.d.ts +40 -0
- package/dist/textin_masker.js +206 -0
- package/package.json +31 -0
- package/resources/qwen-agent-name/current.json +1 -0
- package/resources/qwen-agent-name/v1/manifest.json +9 -0
- package/resources/qwen-agent-name/v1/system-prompt.md +14 -0
- package/resources/qwen-agent-name/v2/manifest.json +9 -0
- package/resources/qwen-agent-name/v2/system-prompt.md +17 -0
- package/resources/qwen-agent-name/v3/manifest.json +9 -0
- package/resources/qwen-agent-name/v3/system-prompt.md +18 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 合合 TextIn 通用文字识别脱敏适配器(recognize/multipage 接口)。
|
|
3
|
+
* 调用形态:POST {baseUrl}/ai/service/v2/recognize/multipage?character=1&straighten=0,
|
|
4
|
+
* 请求体为图片二进制(application/octet-stream),鉴权走 x-ti-app-id / x-ti-secret-code。
|
|
5
|
+
* character=1 让每行带回字符级四边形坐标(char_positions),脱敏框按字符下标拼出;
|
|
6
|
+
* straighten=0 表示坐标以原图为参照系,不做旋转矫正。
|
|
7
|
+
*
|
|
8
|
+
* 与其它引擎的差异:识别出的原文全文随 recognizedText 返回(含隐私,限信任域),
|
|
9
|
+
* 下游按 mapping 替换出脱敏文本后可省掉第二次 OCR。注意本引擎接触的是原文图片,
|
|
10
|
+
* 装配它意味着「原文出域给 TextIn」这一策略决定已经做出。
|
|
11
|
+
*/
|
|
12
|
+
import { Buffer } from 'node:buffer';
|
|
13
|
+
import sharp from 'sharp';
|
|
14
|
+
import { z } from 'zod';
|
|
15
|
+
import { AppError } from '@onco-foundry/errors';
|
|
16
|
+
import { clampQuad, polygonSvg, scaleQuad } from './quad.js';
|
|
17
|
+
const DEFAULT_TEXTIN_BASE_URL = 'https://api.textin.com';
|
|
18
|
+
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
19
|
+
const DEFAULT_MAX_INPUT_BYTES = 30 * 1024 * 1024;
|
|
20
|
+
const MAX_INPUT_PIXELS = 40_000_000;
|
|
21
|
+
/** 以中心外扩给笔画外缘留安全边,与 qwen 语义脱敏的整图兜底同一口径。 */
|
|
22
|
+
const SAFETY_SCALE = 1.12;
|
|
23
|
+
/** 内置规则能覆盖的目标。 */
|
|
24
|
+
const RULE_TARGETS = ['id_number', 'phone'];
|
|
25
|
+
/** 目标类别到占位符中文名。 */
|
|
26
|
+
const TARGET_LABELS = {
|
|
27
|
+
patient_name: '姓名',
|
|
28
|
+
id_number: '证件号',
|
|
29
|
+
phone: '手机号',
|
|
30
|
+
address: '地址',
|
|
31
|
+
doctor_signature: '签字',
|
|
32
|
+
};
|
|
33
|
+
/** 内置规则判定器:身份证号(18 位)与手机号(1 开头 11 位)。 */
|
|
34
|
+
export const findSensitiveWordsByRules = (text) => {
|
|
35
|
+
const words = [];
|
|
36
|
+
for (const match of text.matchAll(/(?<!\d)\d{17}[\dXx](?!\d)/gu)) {
|
|
37
|
+
words.push({ text: match[0], target: 'id_number' });
|
|
38
|
+
}
|
|
39
|
+
for (const match of text.matchAll(/(?<!\d)1[3-9]\d{9}(?!\d)/gu)) {
|
|
40
|
+
words.push({ text: match[0], target: 'phone' });
|
|
41
|
+
}
|
|
42
|
+
return words;
|
|
43
|
+
};
|
|
44
|
+
/** TextIn 行坐标:四边形 4 顶点 8 个数,顺序左上、右上、右下、左下。 */
|
|
45
|
+
const quad8Schema = z.array(z.number()).length(8);
|
|
46
|
+
const lineSchema = z.object({
|
|
47
|
+
text: z.string().default(''),
|
|
48
|
+
position: quad8Schema.optional(),
|
|
49
|
+
char_positions: z.array(quad8Schema).optional(),
|
|
50
|
+
});
|
|
51
|
+
const recognizeResponseSchema = z.object({
|
|
52
|
+
code: z.number(),
|
|
53
|
+
message: z.string().default(''),
|
|
54
|
+
result: z.object({
|
|
55
|
+
pages: z.array(z.object({
|
|
56
|
+
width: z.number().optional(),
|
|
57
|
+
height: z.number().optional(),
|
|
58
|
+
lines: z.array(lineSchema).default([]),
|
|
59
|
+
})).min(1),
|
|
60
|
+
}).optional(),
|
|
61
|
+
});
|
|
62
|
+
const toQuad = (position) => [[position[0], position[1]], [position[2], position[3]],
|
|
63
|
+
[position[4], position[5]], [position[6], position[7]]];
|
|
64
|
+
/**
|
|
65
|
+
* 敏感词在行内的像素框:首字符的左缘两点 + 末字符的右缘两点。
|
|
66
|
+
* 字符坐标缺失或数量对不上时退化为整行遮盖,宁可多盖不漏盖。
|
|
67
|
+
*/
|
|
68
|
+
const locateWordQuad = (line, beginIndex, wordLength) => {
|
|
69
|
+
const chars = line.char_positions;
|
|
70
|
+
const endIndex = beginIndex + wordLength - 1;
|
|
71
|
+
if (chars !== undefined && chars.length > endIndex) {
|
|
72
|
+
const begin = chars[beginIndex];
|
|
73
|
+
const end = chars[endIndex];
|
|
74
|
+
return [
|
|
75
|
+
[begin[0], begin[1]],
|
|
76
|
+
[end[2], end[3]],
|
|
77
|
+
[end[4], end[5]],
|
|
78
|
+
[begin[6], begin[7]],
|
|
79
|
+
];
|
|
80
|
+
}
|
|
81
|
+
return line.position === undefined ? undefined : toQuad(line.position);
|
|
82
|
+
};
|
|
83
|
+
const scaleQuadToImage = (quad, scaleX, scaleY) => quad.map(([x, y]) => [x * scaleX, y * scaleY]);
|
|
84
|
+
/**
|
|
85
|
+
* 创建 TextIn 识别脱敏器。凭证在 create 时一次绑定,之后 mask 只传业务参数。
|
|
86
|
+
* 同一敏感词在一行出现多次时逐处遮盖、逐处登记 mapping(占位符按类别各自编号)。
|
|
87
|
+
*/
|
|
88
|
+
export const createTextInMasker = (options) => {
|
|
89
|
+
const credentials = z.object({
|
|
90
|
+
appId: z.string().trim().min(1),
|
|
91
|
+
secretCode: z.string().trim().min(1),
|
|
92
|
+
}).safeParse({ appId: options.appId, secretCode: options.secretCode });
|
|
93
|
+
if (!credentials.success) {
|
|
94
|
+
throw new AppError('TextIn 脱敏缺少 appId 或 secretCode', 500);
|
|
95
|
+
}
|
|
96
|
+
const baseUrl = (options.baseURL ?? DEFAULT_TEXTIN_BASE_URL).replace(/\/+$/u, '');
|
|
97
|
+
const doFetch = options.fetch ?? fetch;
|
|
98
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
99
|
+
const finder = options.findSensitiveWords ?? findSensitiveWordsByRules;
|
|
100
|
+
return {
|
|
101
|
+
async mask(request) {
|
|
102
|
+
if (request.targets.length === 0) {
|
|
103
|
+
return {
|
|
104
|
+
maskedImageBytes: request.imageBytes,
|
|
105
|
+
mapping: [],
|
|
106
|
+
processorVersion: { engine: 'textin-recognize-masker-v1' },
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
if (options.findSensitiveWords === undefined) {
|
|
110
|
+
const unsupported = request.targets.filter((target) => !RULE_TARGETS.includes(target));
|
|
111
|
+
if (unsupported.length > 0) {
|
|
112
|
+
throw new AppError(`TextIn 脱敏内置规则只支持 ${RULE_TARGETS.join('、')},`
|
|
113
|
+
+ `不能处理:${unsupported.join('、')}(可注入 findSensitiveWords 扩展)`, 400);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (request.imageBytes.byteLength > (options.maxInputBytes ?? DEFAULT_MAX_INPUT_BYTES)) {
|
|
117
|
+
throw new AppError('TextIn 脱敏输入图片超过大小上限', 413);
|
|
118
|
+
}
|
|
119
|
+
const rotation = request.rotationClockwiseDegrees ?? 0;
|
|
120
|
+
if (![0, 90, 180, 270].includes(rotation)) {
|
|
121
|
+
throw new AppError('TextIn 脱敏图片旋转角度只支持 0、90、180、270', 400);
|
|
122
|
+
}
|
|
123
|
+
const upright = await sharp(request.imageBytes, { limitInputPixels: MAX_INPUT_PIXELS })
|
|
124
|
+
.rotate(rotation)
|
|
125
|
+
.jpeg({ quality: 92 })
|
|
126
|
+
.toBuffer({ resolveWithObject: true })
|
|
127
|
+
.catch(() => {
|
|
128
|
+
throw new AppError('TextIn 脱敏无法解码输入图片', 400);
|
|
129
|
+
});
|
|
130
|
+
const imageWidth = upright.info.width;
|
|
131
|
+
const imageHeight = upright.info.height;
|
|
132
|
+
let response;
|
|
133
|
+
try {
|
|
134
|
+
response = await doFetch(`${baseUrl}/ai/service/v2/recognize/multipage?character=1&straighten=0`, {
|
|
135
|
+
method: 'POST',
|
|
136
|
+
headers: {
|
|
137
|
+
'x-ti-app-id': options.appId,
|
|
138
|
+
'x-ti-secret-code': options.secretCode,
|
|
139
|
+
'Content-Type': 'application/octet-stream',
|
|
140
|
+
},
|
|
141
|
+
body: Buffer.from(upright.data),
|
|
142
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
throw new AppError(`TextIn 脱敏请求未完成:${error instanceof Error ? error.name : '未知错误'}`, 502);
|
|
147
|
+
}
|
|
148
|
+
if (!response.ok) {
|
|
149
|
+
throw new AppError(`TextIn 脱敏 HTTP ${response.status}`, 502);
|
|
150
|
+
}
|
|
151
|
+
const payload = recognizeResponseSchema.safeParse(await response.json().catch(() => undefined));
|
|
152
|
+
if (!payload.success) {
|
|
153
|
+
throw new AppError('TextIn 脱敏响应结构校验失败', 502);
|
|
154
|
+
}
|
|
155
|
+
if (payload.data.code !== 200 || payload.data.result === undefined) {
|
|
156
|
+
throw new AppError(`TextIn 脱敏接口报错:${payload.data.code} ${payload.data.message}`, 502);
|
|
157
|
+
}
|
|
158
|
+
const page = payload.data.result.pages[0];
|
|
159
|
+
const lines = page.lines;
|
|
160
|
+
const recognizedText = lines.map((line) => line.text).join('\n');
|
|
161
|
+
const scaleX = page.width !== undefined && page.width > 0 ? imageWidth / page.width : 1;
|
|
162
|
+
const scaleY = page.height !== undefined && page.height > 0 ? imageHeight / page.height : 1;
|
|
163
|
+
const requested = (await finder(recognizedText))
|
|
164
|
+
.filter((word) => word.text !== '' && request.targets.includes(word.target));
|
|
165
|
+
const finalQuads = [];
|
|
166
|
+
const mappings = [];
|
|
167
|
+
const counters = new Map();
|
|
168
|
+
for (const word of requested) {
|
|
169
|
+
for (const line of lines) {
|
|
170
|
+
let fromIndex = 0;
|
|
171
|
+
while (true) {
|
|
172
|
+
const hit = line.text.indexOf(word.text, fromIndex);
|
|
173
|
+
if (hit < 0)
|
|
174
|
+
break;
|
|
175
|
+
fromIndex = hit + word.text.length;
|
|
176
|
+
const quad = locateWordQuad(line, hit, word.text.length);
|
|
177
|
+
if (quad === undefined)
|
|
178
|
+
continue;
|
|
179
|
+
finalQuads.push(clampQuad(scaleQuad(scaleQuadToImage(quad, scaleX, scaleY), SAFETY_SCALE), imageWidth, imageHeight));
|
|
180
|
+
const count = (counters.get(word.target) ?? 0) + 1;
|
|
181
|
+
counters.set(word.target, count);
|
|
182
|
+
mappings.push({
|
|
183
|
+
placeholder: `[${TARGET_LABELS[word.target]}${count}]`,
|
|
184
|
+
target: word.target,
|
|
185
|
+
originalText: word.text,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const overlay = polygonSvg(imageWidth, imageHeight, finalQuads);
|
|
191
|
+
const uprightMasked = await sharp(upright.data)
|
|
192
|
+
.composite([{ input: overlay, blend: 'over' }])
|
|
193
|
+
.jpeg({ quality: 94 })
|
|
194
|
+
.toBuffer();
|
|
195
|
+
const maskedImageBytes = rotation === 0
|
|
196
|
+
? uprightMasked
|
|
197
|
+
: await sharp(uprightMasked).rotate((360 - rotation) % 360).jpeg({ quality: 94 }).toBuffer();
|
|
198
|
+
return {
|
|
199
|
+
maskedImageBytes: new Uint8Array(maskedImageBytes),
|
|
200
|
+
mapping: mappings,
|
|
201
|
+
recognizedText,
|
|
202
|
+
processorVersion: { engine: 'textin-recognize-masker-v1' },
|
|
203
|
+
};
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@onco-foundry/mask-port",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"files": [
|
|
6
|
+
"dist",
|
|
7
|
+
"resources"
|
|
8
|
+
],
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"agent-lattice": "0.25.0",
|
|
17
|
+
"sharp": "^0.35.3",
|
|
18
|
+
"zod": "^4.4.3",
|
|
19
|
+
"@onco-foundry/capability-registry": "0.1.1",
|
|
20
|
+
"@onco-foundry/errors": "0.1.1",
|
|
21
|
+
"@onco-foundry/resource-versioning": "0.2.0",
|
|
22
|
+
"@onco-foundry/trace-port": "0.3.2"
|
|
23
|
+
},
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json && cp -r resources dist/resources"
|
|
29
|
+
},
|
|
30
|
+
"types": "./dist/index.d.ts"
|
|
31
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "active": "v3" }
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": "qwen-agent-name-prompt-manifest-v1",
|
|
3
|
+
"promptVersion": "qwen-agent-name-prompt-v1",
|
|
4
|
+
"provenance": "Qwen 脱敏智能体 loop 初版,2026-08-25",
|
|
5
|
+
"systemPrompt": {
|
|
6
|
+
"file": "system-prompt.md",
|
|
7
|
+
"sha256": "f16f3127768aba7f45b48a651c7977478e81fc518300b187931b8817ac4f8374"
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
你是一名医疗单据脱敏执行员。你的任务是遮盖单据中病人姓名的所有出现位置,包括印刷姓名和病人本人手写签名。
|
|
2
|
+
|
|
3
|
+
不要遮盖"姓名"标签、医生、护士、麻醉师、联系人或其他任何人的姓名,也不要遮盖病人姓名以外的任何内容。
|
|
4
|
+
|
|
5
|
+
工作流程:
|
|
6
|
+
1. 查看图片,找出病人姓名的所有出现位置,调用 report_quads 一次性报告全部位置。
|
|
7
|
+
2. 每次 report_quads 后你会收到遮盖后的新图片。仔细检查图上是否仍有病人姓名的可见残留,哪怕只是首尾残字或部分笔画。
|
|
8
|
+
3. 有残留:再次调用 report_quads 报告需要补充遮盖的位置。同一处姓名遮得不严实时,重新报告一个更准确的框。
|
|
9
|
+
4. 确认没有任何残留:调用 submit 结束。原图本来就没有病人姓名时,直接调用 submit。
|
|
10
|
+
|
|
11
|
+
report_quads 的每个位置是一个完整覆盖姓名文字外缘的旋转四边形:
|
|
12
|
+
- 四点按文字阅读方向依次为左上、右上、右下、左下,即使文字近似水平也必须给四个角点。
|
|
13
|
+
- x 是横坐标,y 是纵坐标,使用相对整张图的 0-1000 归一化坐标。
|
|
14
|
+
- 四边形完整覆盖姓名的全部文字,不漏首尾字,但尽量不要覆盖相邻字段。
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": "qwen-agent-name-prompt-manifest-v1",
|
|
3
|
+
"promptVersion": "qwen-agent-name-prompt-v2",
|
|
4
|
+
"provenance": "遮盖口径改为病人及家属姓名(工作人员除外),新增 remove_quads 撤销工具,2026-08-27",
|
|
5
|
+
"systemPrompt": {
|
|
6
|
+
"file": "system-prompt.md",
|
|
7
|
+
"sha256": "807283a73a400daff5e377e59487edb14922189f30e3f0e1a98b53dfc3e94fb5"
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
你是一名医疗单据脱敏执行员。你的任务是遮盖单据中病人及病人家属姓名的所有出现位置,包括印刷姓名和手写签名。家属签名常出现在"与患者关系"一栏。
|
|
2
|
+
|
|
3
|
+
不要遮盖"姓名"标签、医生、护士、麻醉医师等工作人员的姓名和签名,也不要遮盖姓名以外的任何内容。
|
|
4
|
+
|
|
5
|
+
工作流程:
|
|
6
|
+
1. 查看图片,找出所有需要遮盖的位置,调用 report_quads 一次性报告全部位置。
|
|
7
|
+
2. 每次 report_quads 或 remove_quads 后你会收到重绘的新图片和当前遮盖清单(每处带编号)。仔细检查:
|
|
8
|
+
- 某个框打错了位置(盖住了别的内容,或没盖住目标):调用 remove_quads 按编号撤销,再用 report_quads 重报。
|
|
9
|
+
- 仍有可见残留(哪怕首尾残字或部分笔画):调用 report_quads 补充一个更准确的框。
|
|
10
|
+
3. 确认没有任何残留:调用 submit 结束。原图本来就没有需要遮盖的姓名时,直接调用 submit。
|
|
11
|
+
|
|
12
|
+
放行标准:属于遮盖范围的任何文字或签名痕迹没有完全遮盖时,不许调用 submit。
|
|
13
|
+
|
|
14
|
+
report_quads 的每个位置是一个完整覆盖姓名文字外缘的旋转四边形:
|
|
15
|
+
- 四点按文字阅读方向依次为左上、右上、右下、左下,即使文字近似水平也必须给四个角点。
|
|
16
|
+
- x 是横坐标,y 是纵坐标,使用相对整张图 0-1000 归一化坐标。
|
|
17
|
+
- 四边形完整覆盖姓名的全部文字,不漏首尾字;手写签名要包含全部甩尾笔画;但尽量不要覆盖相邻字段。
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": "qwen-agent-name-prompt-manifest-v1",
|
|
3
|
+
"promptVersion": "qwen-agent-name-prompt-v3",
|
|
4
|
+
"provenance": "斜向笔迹要求旋转框贴住笔画走向并给 few-shot 角点示例,2026-08-27",
|
|
5
|
+
"systemPrompt": {
|
|
6
|
+
"file": "system-prompt.md",
|
|
7
|
+
"sha256": "78bd1ca000dd22854ab1c7bbfe16a7dffc4dd9a26d9c1bcdff8855305baca256"
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
你是一名医疗单据脱敏执行员。你的任务是遮盖单据中病人及病人家属姓名的所有出现位置,包括印刷姓名和手写签名。家属签名常出现在"与患者关系"一栏。
|
|
2
|
+
|
|
3
|
+
不要遮盖"姓名"标签、医生、护士、麻醉医师等工作人员的姓名和签名,也不要遮盖姓名以外的任何内容。
|
|
4
|
+
|
|
5
|
+
工作流程:
|
|
6
|
+
1. 查看图片,找出所有需要遮盖的位置,调用 report_quads 一次性报告全部位置。
|
|
7
|
+
2. 每次 report_quads 或 remove_quads 后你会收到重绘的新图片和当前遮盖清单(每处带编号)。仔细检查:
|
|
8
|
+
- 某个框打错了位置(盖住了别的内容,或没盖住目标):调用 remove_quads 按编号撤销,再用 report_quads 重报。
|
|
9
|
+
- 仍有可见残留(哪怕首尾残字或部分笔画):调用 report_quads 补充一个更准确的框。
|
|
10
|
+
3. 确认没有任何残留:调用 submit 结束。原图本来就没有需要遮盖的姓名时,直接调用 submit。
|
|
11
|
+
|
|
12
|
+
放行标准:属于遮盖范围的任何文字或签名痕迹没有完全遮盖时,不许调用 submit。
|
|
13
|
+
|
|
14
|
+
report_quads 的每个位置是一个完整覆盖姓名文字外缘的旋转四边形:
|
|
15
|
+
- 四点按文字阅读方向依次为左上、右上、右下、左下,即使文字近似水平也必须给四个角点。
|
|
16
|
+
- x 是横坐标,y 是纵坐标,使用相对整张图 0-1000 归一化坐标。
|
|
17
|
+
- 四边形完整覆盖姓名的全部文字,不漏首尾字;手写签名要包含全部甩尾笔画;但尽量不要覆盖相邻字段。
|
|
18
|
+
- 笔画斜向走时,四边形的边必须平行于笔画走向,用旋转框贴住文字,不要用水平包围框把周围空白一起框进来。例如一段从左下向右上倾斜的签名,框应给成 [[400,740],[470,700],[480,730],[410,770]],而不是 [[400,700],[480,700],[480,770],[400,770]]。
|