lyte-codes commited on
Commit
21298ad
·
verified ·
1 Parent(s): 5124c2a

Model card: measured results, including the sim-to-real failure

Browse files
Files changed (1) hide show
  1. README.md +129 -0
README.md CHANGED
@@ -1,3 +1,132 @@
1
  ---
2
  license: apache-2.0
 
 
 
 
 
 
 
3
  ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  license: apache-2.0
3
+ datasets:
4
+ - lyte-codes/clockface-synth-dataset-10k
5
+ pipeline_tag: image-to-text
6
+ tags:
7
+ - analog-clock
8
+ - time-reading
9
+ - sim2real
10
  ---
11
+
12
+ # clockface
13
+
14
+ Reads an analog clock face and returns the time. Two stages: find the dial,
15
+ then read it. 11.5 MB of weights in total.
16
+
17
+ > **This model does not work well on real photographs.** On 1,728 real photos it
18
+ > scores **155.9 minutes** mean error, against **180 minutes for guessing at
19
+ > random**. It scores 4.8 minutes on the synthetic data it was trained on. It is
20
+ > published because the gap between those two numbers is the useful part.
21
+
22
+ Version 263701. Trained on synth v2, revision `main` of the dataset above.
23
+
24
+ ## The metric
25
+
26
+ A clock face is a ring of 720 minutes. Error is the shorter way round it, so it
27
+ lies in [0, 360] and 11:58 against 12:02 is 4 minutes, not 716. AM/PM does not
28
+ exist: a face cannot show it, so 3:47 and 15:47 are the same reading.
29
+
30
+ Guessing uniformly at random scores 180 minutes. That is the number to beat, and
31
+ it is the number this model is measured against.
32
+
33
+ ## Results
34
+
35
+ ### Real photographs (1,728 images, third-party labels)
36
+
37
+ | | MAE | median | within 5 min | worse than 30 min |
38
+ |---|---|---|---|---|
39
+ | two-stage | 155.9 min | 139.9 | 11.4% | 82.2% |
40
+ | whole image, no locator | 151.8 min | 133.7 | 9.2% | 82.5% |
41
+ | **random guessing** | **180 min** | — | — | — |
42
+
43
+ Sources: 1,092 film stills from Marclay's *The Clock*, 506 photographs from
44
+ `moondream/analog-clock-benchmark`, 131 cropped faces from
45
+ `oliverj990/clock-faces-v1-times`. Their labels are third party and mostly
46
+ unverified; restricted to the 106 labels checked by hand against the image, the
47
+ result is MAE 148.5, so label noise does not explain it.
48
+
49
+ By source, the pattern is a domain gap and not much else:
50
+
51
+ | source | MAE | within 5 min |
52
+ |---|---|---|
53
+ | cropped clock faces (dial fills frame) | 87.8 min | 45.0% |
54
+ | photographs of scenes | 163.3 min | 9.3% |
55
+
56
+ The closer an image is to the synthetic framing, the better it does.
57
+
58
+ ### Synthetic validation (1,200 held-out renders) — training telemetry only
59
+
60
+ | | MAE | median | within 1 min | within 5 min |
61
+ |---|---|---|---|---|
62
+ | ground-truth crops | 4.82 min | 0.42 | 79.0% | 92.5% |
63
+ | end to end | 5.07 min | 0.43 | 79.0% | 92.2% |
64
+
65
+ **These numbers are not a result.** The model is being scored by the same
66
+ renderer that produced its labels. They are here to show the size of the
67
+ sim-to-real gap, which is the point: 4.8 minutes on renders, 156 on photographs.
68
+
69
+ ## What goes wrong
70
+
71
+ - **Hand confusion.** 720 of the 1,421 gross failures would improve if the hour
72
+ and minute hands were swapped. The generator used a narrow band of hand
73
+ lengths and widths, and the model learned that convention rather than learning
74
+ which hand is longer.
75
+ - **The confidence signal does not survive.** The two hands' disagreement is a
76
+ good confidence signal on synthetic data (MAE 4.82 falls to 1.64 when keeping
77
+ the most confident 75%) and is inert on real photographs (155.9 to 153.5). The
78
+ model is confidently wrong, which is worse than being uncertain.
79
+ - **Small clocks.** The locator finds a dial to 1.5% of image width on renders
80
+ and fails on wide scenes where the clock is a small part of the frame.
81
+
82
+ ## Architecture
83
+
84
+ stage 1 DialLocator 554K params, 2.2 MB image -> dial centre, radius
85
+ stage 2 PretrainedReader 2.3M params, 9.2 MB crop -> two angle distributions
86
+
87
+ Stage 2 is a MobileNetV3-Small backbone pretrained on ImageNet, with two heads
88
+ predicting a distribution over 180 angular bins for each hand. Targets are von
89
+ Mises bumps rather than one-hot, and decoding takes a circular soft-argmax.
90
+
91
+ An identical from-scratch network never leaves chance on this task: it sits at
92
+ 2*ln(180), a uniform distribution, however long it trains, while overfitting 400
93
+ images perfectly. ImageNet initialisation is what makes it learn at all.
94
+
95
+ Both hands are predicted, never the time directly, so their disagreement gives a
96
+ confidence signal for free. Decoding is a vernier: the minute hand supplies
97
+ precision, the hour hand picks which hour. Measured against reading the hour hand
98
+ alone, that is about 11x more accurate under noise.
99
+
100
+ ## Usage
101
+
102
+ import torch
103
+ from twostage import DialLocator, PretrainedReader, crop_dial
104
+ from model import decode_cls
105
+
106
+ reader = PretrainedReader(bins=180, arch="mobilenet_v3_small")
107
+ reader.load_state_dict(torch.load("stage2_best.pt")["model"])
108
+ hour_logits, minute_logits = reader(crop) # crop: 1x3x256x256, ImageNet norm
109
+ minutes, hour_only, disagreement, sharpness = decode_cls(hour_logits, minute_logits)
110
+
111
+ `minutes` is the position on the 720-minute ring; `disagreement` is how far apart
112
+ the two hands' readings are, in minutes.
113
+
114
+ ## Intended use
115
+
116
+ Reading round analog clock faces. Out of scope: digital-analog hybrids, subdials,
117
+ chronographs, 24-hour faces.
118
+
119
+ **Not suitable for any use where a wrong time carries a cost.** It is wrong by
120
+ more than half an hour on 82% of real photographs.
121
+
122
+ ## Reproducing
123
+
124
+ git clone https://github.com/lyte-codes/clockface
125
+ ./eval.py --self-test # 32/32, checks the metric
126
+ python3 predict.py --labels <labels> --root <dir> \
127
+ --stage2 stage2_best.pt --stage1 stage1_best.pt --out preds.jsonl
128
+ ./eval.py --labels <labels> --pred preds.jsonl
129
+
130
+ ## Licence
131
+
132
+ Apache 2.0. The synthetic dataset is CC BY 4.0.