Ausschnitt in nativer Aufloesung fuer Barcode-Erkennung
FRAME_INSET und frameRect() legen die Rahmenmasse einmalig fest, statt sie nur im Stilblatt zu duplizieren. grabFrameRegion() liefert den Zielrahmen unskaliert aus dem Kamerabild - grabFrame() (fuer OCR) bleibt unveraendert. Ein Code-128 auf kleinem Etikett braucht die volle Aufloesung, sonst verschmieren die Striche unlesbar. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -32,6 +32,39 @@ export async function startCamera(videoElement) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rahmenmasse als Anteil der Bildkanten - einzige Quelle fuer die Lage des
|
||||||
|
* Zielrahmens. scan-view.js uebernimmt daraus den sichtbaren Rahmen,
|
||||||
|
* grabFrameRegion() weiter unten den nativen Ausschnitt - beide koennen so
|
||||||
|
* nicht mehr auseinanderlaufen (frueher stand das, unabhaengig davon, nur
|
||||||
|
* im Stilblatt).
|
||||||
|
*/
|
||||||
|
export const FRAME_INSET = { top: 0.2, right: 0.08, bottom: 0.2, left: 0.08 };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rechnet die Rahmenmasse (Anteile der Kantenlaenge) in Bildpunkte um.
|
||||||
|
* Reine Funktion ohne Bezug zu einem konkreten Bild oder einer Browser-API -
|
||||||
|
* damit auch ohne DOM testbar.
|
||||||
|
*
|
||||||
|
* Breite und Hoehe werden als Differenz aus der Gesamtkante und den beiden
|
||||||
|
* gerundeten Raendern gebildet, nicht unabhaengig aus dem Anteil selbst
|
||||||
|
* gerundet - so bleibt der Ausschnitt bei krummen Bildgroessen garantiert
|
||||||
|
* innerhalb des Bildes, auch wenn die Anteile keine ganzzahligen Ergebnisse
|
||||||
|
* liefern.
|
||||||
|
*/
|
||||||
|
export function frameRect(width, height, inset = FRAME_INSET) {
|
||||||
|
const left = Math.round(width * inset.left);
|
||||||
|
const right = Math.round(width * inset.right);
|
||||||
|
const top = Math.round(height * inset.top);
|
||||||
|
const bottom = Math.round(height * inset.bottom);
|
||||||
|
return {
|
||||||
|
x: left,
|
||||||
|
y: top,
|
||||||
|
width: Math.max(0, width - left - right),
|
||||||
|
height: Math.max(0, height - top - bottom),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function drawScaled(source, sourceWidth, sourceHeight, maxEdge) {
|
function drawScaled(source, sourceWidth, sourceHeight, maxEdge) {
|
||||||
const scale = Math.min(1, maxEdge / Math.max(sourceWidth, sourceHeight));
|
const scale = Math.min(1, maxEdge / Math.max(sourceWidth, sourceHeight));
|
||||||
const width = Math.round(sourceWidth * scale);
|
const width = Math.round(sourceWidth * scale);
|
||||||
@@ -64,6 +97,30 @@ export function grabFrame(videoElement, maxEdge = 1280) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schneidet den Zielrahmen (siehe FRAME_INSET) aus dem laufenden Kamerabild -
|
||||||
|
* in unveraenderter Aufloesung, ohne jede Verkleinerung. Anders als
|
||||||
|
* grabFrame(): fuer die Texterkennung genuegt ein herunterskaliertes
|
||||||
|
* Vollbild, aber ein Code-128 auf einem kleinen Etikett braucht seine
|
||||||
|
* feinen Striche in voller Schaerfe - beim Skalieren auf die fuer OCR
|
||||||
|
* ausreichende Kantenlaenge verschmieren sie unlesbar.
|
||||||
|
*/
|
||||||
|
export function grabFrameRegion(videoElement, inset = FRAME_INSET) {
|
||||||
|
assertHasSize(videoElement.videoWidth, videoElement.videoHeight);
|
||||||
|
const rect = frameRect(videoElement.videoWidth, videoElement.videoHeight, inset);
|
||||||
|
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = rect.width;
|
||||||
|
canvas.height = rect.height;
|
||||||
|
const context = canvas.getContext('2d', { willReadFrequently: true });
|
||||||
|
context.drawImage(
|
||||||
|
videoElement,
|
||||||
|
rect.x, rect.y, rect.width, rect.height,
|
||||||
|
0, 0, rect.width, rect.height,
|
||||||
|
);
|
||||||
|
return context.getImageData(0, 0, rect.width, rect.height);
|
||||||
|
}
|
||||||
|
|
||||||
/** Ersatzweg ohne Kamera: Bilddatei auswaehlen (z. B. am Rechner). */
|
/** Ersatzweg ohne Kamera: Bilddatei auswaehlen (z. B. am Rechner). */
|
||||||
export async function imageDataFromFile(file, maxEdge = 1280) {
|
export async function imageDataFromFile(file, maxEdge = 1280) {
|
||||||
const bitmap = await createImageBitmap(file);
|
const bitmap = await createImageBitmap(file);
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { test } from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { frameRect, FRAME_INSET } from '../src/camera.js';
|
||||||
|
|
||||||
|
// frameRect() ist eine reine Funktion (kein window/document noetig) - sie
|
||||||
|
// laesst sich direkt unter Node pruefen, obwohl camera.js an anderer Stelle
|
||||||
|
// Browser-APIs verwendet (die dort nur beim Aufruf, nicht beim Import
|
||||||
|
// ausgewertet werden).
|
||||||
|
|
||||||
|
test('glatte Standardaufloesung: Rahmenmasse exakt aus den Anteilen', () => {
|
||||||
|
const rect = frameRect(1920, 1080, FRAME_INSET);
|
||||||
|
assert.deepEqual(rect, { x: 154, y: 216, width: 1612, height: 648 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('krumme Bildgroesse: Ausschnitt bleibt vollstaendig innerhalb des Bildes', () => {
|
||||||
|
const rect = frameRect(1921, 1079, FRAME_INSET);
|
||||||
|
assert.deepEqual(rect, { x: 154, y: 216, width: 1613, height: 647 });
|
||||||
|
assert.ok(rect.x >= 0 && rect.y >= 0);
|
||||||
|
assert.ok(rect.x + rect.width <= 1921);
|
||||||
|
assert.ok(rect.y + rect.height <= 1079);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('nicht-ganzzahliges Zwischenergebnis wird gerundet, nicht abgeschnitten', () => {
|
||||||
|
// 999 * 0.08 = 79.92 -> rundet auf 80, nicht auf 79.
|
||||||
|
const rect = frameRect(999, 333, FRAME_INSET);
|
||||||
|
assert.equal(rect.x, 80);
|
||||||
|
assert.equal(rect.width, 999 - 80 - 80);
|
||||||
|
// 333 * 0.2 = 66.6 -> rundet auf 67.
|
||||||
|
assert.equal(rect.y, 67);
|
||||||
|
assert.equal(rect.height, 333 - 67 - 67);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Breite/Hoehe werden als Differenz gebildet, nicht unabhaengig gerundet', () => {
|
||||||
|
// Wuerde man Breite unabhaengig aus dem Anteil runden, koennte sie um ein
|
||||||
|
// Pixel von "Gesamtbreite minus beide Raender" abweichen. Das darf nicht
|
||||||
|
// passieren - hier ueber viele krumme Groessen hinweg geprueft.
|
||||||
|
for (let width = 1; width <= 200; width++) {
|
||||||
|
for (const inset of [FRAME_INSET, { top: 0.2, right: 0.08, bottom: 0.2, left: 0.08 }]) {
|
||||||
|
const rect = frameRect(width, width, inset);
|
||||||
|
const left = Math.round(width * inset.left);
|
||||||
|
const right = Math.round(width * inset.right);
|
||||||
|
assert.equal(rect.width, width - left - right);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('eigene Rahmenmasse werden statt der Voreinstellung verwendet', () => {
|
||||||
|
const rect = frameRect(1000, 1000, { top: 0.1, right: 0.1, bottom: 0.1, left: 0.1 });
|
||||||
|
assert.deepEqual(rect, { x: 100, y: 100, width: 800, height: 800 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ueberlappende Raender ergeben nie eine negative Breite/Hoehe', () => {
|
||||||
|
const rect = frameRect(100, 100, { top: 0.6, bottom: 0.6, left: 0.6, right: 0.6 });
|
||||||
|
assert.equal(rect.width, 0);
|
||||||
|
assert.equal(rect.height, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Standard-Rahmenmasse ohne Argument identisch zu FRAME_INSET', () => {
|
||||||
|
assert.deepEqual(frameRect(1280, 720), frameRect(1280, 720, FRAME_INSET));
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user