/
alignment.go
242 lines (204 loc) · 7.18 KB
/
alignment.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
package eclipse
import(
"fmt"
"image"
"log"
"math"
"strings"
"sync"
"golang.org/x/image/draw" // replace by "image/draw" at some point
"golang.org/x/image/math/f64" // replace by "image/math/f64" at some point
"github.com/abworrall/eclipse-hdr/pkg/emath"
)
// An AlignmentTransform maps a pixel location in a later layer to a
// pixel location in the base layer, that corresponds to the same point
// in the sky.
//
// If you use an equatorial mount, this is all redundant.
type AlignmentTransform struct {
Name string
TranslateByX float64
TranslateByY float64
RotationCenterX float64
RotationCenterY float64
RotateByDeg float64
ErrorMetric float64
}
func (xform AlignmentTransform)String() string {
str := fmt.Sprintf("Align[%s (%6.2f,%6.2f)", xform.Name, xform.TranslateByX, xform.TranslateByY)
if xform.RotateByDeg != 0.0 {
str += fmt.Sprintf(", %5.2fdeg", xform.RotateByDeg)
}
if xform.ErrorMetric != 0.0 {
str += fmt.Sprintf(", err:%6.0f", xform.ErrorMetric)
}
return str + "]"
}
func (xform AlignmentTransform)XFormImage(src image.Image) image.Image {
dst := image.NewRGBA(src.Bounds())
draw.CatmullRom.Transform(dst, f64.Aff3(xform.ToMatrix()), src, src.Bounds(), draw.Src, nil)
return dst
}
func (at AlignmentTransform)ToMatrix() emath.Aff3 {
// Step 1: translate so lunar limb centers are coincident
m := emath.Identity().Translate(at.TranslateByX, at.TranslateByY)
// [TBD] Step 2: scale (about lunar center) so that lunar radius is the same
// Step 3: rotate (about lunar center) so that coronas match
if at.RotateByDeg != 0 {
mR := emath.RotateAbout(at.RotateByDeg, at.RotationCenterX, at.RotationCenterY)
m = mR.Mult(m)
}
return m
}
// AlignLayer figures out the transform that aligns `l2` to `l1`. it
// then uses it to generate l2.Image, which will be pixel-aligned
// with l1.Image.
func AlignLayer(cfg Config, l1, l2 *Layer) {
// To get us in the ballpark, just map the center of the lunar
// limbs. This works better than you'd think, given that the lunar
// limb is itself moving relative to the sun (it's only there for
// the duration of totality !)
cent1 := l1.LunarLimb.Center()
cent2 := l2.LunarLimb.Center()
// Translate s2's lunar limb so that its center lines up with s1's lunar limb center
xform := AlignmentTransform{
Name: strings.ReplaceAll(fmt.Sprintf("%s-%s", l1.Filename(), l2.Filename()), ".tif", ""),
RotationCenterX: float64(cent1.X), // this rotationcenter is a bit approximate
RotationCenterY: float64(cent1.Y),
TranslateByX: float64(cent1.X-cent2.X),
TranslateByY: float64(cent1.Y-cent2.Y),
}
if cfg.DoFineTunedAlignment {
xform = AlignLayerFine(cfg, l1, l2, xform)
cfg.Alignments[xform.Name] = xform
} else if xf, exists := cfg.Alignments[xform.Name]; exists {
log.Printf("Using fine alignment from config file: %s\n", xf)
xform = xf
}
l2.AlignmentTransform = xform
l2.Image = xform.XFormImage(l2.LoadedImage)
}
// AlignLayerFine tries a wide range of possible finetune xforms in
// parallel, to find out which one fits best (i.e. has lowest error
// metric).
func AlignLayerFine(cfg Config, l1, l2 *Layer, baseXform AlignmentTransform) AlignmentTransform {
// The difference in radii found in the images; we start off by
// exploring x2 this amount. We can't need more than that, as the
// lunarlimbs need to line up.
radDelta := math.Abs(float64(l1.LunarLimb.Radius()) - float64(l2.LunarLimb.Radius()))
if radDelta < 2.0 { radDelta = 2.0 }
// Step 0. We start with a translation that superimposes the centre of the lunarlimbs.
best := baseXform
width, step := 0.0, 0.0
xforms := []AlignmentTransform{}
log.Printf("Align finetune:\n")
log.Printf(" -- orig : %s\n", baseXform)
// Step 1. Try various whole-pixel translations. We pick an area to
// look in that's based on the difference in lunar radii in the
// images; more or less, these need to line up, so we don't need to
// explore any further.
width = radDelta
step = 1.0
xforms = xforms[:0]
for x:=-1*width; x<=width; x += step {
for y:=-1*width; y<=width; y += step {
xform := best
xform.TranslateByX += x
xform.TranslateByY += y
xforms = append(xforms, xform)
}
}
best = scoreXFormsConcurrently(cfg, l1, l2, xforms, "pass1a")
// Step 2. In a much smaller area, explore fractional pixel
// translations. This relies on Catmull Rom interpolation.
width = 2.0
step = 0.10
xforms = xforms[:0]
for x:=-1*width; x<=width; x += step {
for y:=-1*width; y<=width; y += step {
xform := best
xform.TranslateByX += x
xform.TranslateByY += y
xforms = append(xforms, xform)
}
}
best = scoreXFormsConcurrently(cfg, l1, l2, xforms, "pass1b")
// Step 3. Now we think we have the images centred on each other,
// try some coarse rotations. (This will only be useful if the
// images were separated by quite a lot of time)
rotWidth := 10.0
rotStep := 1.0
xforms = xforms[:0]
for theta := -1.0*(rotWidth/2.0); theta < rotWidth/2.0; theta += rotStep {
xform := best
// Note - the rotation center is not really well defined here :/
xform.RotateByDeg = theta
xforms = append(xforms, xform)
}
best = scoreXFormsConcurrently(cfg, l1, l2, xforms, "pass2a")
// Step 4. Try a smaller amount of fine-grained rotations.
rotWidth = 2.0 // should be 10
rotStep = 0.05
xforms = xforms[:0]
for theta := -1.0*(rotWidth/2.0); theta < rotWidth/2.0; theta += rotStep {
xform := best
// Note - the rotation center is not really well defined here
xform.RotateByDeg += theta
xforms = append(xforms, xform)
}
best = scoreXFormsConcurrently(cfg, l1, l2, xforms, "pass2b")
if best.RotateByDeg < 0.0001 { best.RotateByDeg = 0.0 }
log.Printf("Align finetune: orig %s\n", baseXform)
log.Printf("Align finetune: final %s\n", best)
return best
}
type fineTuneJob struct {
// Inputs for the job
C Config
L1 *Layer
L2 *Layer
Name string
XForm AlignmentTransform
// Output
ErrorMetric float64
}
// ScoreXFormsConcurrently uses a pool of goroutines to compute the
// error metrics for each of the proposed transform, and return the
// one with the lowest error.
func scoreXFormsConcurrently(cfg Config, l1, l2 *Layer, xforms []AlignmentTransform, name string) AlignmentTransform {
var wg sync.WaitGroup
jobsChan := make(chan fineTuneJob, len(xforms))
resultsChan := make(chan fineTuneJob, len(xforms))
// Kick off worker pool
nWorkers := 20
for i:=0; i<nWorkers; i++ {
wg.Add(1)
go func() {
for job := range jobsChan {
job.ErrorMetric = ImgDiff(job.C, job.L1, job.L2, job.Name, job.XForm)
resultsChan<- job
// log.Printf(" >> finetune [%s], xform %s, err: %6.0f\n", job.Name, job.XForm, job.ErrorMetric)
}
defer wg.Done()
}()
}
// Feed in jobs
for i, xform := range xforms {
job := fineTuneJob{cfg, l1, l2, fmt.Sprintf("%s-%03d", name, i), xform, 0.0}
jobsChan<- job
}
close(jobsChan)
wg.Wait()
close(resultsChan)
// results processor
bestResult := fineTuneJob{ErrorMetric: math.MaxFloat64}
for result := range resultsChan {
if result.ErrorMetric < bestResult.ErrorMetric {
bestResult = result
}
}
xform := bestResult.XForm
xform.ErrorMetric = bestResult.ErrorMetric
log.Printf(" -- %s: %s (%d tried)\n", name, xform, len(xforms))
return xform
}