@jjlmoya/utils-forensic-science 1.2.0 → 1.4.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/package.json +1 -1
- package/src/category/i18n/de.ts +15 -15
- package/src/category/i18n/en.ts +15 -15
- package/src/category/i18n/es.ts +16 -16
- package/src/category/i18n/fr.ts +15 -15
- package/src/category/i18n/id.ts +17 -17
- package/src/category/i18n/it.ts +18 -18
- package/src/category/i18n/ja.ts +15 -16
- package/src/category/i18n/ko.ts +16 -17
- package/src/category/i18n/nl.ts +17 -17
- package/src/category/i18n/pl.ts +18 -18
- package/src/category/i18n/pt.ts +17 -17
- package/src/category/i18n/ru.ts +18 -18
- package/src/category/i18n/sv.ts +18 -18
- package/src/category/i18n/tr.ts +18 -18
- package/src/category/i18n/zh.ts +17 -18
- package/src/category/index.ts +3 -1
- package/src/entries.ts +5 -1
- package/src/index.ts +1 -0
- package/src/tests/locale_completeness.test.ts +2 -2
- package/src/tests/tool_validation.test.ts +2 -2
- package/src/tool/forensic-fingerprint-minutiae-identifier/bibliography.astro +6 -0
- package/src/tool/forensic-fingerprint-minutiae-identifier/bibliography.ts +16 -0
- package/src/tool/forensic-fingerprint-minutiae-identifier/component.astro +429 -0
- package/src/tool/forensic-fingerprint-minutiae-identifier/drawer.ts +94 -0
- package/src/tool/forensic-fingerprint-minutiae-identifier/entry.ts +32 -0
- package/src/tool/forensic-fingerprint-minutiae-identifier/forensic-fingerprint-minutiae-identifier.css +969 -0
- package/src/tool/forensic-fingerprint-minutiae-identifier/i18n/de.ts +298 -0
- package/src/tool/forensic-fingerprint-minutiae-identifier/i18n/en.ts +289 -0
- package/src/tool/forensic-fingerprint-minutiae-identifier/i18n/es.ts +291 -0
- package/src/tool/forensic-fingerprint-minutiae-identifier/i18n/fr.ts +298 -0
- package/src/tool/forensic-fingerprint-minutiae-identifier/i18n/id.ts +298 -0
- package/src/tool/forensic-fingerprint-minutiae-identifier/i18n/it.ts +298 -0
- package/src/tool/forensic-fingerprint-minutiae-identifier/i18n/ja.ts +281 -0
- package/src/tool/forensic-fingerprint-minutiae-identifier/i18n/ko.ts +281 -0
- package/src/tool/forensic-fingerprint-minutiae-identifier/i18n/nl.ts +298 -0
- package/src/tool/forensic-fingerprint-minutiae-identifier/i18n/pl.ts +298 -0
- package/src/tool/forensic-fingerprint-minutiae-identifier/i18n/pt.ts +298 -0
- package/src/tool/forensic-fingerprint-minutiae-identifier/i18n/ru.ts +298 -0
- package/src/tool/forensic-fingerprint-minutiae-identifier/i18n/sv.ts +298 -0
- package/src/tool/forensic-fingerprint-minutiae-identifier/i18n/tr.ts +298 -0
- package/src/tool/forensic-fingerprint-minutiae-identifier/i18n/zh.ts +281 -0
- package/src/tool/forensic-fingerprint-minutiae-identifier/index.ts +11 -0
- package/src/tool/forensic-fingerprint-minutiae-identifier/logic.ts +168 -0
- package/src/tool/forensic-fingerprint-minutiae-identifier/renderer.ts +116 -0
- package/src/tool/forensic-fingerprint-minutiae-identifier/seo.astro +15 -0
- package/src/tools.ts +3 -1
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { bibliography } from '../bibliography';
|
|
2
|
+
import type { ToolLocaleContent } from '../../../types';
|
|
3
|
+
|
|
4
|
+
const slug = 'forensic-fingerprint-minutiae-identifier';
|
|
5
|
+
const title = '指纹纹型分类与细节特征识别工具';
|
|
6
|
+
const description = '上传指纹图像,使用交互式放大镜进行观察,分类脊线流量图案,标记一组细节特征(特征点),并导出证据表。';
|
|
7
|
+
|
|
8
|
+
const howTo = [
|
|
9
|
+
{
|
|
10
|
+
name: '选择 Galton-Henry 纹型',
|
|
11
|
+
text: '上传指纹图像后,选择弓型纹、箕型纹或斗型纹,以记录观察到的脊线流动类别。',
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
name: '调整核心与三叉点',
|
|
15
|
+
text: '调整核心偏移量和三叉点数量滑块。分类器将自动更新 Henry 分类和置信度。',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
name: '选择特征点类型',
|
|
19
|
+
text: '在画布上放置标记之前,从四个特征点按钮中选择脊线终点、分叉点、眼形线或孤立点。',
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: '放置并核对标记',
|
|
23
|
+
text: '点击指纹图像以添加标记。表格将记录坐标并核对是否达到了 5 个终点和 3 个分叉点的练习目标。',
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const faq = [
|
|
28
|
+
{
|
|
29
|
+
key: 'faq-1',
|
|
30
|
+
question: '什么是指纹细节特征(特征点)?',
|
|
31
|
+
answer: '细节特征是脊线的局部解剖结构特征(如脊线终点、分叉点、眼形线和孤立点),用于指纹的比对与司法鉴定。',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
key: 'faq-2',
|
|
35
|
+
question: '弓型纹、箕型纹和斗型纹有什么区别?',
|
|
36
|
+
answer: '弓型纹的脊线从一侧流向另一侧,没有回转弯曲。箕型纹具有一条回转的脊线和一个三叉点。斗型纹呈现圆形或螺旋形图案,通常有两个三叉点。',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
key: 'faq-3',
|
|
40
|
+
question: '该工具能否对真实案件的指纹进行比对鉴定?',
|
|
41
|
+
answer: '不能。本工具仅为教学模拟器,用于指纹特征标记的日常练习。真实的司法鉴定需要经过校准的高清图像、对比材料以及获得认证的指纹专家的专业评估。',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
key: 'faq-4',
|
|
45
|
+
question: '为什么练习要求标记 5 个终点和 3 个分叉点?',
|
|
46
|
+
answer: '这一教学目标旨在帮助学生系统化地学习并掌握最常见指纹特征点的识别和命名方式。',
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
export const content: ToolLocaleContent = {
|
|
51
|
+
slug,
|
|
52
|
+
title,
|
|
53
|
+
description,
|
|
54
|
+
ui: {
|
|
55
|
+
canvasAria: '用于在上传的指纹图像上放置特征点标记的交互式画布',
|
|
56
|
+
workflow: '指纹分析工作流程',
|
|
57
|
+
stepUpload: '上传',
|
|
58
|
+
stepClassify: '分类',
|
|
59
|
+
stepMark: '标记',
|
|
60
|
+
stepReview: '审核',
|
|
61
|
+
stepLabel: '步骤',
|
|
62
|
+
stage1Title: '上传指纹',
|
|
63
|
+
stage1Copy: '先上传一张指纹图片。在文件上传之前,画布将保持锁定状态。',
|
|
64
|
+
stage2Title: '脊线流动分类',
|
|
65
|
+
stage2Copy: '使用图片下方的控件选择弓型纹、箕型纹或斗型纹。',
|
|
66
|
+
stage3Title: '标记要求的特征点',
|
|
67
|
+
stage3Copy: '选择特征点类型,点击图片添加,或点击已有的标记将其删除。',
|
|
68
|
+
stage4Title: '审核证据表',
|
|
69
|
+
stage4Copy: '核对数量,使用垃圾桶按钮删除错误,然后复制格式化好的报告。',
|
|
70
|
+
canvasHint1: '上传指纹图像以开始。',
|
|
71
|
+
canvasHint2: '请先不要标记。使用图片下方的控件确定总体纹型。',
|
|
72
|
+
canvasHint3: '点击图片放置选定的细节特征。点击已有特征点进行选择。',
|
|
73
|
+
canvasHint4: '查看图片下方的证据表。',
|
|
74
|
+
uploadTitle: '上传指纹图像',
|
|
75
|
+
uploadHint: 'PNG、JPG 图像或潜指纹扫描件',
|
|
76
|
+
replaceImage: '替换',
|
|
77
|
+
continueMarking: '继续标记',
|
|
78
|
+
reviewEvidence: '查看证据',
|
|
79
|
+
back: '返回',
|
|
80
|
+
next: '下一步',
|
|
81
|
+
patternType: '总体纹型类型',
|
|
82
|
+
arch: '弓型纹',
|
|
83
|
+
loop: '箕型纹',
|
|
84
|
+
whorl: '斗型纹',
|
|
85
|
+
activeMinutia: '当前特征点',
|
|
86
|
+
markingNow: '标记模式已启用',
|
|
87
|
+
clickToPlace: '点击图片中对应的特征位置',
|
|
88
|
+
ridgeEnding: '脊线终点',
|
|
89
|
+
ridgeEndingShort: '脊线在此终止',
|
|
90
|
+
ridgeEndingHelp: '脊线终点是脊线突然终止、不与任何其他脊线相连的点。',
|
|
91
|
+
bifurcation: '分叉点',
|
|
92
|
+
bifurcationShort: '一分二分叉',
|
|
93
|
+
bifurcationHelp: '分叉点是单一脊线分叉为两条分支的点。',
|
|
94
|
+
island: '眼形线(岛形)',
|
|
95
|
+
islandShort: '较短的孤立脊线',
|
|
96
|
+
islandHelp: '眼形线是具有两个明显断开的端点、并与较长脊线隔离的短脊线段。',
|
|
97
|
+
dot: '孤立点(点状)',
|
|
98
|
+
dotShort: '微小的独立点',
|
|
99
|
+
dotHelp: '孤立点是极短的脊线片段,外观类似于一个独立的圆点。',
|
|
100
|
+
coreOffset: '核心偏移量',
|
|
101
|
+
deltaCount: '三叉点数量',
|
|
102
|
+
henryClass: 'Henry 分类',
|
|
103
|
+
validation: '练习评分',
|
|
104
|
+
legendAria: '特征点图例说明',
|
|
105
|
+
clearMarks: '全部清除',
|
|
106
|
+
undoLast: '撤销上一步',
|
|
107
|
+
deleteSelected: '删除所选',
|
|
108
|
+
selectMarkToDelete: '请先在指纹图上点击选择一个标记',
|
|
109
|
+
copyEvidence: '复制证据数据',
|
|
110
|
+
ridgeFlow: '脊线流量备注',
|
|
111
|
+
type: '类型',
|
|
112
|
+
position: '位置坐标',
|
|
113
|
+
action: '操作',
|
|
114
|
+
remove: '删除',
|
|
115
|
+
endingsGoal: '脊线终点',
|
|
116
|
+
bifurcationsGoal: '分叉点',
|
|
117
|
+
totalMarks: '总标记数',
|
|
118
|
+
plainArch: '普通弓型纹',
|
|
119
|
+
archFlow: '脊线从一侧流入,在中心微微隆起,不带任何回转地流向对侧。',
|
|
120
|
+
ulnarLoop: '尺箕(尺侧箕型纹)',
|
|
121
|
+
radialLoop: '桡箕(桡侧箕型纹)',
|
|
122
|
+
loopFlow: '包含一个三叉点和一条明显的、回转弯曲并朝向脊线流入方向的流线。',
|
|
123
|
+
plainWhorl: '普通斗型纹',
|
|
124
|
+
centralPocketWhorl: '中心袋斗型纹',
|
|
125
|
+
whorlFlow: '脊线围绕中心的核心呈圆形或螺旋形旋转流动,通常伴随有两个三叉点。',
|
|
126
|
+
ridgeEndingLabel: 'Z',
|
|
127
|
+
bifurcationLabel: 'F',
|
|
128
|
+
islandLabel: 'D',
|
|
129
|
+
dotLabel: 'D',
|
|
130
|
+
},
|
|
131
|
+
seo: [
|
|
132
|
+
{
|
|
133
|
+
type: 'title',
|
|
134
|
+
text: '指纹纹型分类与细节特征识别练习',
|
|
135
|
+
level: 2,
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
type: 'diagnostic',
|
|
139
|
+
variant: 'info',
|
|
140
|
+
icon: 'mdi:fingerprint',
|
|
141
|
+
badge: '教学平台',
|
|
142
|
+
title: '本指纹细节特征识别系统的教学目标',
|
|
143
|
+
html: '本工具专门为司法鉴定及刑事侦查学专业的学生设计,用于学习<strong>指纹乳突线流分类</strong>和<strong>细节特征特征点标记</strong>。结合实用的虚拟放大镜和可导出的数据表格,培养细致的专业观察能力。',
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
type: 'stats',
|
|
147
|
+
columns: 4,
|
|
148
|
+
items: [
|
|
149
|
+
{ value: '3', label: '大类指纹纹型', icon: 'mdi:shape-outline' },
|
|
150
|
+
{ value: '4', label: '细节特征类型', icon: 'mdi:map-marker-radius' },
|
|
151
|
+
{ value: '5 + 3', label: '教学练习达标要求', icon: 'mdi:check-decagram' },
|
|
152
|
+
{ value: '上传', label: '图像驱动分析流程', icon: 'mdi:cloud-upload-outline' },
|
|
153
|
+
],
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
type: 'summary',
|
|
157
|
+
title: '推荐分析工作流程',
|
|
158
|
+
items: [
|
|
159
|
+
'首先划分总体的纹型大类:弓型纹、箕型纹或斗型纹。',
|
|
160
|
+
'在开始标记特征点之前,先定位三叉点和核心区的位置。',
|
|
161
|
+
'仅标记可见的清晰特征,避免在模糊或污染区域主观盲测。',
|
|
162
|
+
'详细记录每一个标记点的特征分类与相对坐标。',
|
|
163
|
+
'将客观特征记录与最终的比对推导结论严格区分。',
|
|
164
|
+
],
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
type: 'comparative',
|
|
168
|
+
columns: 3,
|
|
169
|
+
items: [
|
|
170
|
+
{
|
|
171
|
+
title: '弓型纹',
|
|
172
|
+
icon: 'mdi:gesture-swipe-horizontal',
|
|
173
|
+
description: '脊线平缓穿过。普通弓型纹不具备明显的三叉点或箕枝状弯曲。',
|
|
174
|
+
points: ['无三叉点', '波浪状脊线流动', '初学者易于理解的基础纹型'],
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
title: '箕型纹',
|
|
178
|
+
icon: 'mdi:rotate-left',
|
|
179
|
+
highlight: true,
|
|
180
|
+
description: '包含箕枝线绕过核心,具有一个与之相伴的三叉点。',
|
|
181
|
+
points: ['单一三叉点', '桡侧或尺侧流向', '核心点定位至关重要'],
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
title: '斗型纹',
|
|
185
|
+
icon: 'mdi:autorenew',
|
|
186
|
+
description: '乳突脊线围绕中心核心点做同心圆或螺旋状的环流。',
|
|
187
|
+
points: ['通常有两个三叉点', '圆形环绕流动趋势', '细分为普通斗型和中心袋斗型'],
|
|
188
|
+
},
|
|
189
|
+
],
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
type: 'table',
|
|
193
|
+
headers: ['细节特征', '视觉呈现样式', '标记过程常见误区'],
|
|
194
|
+
rows: [
|
|
195
|
+
['脊线终点', '单条脊线在流向中突然停止。', '需排除由墨水不均、图像破损或对比度不足导致的虚假停滞点。'],
|
|
196
|
+
['分叉点', '单条脊线在流向中分裂为两条。', '应注意由于盖印时压力不均所导致的虚假"接触性"分叉。'],
|
|
197
|
+
['眼形线', '一条极短的孤立脊线段,夹在两条长脊线之间。', '仅在两个端点清晰可见且无黏连时进行标记。'],
|
|
198
|
+
['孤立点', '微小的独立脊线斑点,呈点状。', '需格外谨慎,避免与油墨污点或数字像素噪声混淆。'],
|
|
199
|
+
],
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
type: 'title',
|
|
203
|
+
text: '如何正确理解练习达标分',
|
|
204
|
+
level: 3,
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
type: 'paragraph',
|
|
208
|
+
html: '此评分仅供教学指导,不具备司法比对证据效力。这是一个教学检测清单,要求学生至少标记出 5 个脊线终点和 3 个分叉点,从而培养系统化记录指纹细节特征的学习习惯。',
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
type: 'paragraph',
|
|
212
|
+
html: '在真实的司法鉴定实践中(ACE-V 方法),鉴定专家需要全面考量指纹载体质量、变形因素、特征点在三维空间中的拓扑关系及其稀有度等因素,从而得出鉴定意见。',
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
type: 'glossary',
|
|
216
|
+
items: [
|
|
217
|
+
{ term: '核心点', definition: '箕型纹或斗型纹中心的大致几何中心点,是分类的基础参照物。' },
|
|
218
|
+
{ term: '三叉点', definition: '指纹纹型中三个脊线流动方向相交汇的三角形区域。' },
|
|
219
|
+
{ term: '细节特征', definition: '指纹乳突脊线上用于人身同一认定的微观结构特征。' },
|
|
220
|
+
{ term: 'ACE-V', definition: '国际通用鉴定流程:分析、比对、评估、验证。' },
|
|
221
|
+
],
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
type: 'tip',
|
|
225
|
+
title: '课堂实训建议',
|
|
226
|
+
html: '可让学生对同一张指纹标记两次:第一次快速标记,第二次在仔细理清脊线流动关系后标记。前后两次的对比往往能清晰地展示出初学者容易犯的主观臆测错误。',
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
type: 'proscons',
|
|
230
|
+
title: '本模拟工具的优点与局限性',
|
|
231
|
+
items: [
|
|
232
|
+
{ pro: '支持用户上传自己的真实指纹图像,而非采用死板的预设画作。', con: '源图像质量的高低直接影响脊线特征的辨识度。' },
|
|
233
|
+
{ pro: '自动生成清晰、可复制导出的结构化细节特征数据表。', con: '不计算特征点的方向矢量,也不做鉴定数量充分性的实质评估。' },
|
|
234
|
+
{ pro: '将宏观纹型分类与微观特征点标注分为不同步骤,更合乎逻辑。', con: '绝不可用于正式的司法鉴定、诉讼呈堂证供或真实刑事侦查。' },
|
|
235
|
+
],
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
type: 'diagnostic',
|
|
239
|
+
variant: 'warning',
|
|
240
|
+
icon: 'mdi:scale-balance',
|
|
241
|
+
badge: '法律警示',
|
|
242
|
+
title: '仅供学术教学研究使用',
|
|
243
|
+
html: '本模拟器完全面向教学与技能培训,不得将其用作司法案件中人身同一认定的最终依据。',
|
|
244
|
+
},
|
|
245
|
+
],
|
|
246
|
+
faq,
|
|
247
|
+
bibliography,
|
|
248
|
+
howTo,
|
|
249
|
+
schemas: [
|
|
250
|
+
{
|
|
251
|
+
'@context': 'https://schema.org',
|
|
252
|
+
'@type': 'SoftwareApplication',
|
|
253
|
+
name: title,
|
|
254
|
+
description,
|
|
255
|
+
applicationCategory: 'ForensicApplication',
|
|
256
|
+
operatingSystem: 'Any',
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
'@context': 'https://schema.org',
|
|
260
|
+
'@type': 'FAQPage',
|
|
261
|
+
mainEntity: faq.map((item) => ({
|
|
262
|
+
'@type': 'Question',
|
|
263
|
+
name: item.question,
|
|
264
|
+
acceptedAnswer: {
|
|
265
|
+
'@type': 'Answer',
|
|
266
|
+
text: item.answer,
|
|
267
|
+
},
|
|
268
|
+
})),
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
'@context': 'https://schema.org',
|
|
272
|
+
'@type': 'HowTo',
|
|
273
|
+
name: title,
|
|
274
|
+
step: howTo.map((step) => ({
|
|
275
|
+
'@type': 'HowToStep',
|
|
276
|
+
name: step.name,
|
|
277
|
+
text: step.text,
|
|
278
|
+
})),
|
|
279
|
+
},
|
|
280
|
+
],
|
|
281
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { forensicFingerprintMinutiaeIdentifier } from './entry';
|
|
2
|
+
import type { ToolDefinition } from '../../types';
|
|
3
|
+
|
|
4
|
+
export * from './entry';
|
|
5
|
+
|
|
6
|
+
export const FORENSIC_FINGERPRINT_MINUTIAE_IDENTIFIER_TOOL: ToolDefinition = {
|
|
7
|
+
entry: forensicFingerprintMinutiaeIdentifier,
|
|
8
|
+
Component: () => import('./component.astro'),
|
|
9
|
+
SEOComponent: () => import('./seo.astro'),
|
|
10
|
+
BibliographyComponent: () => import('./bibliography.astro'),
|
|
11
|
+
};
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
export type FingerprintPattern = 'arch' | 'loop' | 'whorl';
|
|
2
|
+
export type MinutiaType = 'ridgeEnding' | 'bifurcation' | 'island' | 'dot';
|
|
3
|
+
|
|
4
|
+
export const limits: Record<MinutiaType, number> = {
|
|
5
|
+
ridgeEnding: 5,
|
|
6
|
+
bifurcation: 3,
|
|
7
|
+
island: 2,
|
|
8
|
+
dot: 2,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const colors: Record<MinutiaType, string> = {
|
|
12
|
+
ridgeEnding: '#ef4444',
|
|
13
|
+
bifurcation: '#0ea5e9',
|
|
14
|
+
island: '#f59e0b',
|
|
15
|
+
dot: '#22c55e',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export interface MinutiaMark {
|
|
19
|
+
id: string;
|
|
20
|
+
type: MinutiaType;
|
|
21
|
+
x: number;
|
|
22
|
+
y: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface FingerprintClassification {
|
|
26
|
+
pattern: FingerprintPattern;
|
|
27
|
+
henryLabel: string;
|
|
28
|
+
confidence: number;
|
|
29
|
+
ridgeFlow: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface MinutiaeSummary {
|
|
33
|
+
total: number;
|
|
34
|
+
byType: Record<MinutiaType, number>;
|
|
35
|
+
validationScore: number;
|
|
36
|
+
checklist: {
|
|
37
|
+
ridgeEndings: boolean;
|
|
38
|
+
bifurcations: boolean;
|
|
39
|
+
diversity: boolean;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export class MinutiaeState {
|
|
44
|
+
private marks: MinutiaMark[] = [];
|
|
45
|
+
|
|
46
|
+
addMark(type: MinutiaType, x: number, y: number): MinutiaMark {
|
|
47
|
+
const mark = {
|
|
48
|
+
id: `m-${Date.now()}-${Math.round(x * 10)}-${Math.round(y * 10)}`,
|
|
49
|
+
type,
|
|
50
|
+
x: clamp(x, 0, 100),
|
|
51
|
+
y: clamp(y, 0, 100),
|
|
52
|
+
};
|
|
53
|
+
this.marks = [...this.marks, mark];
|
|
54
|
+
return mark;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
removeMark(id: string): void {
|
|
58
|
+
this.marks = this.marks.filter((mark) => mark.id !== id);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
clear(): void {
|
|
62
|
+
this.marks = [];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
replace(marks: MinutiaMark[]): void {
|
|
66
|
+
this.marks = marks
|
|
67
|
+
.filter((mark) => isMinutiaType(mark.type))
|
|
68
|
+
.map((mark) => ({
|
|
69
|
+
id: mark.id,
|
|
70
|
+
type: mark.type,
|
|
71
|
+
x: clamp(mark.x, 0, 100),
|
|
72
|
+
y: clamp(mark.y, 0, 100),
|
|
73
|
+
}));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
list(): MinutiaMark[] {
|
|
77
|
+
return [...this.marks];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
summarize(): MinutiaeSummary {
|
|
81
|
+
const byType: Record<MinutiaType, number> = {
|
|
82
|
+
ridgeEnding: 0,
|
|
83
|
+
bifurcation: 0,
|
|
84
|
+
island: 0,
|
|
85
|
+
dot: 0,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
this.marks.forEach((mark) => {
|
|
89
|
+
byType[mark.type] += 1;
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const checklist = {
|
|
93
|
+
ridgeEndings: byType.ridgeEnding >= 5,
|
|
94
|
+
bifurcations: byType.bifurcation >= 3,
|
|
95
|
+
diversity: Object.values(byType).filter((count) => count > 0).length >= 3,
|
|
96
|
+
};
|
|
97
|
+
const validationScore = Math.round(
|
|
98
|
+
(Math.min(byType.ridgeEnding / 5, 1) * 42)
|
|
99
|
+
+ (Math.min(byType.bifurcation / 3, 1) * 36)
|
|
100
|
+
+ (checklist.diversity ? 22 : 0),
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
total: this.marks.length,
|
|
105
|
+
byType,
|
|
106
|
+
validationScore,
|
|
107
|
+
checklist,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
canAdd(type: MinutiaType): boolean {
|
|
112
|
+
return this.summarize().byType[type] < limits[type];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
lastMark(): MinutiaMark | null {
|
|
116
|
+
return this.marks[this.marks.length - 1] ?? null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
findMarkAt(x: number, y: number, canvasWidth: number, canvasHeight: number): MinutiaMark | null {
|
|
120
|
+
return [...this.marks].reverse().find((mark) => {
|
|
121
|
+
const markX = (mark.x / 100) * canvasWidth;
|
|
122
|
+
const markY = (mark.y / 100) * canvasHeight;
|
|
123
|
+
return Math.hypot(markX - x, markY - y) <= 18;
|
|
124
|
+
}) ?? null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
firstAvailableMinutia(): MinutiaType | null {
|
|
128
|
+
return (['ridgeEnding', 'bifurcation', 'island', 'dot'] as MinutiaType[]).find((type) => this.canAdd(type)) ?? null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export class FingerprintClassifier {
|
|
133
|
+
classify(pattern: FingerprintPattern, coreOffset: number, deltaCount: number): FingerprintClassification {
|
|
134
|
+
if (pattern === 'arch') {
|
|
135
|
+
return {
|
|
136
|
+
pattern,
|
|
137
|
+
henryLabel: 'plainArch',
|
|
138
|
+
confidence: Math.round(clamp(88 - deltaCount * 11 - Math.abs(coreOffset) * 0.3, 54, 96)),
|
|
139
|
+
ridgeFlow: 'archFlow',
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (pattern === 'loop') {
|
|
144
|
+
const side = coreOffset >= 0 ? 'ulnarLoop' : 'radialLoop';
|
|
145
|
+
return {
|
|
146
|
+
pattern,
|
|
147
|
+
henryLabel: side,
|
|
148
|
+
confidence: Math.round(clamp(82 + Math.min(deltaCount, 1) * 9 - Math.abs(coreOffset - 18) * 0.2, 58, 97)),
|
|
149
|
+
ridgeFlow: 'loopFlow',
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
pattern,
|
|
155
|
+
henryLabel: deltaCount >= 2 ? 'plainWhorl' : 'centralPocketWhorl',
|
|
156
|
+
confidence: Math.round(clamp(74 + Math.min(deltaCount, 2) * 10 - Math.abs(coreOffset) * 0.12, 55, 98)),
|
|
157
|
+
ridgeFlow: 'whorlFlow',
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function isMinutiaType(value: string): value is MinutiaType {
|
|
163
|
+
return value === 'ridgeEnding' || value === 'bifurcation' || value === 'island' || value === 'dot';
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function clamp(value: number, min: number, max: number): number {
|
|
167
|
+
return Math.max(min, Math.min(max, value));
|
|
168
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { FingerprintClassification, MinutiaeSummary, MinutiaMark, MinutiaType } from './logic';
|
|
2
|
+
|
|
3
|
+
function setText(id: string, value: string): void {
|
|
4
|
+
const element = document.getElementById(id);
|
|
5
|
+
if (element) {
|
|
6
|
+
element.textContent = value;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function updateWorkflow(
|
|
11
|
+
activeStep: number,
|
|
12
|
+
ui: Record<string, string>,
|
|
13
|
+
board: HTMLElement | null
|
|
14
|
+
): void {
|
|
15
|
+
document.querySelectorAll<HTMLElement>('[data-fp-step]').forEach((item) => {
|
|
16
|
+
const itemStep = Number(item.dataset.fpStep ?? 1);
|
|
17
|
+
item.dataset.active = itemStep === activeStep ? 'true' : 'false';
|
|
18
|
+
item.dataset.done = itemStep < activeStep ? 'true' : 'false';
|
|
19
|
+
});
|
|
20
|
+
document.querySelectorAll<HTMLElement>('[data-fp-panel]').forEach((panel) => {
|
|
21
|
+
panel.dataset.active = Number(panel.dataset.fpPanel ?? 1) === activeStep ? 'true' : 'false';
|
|
22
|
+
});
|
|
23
|
+
if (board) {
|
|
24
|
+
board.dataset.activeStep = String(activeStep);
|
|
25
|
+
}
|
|
26
|
+
setText('fp-stage-label', `${ui.stepLabel} ${activeStep}/4`);
|
|
27
|
+
setText('fp-stage-title', ui[`stage${activeStep}Title`] ?? '');
|
|
28
|
+
setText('fp-stage-copy', ui[`stage${activeStep}Copy`] ?? '');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function updateUIStates(
|
|
32
|
+
activeMinutia: MinutiaType,
|
|
33
|
+
activePattern: string,
|
|
34
|
+
limits: Record<MinutiaType, number>,
|
|
35
|
+
canAdd: (type: MinutiaType) => boolean
|
|
36
|
+
): void {
|
|
37
|
+
document.querySelectorAll<HTMLButtonElement>('[data-fp-pattern]').forEach((button) => {
|
|
38
|
+
button.dataset.active = button.dataset.fpPattern === activePattern ? 'true' : 'false';
|
|
39
|
+
});
|
|
40
|
+
document.querySelectorAll<HTMLButtonElement>('[data-fp-minutia]').forEach((button) => {
|
|
41
|
+
const type = (button.dataset.fpMinutia || 'ridgeEnding') as MinutiaType;
|
|
42
|
+
button.dataset.active = type === activeMinutia ? 'true' : 'false';
|
|
43
|
+
button.dataset.full = canAdd(type) ? 'false' : 'true';
|
|
44
|
+
button.disabled = !canAdd(type) && type !== activeMinutia;
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function updateActionButtons(
|
|
49
|
+
selectedMark: MinutiaMark | undefined,
|
|
50
|
+
ui: Record<string, string>,
|
|
51
|
+
hasLastMark: boolean
|
|
52
|
+
): void {
|
|
53
|
+
const deleteSelected = document.getElementById('fp-delete-selected') as HTMLButtonElement | null;
|
|
54
|
+
const undo = document.getElementById('fp-undo') as HTMLButtonElement | null;
|
|
55
|
+
if (deleteSelected) {
|
|
56
|
+
deleteSelected.disabled = !selectedMark;
|
|
57
|
+
deleteSelected.title = selectedMark ? `${ui.deleteSelected}: ${ui[selectedMark.type] ?? selectedMark.type}` : ui.selectMarkToDelete;
|
|
58
|
+
}
|
|
59
|
+
if (undo) {
|
|
60
|
+
undo.disabled = !hasLastMark;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function updateClassifierLabels(
|
|
65
|
+
classification: FingerprintClassification,
|
|
66
|
+
coreVal: string,
|
|
67
|
+
deltaVal: string,
|
|
68
|
+
ui: Record<string, string>
|
|
69
|
+
): void {
|
|
70
|
+
setText('fp-core-label', coreVal);
|
|
71
|
+
setText('fp-delta-label', deltaVal);
|
|
72
|
+
const henryLabel = ui[classification.henryLabel] ?? classification.henryLabel;
|
|
73
|
+
const ridgeFlow = ui[classification.ridgeFlow] ?? classification.ridgeFlow;
|
|
74
|
+
setText('fp-class', `${henryLabel} (${classification.confidence}%)`);
|
|
75
|
+
setText('fp-class-review', `${henryLabel} (${classification.confidence}%)`);
|
|
76
|
+
setText('fp-flow', ridgeFlow);
|
|
77
|
+
setText('fp-flow-compact', ridgeFlow);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function updateMinutiaeLabels(
|
|
81
|
+
summary: MinutiaeSummary,
|
|
82
|
+
activeMinutia: MinutiaType,
|
|
83
|
+
ui: Record<string, string>,
|
|
84
|
+
limits: Record<MinutiaType, number>
|
|
85
|
+
): void {
|
|
86
|
+
(['ridgeEnding', 'bifurcation', 'island', 'dot'] as MinutiaType[]).forEach((type) => {
|
|
87
|
+
setText(`fp-count-${type}`, `${summary.byType[type]}/${limits[type]}`);
|
|
88
|
+
});
|
|
89
|
+
setText('fp-active-minutia-label', ui.activeMinutia);
|
|
90
|
+
setText('fp-active-minutia-title', ui[activeMinutia] ?? activeMinutia);
|
|
91
|
+
setText('fp-active-minutia-copy', ui[`${activeMinutia}Help`] ?? '');
|
|
92
|
+
setText('fp-score', `${summary.validationScore}%`);
|
|
93
|
+
setText('fp-score-review', `${summary.validationScore}%`);
|
|
94
|
+
setText('fp-total', String(summary.total));
|
|
95
|
+
setText('fp-checklist', `${ui.endingsGoal}: ${summary.byType.ridgeEnding}/5 | ${ui.bifurcationsGoal}: ${summary.byType.bifurcation}/3 | ${ui.totalMarks}: ${summary.total}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
interface RenderTableParams {
|
|
99
|
+
marks: MinutiaMark[];
|
|
100
|
+
colors: Record<MinutiaType, string>;
|
|
101
|
+
ui: Record<string, string>;
|
|
102
|
+
tableBody: HTMLElement | null;
|
|
103
|
+
trashIconTemplate: HTMLTemplateElement | null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function renderTable(p: RenderTableParams): void {
|
|
107
|
+
if (!p.tableBody) return;
|
|
108
|
+
p.tableBody.innerHTML = '';
|
|
109
|
+
p.marks.forEach((mark) => {
|
|
110
|
+
const row = document.createElement('tr');
|
|
111
|
+
const label = p.ui[mark.type] ?? mark.type;
|
|
112
|
+
const trashIcon = p.trashIconTemplate?.innerHTML || p.ui.remove;
|
|
113
|
+
row.innerHTML = `<td><span style="--dot:${p.colors[mark.type]}"></span>${label}</td><td>${mark.x.toFixed(1)}, ${mark.y.toFixed(1)}</td><td><button type="button" class="fp-icon-button" data-remove="${mark.id}" aria-label="${p.ui.remove} ${label}">${trashIcon}</button></td>`;
|
|
114
|
+
p.tableBody.append(row);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { SEORenderer } from '@jjlmoya/utils-shared';
|
|
3
|
+
import { forensicFingerprintMinutiaeIdentifier } from './index';
|
|
4
|
+
import type { KnownLocale } from '../../types';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
locale?: KnownLocale;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { locale = 'en' } = Astro.props;
|
|
11
|
+
const content = await forensicFingerprintMinutiaeIdentifier.i18n[locale]?.();
|
|
12
|
+
if (!content) return null;
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
{content.seo && content.seo.length > 0 && <SEORenderer content={{ locale, sections: content.seo }} />}
|
package/src/tools.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { FORENSIC_MICROCRYSTAL_DRUG_SIMULATOR_TOOL } from './tool/forensic-micro
|
|
|
12
12
|
import { FORENSIC_GLASS_BECKE_LINE_SIMULATOR_TOOL } from './tool/forensic-glass-becke-line-simulator/index';
|
|
13
13
|
import { FORENSIC_FIBER_COMPARISON_MICROSCOPE_TOOL } from './tool/forensic-fiber-comparison-microscope/index';
|
|
14
14
|
import { BLOODSTAIN_PATTERN_ORIGIN_ANALYZER_TOOL } from './tool/bloodstain-pattern-origin-analyzer/index';
|
|
15
|
+
import { FORENSIC_FINGERPRINT_MINUTIAE_IDENTIFIER_TOOL } from './tool/forensic-fingerprint-minutiae-identifier/index';
|
|
15
16
|
|
|
16
17
|
export const ALL_TOOLS: ToolDefinition[] = [
|
|
17
18
|
FORENSIC_AGE_ESTIMATOR_TOOL,
|
|
@@ -25,5 +26,6 @@ export const ALL_TOOLS: ToolDefinition[] = [
|
|
|
25
26
|
FORENSIC_MICROCRYSTAL_DRUG_SIMULATOR_TOOL,
|
|
26
27
|
FORENSIC_GLASS_BECKE_LINE_SIMULATOR_TOOL,
|
|
27
28
|
FORENSIC_FIBER_COMPARISON_MICROSCOPE_TOOL,
|
|
28
|
-
BLOODSTAIN_PATTERN_ORIGIN_ANALYZER_TOOL
|
|
29
|
+
BLOODSTAIN_PATTERN_ORIGIN_ANALYZER_TOOL,
|
|
30
|
+
FORENSIC_FINGERPRINT_MINUTIAE_IDENTIFIER_TOOL
|
|
29
31
|
];
|