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 { createWorker } from 'tesseract.js';
|
||||||
|
import { computeOtsuThreshold } from './threshold.js';
|
||||||
|
|
||||||
// Der einmal erzeugte Erkennungs-Arbeiter wird als Versprechen zwischen-
|
// Der einmal erzeugte Erkennungs-Arbeiter wird als Versprechen zwischen-
|
||||||
// gespeichert, damit sich auch mehrere schnell hintereinander gestartete
|
// gespeichert, damit sich auch mehrere schnell hintereinander gestartete
|
||||||
@@ -16,65 +17,15 @@ let workerPromise = null;
|
|||||||
// Oberflaeche verschwinden lassen - siehe Bericht, Abschnitt Selbstpruefung.
|
// Oberflaeche verschwinden lassen - siehe Bericht, Abschnitt Selbstpruefung.
|
||||||
let available = true;
|
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.
|
* Graustufen, Otsu-Schwellwert.
|
||||||
* Glaenzende Metalletiketten liefern flaue, kontrastarme Bilder mit
|
* Glaenzende Metalletiketten liefern flaue, kontrastarme Bilder mit
|
||||||
* Lichtreflexen als Normalfall; ohne diese Aufbereitung liest Tesseract dort
|
* Lichtreflexen als Normalfall; ohne diese Aufbereitung liest Tesseract dort
|
||||||
* kaum etwas Brauchbares. Der Schwellwert wird aus der Helligkeitsverteilung
|
* kaum etwas Brauchbares. Der Schwellwert wird aus der Helligkeitsverteilung
|
||||||
* des Bildes selbst bestimmt (Otsu), statt aus einer Kontrastspreizung
|
* des Bildes selbst bestimmt (Otsu, siehe threshold.js), statt aus einer
|
||||||
* zwischen dunkelstem und hellstem Pixel - ein einzelner Reflex kann so
|
* Kontrastspreizung zwischen dunkelstem und hellstem Pixel - ein einzelner
|
||||||
* nicht mehr Schrift und Untergrund gemeinsam auf dieselbe Seite eines
|
* Reflex kann so nicht mehr Schrift und Untergrund gemeinsam auf dieselbe
|
||||||
* festen Schwellwerts ziehen.
|
* Seite eines festen Schwellwerts ziehen.
|
||||||
* Rein rechnend - erzeugt kein Canvas, fasst kein DOM an und ist damit
|
* Rein rechnend - erzeugt kein Canvas, fasst kein DOM an und ist damit
|
||||||
* ohne Browser testbar.
|
* ohne Browser testbar.
|
||||||
* @param {{width: number, height: number, data: Uint8ClampedArray}} imageData
|
* @param {{width: number, height: number, data: Uint8ClampedArray}} imageData
|
||||||
@@ -82,16 +33,7 @@ function otsuThreshold(histogram, totalPixels) {
|
|||||||
*/
|
*/
|
||||||
export function preprocess(imageData) {
|
export function preprocess(imageData) {
|
||||||
const { width, height, data } = imageData;
|
const { width, height, data } = imageData;
|
||||||
const gray = new Uint8ClampedArray(width * height);
|
const { gray, threshold } = computeOtsuThreshold(imageData);
|
||||||
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 out = new Uint8ClampedArray(data.length);
|
const out = new Uint8ClampedArray(data.length);
|
||||||
for (let p = 0; p < gray.length; p += 1) {
|
for (let p = 0; p < gray.length; p += 1) {
|
||||||
const value = gray[p] >= threshold ? 255 : 0;
|
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