improve parsing 3

This commit is contained in:
davmar
2026-09-02 13:38:47 +02:00
parent 98574af903
commit ae42ba8193
10 changed files with 417 additions and 136 deletions
+17 -2
View File
@@ -16,15 +16,17 @@ class ReceiptOcr {
required double imageHeight,
}) {
final elements = <OcrElement>[];
final lines = <OcrLine>[];
var maxRight = 0.0;
var maxBottom = 0.0;
for (final block in recognized.blocks) {
for (final line in block.lines) {
final lineElements = <OcrElement>[];
if (line.elements.isNotEmpty) {
for (final element in line.elements) {
final box = element.boundingBox;
elements.add(
lineElements.add(
OcrElement(
text: element.text,
left: box.left,
@@ -38,7 +40,7 @@ class ReceiptOcr {
}
} else {
final box = line.boundingBox;
elements.add(
lineElements.add(
OcrElement(
text: line.text,
left: box.left,
@@ -50,6 +52,18 @@ class ReceiptOcr {
if (box.right > maxRight) maxRight = box.right;
if (box.bottom > maxBottom) maxBottom = box.bottom;
}
elements.addAll(lineElements);
final box = line.boundingBox;
lines.add(
OcrLine(
text: line.text,
elements: lineElements,
left: box.left,
top: box.top,
right: box.right,
bottom: box.bottom,
),
);
}
}
@@ -58,6 +72,7 @@ class ReceiptOcr {
return OcrDocument(
elements: elements,
lines: lines,
imageWidth: width,
imageHeight: height,
flattenedText: recognized.text.trim(),
+2 -2
View File
@@ -25,7 +25,7 @@ class ReceiptParseConfig {
required this.trailingCategoryLetter,
required this.noiseKeywords,
required this.itemCountKeywords,
this.rowClusterHeightFactor = 1.2,
this.rowClusterHeightFactor = 0.55,
this.priceColumnWidthFraction = 0.25,
});
@@ -106,7 +106,7 @@ class ReceiptParseConfig {
rowClusterHeightFactor: _spatialDouble(
json,
'row_cluster_height_factor',
fallback: 1.2,
fallback: 0.55,
),
priceColumnWidthFraction: _spatialDouble(
json,
+46 -2
View File
@@ -25,7 +25,7 @@ class ReceiptParser {
/// Primary path: positioned ML Kit elements → row clustering → lookahead.
static ReceiptParseResult parseReceiptFromOcr(OcrDocument document) =>
ReceiptSpatialParser.parse(document);
_parseDocument(document);
static ReceiptParseResult parseRecognizedText(
RecognizedText recognized, {
@@ -37,6 +37,50 @@ class ReceiptParser {
imageWidth: imageWidth,
imageHeight: imageHeight,
);
return parseReceiptFromOcr(document);
return _parseDocument(document);
}
static ReceiptParseResult _parseDocument(OcrDocument document) {
if (ReceiptTextParser.looksLikeColumnOcr(document.flattenedText)) {
return _withImageDimensions(
ReceiptTextParser.parseReceipt(document.flattenedText),
document,
);
}
final spatial = ReceiptSpatialParser.parse(document);
if (_shouldFallbackToText(spatial)) {
return _withImageDimensions(
ReceiptTextParser.parseReceipt(document.flattenedText),
document,
);
}
return spatial;
}
static bool _shouldFallbackToText(ReceiptParseResult spatial) {
if (spatial.items.isEmpty) return true;
final flagged =
spatial.items.where((item) => item.needsManualReview).length;
if (flagged > 0) return true;
if (spatial.items.any((item) => item.price <= 0)) return true;
if (spatial.receiptTotal != null && !spatial.validationPassed) return true;
return false;
}
static ReceiptParseResult _withImageDimensions(
ReceiptParseResult result,
OcrDocument document,
) {
return ReceiptParseResult(
items: result.items,
store: result.store,
receiptTotal: result.receiptTotal,
totalDiscount: result.totalDiscount,
validationPassed: result.validationPassed,
expectedItemCount: result.expectedItemCount,
ocrImageWidth: document.imageWidth,
ocrImageHeight: document.imageHeight,
);
}
}
+19 -3
View File
@@ -28,13 +28,28 @@ class ReceiptRowBuilder {
List<ReceiptRow> buildRows(OcrDocument document) {
if (document.elements.isEmpty) return const [];
final rowClusterThreshold = _rowClusterThreshold(document.elements);
final clustered = _clusterByY(document.elements, rowClusterThreshold);
final contentWidth = _contentWidth(document.elements);
final contentLeft = _contentLeft(document.elements);
final priceColumnStart =
contentLeft + contentWidth * (1 - config.priceColumnWidthFraction);
// Prefer ML Kit line boundaries — element Y-clustering merges adjacent lines.
if (document.lines.isNotEmpty) {
return [
for (final line in document.lines)
_classifyRow(
line.elements,
priceColumnStart: priceColumnStart,
imageWidth: document.imageWidth,
imageHeight: document.imageHeight,
rowText: line.text,
),
];
}
final rowClusterThreshold = _rowClusterThreshold(document.elements);
final clustered = _clusterByY(document.elements, rowClusterThreshold);
return [
for (final rowElements in clustered)
_classifyRow(
@@ -116,8 +131,9 @@ class ReceiptRowBuilder {
required double priceColumnStart,
required double imageWidth,
required double imageHeight,
String? rowText,
}) {
final text = rowElements.map((e) => e.text).join(' ').trim();
final text = rowText ?? rowElements.map((e) => e.text).join(' ').trim();
final normalized = _preprocess(text);
final bounds = _rowBounds(rowElements, imageWidth, imageHeight);
+11
View File
@@ -116,6 +116,17 @@ class ReceiptTextParser {
return _parseSequential(lines, config, rawText);
}
/// True when OCR dumped an entire name column before a price column (Jumbo-style).
static bool looksLikeColumnOcr(String rawText) {
final config = receiptParseConfig;
final lines = rawText
.split('\n')
.map(_preprocess)
.where((line) => line.isNotEmpty)
.toList();
return _looksLikeColumnOcr(lines, config);
}
static String _preprocess(String line) =>
line.trim().replaceAll(RegExp(r'\s+'), ' ');