Compare commits

..
2 Commits
Author SHA1 Message Date
TanerUsluandClaude Opus 5 6688efc546 README: Zählen-Abschnitte auf Aufnahme statt Dauerbetrieb aktualisiert
Beschreibt die neue Aufnahme-Schaltfläche ("Zählen"/"Nochmal zählen"), die
fünf Bilder je Aufnahme samt Median, die um die Einzelmessungen erweiterte
Diagnosezeile und das Zurücksetzen beim Verlassen der Funktion. Aufbau-
Tabelle, Testanzahl und der Prüfabschnitt "Am Gerät noch zu prüfen" folgen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 09:37:10 +02:00
TanerUsluandClaude Opus 5 4b6bb065b1 Zählen: Aufnahme statt Dauerbetrieb
Die Zaehlanzeige sprang beim Nutzer im Dauerbetrieb wild zwischen etwa 3
und 60, obwohl dasselbe Verfahren an einem Standfoto stabil 18 lieferte -
eine Schaetzung wird durch staendiges Neuanzeigen nicht praeziser, nur
unruhig. "Zählen" laeuft jetzt wie "Text erkennen" auf Knopfdruck: eine
Aufnahme nimmt binnen rund einer Sekunde fuenf Bilder aus dem Zielrahmen
auf, zaehlt jedes einzeln (count-objects.js unveraendert) und zeigt deren
Median (neues Modul count-capture.js). Das Ergebnis bleibt stehen, bis
erneut angetippt wird; die Diagnosezeile zeigt zusaetzlich die fuenf
Einzelmessungen. Der gleitende Median ueber neun Durchlaeufe der
Dauerzaehlung (count-history.js) entfaellt, da die Aufnahme dieselbe
Robustheit bereits selbst leistet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 09:36:43 +02:00
11 changed files with 432 additions and 305 deletions
+102 -45
View File
@@ -9,8 +9,9 @@ QR-Code, Text erkennen oder Zählen (siehe „Funktionsweise" unten). Jede
Funktion nutzt ausschließlich ihre eigene Quelle; anders als früher weicht die Funktion nutzt ausschließlich ihre eigene Quelle; anders als früher weicht die
App nicht mehr automatisch auf Texterkennung aus, wenn kein Code gefunden App nicht mehr automatisch auf Texterkennung aus, wenn kein Code gefunden
wird. Die vierte Funktion, „Zählen", gehört nicht zum eigentlichen Sortieren: wird. Die vierte Funktion, „Zählen", gehört nicht zum eigentlichen Sortieren:
Sie bucht nichts, sondern zeigt beim Auslegen von Kleinteilen (z. B. Sie bucht nichts, sondern ermittelt auf Knopfdruck beim Auslegen von
Festplattenschrauben) fortlaufend deren Anzahl an. Kleinteilen (z. B. Festplattenschrauben) deren Anzahl und zeigt sie an, bis
der Nutzer erneut antippt.
Zwei Module gelten als zusammengehörig, wenn sie denselben Barcode-Inhalt Zwei Module gelten als zusammengehörig, wenn sie denselben Barcode-Inhalt
tragen — ein Barcode ist exakt gelesen, das ist der zuverlässigste Maßstab. tragen — ein Barcode ist exakt gelesen, das ist der zuverlässigste Maßstab.
@@ -65,12 +66,12 @@ keine Daten verlassen das Gerät.
ausschließlich in `src/scan-modes.js` — es gibt keine zweite Stelle, an der ausschließlich in `src/scan-modes.js` — es gibt keine zweite Stelle, an der
das steht: das steht:
| Funktion | Liest | Laufende Suche/Zählung | Texterkennung | | Funktion | Liest | Laufende Codesuche | Texterkennung |
|---|---|---|---| |---|---|---|---|
| **Strichcode** | Code128, Code39, Code93, ITF, EAN-13, EAN-8, UPC-A, UPC-E, Codabar | ja (Codesuche) | nein | | **Strichcode** | Code128, Code39, Code93, ITF, EAN-13, EAN-8, UPC-A, UPC-E, Codabar | ja | nein |
| **QR-Code** | QRCode, MicroQRCode, RMQRCode, DataMatrix, Aztec, PDF417 | ja (Codesuche) | nein | | **QR-Code** | QRCode, MicroQRCode, RMQRCode, DataMatrix, Aztec, PDF417 | ja | nein |
| **Text erkennen** | keine Codes | nein | ja, auf Knopfdruck | | **Text erkennen** | keine Codes | nein | ja, auf Knopfdruck |
| **Zählen** | keine Codes | ja (Zählung, keine Buchung) | nein | | **Zählen** | keine Codes | nein | nein, dafür Zählung auf Knopfdruck (keine Buchung) |
„QR-Code" heißt auf Wunsch des Auftraggebers „QR-Code", liest aber bewusst „QR-Code" heißt auf Wunsch des Auftraggebers „QR-Code", liest aber bewusst
nicht nur QR-Varianten, sondern auch DataMatrix, Aztec und PDF417. Der nicht nur QR-Varianten, sondern auch DataMatrix, Aztec und PDF417. Der
@@ -80,18 +81,24 @@ keine Daten verlassen das Gerät.
`src/scan-modes.js`), und der Name ist Absicht — es ist der Wunsch des `src/scan-modes.js`), und der Name ist Absicht — es ist der Wunsch des
Auftraggebers für seinen Sprachgebrauch. Auftraggebers für seinen Sprachgebrauch.
„Modul scannen" erscheint nur in „Text erkennen" — in „Strichcode" und Eine einzige Aufnahme-Schaltfläche bedient beide Knopfdruck-Funktionen —
QR-Code" sucht die App ohnehin laufend, ein Knopf ohne Texterkennung hätte Modul scannen" in „Text erkennen", „Zählen" bzw. (nach der ersten
dort nichts zu tun. Der Zielrahmen hebt sich in „Strichcode" und „QR-Code" Aufnahme seit Betreten der Funktion) „Nochmal zählen" in „Zählen" — statt
hervor, sobald ein passender Code im Bild ist; in „Text erkennen" bleibt er einer zweiten Schaltfläche daneben; nur Wortlaut und Rückruf unterscheiden
sich
(`setCaptureLabel` in `src/ui/scan-view.js`, verdrahtet in
`src/main.js`/`applyMode()`). In „Strichcode" und „QR-Code" bleibt sie
verborgen — dort sucht die App ohnehin laufend, ein Knopf hätte nichts zu
tun. Der Zielrahmen hebt sich in „Strichcode" und „QR-Code" hervor, sobald
ein passender Code im Bild ist; in „Text erkennen" und „Zählen" bleibt er
schlicht, weil dort nichts laufend erkannt wird. Trotzdem ist der Rahmen schlicht, weil dort nichts laufend erkannt wird. Trotzdem ist der Rahmen
auch dort wirksam: „Modul scannen" liest genau den Bildausschnitt innerhalb auch dort wirksam: Sowohl „Modul scannen" als auch „Zählen" lesen genau den
des Rahmens, in voller Kameraauflösung, nicht mehr das gesamte, Bildausschnitt innerhalb des Rahmens, in voller Kameraauflösung, nicht das
heruntergerechnete Kamerabild (`grabFrameRegion` in `src/camera.js` gesamte, heruntergerechnete Kamerabild (`grabFrameRegion` in
derselbe Ausschnitt, den auch die laufende Barcode-Suche benutzt). Ein `src/camera.js` derselbe Ausschnitt, den auch die laufende Barcode-Suche
Wechsel der Funktion während des Sortierens (z. B. bei gemischter Ware) benutzt). Ein Wechsel der Funktion während des Sortierens (z. B. bei
gilt sofort: laufende Suche startet oder stoppt, der Knopf erscheint oder gemischter Ware) gilt sofort: laufende Suche startet oder stoppt, die
verschwindet. Schaltfläche erscheint, verschwindet oder ändert ihren Wortlaut.
Liegt beim Start eine gesicherte Sitzung vor, erscheint zuerst die Frage Liegt beim Start eine gesicherte Sitzung vor, erscheint zuerst die Frage
nach dem Fortsetzen (siehe Punkt 6 unten) und erst danach die Funktionswahl nach dem Fortsetzen (siehe Punkt 6 unten) und erst danach die Funktionswahl
@@ -172,15 +179,36 @@ keine Daten verlassen das Gerät.
`src/ui/result-overlay.js`). Der Rohtext jedes Eintrags lässt sich später `src/ui/result-overlay.js`). Der Rohtext jedes Eintrags lässt sich später
auch in der Sitzungsliste nachlesen. auch in der Sitzungsliste nachlesen.
4. **Zählen** (ausschließlich in Funktion „Zählen"). Reine Anzeige, ohne 4. **Zählen** (ausschließlich in Funktion „Zählen"). Reine Anzeige, ohne
jeden Bezug zur Sitzung: Solange die Kamera läuft, wird der Zielrahmen jeden Bezug zur Sitzung — und, anders als die drei übrigen Funktionen mit
mehrmals pro Sekunde ausgewertet und die Anzahl der darin gefundenen laufender Erkennung, auf **Aufnahme statt Dauerbetrieb**: Eine Zählung ist
Fundstücke groß im Bild eingeblendet (`countObjects` in eine Schätzung, kein eindeutiger Treffer wie ein Barcode; sie mehrmals pro
`src/count-objects.js`, aufgerufen über denselben Zeitgeber/dieselbe Sekunde neu anzuzeigen macht sie nicht genauer, nur unruhig (am
Überlappungssperre wie die Barcode-Dauersuche — siehe Nutzergerät sprang die Anzeige im Dauerbetrieb zwischen etwa 3 und 60, an
`continuousSearchLoop` in `src/main.js`). Es gibt in dieser Funktion einem Standfoto derselben Teile lieferte dasselbe Verfahren stabil 18).
keinen „Modul scannen"-Knopf, keine Treffer-Rückmeldung und keine Antippen der Schaltfläche „Zählen“/„Nochmal zählen“ nimmt deshalb binnen
Stapel-Zuweisung; die Zahl wird nirgends gespeichert, nicht einmal rund einer Sekunde fünf Bilder aus dem Zielrahmen des laufenden
flüchtig für die Dauer der Sitzung. Videobilds auf (`grabFrameRegion` in `src/camera.js` — bewusst nicht die
native Fotoaufnahme des Geräts, deren Bildausschnitt von der Vorschau
abweichen kann), zählt jedes einzeln (`countObjects` in
`src/count-objects.js`, unverändert) und zeigt als Ergebnis deren Median
(`combineCaptureCounts` in `src/count-capture.js`) — robust gegen ein
einzelnes verwackeltes oder mitten in eine Fokusregelung fallendes Bild,
ohne die frühere fortlaufende Glättung über die letzten neun Messungen
(`count-history.js`, entfallen: eine Aufnahme mit fünf Bildern leistet
dieselbe Robustheit bereits selbst). Während der rund einen Sekunde zeigt
die Statuszeile „zähle …“, damit der Nutzer die Kamera ruhig hält und den
Knopfdruck nicht für wirkungslos hält (`runCountCapture` in
`src/main.js`). Das Ergebnis bleibt danach stehen, bis der Nutzer erneut
antippt — verlässt er die Funktion oder wechselt zu einer anderen, werden
Ergebnis und Diagnosezeile zurückgesetzt. Es gibt in dieser Funktion keine
Treffer-Rückmeldung und keine Stapel-Zuweisung; die Zahl wird nirgends
gespeichert, nicht einmal flüchtig für die Dauer der Sitzung. Die
Diagnosezeile unter der großen Zahl (Antippen der Zahl blendet sie ein
oder aus) zeigt zusätzlich zu den übrigen Zwischenwerten (die sich auf die
*letzte* der fünf Einzelmessungen beziehen) die fünf Einzelmessungen
dieser Aufnahme selbst — daran erkennen Nutzer und Entwickler sofort, ob
das Ergebnis belastbar ist: Fünf Messungen von 17 bis 19 bedeuten etwas
anderes als fünf Messungen von 4 bis 50.
Kein Bilderkennungsmodell: Der **Otsu-Schwellwert** (`threshold.js`), Kein Bilderkennungsmodell: Der **Otsu-Schwellwert** (`threshold.js`),
den die Texterkennung weiterhin nutzt, taugt für „Zählen" nicht — er den die Texterkennung weiterhin nutzt, taugt für „Zählen" nicht — er
@@ -266,7 +294,7 @@ korrekt funktioniert.
npm test npm test
``` ```
165 Tests, alle grün (Stand dieses Dokuments). Getestet werden ausschließlich 179 Tests, alle grün (Stand dieses Dokuments). Getestet werden ausschließlich
die reinen Module ohne Browser-Zugriff: Teilenummer-Decoder, Spec-Normalisierung die reinen Module ohne Browser-Zugriff: Teilenummer-Decoder, Spec-Normalisierung
und toleranter Vergleich, OCR-Bildaufbereitung (Otsu-Schwellwert, jetzt in und toleranter Vergleich, OCR-Bildaufbereitung (Otsu-Schwellwert, jetzt in
`threshold.js`) und -Feldextraktion, Stapel-Zuweisung, die Erkennungs-Pipeline `threshold.js`) und -Feldextraktion, Stapel-Zuweisung, die Erkennungs-Pipeline
@@ -282,7 +310,11 @@ nie in „Text erkennen"/„Zählen"). Dazu die Zähl-Berechnung selbst
(`count-objects.js` — leeres Bild, einzelne und mehrere getrennte Flächen, (`count-objects.js` — leeres Bild, einzelne und mehrere getrennte Flächen,
verworfenes Rauschen, Hochrechnung berührender Teile ohne ein nur leicht verworfenes Rauschen, Hochrechnung berührender Teile ohne ein nur leicht
größeres Einzelteil zu verdoppeln, helle wie dunkle Objekte, ein Bild ohne größeres Einzelteil zu verdoppeln, helle wie dunkle Objekte, ein Bild ohne
Bildpunkte). Kamera, Barcode-/OCR-Adapter selbst, die Auswahl-Oberfläche und Bildpunkte). Dazu die reine Zusammenfassung der fünf Einzelmessungen einer
Zaehl-Aufnahme zu deren Median (`count-capture.js` — auch für den Fall
weniger als fünf verwertbarer Bilder, sowie eine leere Messreihe, die
regulär wirft statt stillschweigend `NaN` zu liefern). Kamera,
Barcode-/OCR-Adapter selbst, die Auswahl-Oberfläche und
die Verdrahtung in `main.js` laufen nur im echten Browser und werden dort die Verdrahtung in `main.js` laufen nur im echten Browser und werden dort
manuell geprüft (siehe „Am Gerät noch zu prüfen" unten) — `node --test` kennt manuell geprüft (siehe „Am Gerät noch zu prüfen" unten) — `node --test` kennt
kein DOM. kein DOM.
@@ -350,15 +382,16 @@ worauf du bei der Veröffentlichung achten solltest.
| `src/ocr-extract.js` | OCR-Rohtext → Spec-Felder | | `src/ocr-extract.js` | OCR-Rohtext → Spec-Felder |
| `src/session.js` | Stapel halten, vorschlagen, buchen, umsortieren, zurücknehmen — inklusive der aus den Einträgen abgeleiteten, je Stapel bekannten Barcode-Inhalte (`stack.codes`), die vor dem Vergleich der technischen Angaben über die Zuordnung entscheiden, sofern diese Angaben nicht widersprechen | | `src/session.js` | Stapel halten, vorschlagen, buchen, umsortieren, zurücknehmen — inklusive der aus den Einträgen abgeleiteten, je Stapel bekannten Barcode-Inhalte (`stack.codes`), die vor dem Vergleich der technischen Angaben über die Zuordnung entscheiden, sofern diese Angaben nicht widersprechen |
| `src/pipeline.js` | Barcode → Teilenummer → OCR → Ampelfarbe, inkl. Zeitgrenzen, Mehrdeutigkeitsbehandlung und Kurzschluss über einen bereits bekannten Barcode-Inhalt (`deps.isKnownCode`) — kennt keine Scan-Funktionen; liefert zusätzlich (additiv) den ungefilterten OCR-Rohtext (`rawText`), leer, wenn keine Texterkennung lief | | `src/pipeline.js` | Barcode → Teilenummer → OCR → Ampelfarbe, inkl. Zeitgrenzen, Mehrdeutigkeitsbehandlung und Kurzschluss über einen bereits bekannten Barcode-Inhalt (`deps.isKnownCode`) — kennt keine Scan-Funktionen; liefert zusätzlich (additiv) den ungefilterten OCR-Rohtext (`rawText`), leer, wenn keine Texterkennung lief |
| `src/scan-modes.js` | Die vier Scan-Funktionen (Strichcode, QR-Code, Text erkennen, Zählen): Kennung, deutsche Beschriftung, gelesene Codearten, laufende Suche ja/nein, Texterkennung ja/nein, laufende Zählung ja/nein — einzige Quelle, sowohl für die Auswahl-Oberfläche als auch für `main.js` | | `src/scan-modes.js` | Die vier Scan-Funktionen (Strichcode, QR-Code, Text erkennen, Zählen): Kennung, deutsche Beschriftung, gelesene Codearten, laufende Codesuche ja/nein, Texterkennung ja/nein — einzige Quelle, sowohl für die Auswahl-Oberfläche als auch für `main.js`. „Zählen" läuft wie „Text erkennen" auf Knopfdruck (`continuousSearch: false`) und braucht dafür kein eigenes Merkmal mehr |
| `src/scan-recognition.js` | Baut die an `recognize()` übergebenen Adapter anhand der gewählten Funktion: in „Strichcode"/„QR-Code" liefert die Texterkennung sofort leeren Text ohne Tesseract anzustoßen, in „Text erkennen" liefert die Barcode-Dekodierung sofort eine leere Liste ohne `zxing-wasm` anzustoßen, in „Zählen" liefern beide sofort leer (diese Funktion ruft `recognize()` im normalen Betrieb ohnehin nicht auf) | | `src/scan-recognition.js` | Baut die an `recognize()` übergebenen Adapter anhand der gewählten Funktion: liest eine Funktion keine Codearten (leeres `barcodeFormats`, bei „Text erkennen" und „Zählen"), liefert die Barcode-Dekodierung sofort eine leere Liste ohne `zxing-wasm` anzustoßen; nur bei `useOcr` läuft die echte Texterkennung, sonst liefert sie sofort leeren Text ohne Tesseract anzustoßen (in „Zählen" ruft `recognize()` im normalen Betrieb ohnehin nicht auf — siehe `count-capture.js`) |
| `src/count-capture.js` | Reine Berechnung für die Aufnahme in Funktion „Zählen": bildet aus den (bis zu fünf) Einzelmessungen einer Aufnahme (`countObjects()` je Bild) deren Median (`combineCaptureCounts`) — robust gegen ein einzelnes verwackeltes oder unscharfes Bild, bleibt auch bei weniger als fünf verwertbaren Bildern sinnvoll |
| `src/threshold.js` | Otsu-Schwellwertbestimmung (Graustufen, Histogramm, Schwellwert) — reines Modul ohne Browser-Zugriff, aus `ocr.js` herausgezogen; gemeinsame Grundlage für `preprocess()` (ocr.js) und `countObjects()` (count-objects.js) | | `src/threshold.js` | Otsu-Schwellwertbestimmung (Graustufen, Histogramm, Schwellwert) — reines Modul ohne Browser-Zugriff, aus `ocr.js` herausgezogen; gemeinsame Grundlage für `preprocess()` (ocr.js) und `countObjects()` (count-objects.js) |
| `src/count-objects.js` | Reine Berechnung für Funktion „Zählen": zählt zusammenhängende Objektflächen in einem Bildausschnitt über örtlichen Kontrast (Normierung auf 800 Bildpunkte Breite, Glättung und örtlicher Hintergrund je über Kastenfilter/Summenbild, Maske aus geglättetem Bild vs. örtlichem Hintergrund ± Marge, beide Polaritäten, Connected-Component-Labeling mit eigener Arbeitsliste, Rauschfilter, Massstab relativ zur größten Fläche, Hochrechnung berührender Teile über den Flächen-Median) — nutzt `threshold.js`/Otsu bewusst **nicht** mehr (siehe „Funktionsweise" oben) | | `src/count-objects.js` | Reine Berechnung für Funktion „Zählen": zählt zusammenhängende Objektflächen in einem Bildausschnitt über örtlichen Kontrast (Normierung auf 800 Bildpunkte Breite, Glättung und örtlicher Hintergrund je über Kastenfilter/Summenbild, Maske aus geglättetem Bild vs. örtlichem Hintergrund ± Marge, beide Polaritäten, Connected-Component-Labeling mit eigener Arbeitsliste, Rauschfilter, Massstab relativ zur größten Fläche, Hochrechnung berührender Teile über den Flächen-Median) — nutzt `threshold.js`/Otsu bewusst **nicht** mehr (siehe „Funktionsweise" oben) |
| `src/storage.js` | Absturzschutz der laufenden Sitzung: sichert und lädt aus `localStorage`, verwirft beim Laden jeden in sich unstimmigen Zustand vollständig (siehe unten); `entry.rawText` ist wie `entry.codes` optional und wird bei Fehlen als leerer Rohtext wiederhergestellt | | `src/storage.js` | Absturzschutz der laufenden Sitzung: sichert und lädt aus `localStorage`, verwirft beim Laden jeden in sich unstimmigen Zustand vollständig (siehe unten); `entry.rawText` ist wie `entry.codes` optional und wird bei Fehlen als leerer Rohtext wiederhergestellt |
| `src/camera.js` | Kamerastart, Einzelbildaufnahme (herunterskaliert, für die Dateiauswahl), Rahmenausschnitt in voller Auflösung (`grabFrameRegion`, für die laufende Barcode-Suche, „Modul scannen" in „Text erkennen" **und** die laufende Zählung in „Zählen"), Bilddatei-Ersatzweg | | `src/camera.js` | Kamerastart, Einzelbildaufnahme (herunterskaliert, für die Dateiauswahl), Rahmenausschnitt in voller Auflösung (`grabFrameRegion`, für die laufende Barcode-Suche, „Modul scannen" in „Text erkennen" **und** jedes der fünf Bilder einer Aufnahme in „Zählen"), Bilddatei-Ersatzweg |
| `src/barcode.js` | Adapter zu `zxing-wasm`; `decodeBarcodes(imageData, formats)` bekommt die Codearten übergeben, ohne Angabe wie bisher (Code128 + DataMatrix) | | `src/barcode.js` | Adapter zu `zxing-wasm`; `decodeBarcodes(imageData, formats)` bekommt die Codearten übergeben, ohne Angabe wie bisher (Code128 + DataMatrix) |
| `src/ocr.js` | Bildaufbereitung (Otsu, über `threshold.js`) und Adapter zu `tesseract.js`; Zeichen-Whitelist deckt Groß- und Kleinbuchstaben, Ziffern sowie übliche Etiketten-Sonderzeichen ab | | `src/ocr.js` | Bildaufbereitung (Otsu, über `threshold.js`) und Adapter zu `tesseract.js`; Zeichen-Whitelist deckt Groß- und Kleinbuchstaben, Ziffern sowie übliche Etiketten-Sonderzeichen ab |
| `src/ui/scan-view.js` | Scan-Ansicht: Kamera-Vorschau, antippbare Anzeige der gewählten Funktion (Wechsel), Scan-Knopf (nur in „Text erkennen" sichtbar), große Zählanzeige (nur in „Zählen" sichtbar), Stapel-Leiste, „Zuletzt"-Zeile, Ersatzweg-Schaltfläche bei fehlender Kamera | | `src/ui/scan-view.js` | Scan-Ansicht: Kamera-Vorschau, antippbare Anzeige der gewählten Funktion (Wechsel), eine gemeinsame Aufnahme-Schaltfläche für beide Knopfdruck-Funktionen (sichtbar in „Text erkennen" und „Zählen", Wortlaut je nach Funktion und Zustand über `setCaptureLabel`), große Zählanzeige samt Diagnosezeile (nur in „Zählen" sichtbar), Stapel-Leiste, „Zuletzt"-Zeile, Ersatzweg-Schaltfläche bei fehlender Kamera |
| `src/ui/mode-dialog.js` | Vollbild-Auswahl der Scan-Funktion vor dem Start und beim Wechsel: vier große Flächen, eine je Funktion aus `src/scan-modes.js` | | `src/ui/mode-dialog.js` | Vollbild-Auswahl der Scan-Funktion vor dem Start und beim Wechsel: vier große Flächen, eine je Funktion aus `src/scan-modes.js` |
| `src/ui/result-overlay.js` | Treffer-Rückmeldung (grün/gelb): blendet sich in „Strichcode"/„QR-Code" nach kurzer Zeit selbst aus; in „Text erkennen" bleibt sie stehen, zeigt den vollen OCR-Rohtext und wartet auf eine Eingabe, bevor sie schließt | | `src/ui/result-overlay.js` | Treffer-Rückmeldung (grün/gelb): blendet sich in „Strichcode"/„QR-Code" nach kurzer Zeit selbst aus; in „Text erkennen" bleibt sie stehen, zeigt den vollen OCR-Rohtext und wartet auf eine Eingabe, bevor sie schließt |
| `src/ui/ambiguous-dialog.js` | Rot-Dialog bei roter Konfidenz oder mehrdeutiger Stapelzuordnung | | `src/ui/ambiguous-dialog.js` | Rot-Dialog bei roter Konfidenz oder mehrdeutiger Stapelzuordnung |
@@ -371,9 +404,10 @@ worauf du bei der Veröffentlichung achten solltest.
| `.vch/deploy.yaml` | Bau- und Startbefehl sowie Gesundheitspfad für die Hosting-Umgebung | | `.vch/deploy.yaml` | Bau- und Startbefehl sowie Gesundheitspfad für die Hosting-Umgebung |
`spec`, `pn-decoder`, `ocr-extract`, `session`, `pipeline`, `storage`, `spec`, `pn-decoder`, `ocr-extract`, `session`, `pipeline`, `storage`,
`scan-modes`, `scan-recognition`, `threshold` und `count-objects` sind reine `scan-modes`, `scan-recognition`, `threshold`, `count-objects` und
Funktionen ohne Browser-Zugriff (kein `window`, `document` oder `count-capture` sind reine Funktionen ohne Browser-Zugriff (kein `window`,
`localStorage` direkt) und deshalb vollständig mit `node:test` prüfbar. `document` oder `localStorage` direkt) und deshalb vollständig mit
`node:test` prüfbar.
`camera.js`, `barcode.js`, `ocr.js` und `src/ui/` brauchen einen echten `camera.js`, `barcode.js`, `ocr.js` und `src/ui/` brauchen einen echten
Browser und werden nur manuell geprüft. Browser und werden nur manuell geprüft.
@@ -586,18 +620,41 @@ werden:
dass die Vorbereitung tatsächlich nur einmal läuft)? In Funktion „Text dass die Vorbereitung tatsächlich nur einmal läuft)? In Funktion „Text
erkennen" darf dieses Laden dagegen gar nicht erst anlaufen, da dort nie erkennen" darf dieses Laden dagegen gar nicht erst anlaufen, da dort nie
`decodeBarcodes` aufgerufen wird. `decodeBarcodes` aufgerufen wird.
**Zählen (neu)** **Zählen (Aufnahme statt Dauerbetrieb)**
- „Zählen" wählen, Kamera auf ausgelegte Kleinteile richten (z. B. silberne - „Zählen" wählen, Kamera auf ausgelegte Kleinteile richten (z. B. silberne
Festplattenschrauben auf dunklem Untergrund): Eine große Zahl erscheint im Festplattenschrauben auf dunklem Untergrund): Es erscheint **keine**
Bild und aktualisiert sich laufend, ohne dass etwas angetippt werden muss. Zahl und **keine** fortlaufende Aktualisierung, sondern eine gut
Kein „Modul scannen"-Knopf, keine Treffer-Rückmeldung, kein roter Dialog, erreichbare Schaltfläche mit der Aufschrift „Zählen". Kein roter Dialog,
keine Stapel-Zuweisung — der Zielrahmen bleibt sichtbar und zeigt weiterhin, keine Treffer-Rückmeldung, keine Stapel-Zuweisung — der Zielrahmen bleibt
was gezählt wird. sichtbar und zeigt, was beim nächsten Antippen gezählt wird.
- Die angezeigte Zahl aus normalem Bedienabstand (Armlänge) lesbar prüfen. - „Zählen" antippen: Für rund eine Sekunde zeigt die Statuszeile „zähle …“,
- Ein paar Teile wegnehmen bzw. hinzufügen, während die Kamera läuft: Die danach erscheint eine große Zahl im Bild und bleibt stehen — sie
Zahl folgt ohne spürbare Verzögerung. aktualisiert sich **nicht** von selbst weiter. Die Schaltfläche trägt jetzt
die Aufschrift „Nochmal zählen".
- Während der rund einen Sekunde Kamera und Teile ruhig halten (leichte
Bewegung zwischendurch simulieren) und danach prüfen, dass das Ergebnis
plausibel bleibt — die App nimmt in dieser Zeit fünf Bilder auf und zeigt
deren Median.
- Die Zahl antippen: Diagnosezeile blendet sich ein und zeigt (in dieser
Reihenfolge) `Roh: …`, `Messungen: …` (die fünf Einzelmessungen dieser
Aufnahme, z. B. `17, 18, 19, 18, 20`), `Bild: …`, `Flächen: …`,
`Größte Fläche: …`, `Typische Größe: …`, `Art: …`, `Dauer: …`. Nochmaliges
Antippen blendet sie wieder aus. Prüfen, dass die fünf Messungen tatsächlich
eng beieinanderliegen, wenn die Teile ruhig lagen, und spürbar streuen,
wenn während der Aufnahme bewegt wurde.
- „Nochmal zählen" antippen: neue Aufnahme, neues Ergebnis ersetzt das alte
vollständig (Zahl **und** Diagnosezeile).
- Zur Funktionswahl wechseln und zurück zu „Zählen": Zahl und Diagnosezeile
sind zurückgesetzt (nichts angezeigt), Schaltfläche zeigt wieder „Zählen"
statt „Nochmal zählen". Dasselbe beim Wechsel zu einer anderen Funktion und
zurück.
- Zugänge zur Sitzungsliste und zum Funktionswechsel bleiben auch in dieser - Zugänge zur Sitzungsliste und zum Funktionswechsel bleiben auch in dieser
Funktion erreichbar (Anzeige der Funktion antippen, Stapel-Leiste antippen). Funktion erreichbar (Anzeige der Funktion antippen, Stapel-Leiste antippen)
— außer während die rund einssekündige Aufnahme selbst läuft, genau wie bei
einem laufenden Scan in „Text erkennen".
- Aufnahme antippen, bevor das Kamerabild bereit ist (z. B. gleich nach dem
Start): verständliche Meldung „Zählen nicht möglich — Kamerabild noch
nicht bereit" statt Absturz oder stiller Nichtreaktion.
- Gegenprobe der Grenzen (siehe „Grenzen" oben): Teile eng aneinanderlegen - Gegenprobe der Grenzen (siehe „Grenzen" oben): Teile eng aneinanderlegen
(Hochrechnung), Teile stapeln (nur oberste Schicht zählt), einseitige (Hochrechnung), Teile stapeln (nur oberste Schicht zählt), einseitige
Beleuchtung, geringer Kontrast zum Untergrund — beobachten, wo die Zahl Beleuchtung, geringer Kontrast zum Untergrund — beobachten, wo die Zahl
+39
View File
@@ -0,0 +1,39 @@
// Bildet aus den Einzelmessungen einer Zaehl-Aufnahme (Funktion "Zählen",
// siehe main.js) das anzuzeigende Endergebnis. main.js nimmt je Antippen der
// Schaltflaeche "Zählen"/"Nochmal zählen" binnen rund einer Sekunde mehrere
// Bilder aus dem Zielrahmen auf und zaehlt jedes einzeln (countObjects() in
// count-objects.js, unveraendert) - ein einzelnes verwackeltes oder mitten in
// eine Fokusregelung fallendes Bild soll das Ergebnis nicht verzerren. Frueher
// glaettete ein gleitender Median ueber die letzten neun Durchlaeufe einer
// Dauerzaehlung (siehe .superpowers/sdd/counting-capture-report.md); das ist
// entfallen, weil eine Aufnahme mit mehreren Bildern dieselbe Robustheit
// bereits in sich selbst leistet - zwei Glaettungen uebereinander waeren nur
// zusaetzliche Traegheit ohne zusaetzlichen Nutzen.
//
// Der Median der Einzelmessungen ist robust gegen einen einzelnen groben
// Ausreisser (anders als deren Mittelwert) - siehe median() in
// count-objects.js fuer dieselbe Ueberlegung bei den dortigen
// Flaechengroessen.
//
// Reines, browserfreies Modul wie count-objects.js - kennt weder Kamera noch
// Bilddaten, nur die Zahlenreihe der Einzelmessungen selbst.
import { median } from './count-objects.js';
/**
* Bildet den Median der Einzelmessungen einer Aufnahme (siehe Moduldoku
* oben). Bleibt auch dann sinnvoll, wenn eine Aufnahme aus weniger als den
* vorgesehenen fuenf Bildern besteht (z. B. weil das Kamerabild waehrend
* eines Versuchs kurz nicht bereit war, siehe grabFrameRegion() in
* camera.js) - nur eine vollstaendig leere Aufnahme (kein einziges
* verwertbares Bild) ist kein regulaerer Ausgang und wirft; main.js prueft
* diesen Fall vorher selbst und ruft diese Funktion dann gar nicht erst auf.
* @param {number[]} counts Einzelmessungen dieser Aufnahme - nicht-leer.
* @returns {number}
* @throws {Error} wenn counts leer ist.
*/
export function combineCaptureCounts(counts) {
if (counts.length === 0) {
throw new Error('combineCaptureCounts: keine Messung vorhanden');
}
return median(counts);
}
-50
View File
@@ -1,50 +0,0 @@
// Verwaltet den gleitenden Verlauf der letzten Zaehlmessungen der Funktion
// "Zählen" (siehe count-objects.js, verdrahtet in main.js). Die App zeigt
// zusaetzlich zur rohen letzten Messung deren Median ueber die letzten
// HISTORY_WINDOW Durchlaeufe als grosse Zahl an: ein einzelner Ausreisser
// (Bewegungsunschaerfe, nachregelnder Autofokus, kurzzeitig verdeckte Teile)
// verschwindet dadurch, ohne dass die Anzeige traege wird - eine echte,
// anhaltende Aenderung (der Nutzer nimmt tatsaechlich Teile weg) schlaegt
// nach genuegend neuen Messungen weiterhin durch.
//
// Reines, browserfreies Modul wie count-objects.js - kennt weder Bilddaten
// noch die Oberflaeche, nur die Zahlenreihe selbst. Verwendet denselben
// robusten Median wie die Flaechengroessen dort (siehe median() in
// count-objects.js).
import { median } from './count-objects.js';
// Fenstergroesse fuer den gleitenden Median (siehe Moduldoku oben) - neun
// Messungen bei rund fuenf Durchlaeufen pro Sekunde (siehe
// SEARCH_INTERVAL_MS in main.js) sind knapp zwei Sekunden Verlauf: kurz
// genug, um eine tatsaechliche Aenderung zuegig zu uebernehmen, lang genug,
// um einen einzelnen Ausreisser zuverlaessig zu unterdruecken.
export const HISTORY_WINDOW = 9;
/**
* Erstellt einen leeren Verlauf. Wird verwendet, wenn die Funktion "Zählen"
* (neu) betreten wird oder der Nutzer die Scan-Funktion wechselt - der
* Verlauf einer anderen Situation darf nicht in die neue hinueberwirken
* (siehe main.js/applyMode()).
* @returns {number[]}
*/
export function createCountHistory() {
return [];
}
/**
* Haengt eine neue rohe Messung an den Verlauf an (behaelt hoechstens die
* letzten HISTORY_WINDOW Messungen, die aeltesten fallen zuerst heraus) und
* bildet daraus den Median. Reine Funktion - veraendert `history` selbst
* nicht, sondern liefert einen neuen Verlauf zurueck; der Aufrufer haelt
* seinen eigenen Zustand (siehe main.js).
* @param {number[]} history bisheriger Verlauf, aelteste Messung zuerst
* @param {number} rawCount rohe, gerade gemessene Anzahl
* @returns {{history: number[], median: number}}
*/
export function pushCount(history, rawCount) {
const updated = [...history, rawCount];
if (updated.length > HISTORY_WINDOW) {
updated.shift();
}
return { history: updated, median: median(updated) };
}
+179 -103
View File
@@ -4,7 +4,7 @@ import { startCamera, grabFrameRegion, imageDataFromFile } from './camera.js';
import { decodeBarcodes } from './barcode.js'; import { decodeBarcodes } from './barcode.js';
import { runOcr, isOcrAvailable } from './ocr.js'; import { runOcr, isOcrAvailable } from './ocr.js';
import { countObjects } from './count-objects.js'; import { countObjects } from './count-objects.js';
import { createCountHistory, pushCount } from './count-history.js'; import { combineCaptureCounts } from './count-capture.js';
import { recognize } from './pipeline.js'; import { recognize } from './pipeline.js';
import { normalizeToken } from './spec.js'; import { normalizeToken } from './spec.js';
import { getScanMode } from './scan-modes.js'; import { getScanMode } from './scan-modes.js';
@@ -51,23 +51,33 @@ let cameraUnavailableMessage = null;
// ohnehin unerreichbar. // ohnehin unerreichbar.
let currentModeId = null; let currentModeId = null;
// Gleitender Verlauf der letzten Zaehlmessungen (Funktion "Zählen", siehe // Ob die aktuelle Sitzung in Funktion "Zählen" bereits eine Aufnahme gemacht
// count-history.js) - Grundlage der beruhigten grossen Zahl (Median der // hat - traegt die Beschriftung der Aufnahme-Schaltflaeche ("Zählen" vor der
// letzten neun Messungen). Wird bei jedem Aufruf von applyMode() neu // ersten Aufnahme seit Betreten der Funktion, "Nochmal zählen" danach, siehe
// angelegt: sowohl ein Wechsel weg von "Zählen" als auch ein (erneuter) // updateCountCaptureLabel() unten). Wird bei jedem Aufruf von applyMode()
// Eintritt in die Funktion soll mit einem leeren Verlauf beginnen, damit die // zurueckgesetzt: sowohl ein Wechsel weg von "Zählen" als auch ein (erneuter)
// Anzeige nie Werte einer anderen Situation (anderes Teil, andere Stelle) // Eintritt in die Funktion beginnt wieder bei "Zählen" - siehe auch
// zeigt. // view.setCountVisible() (loescht das stehende Ergebnis samt Diagnose beim
let countHistory = createCountHistory(); // Verlassen).
let hasCountResult = false;
const view = renderScanView(app, { const view = renderScanView(app, {
// "Modul scannen" ist nur in Funktion 3 ("Text erkennen") sichtbar (siehe // Eine gemeinsame Schaltflaeche fuer beide Knopfdruck-Funktionen (siehe
// applyMode()/setCaptureVisible) - der Zielrahmen suggeriert dem Nutzer dort // scan-view.js/setCaptureLabel()): in Funktion "Text erkennen" loest sie
// einen Ausschnitt, den grabFrame() (Vollbild, herunterskaliert) bisher gar // processCapture() aus (Erkennung + Buchung), in Funktion "Zählen"
// nicht einloeste. grabFrameRegion() liest genau den Rahmenausschnitt, in // stattdessen runCountCapture() (Aufnahme mehrerer Bilder, keine Buchung).
// voller Aufloesung - derselbe Ausschnitt, den auch die Barcode-Dauersuche // Der Zielrahmen suggeriert dem Nutzer in beiden Faellen einen Ausschnitt,
// den grabFrame() (Vollbild, herunterskaliert) nicht einloeste -
// grabFrameRegion() liest genau den Rahmenausschnitt, in voller
// Aufloesung - derselbe Ausschnitt, den auch die Barcode-Dauersuche
// benutzt (siehe runSearchAttempt() weiter unten). // benutzt (siehe runSearchAttempt() weiter unten).
onCapture: () => processCapture(() => grabFrameRegion(view.video)), onCapture: () => {
if (currentModeId === 'count') {
runCountCapture();
} else {
processCapture(() => grabFrameRegion(view.video));
}
},
onUndo: () => { onUndo: () => {
// Waehrend eine Erkennung laeuft, darf kein Eintrag zurueckgenommen // Waehrend eine Erkennung laeuft, darf kein Eintrag zurueckgenommen
// werden - sonst koennte die Rueckgaengig-Flaeche einen Eintrag treffen, // werden - sonst koennte die Rueckgaengig-Flaeche einen Eintrag treffen,
@@ -93,32 +103,38 @@ const view = renderScanView(app, {
}); });
/** /**
* Uebernimmt eine (neu gewaehlte) Scan-Funktion: Anzeige, Sichtbarkeit von * Uebernimmt eine (neu gewaehlte) Scan-Funktion: Anzeige, Sichtbarkeit und
* "Modul scannen" und - bei einem Wechsel weg von laufender Codesuche - der * Beschriftung der Aufnahme-Schaltflaeche und - bei einem Wechsel weg von
* Zielrahmen wird sofort zurueckgesetzt, statt auf den naechsten Durchlauf * laufender Codesuche - der Zielrahmen wird sofort zurueckgesetzt, statt auf
* der Dauersuche zu warten. * den naechsten Durchlauf der Dauersuche zu warten.
* @param {string} modeId * @param {string} modeId
*/ */
function applyMode(modeId) { function applyMode(modeId) {
currentModeId = modeId; currentModeId = modeId;
// Verlauf der Dauerzaehlung zuruecksetzen (siehe Kommentar bei der
// Deklaration oben) - unabhaengig davon, ob die neue Funktion "Zählen"
// ist: ein Wechsel weg davon soll den Verlauf ebenso wenig ueberleben wie
// ein erneuter Eintritt mit alten Werten.
countHistory = createCountHistory();
const mode = getScanMode(modeId); const mode = getScanMode(modeId);
view.setMode(mode.label); view.setMode(mode.label);
// "Modul scannen" hat nur dort etwas zu tun, wo es weder laufende // Die Aufnahme-Schaltflaeche hat nur dort etwas zu tun, wo es keine
// Codesuche noch laufende Zaehlung gibt (Funktion 3) - siehe Kommentar in // laufende Codesuche gibt (Funktion 3 "Text erkennen" und Funktion 4
// scan-view.js/setCaptureVisible. // "Zählen") - siehe Kommentar in scan-view.js/setCaptureVisible.
view.setCaptureVisible(!mode.continuousSearch && !mode.continuousCount); view.setCaptureVisible(!mode.continuousSearch);
if (!mode.continuousSearch) { if (!mode.continuousSearch) {
view.setFrameDetected(false); view.setFrameDetected(false);
} }
// "Zählen" beginnt bei jedem (erneuten) Betreten wieder bei der
// Beschriftung "Zählen" statt "Nochmal zählen" - ein Wechsel weg von der
// Funktion soll das ebenso wenig ueberleben wie ein erneuter Eintritt mit
// altem Zustand (siehe Deklaration von hasCountResult oben).
hasCountResult = false;
if (mode.id === 'count') {
updateCountCaptureLabel();
} else {
view.setCaptureLabel(mode.useOcr ? 'Modul scannen' : '');
}
// Die grosse Zaehlanzeige gehoert ausschliesslich zu Funktion "Zählen" - // Die grosse Zaehlanzeige gehoert ausschliesslich zu Funktion "Zählen" -
// beim Verlassen sofort ausblenden, statt auf den naechsten (dann gar // beim Verlassen sofort ausblenden (loescht dabei auch ein stehendes
// nicht mehr laufenden) Durchlauf der Dauerzaehlung zu warten. // Ergebnis samt Diagnose, siehe scan-view.js/setCountVisible), statt auf
view.setCountVisible(mode.continuousCount); // die naechste Aufnahme zu warten.
view.setCountVisible(mode.id === 'count');
} }
/** /**
@@ -143,6 +159,25 @@ function describeEntry(spec, stackId) {
return `${describeSpec(spec)} → Stapel ${stackId}`; return `${describeSpec(spec)} → Stapel ${stackId}`;
} }
/**
* Stellt die Statuszeile nach einem erfolgreich abgeschlossenen Durchlauf
* (Erkennung wie Zaehl-Aufnahme) wieder her - von processCapture() und
* runCountCapture() gemeinsam genutzt, statt denselben Vorrang zweimal
* hinzuschreiben. Der Hinweis auf eine nicht verfuegbare Kamera hat Vorrang
* vor der OCR-Verfuegbarkeitsanzeige und bleibt dauerhaft stehen.
*/
function restoreIdleStatus() {
if (cameraUnavailableMessage) {
view.setStatus(cameraUnavailableMessage, true);
} else {
const available = isOcrAvailable();
view.setStatus(
available ? '' : 'Texterkennung nicht verfuegbar — nur Barcodes werden gelesen',
!available,
);
}
}
/** /**
* Ein Scan-Durchlauf: Bild beschaffen, erkennen, bei unsicherer Erkennung * Ein Scan-Durchlauf: Bild beschaffen, erkennen, bei unsicherer Erkennung
* nachfragen, buchen, kurz rueckmelden. Laeuft ohne Bestaetigung durch, * nachfragen, buchen, kurz rueckmelden. Laeuft ohne Bestaetigung durch,
@@ -241,19 +276,8 @@ async function processCapture(getFrame) {
} finally { } finally {
busy = false; busy = false;
// Die OCR-Verfuegbarkeitsanzeige darf eine Fehlermeldung, die der Nutzer // Die OCR-Verfuegbarkeitsanzeige darf eine Fehlermeldung, die der Nutzer
// noch lesen muss, nicht ueberschreiben. Der Hinweis auf eine nicht // noch lesen muss, nicht ueberschreiben.
// verfuegbare Kamera hat Vorrang vor beidem und bleibt dauerhaft stehen. if (!failed) restoreIdleStatus();
if (!failed) {
if (cameraUnavailableMessage) {
view.setStatus(cameraUnavailableMessage, true);
} else {
const available = isOcrAvailable();
view.setStatus(
available ? '' : 'Texterkennung nicht verfuegbar — nur Barcodes werden gelesen',
!available,
);
}
}
} }
} }
@@ -384,95 +408,147 @@ function describePolarity(polarity) {
* ui/scan-view.js/setCountDiagnostics()). Kurze, unfachliche Bezeichnungen * ui/scan-view.js/setCountDiagnostics()). Kurze, unfachliche Bezeichnungen
* in eigenen Zeilen statt einer einzigen langen Zeile - der Nutzer soll sie * in eigenen Zeilen statt einer einzigen langen Zeile - der Nutzer soll sie
* bei Bedarf jemandem am Telefon vorlesen koennen, ohne sich zu verhaspeln. * bei Bedarf jemandem am Telefon vorlesen koennen, ohne sich zu verhaspeln.
* Enthaelt bewusst sowohl die rohe letzte Messung (zeigt die tatsaechliche * Enthaelt zusaetzlich zu den schon vorhandenen Werten die fuenf
* Schwankung, die die grosse - geglaettete - Zahl sonst verbergen wuerde) * Einzelmessungen dieser Aufnahme (`counts`) - daran erkennen Nutzer und
* als auch den Zeitbedarf des letzten Zaehldurchlaufs (zeigt, ob das Geraet * Entwickler sofort, ob das Ergebnis belastbar ist (fuenf Messungen von 17
* ueberhaupt hinterherkommt). * bis 19 bedeuten etwas anderes als fuenf Messungen von 4 bis 50). Die
* @param {ReturnType<typeof countObjects>} result * uebrigen Werte (`last`) beziehen sich weiterhin auf die *letzte* der
* @param {number} durationMs * fuenf Einzelmessungen - der gleitende Median ueber mehrere Durchlaeufe
* (frueher: count-history.js) entfaellt, die Aufnahme mit ihren fuenf
* Bildern leistet die noetige Beruhigung bereits selbst (siehe
* count-capture.js).
* @param {number[]} counts die (bis zu fuenf) Einzelmessungen dieser Aufnahme
* @param {ReturnType<typeof countObjects>} last Ergebnis der letzten der
* fuenf Einzelmessungen
* @param {number} durationMs Zeitbedarf der letzten Einzelmessung
*/ */
function describeCountDiagnostics(result, durationMs) { function describeCountDiagnostics(counts, last, durationMs) {
return [ return [
`Roh: ${result.count}`, `Roh: ${last.count}`,
`Bild: ${result.width}×${result.height}`, `Messungen: ${counts.join(', ')}`,
`Flächen: ${result.regionsFound}${result.regionsKept}`, `Bild: ${last.width}×${last.height}`,
`Größte Fläche: ${result.largestArea} Bildpunkte`, `Flächen: ${last.regionsFound} ${last.regionsKept}`,
`Typische Größe: ${result.typicalArea} Bildpunkte`, `Größte Fläche: ${last.largestArea} Bildpunkte`,
`Art: ${describePolarity(result.polarity)}`, `Typische Größe: ${last.typicalArea} Bildpunkte`,
`Art: ${describePolarity(last.polarity)}`,
`Dauer: ${durationMs.toFixed(0)} ms`, `Dauer: ${durationMs.toFixed(0)} ms`,
].join('\n'); ].join('\n');
} }
/** // Anzahl der Bilder je Zaehl-Aufnahme (siehe runCountCapture() unten): ein
* Ein einzelner Durchlauf der Dauerzaehlung (Funktion "Zählen"): Ausschnitt // einzelnes Videobild kann verwackelt sein oder mitten in eine
* in nativer Aufloesung holen, Fundstuecke zaehlen (siehe count-objects.js) // Fokusregelung fallen - fuenf einzeln gezaehlte Bilder, zusammengefasst
* und die grosse Anzeige aktualisieren. Anders als runSearchAttempt() wird // ueber ihren Median (count-capture.js), sind robuster als eines allein.
* hier nie gebucht und nichts gesperrt - reine Anzeige, busy bleibt const CAPTURE_SAMPLE_SIZE = 5;
* unberuehrt, weil kein Ergebnis in die Sitzung geht und daher auch kein
* "Modul scannen", keine Treffer-Rueckmeldung und keine Stapel-Zuweisung
* dazugehoeren (siehe scan-modes.js).
*
* Die grosse Zahl selbst zeigt nicht die rohe Messung, sondern den Median
* der letzten neun Messungen (siehe count-history.js): ein einzelner
* Ausreisser - Bewegungsunschaerfe, nachregelnder Autofokus, ein kurz
* verdecktes Teil - verschwindet damit, ohne dass die Anzeige traege wird.
* Die rohe Messung selbst geht nicht verloren, sondern steht (zusammen mit
* den uebrigen Zwischenwerten) in der Diagnosezeile - siehe
* describeCountDiagnostics() oben.
*/
async function runCountAttempt() {
// Nur in Funktion "Zählen" aktiv - siehe scan-modes.js.
if (!getScanMode(currentModeId).continuousCount) return;
// Dieselbe Pausenbedingung wie bei der Barcode-Dauersuche (laufende // Zeitspanne, ueber die die CAPTURE_SAMPLE_SIZE Bilder einer Aufnahme verteilt
// Erkennung oder offenes Vollbild-Overlay) - siehe searchPaused() oben. // aufgenommen werden - kurz genug, dass der Nutzer die Kamera dafuer ohne
if (searchPaused()) return; // Muehe ruhig haelt, lang genug, dass aufeinanderfolgende Bilder sich
// tatsaechlich unterscheiden (statt praktisch identische Wiederholungen
// desselben Videobilds zu zaehlen).
const CAPTURE_DURATION_MS = 1000;
/**
* Setzt die Beschriftung der Aufnahme-Schaltflaeche in Funktion "Zählen"
* anhand von hasCountResult (siehe Deklaration oben): "Zählen" vor der
* ersten Aufnahme seit Betreten der Funktion, "Nochmal zählen" danach.
*/
function updateCountCaptureLabel() {
view.setCaptureLabel(hasCountResult ? 'Nochmal zählen' : 'Zählen');
}
/**
* Eine Aufnahme der Funktion "Zählen" (ausgeloest durch die Schaltflaeche
* "Zählen"/"Nochmal zählen"): nimmt binnen rund CAPTURE_DURATION_MS
* insgesamt CAPTURE_SAMPLE_SIZE Bilder aus dem Zielrahmen des laufenden
* Videobilds auf (`grabFrameRegion`, derselbe Ausschnitt wie ueberall sonst
* - bewusst *nicht* die native Fotoaufnahme des Geraets, deren
* Bildausschnitt von der Vorschau abweichen kann), zaehlt jedes einzeln
* (`countObjects`, unveraendert) und zeigt als Ergebnis deren Median
* (`combineCaptureCounts`, count-capture.js) - robust gegen ein einzelnes
* verwackeltes oder unscharfes Bild. Waehrend der Aufnahme zeigt die
* Statuszeile "zähle …" (dasselbe Muster wie "erkenne …" in
* processCapture()), damit der Nutzer das Handy ruhig haelt und den
* Knopfdruck nicht fuer wirkungslos haelt.
*
* Anders als frueher (Dauerzaehlung) bleibt das Ergebnis danach stehen, bis
* der Nutzer erneut antippt - eine Zaehlung ist eine Schaetzung, die durch
* staendiges Neuanzeigen nicht praeziser wird, nur unruhig. Bucht nichts in
* die Sitzung, sperrt aber wie processCapture() ueber `busy` waehrend sie
* laeuft (Undo, Sitzungsliste, Funktionswechsel bleiben so lange
* unerreichbar - dieselbe kurze, unproblematische Sperre wie bei jedem
* anderen Scan-Durchlauf).
*/
async function runCountCapture() {
if (busy) return;
busy = true;
view.setStatus('zähle …');
let failed = false;
try {
const counts = [];
let last = null;
for (let i = 0; i < CAPTURE_SAMPLE_SIZE; i += 1) {
if (i > 0) {
await sleep(CAPTURE_DURATION_MS / (CAPTURE_SAMPLE_SIZE - 1));
}
let region; let region;
try { try {
region = grabFrameRegion(view.video); region = grabFrameRegion(view.video);
} catch { } catch {
// Kamera liefert noch keine brauchbare Bildgroesse - naechster Durchlauf // Kamerabild in diesem Augenblick nicht bereit - dieses Einzelbild
// versucht es erneut. // faellt aus, die Aufnahme laeuft mit den uebrigen weiter (siehe
return; // combineCaptureCounts() fuer den Fall weniger als fuenf Bilder).
continue;
} }
// Nur der eigentliche Zaehldurchlauf wird gestoppt (nicht das Holen des
// Kamera-Ausschnitts) - so sieht man in der Diagnosezeile gezielt, ob das
// Zaehlverfahren selbst mit dem Geraet hinterherkommt (siehe
// test/count-objects-photo.test.js fuer dieselbe Messweise).
const start = performance.now(); const start = performance.now();
const result = countObjects(region); const result = countObjects(region);
const durationMs = performance.now() - start; const durationMs = performance.now() - start;
counts.push(result.count);
last = { result, durationMs };
}
const { history, median: smoothedCount } = pushCount(countHistory, result.count); if (last === null) {
countHistory = history; // Kein einziges Bild dieser Aufnahme war verwertbar - kein regulaeres
// Ergebnis, siehe combineCaptureCounts() (count-capture.js). Bleibt
// wie ein Fehlschlag stehen, statt sofort von restoreIdleStatus()
// ueberschrieben zu werden.
failed = true;
view.setStatus('Zählen nicht möglich — Kamerabild noch nicht bereit', true);
return;
}
// Bei einer noch geraden Verlaufslaenge (bis der Verlauf HISTORY_WINDOW const finalCount = combineCaptureCounts(counts);
// erreicht) liegt der Median genau zwischen zwei ganzen Messungen - die view.setCount(Math.round(finalCount));
// Zaehlanzeige selbst zeigt trotzdem stets eine ganze Zahl (siehe view.setCountDiagnostics(describeCountDiagnostics(counts, last.result, last.durationMs));
// Rundung Schritt 10 in count-objects.js, hier fuer die Anzeige ebenso hasCountResult = true;
// gehandhabt). updateCountCaptureLabel();
view.setCount(Math.round(smoothedCount)); } catch (error) {
view.setCountDiagnostics(describeCountDiagnostics(result, durationMs)); failed = true;
view.setStatus(`Fehler: ${error.message}`, true);
} finally {
busy = false;
if (!failed) restoreIdleStatus();
}
} }
/** /**
* Laeuft, solange die Kamera aktiv ist. Ein neuer Versuch beginnt erst, wenn * Laeuft, solange die Kamera aktiv ist. Ein neuer Versuch beginnt erst, wenn
* der vorherige vollstaendig fertig ist - nie ueberlappend. Fuehrt pro * der vorherige vollstaendig fertig ist - nie ueberlappend. Fuehrt pro
* Durchlauf sowohl den Barcode- als auch den Zaehl-Versuch aus; jeder der * Durchlauf die Barcode-Dauersuche aus; sie bricht selbst sofort ab, wenn
* beiden bricht selbst sofort ab, wenn die gewaehlte Funktion nicht zu ihm * die gewaehlte Funktion keine laufende Suche vorsieht (siehe
* passt (siehe runSearchAttempt()/runCountAttempt()) - so bleibt es bei * runSearchAttempt()). Funktion "Zählen" laeuft seit dem Umbau auf
* genau einem Zeitgeber und derselben Ueberlappungssperre fuer alle * Aufnahmen statt Dauerbetrieb (siehe runCountCapture()) und braucht hier
* fortlaufenden Funktionen, statt einen zweiten, parallelen Mechanismus * deshalb keinen eigenen Aufruf mehr.
* fuer "Zählen" danebenzustellen.
*/ */
async function continuousSearchLoop() { async function continuousSearchLoop() {
for (;;) { for (;;) {
const started = performance.now(); const started = performance.now();
try { try {
await runSearchAttempt(); await runSearchAttempt();
await runCountAttempt();
} catch { } catch {
// Ein einzelner fehlgeschlagener Versuch darf die Dauersuche nicht // Ein einzelner fehlgeschlagener Versuch darf die Dauersuche nicht
// dauerhaft abbrechen. // dauerhaft abbrechen.
+13 -10
View File
@@ -14,9 +14,13 @@
// //
// Funktion 4 ("Zaehlen") ist reine Anzeige: Sie liest weder Codes noch Text // Funktion 4 ("Zaehlen") ist reine Anzeige: Sie liest weder Codes noch Text
// und bucht nichts in die Sitzung - deshalb `continuousSearch: false` und // und bucht nichts in die Sitzung - deshalb `continuousSearch: false` und
// `useOcr: false`, obwohl sie (wie die Barcode-Dauersuche) fortwaehrend // `useOcr: false`. Anders als frueher laeuft sie nicht mehr fortwaehrend,
// laeuft, solange die Kamera aktiv ist. Dafuer eigenes Merkmal // solange die Kamera aktiv ist, sondern - wie Funktion 3 ("Text erkennen") -
// `continuousCount`, siehe main.js. // auf Knopfdruck: eine Zaehlung ist eine Schaetzung, die durch staendiges
// Neuanzeigen nicht praeziser wird, nur unruhig. Ein eigenes Merkmal dafuer
// braucht es nicht mehr - `continuousSearch: false` allein genuegt bereits,
// um main.js (siehe applyMode()) und scan-recognition.js dieselbe
// Knopfdruck-Behandlung wie bei Funktion 3 zukommen zu lassen.
/** /**
* @typedef {object} ScanMode * @typedef {object} ScanMode
@@ -26,12 +30,11 @@
* Codearten (zxing-wasm-Formatnamen). Leer, wenn die Funktion keine * Codearten (zxing-wasm-Formatnamen). Leer, wenn die Funktion keine
* Barcode-Dekodierung nutzt. * Barcode-Dekodierung nutzt.
* @property {boolean} continuousSearch Ob waehrend die Kamera laeuft * @property {boolean} continuousSearch Ob waehrend die Kamera laeuft
* fortwaehrend nach Codes gesucht wird (statt nur auf Knopfdruck). * fortwaehrend nach Codes gesucht wird (statt nur auf Knopfdruck). Bei
* Funktion "Zaehlen" ebenso `false` wie bei "Text erkennen" - siehe
* count-objects.js fuer das Zaehlverfahren selbst und main.js
* (runCountCapture()) fuer den Knopfdruck-Ablauf.
* @property {boolean} useOcr Ob die Texterkennung (Tesseract) genutzt wird. * @property {boolean} useOcr Ob die Texterkennung (Tesseract) genutzt wird.
* @property {boolean} continuousCount Ob waehrend die Kamera laeuft
* fortwaehrend die Fundstuecke im Zielrahmen gezaehlt werden (siehe
* count-objects.js) - reine Anzeige, ohne Buchung. Nur bei Funktion
* "Zaehlen" true.
*/ */
/** @type {ScanMode[]} */ /** @type {ScanMode[]} */
@@ -70,11 +73,11 @@ export const SCAN_MODES = [
id: 'count', id: 'count',
label: 'Zählen', label: 'Zählen',
// Keine Codes, keine Texterkennung - reine Bildauswertung (siehe // Keine Codes, keine Texterkennung - reine Bildauswertung (siehe
// count-objects.js), die nichts bucht. // count-objects.js), die nichts bucht. Auf Knopfdruck wie Funktion 3,
// siehe Moduldoku oben.
barcodeFormats: [], barcodeFormats: [],
continuousSearch: false, continuousSearch: false,
useOcr: false, useOcr: false,
continuousCount: true,
}, },
]; ];
+10 -4
View File
@@ -12,10 +12,16 @@
// aufgerufen wird. // aufgerufen wird.
// Funktion 4 (Zaehlen): weder Barcode-Dekodierung noch Texterkennung - // Funktion 4 (Zaehlen): weder Barcode-Dekodierung noch Texterkennung -
// recognize() (pipeline.js) wird im normalen Ablauf fuer diese Funktion gar // recognize() (pipeline.js) wird im normalen Ablauf fuer diese Funktion gar
// nicht aufgerufen (siehe main.js: kein Knopf, keine Dauersuche im Sinne // nicht aufgerufen (siehe main.js: die Schaltflaeche "Zählen"/"Nochmal
// dieses Moduls); dieselbe leere Ruecklieferung wie in Funktion 3 dient hier // zählen" ruft stattdessen runCountCapture(), das direkt countObjects()
// nur als Absicherung fuer den seltenen Fall, dass der Ersatzweg ohne Kamera // verwendet); dieselbe leere Ruecklieferung wie in Funktion 3 dient hier nur
// als Absicherung fuer den seltenen Fall, dass der Ersatzweg ohne Kamera
// (Dateiauswahl) waehrend gewaehlter Funktion "Zaehlen" ausgeloest wird. // (Dateiauswahl) waehrend gewaehlter Funktion "Zaehlen" ausgeloest wird.
//
// Sowohl Funktion 3 als auch Funktion 4 haben `barcodeFormats: []` (siehe
// scan-modes.js) - eine leere Codeartenliste ist deshalb das gemeinsame,
// einzige Merkmal, an dem sich "keine Barcode-Dekodierung noetig" ablesen
// laesst, statt je Funktion einzeln nachzufragen.
import { getScanMode } from './scan-modes.js'; import { getScanMode } from './scan-modes.js';
@@ -30,7 +36,7 @@ export function buildRecognitionAdapters(modeId, adapters) {
const mode = getScanMode(modeId); const mode = getScanMode(modeId);
return { return {
decodeBarcodes: (mode.useOcr || mode.continuousCount) decodeBarcodes: mode.barcodeFormats.length === 0
? async () => [] ? async () => []
: (imageData) => adapters.decodeBarcodes(imageData, mode.barcodeFormats), : (imageData) => adapters.decodeBarcodes(imageData, mode.barcodeFormats),
runOcr: mode.useOcr runOcr: mode.useOcr
+5 -2
View File
@@ -5,8 +5,11 @@ import { SCAN_MODES } from '../scan-modes.js';
* scan-modes.js) - keine zweite Stelle mit eigenem Text je Funktion. * scan-modes.js) - keine zweite Stelle mit eigenem Text je Funktion.
*/ */
function describeMode(mode) { function describeMode(mode) {
if (mode.useOcr) return 'Auf Knopfdruck'; // "Text erkennen" und "Zählen" laufen beide auf Knopfdruck (siehe
if (mode.continuousCount) return 'Zählt fortlaufend, solange die Kamera läuft'; // scan-modes.js) - `continuousSearch: false` ist bei beiden das
// gemeinsame Merkmal dafuer, eine Fallunterscheidung je Funktion braucht
// es hier nicht.
if (!mode.continuousSearch) return 'Auf Knopfdruck';
return 'Läuft von selbst, sobald ein Code im Bild ist'; return 'Läuft von selbst, sobald ein Code im Bild ist';
} }
+32 -11
View File
@@ -17,7 +17,7 @@ export function renderScanView(root, { onCapture, onUndo, onOpenList, onPickFile
</div> </div>
</div> </div>
<div class="status" id="status"></div> <div class="status" id="status"></div>
<button class="action capture" id="capture" hidden>Modul scannen</button> <button class="action capture" id="capture" hidden></button>
<button class="action secondary" id="pick-file-button" hidden>Bild auswählen</button> <button class="action secondary" id="pick-file-button" hidden>Bild auswählen</button>
<div class="stacks" id="stacks"></div> <div class="stacks" id="stacks"></div>
<div class="last"> <div class="last">
@@ -115,15 +115,30 @@ export function renderScanView(root, { onCapture, onUndo, onOpenList, onPickFile
}, },
/** /**
* "Modul scannen" hat nur in Funktionen mit Texterkennung auf Knopfdruck * Diese eine Schaltflaeche bedient beide Knopfdruck-Funktionen ("Modul
* etwas zu tun (Funktion 3) - in Funktionen mit laufender Codesuche sucht * scannen" in Funktion 3 "Text erkennen", "Zählen"/"Nochmal zählen" in
* die App ohnehin durchgehend, ein Knopf dafuer waere wirkungslos. * Funktion 4 "Zählen") - nur ihr Wortlaut und ihr Rueckruf unterscheiden
* sich, siehe setCaptureLabel() unten und main.js/applyMode(). In
* Funktionen mit laufender Codesuche sucht die App ohnehin durchgehend,
* ein Knopf dafuer waere wirkungslos.
* @param {boolean} visible * @param {boolean} visible
*/ */
setCaptureVisible(visible) { setCaptureVisible(visible) {
captureEl.hidden = !visible; captureEl.hidden = !visible;
}, },
/**
* Setzt den Wortlaut der Aufnahme-Schaltflaeche - siehe setCaptureVisible()
* oben fuer den Grund, warum es dafuer nur eine gemeinsame Flaeche statt
* einer zweiten daneben gibt. main.js setzt hier "Modul scannen" (Funktion
* "Text erkennen"), "Zählen" (Funktion "Zählen", vor der ersten Aufnahme
* seit Betreten der Funktion) bzw. "Nochmal zählen" (danach).
* @param {string} text
*/
setCaptureLabel(text) {
captureEl.textContent = text;
},
/** @param {string} text @param {boolean} warn */ /** @param {string} text @param {boolean} warn */
setStatus(text, warn = false) { setStatus(text, warn = false) {
statusEl.textContent = text; statusEl.textContent = text;
@@ -162,7 +177,10 @@ export function renderScanView(root, { onCapture, onUndo, onOpenList, onPickFile
* Diagnosezeile: sie faellt dabei wieder in ihren Ausgangszustand * Diagnosezeile: sie faellt dabei wieder in ihren Ausgangszustand
* (verborgen, leer) zurueck, damit sie nach einem Funktionswechsel nicht * (verborgen, leer) zurueck, damit sie nach einem Funktionswechsel nicht
* ungefragt (weil sie beim letzten Mal offen war) oder mit veralteten * ungefragt (weil sie beim letzten Mal offen war) oder mit veralteten
* Werten wieder auftaucht. * Werten wieder auftaucht. Das gilt sowohl beim Verlassen der Funktion
* "Zählen" als auch beim erneuten Betreten - ein stehendes Ergebnis
* einer frueheren Aufnahme (andere Teile, andere Stelle) darf nicht
* hinueberwirken (siehe main.js/applyMode()).
* @param {boolean} visible * @param {boolean} visible
*/ */
setCountVisible(visible) { setCountVisible(visible) {
@@ -176,11 +194,13 @@ export function renderScanView(root, { onCapture, onUndo, onOpenList, onPickFile
}, },
/** /**
* Zeigt die (bereits ueber die letzten Messungen geglaettete) Anzahl * Zeigt das Ergebnis der zuletzt angetippten Zaehl-Aufnahme (Median der
* gefundener Fundstuecke (siehe count-history.js) - gross und aus * fuenf Einzelmessungen dieser Aufnahme, siehe count-capture.js und
* Armlaenge lesbar, aktualisiert sich mit jedem Durchlauf der * runCountCapture() in main.js) - gross und aus Armlaenge lesbar. Bleibt
* Dauerzaehlung. Ein Antippen blendet die Diagnosezeile darunter ein * unveraendert stehen, bis der Nutzer erneut auf die Schaltflaeche
* oder aus (siehe Klick-Handler oben). * "Nochmal zählen" tippt (keine fortlaufende Aktualisierung mehr). Ein
* Antippen der Zahl selbst blendet die Diagnosezeile darunter ein oder
* aus (siehe Klick-Handler oben).
* @param {number} count * @param {number} count
*/ */
setCount(count) { setCount(count) {
@@ -189,7 +209,8 @@ export function renderScanView(root, { onCapture, onUndo, onOpenList, onPickFile
/** /**
* Setzt den Text der kleinen Diagnosezeile unter der grossen Zahl (siehe * Setzt den Text der kleinen Diagnosezeile unter der grossen Zahl (siehe
* main.js fuer den genauen Wortlaut) - unabhaengig davon, ob sie gerade * main.js fuer den genauen Wortlaut, u. a. die fuenf Einzelmessungen der
* zuletzt angetippten Aufnahme) - unabhaengig davon, ob sie gerade
* eingeblendet ist. Reine Textaktualisierung, die Sichtbarkeit steuert * eingeblendet ist. Reine Textaktualisierung, die Sichtbarkeit steuert
* ausschliesslich der Klick auf die grosse Zahl (siehe oben). * ausschliesslich der Klick auf die grosse Zahl (siehe oben).
* @param {string} text * @param {string} text
+44
View File
@@ -0,0 +1,44 @@
// Reine Rechenlogik der Zaehl-Aufnahme (Funktion "Zählen", siehe main.js):
// aus den (bis zu fuenf) Einzelmessungen einer Aufnahme wird deren Median
// gebildet. Siehe count-capture.js fuer die Begruendung (robust gegen einen
// einzelnen Ausreisser, ohne wie eine fortlaufende Glaettung traege zu sein).
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { combineCaptureCounts } from '../src/count-capture.js';
test('fuenf gleiche Messungen liefern diese Zahl', () => {
assert.equal(combineCaptureCounts([18, 18, 18, 18, 18]), 18);
});
test('fuenf verschiedene Messungen liefern deren Median (ungerade Anzahl)', () => {
assert.equal(combineCaptureCounts([17, 19, 18, 20, 18]), 18);
});
test('ein einzelner grober Ausreisser unter fuenf Messungen schlaegt nicht durch', () => {
// Vier stabile Messungen um 18 herum, eine grob daneben (z. B.
// Bewegungsunschaerfe oder Autofokus mitten in der Aufnahme) - der Median
// bleibt bei 18, ein Mittelwert waere durch den Ausreisser spuerbar verzerrt.
assert.equal(combineCaptureCounts([18, 18, 3, 18, 60]), 18);
});
test('weniger als fuenf Messungen (eine Kameraaufnahme scheiterte) liefern trotzdem ein Ergebnis', () => {
// Gerade Anzahl (vier statt fuenf) - Median ist der Mittelwert der beiden
// mittleren Werte.
const result = combineCaptureCounts([17, 18, 18, 19]);
assert.equal(result, 18);
});
test('nur eine einzige verwertbare Messung ist ihr eigener Median', () => {
assert.equal(combineCaptureCounts([21]), 21);
});
test('eine leere Messreihe wirft, statt stillschweigend ein sinnloses Ergebnis (NaN) zu liefern', () => {
assert.throws(() => combineCaptureCounts([]));
});
test('combineCaptureCounts veraendert die uebergebene Liste nicht (reine Funktion)', () => {
const counts = [5, 1, 3];
const untouched = [...counts];
combineCaptureCounts(counts);
assert.deepEqual(counts, untouched);
});
-71
View File
@@ -1,71 +0,0 @@
// Gleitender Verlauf der letzten Zaehlmessungen (Funktion "Zählen") - die
// grosse Anzeige zeigt zusaetzlich zur rohen Messung deren Median ueber die
// letzten neun Durchlaeufe, damit ein einzelner Ausreisser (Bewegungs-
// unschaerfe, nachregelnder Autofokus) nicht sofort die Anzeige springen
// laesst. Reines, browserfreies Modul wie count-objects.js - hier ohne
// jeden Bezug zu Bilddaten, nur die Zahlenreihe selbst.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { createCountHistory, pushCount, HISTORY_WINDOW } from '../src/count-history.js';
test('leerer Verlauf: die erste Messung ist zugleich ihr eigener Median', () => {
const history = createCountHistory();
const result = pushCount(history, 18);
assert.deepEqual(result.history, [18]);
assert.equal(result.median, 18);
});
test('ein einzelner Ausreisser wird vom Median unterdrueckt', () => {
let history = createCountHistory();
// Acht stabile Messungen ...
for (let i = 0; i < 8; i += 1) {
({ history } = pushCount(history, 18));
}
// ... und ein einzelner Ausreisser (z. B. Bewegungsunschaerfe oder
// nachregelnder Autofokus, siehe Modul-Doku).
const result = pushCount(history, 60);
assert.equal(
result.median,
18,
'ein einzelner Ausreisser darf die geglaettete Anzeige nicht verschieben',
);
});
test('nach genuegend gleichbleibenden neuen Messungen folgt der Median (nicht traege)', () => {
// Der Median glaettet nur einzelne Ausreisser, nicht eine echte,
// anhaltende Aenderung (z. B. weil der Nutzer tatsaechlich Teile
// weggenommen hat) - sonst waere die Anzeige "beruhigt", aber nicht mehr
// ehrlich.
let history = createCountHistory();
for (let i = 0; i < 8; i += 1) {
({ history } = pushCount(history, 18));
}
let result;
for (let i = 0; i < HISTORY_WINDOW; i += 1) {
result = pushCount(history, 5);
history = result.history;
}
assert.equal(
result.median,
5,
'nach HISTORY_WINDOW gleichen neuen Messungen ist der alte Wert vollstaendig aus dem Fenster verdraengt',
);
});
test('das Fenster behaelt hoechstens HISTORY_WINDOW Messungen - aeltere fallen heraus', () => {
let history = createCountHistory();
for (let i = 1; i <= HISTORY_WINDOW + 3; i += 1) {
({ history } = pushCount(history, i));
}
assert.equal(history.length, HISTORY_WINDOW);
// Die drei aeltesten (1, 2, 3) sind bereits herausgefallen.
assert.deepEqual(history, [4, 5, 6, 7, 8, 9, 10, 11, 12]);
});
test('pushCount veraendert den uebergebenen Verlauf nicht (reine Funktion)', () => {
const history = createCountHistory();
const before = pushCount(history, 3).history;
const untouched = [...history];
pushCount(before, 4);
assert.deepEqual(history, untouched, 'der urspruengliche Verlauf bleibt unangetastet');
});
+1 -2
View File
@@ -43,13 +43,12 @@ test('Funktion 3 (Text erkennen): keine Codearten, keine laufende Suche, Texterk
assert.equal(mode.useOcr, true); assert.equal(mode.useOcr, true);
}); });
test('Funktion 4 (Zählen): keine Codearten, keine laufende Codesuche, keine Texterkennung, dafuer laufende Zaehlung', () => { test('Funktion 4 (Zählen): keine Codearten, keine laufende Codesuche, keine Texterkennung - auf Knopfdruck wie Funktion 3', () => {
const mode = getScanMode('count'); const mode = getScanMode('count');
assert.equal(mode.label, 'Zählen'); assert.equal(mode.label, 'Zählen');
assert.deepEqual(mode.barcodeFormats, []); assert.deepEqual(mode.barcodeFormats, []);
assert.equal(mode.continuousSearch, false); assert.equal(mode.continuousSearch, false);
assert.equal(mode.useOcr, false); assert.equal(mode.useOcr, false);
assert.equal(mode.continuousCount, true);
}); });
test('unbekannte Kennung wirft statt undefined zurueckzugeben', () => { test('unbekannte Kennung wirft statt undefined zurueckzugeben', () => {