Otsu-Schwellwert in eigenes reines Modul ausgelagert (threshold.js)
Zieht otsuThreshold()/grayscaleHistogram() aus ocr.js in src/threshold.js, ohne das Verhalten von preprocess() zu aendern (bestehende Tests in ocr-preprocess.test.js bleiben unveraendert gruen). Vorbereitung fuer die neue Zaehl-Funktion, die dieselbe Schwellwertbestimmung braucht, aber nichts mit tesseract.js (ocr.js) zu tun haben soll. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+6
-64
@@ -1,4 +1,5 @@
|
||||
import { createWorker } from 'tesseract.js';
|
||||
import { computeOtsuThreshold } from './threshold.js';
|
||||
|
||||
// Der einmal erzeugte Erkennungs-Arbeiter wird als Versprechen zwischen-
|
||||
// gespeichert, damit sich auch mehrere schnell hintereinander gestartete
|
||||
@@ -16,65 +17,15 @@ let workerPromise = null;
|
||||
// Oberflaeche verschwinden lassen - siehe Bericht, Abschnitt Selbstpruefung.
|
||||
let available = true;
|
||||
|
||||
/**
|
||||
* Bestimmt den Otsu-Schwellwert aus einem Histogramm der 256 Helligkeits-
|
||||
* stufen. Otsu sucht den Schwellwert, der die Pixel in zwei Gruppen teilt
|
||||
* (dunkler / heller) und dabei die Streuung innerhalb jeder Gruppe minimiert
|
||||
* bzw. die Streuung zwischen den Gruppen maximiert - beides ist aequivalent
|
||||
* und wird hier ueber die Zwischen-Klassen-Varianz berechnet.
|
||||
* Da der Schwellwert aus der gesamten Verteilung stammt, verschiebt ein
|
||||
* einzelnes Ausreisserpixel (z. B. ein Lichtreflex) das Histogramm kaum und
|
||||
* damit auch den Schwellwert kaum - anders als eine Min/Max-Spreizung, die
|
||||
* von genau diesem einen Pixel dominiert wird.
|
||||
* @param {Uint32Array | number[]} histogram 256 Eintraege, einer je Helligkeitsstufe
|
||||
* @param {number} totalPixels Anzahl der Pixel, aus denen das Histogramm stammt
|
||||
* @returns {number} Schwellwert (0-255): Helligkeit darunter wird schwarz, ab hier weiss
|
||||
*/
|
||||
function otsuThreshold(histogram, totalPixels) {
|
||||
if (totalPixels === 0) return 128;
|
||||
|
||||
let sumAll = 0;
|
||||
for (let level = 0; level < 256; level += 1) {
|
||||
sumAll += level * histogram[level];
|
||||
}
|
||||
|
||||
let sumBackground = 0;
|
||||
let weightBackground = 0;
|
||||
let bestVariance = -1;
|
||||
let bestThreshold = 128;
|
||||
|
||||
for (let level = 0; level < 256; level += 1) {
|
||||
weightBackground += histogram[level];
|
||||
if (weightBackground === 0) continue;
|
||||
|
||||
const weightForeground = totalPixels - weightBackground;
|
||||
if (weightForeground === 0) break;
|
||||
|
||||
sumBackground += level * histogram[level];
|
||||
|
||||
const meanBackground = sumBackground / weightBackground;
|
||||
const meanForeground = (sumAll - sumBackground) / weightForeground;
|
||||
const meanDiff = meanBackground - meanForeground;
|
||||
const betweenClassVariance = weightBackground * weightForeground * meanDiff * meanDiff;
|
||||
|
||||
if (betweenClassVariance > bestVariance) {
|
||||
bestVariance = betweenClassVariance;
|
||||
bestThreshold = level + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return bestThreshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* Graustufen, Otsu-Schwellwert.
|
||||
* Glaenzende Metalletiketten liefern flaue, kontrastarme Bilder mit
|
||||
* Lichtreflexen als Normalfall; ohne diese Aufbereitung liest Tesseract dort
|
||||
* kaum etwas Brauchbares. Der Schwellwert wird aus der Helligkeitsverteilung
|
||||
* des Bildes selbst bestimmt (Otsu), statt aus einer Kontrastspreizung
|
||||
* zwischen dunkelstem und hellstem Pixel - ein einzelner Reflex kann so
|
||||
* nicht mehr Schrift und Untergrund gemeinsam auf dieselbe Seite eines
|
||||
* festen Schwellwerts ziehen.
|
||||
* des Bildes selbst bestimmt (Otsu, siehe threshold.js), statt aus einer
|
||||
* Kontrastspreizung zwischen dunkelstem und hellstem Pixel - ein einzelner
|
||||
* Reflex kann so nicht mehr Schrift und Untergrund gemeinsam auf dieselbe
|
||||
* Seite eines festen Schwellwerts ziehen.
|
||||
* Rein rechnend - erzeugt kein Canvas, fasst kein DOM an und ist damit
|
||||
* ohne Browser testbar.
|
||||
* @param {{width: number, height: number, data: Uint8ClampedArray}} imageData
|
||||
@@ -82,16 +33,7 @@ function otsuThreshold(histogram, totalPixels) {
|
||||
*/
|
||||
export function preprocess(imageData) {
|
||||
const { width, height, data } = imageData;
|
||||
const gray = new Uint8ClampedArray(width * height);
|
||||
const histogram = new Uint32Array(256);
|
||||
|
||||
for (let i = 0, p = 0; i < data.length; i += 4, p += 1) {
|
||||
const value = Math.round(0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2]);
|
||||
gray[p] = value;
|
||||
histogram[value] += 1;
|
||||
}
|
||||
|
||||
const threshold = otsuThreshold(histogram, gray.length);
|
||||
const { gray, threshold } = computeOtsuThreshold(imageData);
|
||||
const out = new Uint8ClampedArray(data.length);
|
||||
for (let p = 0; p < gray.length; p += 1) {
|
||||
const value = gray[p] >= threshold ? 255 : 0;
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
// Otsu-Schwellwertbestimmung: reines Modul ohne jeden Browser-Zugriff (kein
|
||||
// window/document/canvas). Vorher steckte das ausschließlich in `preprocess`
|
||||
// in ocr.js - das aber tesseract.js einbindet und damit nichts mit reiner
|
||||
// Bildauswertung zu tun hat. Sowohl `preprocess` (ocr.js, für die
|
||||
// Texterkennung) als auch `countObjects` (count-objects.js, für die
|
||||
// Zähl-Funktion) brauchen denselben ersten Schritt - Graustufen bilden und
|
||||
// die Helligkeitsverteilung in zwei Klassen teilen - und lesen ihn deshalb
|
||||
// beide von hier.
|
||||
|
||||
/**
|
||||
* Bestimmt den Otsu-Schwellwert aus einem Histogramm der 256 Helligkeits-
|
||||
* stufen. Otsu sucht den Schwellwert, der die Pixel in zwei Gruppen teilt
|
||||
* (dunkler / heller) und dabei die Streuung innerhalb jeder Gruppe minimiert
|
||||
* bzw. die Streuung zwischen den Gruppen maximiert - beides ist aequivalent
|
||||
* und wird hier ueber die Zwischen-Klassen-Varianz berechnet.
|
||||
* Da der Schwellwert aus der gesamten Verteilung stammt, verschiebt ein
|
||||
* einzelnes Ausreisserpixel (z. B. ein Lichtreflex) das Histogramm kaum und
|
||||
* damit auch den Schwellwert kaum - anders als eine Min/Max-Spreizung, die
|
||||
* von genau diesem einen Pixel dominiert wird.
|
||||
* @param {Uint32Array | number[]} histogram 256 Eintraege, einer je Helligkeitsstufe
|
||||
* @param {number} totalPixels Anzahl der Pixel, aus denen das Histogramm stammt
|
||||
* @returns {number} Schwellwert (0-255): Helligkeit darunter wird schwarz, ab hier weiss
|
||||
*/
|
||||
export function otsuThreshold(histogram, totalPixels) {
|
||||
if (totalPixels === 0) return 128;
|
||||
|
||||
let sumAll = 0;
|
||||
for (let level = 0; level < 256; level += 1) {
|
||||
sumAll += level * histogram[level];
|
||||
}
|
||||
|
||||
let sumBackground = 0;
|
||||
let weightBackground = 0;
|
||||
let bestVariance = -1;
|
||||
let bestThreshold = 128;
|
||||
|
||||
for (let level = 0; level < 256; level += 1) {
|
||||
weightBackground += histogram[level];
|
||||
if (weightBackground === 0) continue;
|
||||
|
||||
const weightForeground = totalPixels - weightBackground;
|
||||
if (weightForeground === 0) break;
|
||||
|
||||
sumBackground += level * histogram[level];
|
||||
|
||||
const meanBackground = sumBackground / weightBackground;
|
||||
const meanForeground = (sumAll - sumBackground) / weightForeground;
|
||||
const meanDiff = meanBackground - meanForeground;
|
||||
const betweenClassVariance = weightBackground * weightForeground * meanDiff * meanDiff;
|
||||
|
||||
if (betweenClassVariance > bestVariance) {
|
||||
bestVariance = betweenClassVariance;
|
||||
bestThreshold = level + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return bestThreshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* Graustufen (gewichtetes RGB, wie menschliche Helligkeitswahrnehmung) und
|
||||
* das zugehörige 256-stufige Helligkeitshistogramm - der gemeinsame erste
|
||||
* Schritt für `preprocess` (ocr.js) und `countObjects` (count-objects.js).
|
||||
* @param {{width: number, height: number, data: Uint8ClampedArray}} imageData
|
||||
* @returns {{gray: Uint8ClampedArray, histogram: Uint32Array}}
|
||||
*/
|
||||
export function grayscaleHistogram(imageData) {
|
||||
const { data } = imageData;
|
||||
const gray = new Uint8ClampedArray(data.length / 4);
|
||||
const histogram = new Uint32Array(256);
|
||||
|
||||
for (let i = 0, p = 0; i < data.length; i += 4, p += 1) {
|
||||
const value = Math.round(0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2]);
|
||||
gray[p] = value;
|
||||
histogram[value] += 1;
|
||||
}
|
||||
|
||||
return { gray, histogram };
|
||||
}
|
||||
|
||||
/**
|
||||
* Graustufen und Otsu-Schwellwert in einem Schritt - der gemeinsame
|
||||
* Ausgangspunkt für `preprocess` und `countObjects`.
|
||||
* @param {{width: number, height: number, data: Uint8ClampedArray}} imageData
|
||||
* @returns {{gray: Uint8ClampedArray, threshold: number}}
|
||||
*/
|
||||
export function computeOtsuThreshold(imageData) {
|
||||
const { gray, histogram } = grayscaleHistogram(imageData);
|
||||
const threshold = otsuThreshold(histogram, gray.length);
|
||||
return { gray, threshold };
|
||||
}
|
||||
Reference in New Issue
Block a user