From 44526e7d3b1eec3943660053e69671d292e8d091 Mon Sep 17 00:00:00 2001 From: TanerUslu Date: Wed, 29 Jul 2026 12:59:08 +0200 Subject: [PATCH] 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) --- src/camera.js | 57 ++++++++++++++++++++++++++++++++++++++++++ test/camera.test.js | 60 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+) create mode 100644 test/camera.test.js diff --git a/src/camera.js b/src/camera.js index 3fddf9f..04807bc 100644 --- a/src/camera.js +++ b/src/camera.js @@ -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) { const scale = Math.min(1, maxEdge / Math.max(sourceWidth, sourceHeight)); 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). */ export async function imageDataFromFile(file, maxEdge = 1280) { const bitmap = await createImageBitmap(file); diff --git a/test/camera.test.js b/test/camera.test.js new file mode 100644 index 0000000..f1c730a --- /dev/null +++ b/test/camera.test.js @@ -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)); +});