Reine Zaehl-Berechnung fuer Fundstuecke in einem Bildausschnitt
Neues Modul src/count-objects.js: Otsu-Schwellwert (threshold.js) teilt den Ausschnitt in zwei Klassen, die flaechenmaessig kleinere gilt als Objekt (funktioniert fuer helle wie dunkle Teile auf beliebigem Untergrund). Connected-Component-Labeling ueber eine eigene Arbeitsliste statt Rekursion (vermeidet Stapelueberlauf bei ~1 Mio. Bildpunkten), Rauschflaechen werden verworfen, beruehrende Teile anhand des Flaechen-Medians hochgerechnet. Test-Driven: leeres Bild, einzelne/mehrere getrennte Flaechen, verworfenes Rauschen, Hochrechnung ohne ein nur leicht groesseres Einzelteil zu verdoppeln, helle wie dunkle Objekte, Bild ohne Bildpunkte, diagonal statt flaechig beruehrende Teile bleiben getrennt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { countObjects } from '../src/count-objects.js';
|
||||
|
||||
/**
|
||||
* Baut ein ImageData-artiges Objekt aus einem Raster von Graustufenwerten
|
||||
* (0-255 je Zelle) - derselbe minimale Ersatz wie in ocr-preprocess.test.js,
|
||||
* hier zweidimensional, weil countObjects() auf zusammenhaengende Flaechen
|
||||
* angewiesen ist.
|
||||
* @param {number[][]} rows gleich lange Zeilen von Graustufenwerten
|
||||
*/
|
||||
function image(rows) {
|
||||
const height = rows.length;
|
||||
const width = height > 0 ? rows[0].length : 0;
|
||||
const data = new Uint8ClampedArray(width * height * 4);
|
||||
let i = 0;
|
||||
for (const row of rows) {
|
||||
for (const value of row) {
|
||||
data[i] = value;
|
||||
data[i + 1] = value;
|
||||
data[i + 2] = value;
|
||||
data[i + 3] = 255;
|
||||
i += 4;
|
||||
}
|
||||
}
|
||||
return { width, height, data };
|
||||
}
|
||||
|
||||
/** Erzeugt ein width x height grosses Raster, komplett mit `background` gefuellt. */
|
||||
function grid(width, height, background) {
|
||||
return Array.from({ length: height }, () => new Array(width).fill(background));
|
||||
}
|
||||
|
||||
/** Malt ein width x height grosses Quadrat/Rechteck an Position (x, y) mit `value`. */
|
||||
function paint(rows, x, y, width, height, value) {
|
||||
for (let row = y; row < y + height; row += 1) {
|
||||
for (let col = x; col < x + width; col += 1) {
|
||||
rows[row][col] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test('leeres Bild (kein Objekt, gleichmaessiger Untergrund) ergibt null', () => {
|
||||
const rows = grid(20, 20, 60);
|
||||
const result = countObjects(image(rows));
|
||||
assert.equal(result.count, 0);
|
||||
});
|
||||
|
||||
test('eine einzelne Flaeche ergibt eins', () => {
|
||||
const rows = grid(40, 40, 30);
|
||||
paint(rows, 15, 15, 10, 10, 220);
|
||||
const result = countObjects(image(rows));
|
||||
assert.equal(result.count, 1);
|
||||
});
|
||||
|
||||
test('mehrere getrennte, gleich grosse Flaechen ergeben ihre Anzahl', () => {
|
||||
const rows = grid(80, 80, 30);
|
||||
paint(rows, 5, 5, 10, 10, 220);
|
||||
paint(rows, 30, 5, 10, 10, 220);
|
||||
paint(rows, 5, 30, 10, 10, 220);
|
||||
paint(rows, 30, 30, 10, 10, 220);
|
||||
const result = countObjects(image(rows));
|
||||
assert.equal(result.count, 4);
|
||||
});
|
||||
|
||||
test('winzige Stoerflaechen (Staub, Reflexe) werden nicht mitgezaehlt', () => {
|
||||
const rows = grid(80, 80, 30);
|
||||
paint(rows, 5, 5, 10, 10, 220);
|
||||
paint(rows, 30, 5, 10, 10, 220);
|
||||
paint(rows, 5, 30, 10, 10, 220);
|
||||
paint(rows, 30, 30, 10, 10, 220);
|
||||
// Einzelpixel- bis Doppelpixel-Stoerflaechen, weit entfernt von den
|
||||
// echten Flaechen und voneinander - deutlich unter einem Viertel der
|
||||
// typischen Flaechengroesse (100 Bildpunkte).
|
||||
rows[60][60] = 220;
|
||||
rows[65][65] = 220;
|
||||
rows[65][66] = 220;
|
||||
rows[70][10] = 220;
|
||||
const result = countObjects(image(rows));
|
||||
assert.equal(result.count, 4, 'Stoerflaechen duerfen die Anzahl nicht erhoehen');
|
||||
});
|
||||
|
||||
test('eine Flaeche von etwa vierfacher Einzelgroesse wird als vier gezaehlt', () => {
|
||||
const rows = grid(80, 80, 30);
|
||||
paint(rows, 5, 5, 10, 10, 220); // 100 Bildpunkte, typische Groesse
|
||||
paint(rows, 30, 5, 10, 10, 220); // 100
|
||||
paint(rows, 5, 30, 10, 10, 220); // 100
|
||||
// Verschmolzene Flaeche mehrerer beruehrender Teile: 20x20 = 400 = 4x100.
|
||||
paint(rows, 30, 30, 20, 20, 220);
|
||||
const result = countObjects(image(rows));
|
||||
assert.equal(result.count, 7, '3 Einzelflaechen (je 1) + hochgerechnete Flaeche (4)');
|
||||
});
|
||||
|
||||
test('ein einzelnes, nur leicht groesseres Teil wird nicht als zwei gezaehlt', () => {
|
||||
const rows = grid(80, 80, 30);
|
||||
paint(rows, 5, 5, 10, 10, 220); // 100
|
||||
paint(rows, 30, 5, 10, 10, 220); // 100
|
||||
paint(rows, 5, 30, 10, 10, 220); // 100
|
||||
// Rund 30% groesser als die anderen (11x12=132 statt 100) - haeufiger
|
||||
// Fall, wenn ein Teil im Bild etwas naeher an der Kamera liegt.
|
||||
paint(rows, 30, 30, 11, 12, 220);
|
||||
const result = countObjects(image(rows));
|
||||
assert.equal(result.count, 4, 'leicht groessere Einzelflaeche zaehlt weiterhin als eins');
|
||||
});
|
||||
|
||||
test('dunkle Objekte auf hellem Grund werden genauso gezaehlt wie helle auf dunklem', () => {
|
||||
const darkOnLight = grid(80, 80, 220);
|
||||
paint(darkOnLight, 5, 5, 10, 10, 30);
|
||||
paint(darkOnLight, 30, 5, 10, 10, 30);
|
||||
paint(darkOnLight, 5, 30, 10, 10, 30);
|
||||
paint(darkOnLight, 30, 30, 10, 10, 30);
|
||||
|
||||
const lightOnDark = grid(80, 80, 30);
|
||||
paint(lightOnDark, 5, 5, 10, 10, 220);
|
||||
paint(lightOnDark, 30, 5, 10, 10, 220);
|
||||
paint(lightOnDark, 5, 30, 10, 10, 220);
|
||||
paint(lightOnDark, 30, 30, 10, 10, 220);
|
||||
|
||||
const darkResult = countObjects(image(darkOnLight));
|
||||
const lightResult = countObjects(image(lightOnDark));
|
||||
assert.equal(darkResult.count, 4);
|
||||
assert.equal(lightResult.count, 4);
|
||||
});
|
||||
|
||||
test('ein Bild ohne Bildpunkte stuerzt nicht ab', () => {
|
||||
assert.doesNotThrow(() => {
|
||||
const result = countObjects({ width: 0, height: 0, data: new Uint8ClampedArray(0) });
|
||||
assert.equal(result.count, 0);
|
||||
});
|
||||
});
|
||||
|
||||
test('zwei aneinanderliegende, aber nur diagonal beruehrende Flaechen zaehlen getrennt (4er-Nachbarschaft)', () => {
|
||||
const rows = grid(40, 40, 30);
|
||||
// Zwei 5x5-Quadrate, die sich nur in einer Ecke (ein Bildpunkt) beruehren.
|
||||
paint(rows, 10, 10, 5, 5, 220);
|
||||
paint(rows, 15, 15, 5, 5, 220);
|
||||
const result = countObjects(image(rows));
|
||||
assert.equal(result.count, 2, 'nur diagonal beruehrende Flaechen bleiben getrennt');
|
||||
});
|
||||
Reference in New Issue
Block a user