kagabo commited on
Commit
67ae379
·
verified ·
1 Parent(s): 0ed8fd9

Recover uncertain trailing digits and use Poppins

Browse files
Files changed (4) hide show
  1. README.md +3 -0
  2. app.js +64 -2
  3. index.html +3 -0
  4. styles.css +1 -1
README.md CHANGED
@@ -18,5 +18,8 @@ The interface includes final-model validation metrics and evidence from the
18
  52-image combat test. During prediction, low-confidence digit detections are
19
  shown as `?` in the safe reading while preserving the model's raw digit guess;
20
  `unknown` is post-processing logic, not a trained class in the final model.
 
 
 
21
 
22
  Source model: [`kagabo/hf_water_meter_models`](https://huggingface.co/kagabo/hf_water_meter_models)
 
18
  52-image combat test. During prediction, low-confidence digit detections are
19
  shown as `?` in the safe reading while preserving the model's raw digit guess;
20
  `unknown` is post-processing logic, not a trained class in the final model.
21
+ The browser decoder can also recover one very-low-confidence trailing digit
22
+ when it is inside the detected window and follows the established digit spacing.
23
+ Recovered digits remain unknown (`?`) unless they pass the unknown threshold.
24
 
25
  Source model: [`kagabo/hf_water_meter_models`](https://huggingface.co/kagabo/hf_water_meter_models)
app.js CHANGED
@@ -1,5 +1,6 @@
1
  const MODEL_SIZE = 960;
2
  const MODEL_URL = "models/new_clean_yolo12n_raw_pascal_best.onnx";
 
3
  const CLASS_NAMES = ["meter", "window", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9"];
4
  const COLORS = ["#51a7ff", "#39e6c6", "#ffc66d", "#ffc66d", "#ffc66d", "#ffc66d", "#ffc66d", "#ffc66d", "#ffc66d", "#ffc66d", "#ffc66d", "#ffc66d"];
5
  const HISTORY_KEY = "aquavision-reading-history-v1";
@@ -220,7 +221,7 @@ function decodeOutput(output, transform, image) {
220
  classId = channel - 4;
221
  }
222
  }
223
- if (confidence < confidenceThreshold() || classId >= CLASS_NAMES.length) continue;
224
 
225
  const cx = outputValue(output, 0, prediction, channels, count);
226
  const cy = outputValue(output, 1, prediction, channels, count);
@@ -235,7 +236,9 @@ function decodeOutput(output, transform, image) {
235
  }
236
 
237
  candidates.sort((a, b) => b.confidence - a.confidence);
238
- return nonMaxSuppression(candidates.slice(0, 1000));
 
 
239
  }
240
 
241
  function intersectionOverUnion(a, b) {
@@ -267,6 +270,65 @@ function centerInside(box, container) {
267
  return centerX >= container.x1 && centerX <= container.x2 && centerY >= container.y1 && centerY <= container.y2;
268
  }
269
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
270
  function reconstructReading(detections) {
271
  const windows = detections.filter((item) => item.classId === 1);
272
  let digits = detections.filter((item) => item.classId >= 2 && item.classId <= 11);
 
1
  const MODEL_SIZE = 960;
2
  const MODEL_URL = "models/new_clean_yolo12n_raw_pascal_best.onnx";
3
+ const RECOVERY_CONFIDENCE_FLOOR = 0.001;
4
  const CLASS_NAMES = ["meter", "window", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9"];
5
  const COLORS = ["#51a7ff", "#39e6c6", "#ffc66d", "#ffc66d", "#ffc66d", "#ffc66d", "#ffc66d", "#ffc66d", "#ffc66d", "#ffc66d", "#ffc66d", "#ffc66d"];
6
  const HISTORY_KEY = "aquavision-reading-history-v1";
 
221
  classId = channel - 4;
222
  }
223
  }
224
+ if (confidence < RECOVERY_CONFIDENCE_FLOOR || classId >= CLASS_NAMES.length) continue;
225
 
226
  const cx = outputValue(output, 0, prediction, channels, count);
227
  const cy = outputValue(output, 1, prediction, channels, count);
 
236
  }
237
 
238
  candidates.sort((a, b) => b.confidence - a.confidence);
239
+ const allDetections = nonMaxSuppression(candidates.slice(0, 1000));
240
+ const regularDetections = allDetections.filter((item) => item.confidence >= confidenceThreshold());
241
+ return recoverTrailingDigit(allDetections, regularDetections);
242
  }
243
 
244
  function intersectionOverUnion(a, b) {
 
270
  return centerX >= container.x1 && centerX <= container.x2 && centerY >= container.y1 && centerY <= container.y2;
271
  }
272
 
273
+ function median(values) {
274
+ if (!values.length) return 0;
275
+ const sorted = [...values].sort((a, b) => a - b);
276
+ const middle = Math.floor(sorted.length / 2);
277
+ return sorted.length % 2 ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2;
278
+ }
279
+
280
+ function recoverTrailingDigit(allDetections, regularDetections) {
281
+ const bestWindow = regularDetections
282
+ .filter((item) => item.classId === 1)
283
+ .sort((a, b) => b.confidence - a.confidence)[0];
284
+ if (!bestWindow) return regularDetections;
285
+
286
+ const trustedDigits = regularDetections
287
+ .filter((item) => item.classId >= 2 && item.classId <= 11 && centerInside(item, bestWindow))
288
+ .sort((a, b) => (a.x1 + a.x2) / 2 - (b.x1 + b.x2) / 2);
289
+ if (trustedDigits.length < 4) return regularDetections;
290
+
291
+ const centers = trustedDigits.map((item) => (item.x1 + item.x2) / 2);
292
+ const pitch = median(centers.slice(1).map((center, index) => center - centers[index]));
293
+ if (pitch <= 0) return regularDetections;
294
+
295
+ const lastCenter = centers.at(-1);
296
+ const expectedCenter = lastCenter + pitch;
297
+ const medianY = median(trustedDigits.map((item) => (item.y1 + item.y2) / 2));
298
+ const medianHeight = median(trustedDigits.map((item) => item.y2 - item.y1));
299
+
300
+ const candidates = allDetections
301
+ .filter((item) => (
302
+ item.classId >= 2
303
+ && item.classId <= 11
304
+ && item.confidence < confidenceThreshold()
305
+ && centerInside(item, bestWindow)
306
+ ))
307
+ .map((item) => {
308
+ const centerX = (item.x1 + item.x2) / 2;
309
+ const centerY = (item.y1 + item.y2) / 2;
310
+ return {
311
+ item,
312
+ centerX,
313
+ positionError: Math.abs(centerX - expectedCenter),
314
+ verticalError: Math.abs(centerY - medianY),
315
+ };
316
+ })
317
+ .filter(({ centerX, positionError, verticalError }) => (
318
+ centerX > lastCenter + pitch * 0.45
319
+ && centerX < lastCenter + pitch * 1.6
320
+ && positionError <= pitch * 0.55
321
+ && verticalError <= Math.max(4, medianHeight * 0.7)
322
+ ))
323
+ .sort((a, b) => (
324
+ (a.positionError / pitch) - (b.positionError / pitch)
325
+ || b.item.confidence - a.item.confidence
326
+ ));
327
+
328
+ if (!candidates.length) return regularDetections;
329
+ return [...regularDetections, { ...candidates[0].item, recovered: true }];
330
+ }
331
+
332
  function reconstructReading(detections) {
333
  const windows = detections.filter((item) => item.classId === 1);
334
  let digits = detections.filter((item) => item.classId >= 2 && item.classId <= 11);
index.html CHANGED
@@ -7,6 +7,9 @@
7
  <meta name="description" content="A privacy-first AI system for detecting and reading water meters." />
8
  <title>AquaVision AI | Water Meter Reader</title>
9
  <link rel="preconnect" href="https://cdn.jsdelivr.net" />
 
 
 
10
  <link rel="stylesheet" href="styles.css" />
11
  <script src="https://cdn.jsdelivr.net/npm/onnxruntime-web@1.23.2/dist/ort.min.js"></script>
12
  <script src="app.js" defer></script>
 
7
  <meta name="description" content="A privacy-first AI system for detecting and reading water meters." />
8
  <title>AquaVision AI | Water Meter Reader</title>
9
  <link rel="preconnect" href="https://cdn.jsdelivr.net" />
10
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
11
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
12
+ <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
13
  <link rel="stylesheet" href="styles.css" />
14
  <script src="https://cdn.jsdelivr.net/npm/onnxruntime-web@1.23.2/dist/ort.min.js"></script>
15
  <script src="app.js" defer></script>
styles.css CHANGED
@@ -18,7 +18,7 @@
18
 
19
  * { box-sizing: border-box; }
20
  html { scroll-behavior: smooth; }
21
- body { margin: 0; min-height: 100vh; overflow-x: hidden; background: var(--bg); color: var(--text); font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
22
  body::before { content: ""; position: fixed; inset: 0; z-index: -3; opacity: .28; background-image: linear-gradient(rgba(87, 145, 180, .08) 1px, transparent 1px), linear-gradient(90deg, rgba(87, 145, 180, .08) 1px, transparent 1px); background-size: 48px 48px; mask-image: linear-gradient(to bottom, black, transparent 80%); }
23
  button, input { font: inherit; }
24
  button, a { -webkit-tap-highlight-color: transparent; }
 
18
 
19
  * { box-sizing: border-box; }
20
  html { scroll-behavior: smooth; }
21
+ body { margin: 0; min-height: 100vh; overflow-x: hidden; background: var(--bg); color: var(--text); font-family: "Poppins", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
22
  body::before { content: ""; position: fixed; inset: 0; z-index: -3; opacity: .28; background-image: linear-gradient(rgba(87, 145, 180, .08) 1px, transparent 1px), linear-gradient(90deg, rgba(87, 145, 180, .08) 1px, transparent 1px); background-size: 48px 48px; mask-image: linear-gradient(to bottom, black, transparent 80%); }
23
  button, input { font: inherit; }
24
  button, a { -webkit-tap-highlight-color: transparent; }