Skip to content

Commit d9e9bf5

Browse files
committed
feat: Support merging OCR'ed texts
Useful when characters on the screen split the number into separate readings.
1 parent 9eb332f commit d9e9bf5

5 files changed

Lines changed: 80 additions & 3 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ All options can be set via CLI flags or environment variables. Flags take preced
123123
| `--crop` | `CROP` | *(disabled)* | Crop rectangle as `x0,y0,x1,y1` applied before OCR |
124124
| `--ocr-match-regex` | `OCR_MATCH_REGEX` | `^000\d+$` | Regex to identify the meter reading from OCR text |
125125
| `--ocr-fix-regex` | `OCR_FIX_REGEX` | *(disabled)* | Comma-separated list of regex substitutions as `pattern=replacement` applied in order before matching (e.g. `^O=0,^030=000`) |
126+
| `--ocr-merge-texts` | `OCR_MERGE_TEXTS` | `false` | Concatenate all OCR text results into a single string before applying fix/match regexes (useful when readings are split across multiple detections, e.g. `["00036", "128"]` → `"00036128"`) |
126127
| `--ocr-mask-regions` | `OCR_MASK_REGIONS` | *(disabled)* | Comma-separated rectangle coordinates to mask before OCR, as `x1,y1,x2,y2[,x3,y3,x4,y4,...]` (applied after crop) |
127128
| `--ocr-mask-colors` | `OCR_MASK_COLORS` | `000000` | Comma-separated hex colors for mask regions. One color applies to all; otherwise must match the number of regions |
128129
| `--mqtt-broker` | `MQTT_BROKER` | *(disabled)* | MQTT broker URL, e.g. `tcp://192.168.1.100:1883` |

http.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ func processOCR(imageData []byte, batLevel, batVoltage int) {
111111
return
112112
}
113113

114-
reading := extractReading(ocrOut.Texts, ocrMatchRe, ocrFixRules)
114+
reading := extractReading(ocrOut.Texts, ocrMatchRe, ocrFixRules, ocrMergeTexts)
115115

116116
// Append reading to CSV unconditionally so discarded values are still on disk.
117117
storeReading(imagePath, reading)

main.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ var (
3131
mqttDeviceModel string
3232
meterDivisor float64
3333
ocrIncrOnly bool
34+
ocrMergeTexts bool
3435
)
3536

3637
func main() {
@@ -77,6 +78,11 @@ func main() {
7778
Usage: "Comma-separated list of regex substitutions applied to OCR text before matching, each as pattern=replacement (e.g. ^O=0,^030=000)",
7879
Sources: cli.EnvVars("OCR_FIX_REGEX"),
7980
},
81+
&cli.BoolFlag{
82+
Name: "ocr-merge-texts",
83+
Usage: "Concatenate all OCR text results into a single string before applying fix/match regexes (useful when readings are split across multiple detections)",
84+
Sources: cli.EnvVars("OCR_MERGE_TEXTS"),
85+
},
8086
&cli.StringFlag{
8187
Name: "ocr-mask-regions",
8288
Usage: "Comma-separated rectangle coordinates to mask before OCR, as x1,y1,x2,y2[,x3,y3,x4,y4,...] (applied after crop)",
@@ -165,6 +171,7 @@ func run(_ context.Context, cmd *cli.Command) error {
165171
mqttDeviceModel = cmd.String("mqtt-device-model")
166172
meterDivisor = cmd.Float("meter-divisor")
167173
ocrIncrOnly = cmd.Bool("ocr-incr-only")
174+
ocrMergeTexts = cmd.Bool("ocr-merge-texts")
168175

169176
if mqttBroker != "" {
170177
initMQTT()

ocr.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,14 @@ func applyFixRules(s string, rules []ocrFixRule) string {
239239
return s
240240
}
241241

242-
func extractReading(texts []string, matchRe *regexp.Regexp, fixRules []ocrFixRule) string {
242+
func extractReading(texts []string, matchRe *regexp.Regexp, fixRules []ocrFixRule, mergeTexts bool) string {
243+
if mergeTexts {
244+
var merged strings.Builder
245+
for _, t := range texts {
246+
merged.WriteString(strings.TrimSpace(t))
247+
}
248+
texts = []string{merged.String()}
249+
}
243250
// First pass: apply fix rules, then match.
244251
for _, t := range texts {
245252
t = applyFixRules(strings.TrimSpace(t), fixRules)

ocr_test.go

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,14 +127,76 @@ func TestExtractReading(t *testing.T) {
127127

128128
for _, tt := range tests {
129129
t.Run(tt.name, func(t *testing.T) {
130-
got := extractReading(tt.texts, tt.matchRe, tt.fixRules)
130+
got := extractReading(tt.texts, tt.matchRe, tt.fixRules, false)
131131
if got != tt.want {
132132
t.Errorf("extractReading() = %q, want %q", got, tt.want)
133133
}
134134
})
135135
}
136136
}
137137

138+
func TestExtractReadingMerge(t *testing.T) {
139+
defaultMatchRe := regexp.MustCompile(`^000\d+$`)
140+
141+
tests := []struct {
142+
name string
143+
texts []string
144+
matchRe *regexp.Regexp
145+
fixRules []ocrFixRule
146+
want string
147+
}{
148+
{
149+
name: "merge splits into single reading",
150+
texts: []string{"00036", "128"},
151+
matchRe: regexp.MustCompile(`^\d+$`),
152+
want: "00036128",
153+
},
154+
{
155+
name: "merge with match regex",
156+
texts: []string{"000", "354225"},
157+
matchRe: defaultMatchRe,
158+
want: "000354225",
159+
},
160+
{
161+
name: "merge with fix rules",
162+
texts: []string{"O30", "354225"},
163+
matchRe: defaultMatchRe,
164+
fixRules: []ocrFixRule{
165+
{Pattern: regexp.MustCompile(`^O`), Replacement: "0"},
166+
{Pattern: regexp.MustCompile(`^030`), Replacement: "000"},
167+
},
168+
want: "000354225",
169+
},
170+
{
171+
name: "merge single element unchanged",
172+
texts: []string{"000354225"},
173+
matchRe: defaultMatchRe,
174+
want: "000354225",
175+
},
176+
{
177+
name: "merge trims whitespace from each part",
178+
texts: []string{" 000 ", " 354225 "},
179+
matchRe: defaultMatchRe,
180+
want: "000354225",
181+
},
182+
{
183+
name: "merge empty input returns empty",
184+
texts: []string{},
185+
matchRe: defaultMatchRe,
186+
want: "",
187+
},
188+
}
189+
190+
for _, tt := range tests {
191+
t.Run(tt.name, func(t *testing.T) {
192+
got := extractReading(tt.texts, tt.matchRe, tt.fixRules, true)
193+
if got != tt.want {
194+
t.Errorf("extractReading(merge=true) = %q, want %q", got, tt.want)
195+
}
196+
})
197+
}
198+
}
199+
138200
func TestParseOCRFixRules(t *testing.T) {
139201
tests := []struct {
140202
name string

0 commit comments

Comments
 (0)