-
Notifications
You must be signed in to change notification settings - Fork 15
/
mrcal-show-residuals
executable file
·271 lines (221 loc) · 11.3 KB
/
mrcal-show-residuals
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
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
#!/usr/bin/env python3
# Copyright (c) 2017-2023 California Institute of Technology ("Caltech"). U.S.
# Government sponsorship acknowledged. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
r'''Visualize calibration residuals in an imager
SYNOPSIS
$ mrcal-show-residuals --vectorfield left.cameramodel
... a plot pops up showing the vector field of residuals for this camera
This tool supports several different modes, selected by the commandline
arguments. Exactly one of these mode options must be given:
--vectorfield Visualize the optimized residuals as a vector field. Each
vector runs from the observed chessboard corner to its
prediction at the optimal solution.
--magnitudes Visualize the optimized residual magnitudes as color-coded
points. Similar to --vectorfield, but instead of a vector, each
residual is plotted as a colored circle, coded by the MAGNITUDE
of the error. This is usually more legible.
--directions Visualize the optimized residual directions as color-coded
points. Similar to --vectorfield, but instead of a vector, each
residual is plotted as a colored circle, coded by the DIRECTION
of the error. This is very useful in detecting biases caused by
a poorly-fitting lens model: these show up as clusters of
similar color, instead of a random distribution.
--regional Visualize the optimized residuals, broken up by region. The imager
of a camera is subdivided into bins. The residual statistics are
then computed for each bin separately. We can then clearly see
areas of insufficient data (observation counts will be low). And
we can clearly see lens-model-induced biases (non-zero mean) and
we can see heteroscedasticity (uneven standard deviation). The
mrcal-calibrate-cameras tool uses these metrics to construct a
valid-intrinsics region for the models it computes. This serves as
a quick/dirty method of modeling projection reliability, which can
be used even if projection uncertainty cannot be computed.
--histogram Visualize the distribution of the optimized residuals. We display
a histogram of residuals and overlay it with an idealized
gaussian distribution.
'''
import sys
import argparse
import re
import os
def parse_args():
parser = \
argparse.ArgumentParser(description = __doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
mode_group = parser.add_mutually_exclusive_group(required = True)
mode_group.add_argument('--vectorfield',
action='store_true',
help='''Visualize the optimized residuals as a vector field''')
mode_group.add_argument('--magnitudes',
action='store_true',
help='''Visualize the optimized residual magnitudes as color-coded points''')
mode_group.add_argument('--directions',
action='store_true',
help='''Visualize the optimized residual directions as color-coded points''')
mode_group.add_argument('--regional',
action='store_true',
help='''Visualize the optimized residuals, broken up by region''')
mode_group.add_argument('--histogram',
action='store_true',
help='''Visualize the distribution of the optimized residuals''')
mode_group.add_argument('--histogram-this-camera',
action='store_true',
help='''If given, we show the histogram for residuals for THIS camera only. Otherwise
(by default) we display the residuals for all the
cameras in this solve. Implies --histogram''')
parser.add_argument('--valid-intrinsics-region',
action='store_true',
help='''If given, I overlay the valid-intrinsics regions onto the plot. Applies to
all the modes except --histogram''')
parser.add_argument('--gridn',
type=int,
default = (20,14),
nargs = 2,
help='''How densely we should bin the imager. By default we use a 20x14 grid of bins.
Applies only if --regional''')
parser.add_argument('--binwidth',
type=float,
default=0.02,
help='''The width of binds used for the histogram. Defaults to 0.02 pixels. Applies
only if --histogram''')
parser.add_argument('--vectorscale',
type = float,
default = 1.0,
help='''Scale all the vectors by this factor. Useful to improve legibility if the
vectors are too small to see. Applies only if --vectorfield''')
parser.add_argument('--cbmax',
type=float,
help='''Maximum range of the colorbar used in
--vectorfield, --magnitudes. If omitted, we autoscale''')
parser.add_argument('--title',
type=str,
default = None,
help='''Title string for the plot. Overrides the default
title. Exclusive with --extratitle''')
parser.add_argument('--extratitle',
type=str,
default = None,
help='''Additional string for the plot to append to the
default title. Exclusive with --title''')
parser.add_argument('--hardcopy',
type=str,
help='''Write the output to disk, instead of making an
interactive plot. If --regional, then several plots are
made, and the --hardcopy argument is a base name:
"--hardcopy /a/b/c/d.pdf" will produce plots in
"/a/b/c/d.XXX.pdf" where XXX is the type of plot being
made''')
parser.add_argument('--terminal',
type=str,
help=r'''gnuplotlib terminal. The default is good almost always, so most people don't
need this option''')
parser.add_argument('--set',
type=str,
action='append',
help='''Extra 'set' directives to gnuplotlib. Can be given multiple times''')
parser.add_argument('--unset',
type=str,
action='append',
help='''Extra 'unset' directives to gnuplotlib. Can be given multiple times''')
parser.add_argument('model',
type=str,
help=r'''Camera model that contains the optimization_inputs that describe the solve.
The displayed observations may come from ANY of the
cameras in the solve, not necessarily the one given by
this model''')
args = parser.parse_args()
if args.histogram_this_camera:
args.histogram = True
if args.title is not None and \
args.extratitle is not None:
print("--title and --extratitle are exclusive", file=sys.stderr)
sys.exit(1)
return args
args = parse_args()
# arg-parsing is done before the imports so that --help works without building
# stuff, so that I can generate the manpages and README
import mrcal
import numpy as np
import numpysane as nps
plotkwargs_extra = {}
if args.set is not None:
plotkwargs_extra['set'] = args.set
if args.unset is not None:
plotkwargs_extra['unset'] = args.unset
if args.title is not None:
plotkwargs_extra['title'] = args.title
if args.extratitle is not None:
plotkwargs_extra['extratitle'] = args.extratitle
try:
model = mrcal.cameramodel(args.model)
except Exception as e:
print(f"Couldn't load camera model '{args.model}': {e}", file=sys.stderr)
sys.exit(1)
optimization_inputs = model.optimization_inputs()
if optimization_inputs is None:
print(f"Camera model '{args.model}' does not contain optimization inputs. Residuals aren't available", file=sys.stderr)
sys.exit(1)
residuals = mrcal.optimizer_callback(**optimization_inputs)[1]
if not args.hardcopy:
plotkwargs_extra['wait'] = True
kwargs = dict(residuals = residuals,
hardcopy = args.hardcopy,
terminal = args.terminal,
**plotkwargs_extra)
if args.vectorfield:
mrcal.show_residuals_vectorfield(model,
vectorscale = args.vectorscale,
valid_intrinsics_region = args.valid_intrinsics_region,
cbmax = args.cbmax,
**kwargs)
elif args.magnitudes:
mrcal.show_residuals_magnitudes(model,
valid_intrinsics_region = args.valid_intrinsics_region,
cbmax = args.cbmax,
**kwargs)
elif args.directions:
mrcal.show_residuals_directions(model,
valid_intrinsics_region = args.valid_intrinsics_region,
**kwargs)
elif args.histogram:
mrcal.show_residuals_histogram(optimization_inputs,
icam_intrinsics = model.icam_intrinsics() if args.histogram_this_camera else None,
binwidth = args.binwidth,
**kwargs)
elif args.regional:
plotargs = \
mrcal.show_residuals_regional(model,
valid_intrinsics_region = args.valid_intrinsics_region,
gridn_width = args.gridn[0],
gridn_height = args.gridn[1],
return_plot_args = args.hardcopy is None,
**kwargs)
if args.hardcopy:
sys.exit(0)
# This produces 3 plots. I want to pop up the 3 separate windows at the same
# time, and I want to exit when all 3 are done
import gnuplotlib as gp
pids = [0,0,0]
for i in range(3):
pid = os.fork()
if pid == 0:
# child
# make the plot, and wait for it to be closed by the user
(data_tuples,plot_options) = plotargs[i]
p = gp.gnuplotlib(**plot_options)
p.plot(*data_tuples)
sys.exit()
# parent
pids[i] = pid
for i in range(3):
os.waitpid(pids[i], 0)
sys.exit()
else:
print("Unknown mode. Exactly one of the mutually-exclusive variables should have gotten through the argument parser. Is this an argparse bug?",
file=sys.stderr)
sys.exit(1)