capacitor-card-scanner 0.3.1 → 0.3.2
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/README.md +1 -0
- package/android/src/main/java/pt/tcgclassifier/cardscanner/CardDetector.java +41 -6
- package/android/src/main/java/pt/tcgclassifier/cardscanner/CardScannerPlugin.java +1 -0
- package/dist/docs.json +12 -0
- package/dist/esm/definitions.d.ts +6 -0
- package/dist/esm/definitions.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -340,6 +340,7 @@ requestPermissions() => Promise<PermissionStatus>
|
|
|
340
340
|
| **`requireCard`** | <code>boolean</code> | Only read text when a card-shaped object is visible. When false, the whole frame is read if no card is found. | <code>true</code> |
|
|
341
341
|
| **`debug`** | <code>boolean</code> | Every object found is always outlined: red when accepted as a card, grey when rejected. With `debug`, each outline is also labelled with its area (`a`), width/height ratio (`r`) and the filter that rejected it. Use it to tune the filters above. | <code>false</code> |
|
|
342
342
|
| **`showRejected`** | <code>boolean</code> | Outline (in grey) the objects that were not accepted as a card. The accepted card is always outlined in red. | <code>true</code> |
|
|
343
|
+
| **`repeatAfterMs`** | <code>number</code> | How long (ms) the card must be out of view before the same card can be reported again. While a card stays in view it is reported only once. | <code>1500</code> |
|
|
343
344
|
|
|
344
345
|
|
|
345
346
|
#### PluginListenerHandle
|
|
@@ -64,6 +64,8 @@ public class CardDetector implements ImageAnalysis.Analyzer {
|
|
|
64
64
|
public boolean debug = false;
|
|
65
65
|
/** Outline (in grey) the objects that were not accepted as a card. */
|
|
66
66
|
public boolean showRejected = true;
|
|
67
|
+
/** How long the card must be out of view before the same card can be reported again. */
|
|
68
|
+
public long repeatAfterMs = 1500;
|
|
67
69
|
}
|
|
68
70
|
|
|
69
71
|
/** An object found by ML Kit, with the values the card filters were checked against. */
|
|
@@ -108,8 +110,8 @@ public class CardDetector implements ImageAnalysis.Analyzer {
|
|
|
108
110
|
);
|
|
109
111
|
private static final Pattern HP_SUFFIX = Pattern.compile("\\s*HP\\s*\\d+\\s*$", Pattern.CASE_INSENSITIVE);
|
|
110
112
|
private static final Pattern LETTER = Pattern.compile("\\p{L}");
|
|
111
|
-
/**
|
|
112
|
-
private static final
|
|
113
|
+
/** Names at least this similar are treated as the same card. */
|
|
114
|
+
private static final float SAME_NAME_SIMILARITY = 0.8f;
|
|
113
115
|
/** Extra margin around the detected card before running OCR. */
|
|
114
116
|
private static final float CROP_MARGIN = 0.04f;
|
|
115
117
|
|
|
@@ -126,7 +128,10 @@ public class CardDetector implements ImageAnalysis.Analyzer {
|
|
|
126
128
|
private long lastSeenAt;
|
|
127
129
|
private String candidateKey;
|
|
128
130
|
private int candidateCount;
|
|
129
|
-
|
|
131
|
+
// Last reported card. A reading is the same card if either its name or its
|
|
132
|
+
// number matches, because OCR does not read the number on every frame.
|
|
133
|
+
private String lastEmittedName;
|
|
134
|
+
private String lastEmittedNumber;
|
|
130
135
|
|
|
131
136
|
public CardDetector(Options options, Listener listener) {
|
|
132
137
|
this.options = options;
|
|
@@ -169,6 +174,9 @@ public class CardDetector implements ImageAnalysis.Analyzer {
|
|
|
169
174
|
List<Candidate> candidates = new ArrayList<>();
|
|
170
175
|
Rect card = pickCard(objects, frameW, frameH, candidates);
|
|
171
176
|
RectF cardBox = card == null ? null : normalize(card, frameW, frameH);
|
|
177
|
+
// The card counts as "in view" while its outline is visible, even if
|
|
178
|
+
// its text is not readable on this frame.
|
|
179
|
+
if (card != null) lastSeenAt = now;
|
|
172
180
|
BoxListener bl = boxListener;
|
|
173
181
|
if (bl != null) bl.onDetections(cardBox, candidates, frameW, frameH);
|
|
174
182
|
|
|
@@ -287,8 +295,9 @@ public class CardDetector implements ImageAnalysis.Analyzer {
|
|
|
287
295
|
candidateKey = key;
|
|
288
296
|
candidateCount = 1;
|
|
289
297
|
}
|
|
290
|
-
if (candidateCount < options.stableFrames ||
|
|
291
|
-
|
|
298
|
+
if (candidateCount < options.stableFrames || isLastEmitted(name, number)) return;
|
|
299
|
+
lastEmittedName = name;
|
|
300
|
+
lastEmittedNumber = number;
|
|
292
301
|
|
|
293
302
|
JSObject result = new JSObject();
|
|
294
303
|
result.put("name", name);
|
|
@@ -309,7 +318,33 @@ public class CardDetector implements ImageAnalysis.Analyzer {
|
|
|
309
318
|
private void noCardRead(long now) {
|
|
310
319
|
candidateKey = null;
|
|
311
320
|
candidateCount = 0;
|
|
312
|
-
if (now - lastSeenAt >
|
|
321
|
+
if (now - lastSeenAt > options.repeatAfterMs) {
|
|
322
|
+
lastEmittedName = null;
|
|
323
|
+
lastEmittedNumber = null;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private boolean isLastEmitted(String name, String number) {
|
|
328
|
+
if (number != null && number.equals(lastEmittedNumber)) return true;
|
|
329
|
+
return name != null && lastEmittedName != null && similarity(name, lastEmittedName) >= SAME_NAME_SIMILARITY;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** 0-1 similarity (LCS based), so an OCR slip like "Evolutlon" still matches "Evolution". */
|
|
333
|
+
private static float similarity(String a, String b) {
|
|
334
|
+
a = a.toLowerCase(Locale.ROOT);
|
|
335
|
+
b = b.toLowerCase(Locale.ROOT);
|
|
336
|
+
if (a.equals(b)) return 1f;
|
|
337
|
+
int[] prev = new int[b.length() + 1];
|
|
338
|
+
int[] cur = new int[b.length() + 1];
|
|
339
|
+
for (int i = 1; i <= a.length(); i++) {
|
|
340
|
+
for (int j = 1; j <= b.length(); j++) {
|
|
341
|
+
cur[j] = a.charAt(i - 1) == b.charAt(j - 1) ? prev[j - 1] + 1 : Math.max(prev[j], cur[j - 1]);
|
|
342
|
+
}
|
|
343
|
+
int[] tmp = prev;
|
|
344
|
+
prev = cur;
|
|
345
|
+
cur = tmp;
|
|
346
|
+
}
|
|
347
|
+
return 2f * prev[b.length()] / (a.length() + b.length());
|
|
313
348
|
}
|
|
314
349
|
|
|
315
350
|
private static JSObject box(float x, float y, float width, float height) {
|
|
@@ -102,6 +102,7 @@ public class CardScannerPlugin extends Plugin {
|
|
|
102
102
|
options.requireCard = call.getBoolean("requireCard", options.requireCard);
|
|
103
103
|
options.debug = call.getBoolean("debug", options.debug);
|
|
104
104
|
options.showRejected = call.getBoolean("showRejected", options.showRejected);
|
|
105
|
+
options.repeatAfterMs = call.getInt("repeatAfterMs", (int) options.repeatAfterMs);
|
|
105
106
|
|
|
106
107
|
CardDetector detector = new CardDetector(options, result -> notifyListeners("cardDetected", result));
|
|
107
108
|
getActivity().runOnUiThread(() -> {
|
package/dist/docs.json
CHANGED
|
@@ -420,6 +420,18 @@
|
|
|
420
420
|
"docs": "Outline (in grey) the objects that were not accepted as a card.\nThe accepted card is always outlined in red.",
|
|
421
421
|
"complexTypes": [],
|
|
422
422
|
"type": "boolean | undefined"
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
"name": "repeatAfterMs",
|
|
426
|
+
"tags": [
|
|
427
|
+
{
|
|
428
|
+
"text": "1500",
|
|
429
|
+
"name": "default"
|
|
430
|
+
}
|
|
431
|
+
],
|
|
432
|
+
"docs": "How long (ms) the card must be out of view before the same card can be\nreported again. While a card stays in view it is reported only once.",
|
|
433
|
+
"complexTypes": [],
|
|
434
|
+
"type": "number | undefined"
|
|
423
435
|
}
|
|
424
436
|
]
|
|
425
437
|
},
|
|
@@ -90,6 +90,12 @@ export interface DetectionOptions {
|
|
|
90
90
|
* @default true
|
|
91
91
|
*/
|
|
92
92
|
showRejected?: boolean;
|
|
93
|
+
/**
|
|
94
|
+
* How long (ms) the card must be out of view before the same card can be
|
|
95
|
+
* reported again. While a card stays in view it is reported only once.
|
|
96
|
+
* @default 1500
|
|
97
|
+
*/
|
|
98
|
+
repeatAfterMs?: number;
|
|
93
99
|
}
|
|
94
100
|
/** Normalized rectangle (0-1). */
|
|
95
101
|
export interface Box {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["import type { PermissionState, PluginListenerHandle } from '@capacitor/core';\n\nexport type CameraPosition = 'rear' | 'front';\n\nexport interface StartOptions {\n /**\n * Which camera to use.\n * @default 'rear'\n */\n position?: CameraPosition;\n}\n\nexport interface CaptureOptions {\n /**\n * JPEG quality (0-100).\n * @default 85\n */\n quality?: number;\n}\n\nexport interface CaptureResult {\n /** Base64 encoded JPEG (without the `data:` prefix). */\n base64: string;\n width: number;\n height: number;\n}\n\nexport interface TorchOptions {\n enabled: boolean;\n}\n\nexport interface ZoomOptions {\n /** Zoom ratio, clamped between the camera's min and max. */\n ratio: number;\n}\n\nexport interface PermissionStatus {\n camera: PermissionState;\n}\n\nexport interface DetectionOptions {\n /**\n * Minimum time between two OCR runs, in milliseconds.\n * The red rectangle is updated on every frame regardless.\n * @default 300\n */\n intervalMs?: number;\n /**\n * How many consecutive OCR runs must read the same card before\n * `cardDetected` fires.\n * @default 2\n */\n stableFrames?: number;\n /**\n * Include a JPEG of the detected card in the event.\n * @default false\n */\n includeImage?: boolean;\n /**\n * Expected width / height of an upright card. Only upright cards are\n * detected; a card lying sideways falls outside the tolerance.\n * @default 0.716 (63 x 88 mm)\n */\n cardAspectRatio?: number;\n /**\n * Allowed relative deviation from `cardAspectRatio` (0.15 = ±15%).\n * Increase it if tilted cards are not detected.\n * @default 0.15\n */\n aspectTolerance?: number;\n /**\n * Smallest card size, as a fraction of the camera frame (0-1).\n * @default 0.08\n */\n minCardArea?: number;\n /**\n * Largest card size, as a fraction of the camera frame (0-1).\n * @default 0.9\n */\n maxCardArea?: number;\n /**\n * Only read text when a card-shaped object is visible. When false, the whole\n * frame is read if no card is found.\n * @default true\n */\n requireCard?: boolean;\n /**\n * Every object found is always outlined: red when accepted as a card, grey\n * when rejected. With `debug`, each outline is also labelled with its area\n * (`a`), width/height ratio (`r`) and the filter that rejected it. Use it to\n * tune the filters above.\n * @default false\n */\n debug?: boolean;\n /**\n * Outline (in grey) the objects that were not accepted as a card.\n * The accepted card is always outlined in red.\n * @default true\n */\n showRejected?: boolean;\n}\n\n/** Normalized rectangle (0-1). */\nexport interface Box {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\nexport interface TextLine {\n text: string;\n /** Position inside the card image. */\n box: Box;\n}\n\nexport interface CardDetectedEvent {\n /** Best guess for the card name (topmost line of text on the card). */\n name?: string;\n /** Collector number, e.g. `025/165`. */\n collectorNumber?: string;\n /** All text read on the card. */\n text: string;\n lines: TextLine[];\n /** Where the card is in the camera frame. Missing if no card outline was found. */\n cardBox?: Box;\n /** Base64 JPEG of the card, when `includeImage` is true. */\n image?: string;\n}\n\nexport interface CardScannerPlugin {\n /**\n * Opens the camera **behind** the WebView. The WebView background is made\n * transparent, so the HTML page must also use transparent backgrounds\n * where the camera should be visible.\n */\n start(options?: StartOptions): Promise<void>;\n\n /** Stops the camera and removes the preview. */\n stop(): Promise<void>;\n\n /** Takes a photo from the running camera. */\n capture(options?: CaptureOptions): Promise<CaptureResult>;\n\n /** Turns the flashlight on/off. */\n setTorch(options: TorchOptions): Promise<void>;\n\n /** Sets the zoom ratio. */\n setZoom(options: ZoomOptions): Promise<void>;\n\n /** Switches between the rear and front camera. */\n flip(): Promise<void>;\n\n /**\n * Starts looking for cards: draws a red rectangle around the card and reads\n * its text, emitting `cardDetected`. Requires `start()` first. Android only.\n */\n startDetection(options?: DetectionOptions): Promise<void>;\n\n /** Stops card detection and hides the rectangle. */\n stopDetection(): Promise<void>;\n\n /** Fired once per card, when its text has been read consistently. */\n addListener(eventName: 'cardDetected', listener: (event: CardDetectedEvent) => void): Promise<PluginListenerHandle>;\n\n removeAllListeners(): Promise<void>;\n\n checkPermissions(): Promise<PermissionStatus>;\n\n requestPermissions(): Promise<PermissionStatus>;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["import type { PermissionState, PluginListenerHandle } from '@capacitor/core';\n\nexport type CameraPosition = 'rear' | 'front';\n\nexport interface StartOptions {\n /**\n * Which camera to use.\n * @default 'rear'\n */\n position?: CameraPosition;\n}\n\nexport interface CaptureOptions {\n /**\n * JPEG quality (0-100).\n * @default 85\n */\n quality?: number;\n}\n\nexport interface CaptureResult {\n /** Base64 encoded JPEG (without the `data:` prefix). */\n base64: string;\n width: number;\n height: number;\n}\n\nexport interface TorchOptions {\n enabled: boolean;\n}\n\nexport interface ZoomOptions {\n /** Zoom ratio, clamped between the camera's min and max. */\n ratio: number;\n}\n\nexport interface PermissionStatus {\n camera: PermissionState;\n}\n\nexport interface DetectionOptions {\n /**\n * Minimum time between two OCR runs, in milliseconds.\n * The red rectangle is updated on every frame regardless.\n * @default 300\n */\n intervalMs?: number;\n /**\n * How many consecutive OCR runs must read the same card before\n * `cardDetected` fires.\n * @default 2\n */\n stableFrames?: number;\n /**\n * Include a JPEG of the detected card in the event.\n * @default false\n */\n includeImage?: boolean;\n /**\n * Expected width / height of an upright card. Only upright cards are\n * detected; a card lying sideways falls outside the tolerance.\n * @default 0.716 (63 x 88 mm)\n */\n cardAspectRatio?: number;\n /**\n * Allowed relative deviation from `cardAspectRatio` (0.15 = ±15%).\n * Increase it if tilted cards are not detected.\n * @default 0.15\n */\n aspectTolerance?: number;\n /**\n * Smallest card size, as a fraction of the camera frame (0-1).\n * @default 0.08\n */\n minCardArea?: number;\n /**\n * Largest card size, as a fraction of the camera frame (0-1).\n * @default 0.9\n */\n maxCardArea?: number;\n /**\n * Only read text when a card-shaped object is visible. When false, the whole\n * frame is read if no card is found.\n * @default true\n */\n requireCard?: boolean;\n /**\n * Every object found is always outlined: red when accepted as a card, grey\n * when rejected. With `debug`, each outline is also labelled with its area\n * (`a`), width/height ratio (`r`) and the filter that rejected it. Use it to\n * tune the filters above.\n * @default false\n */\n debug?: boolean;\n /**\n * Outline (in grey) the objects that were not accepted as a card.\n * The accepted card is always outlined in red.\n * @default true\n */\n showRejected?: boolean;\n /**\n * How long (ms) the card must be out of view before the same card can be\n * reported again. While a card stays in view it is reported only once.\n * @default 1500\n */\n repeatAfterMs?: number;\n}\n\n/** Normalized rectangle (0-1). */\nexport interface Box {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\nexport interface TextLine {\n text: string;\n /** Position inside the card image. */\n box: Box;\n}\n\nexport interface CardDetectedEvent {\n /** Best guess for the card name (topmost line of text on the card). */\n name?: string;\n /** Collector number, e.g. `025/165`. */\n collectorNumber?: string;\n /** All text read on the card. */\n text: string;\n lines: TextLine[];\n /** Where the card is in the camera frame. Missing if no card outline was found. */\n cardBox?: Box;\n /** Base64 JPEG of the card, when `includeImage` is true. */\n image?: string;\n}\n\nexport interface CardScannerPlugin {\n /**\n * Opens the camera **behind** the WebView. The WebView background is made\n * transparent, so the HTML page must also use transparent backgrounds\n * where the camera should be visible.\n */\n start(options?: StartOptions): Promise<void>;\n\n /** Stops the camera and removes the preview. */\n stop(): Promise<void>;\n\n /** Takes a photo from the running camera. */\n capture(options?: CaptureOptions): Promise<CaptureResult>;\n\n /** Turns the flashlight on/off. */\n setTorch(options: TorchOptions): Promise<void>;\n\n /** Sets the zoom ratio. */\n setZoom(options: ZoomOptions): Promise<void>;\n\n /** Switches between the rear and front camera. */\n flip(): Promise<void>;\n\n /**\n * Starts looking for cards: draws a red rectangle around the card and reads\n * its text, emitting `cardDetected`. Requires `start()` first. Android only.\n */\n startDetection(options?: DetectionOptions): Promise<void>;\n\n /** Stops card detection and hides the rectangle. */\n stopDetection(): Promise<void>;\n\n /** Fired once per card, when its text has been read consistently. */\n addListener(eventName: 'cardDetected', listener: (event: CardDetectedEvent) => void): Promise<PluginListenerHandle>;\n\n removeAllListeners(): Promise<void>;\n\n checkPermissions(): Promise<PermissionStatus>;\n\n requestPermissions(): Promise<PermissionStatus>;\n}\n"]}
|