c4dynamics 2.0.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- c4dynamics/__init__.py +240 -0
- c4dynamics/datasets/__init__.py +95 -0
- c4dynamics/datasets/_manager.py +596 -0
- c4dynamics/datasets/_registry.py +80 -0
- c4dynamics/detectors/__init__.py +37 -0
- c4dynamics/detectors/yolo3_opencv.py +686 -0
- c4dynamics/detectors/yolo3_tf.py +124 -0
- c4dynamics/eqm/__init__.py +324 -0
- c4dynamics/eqm/derivs.py +212 -0
- c4dynamics/eqm/integrate.py +359 -0
- c4dynamics/filters/__init__.py +1373 -0
- c4dynamics/filters/a.py +48 -0
- c4dynamics/filters/ekf.py +320 -0
- c4dynamics/filters/kalman.py +725 -0
- c4dynamics/filters/kalman_v0.py +1071 -0
- c4dynamics/filters/kalman_v1.py +821 -0
- c4dynamics/filters/lowpass.py +123 -0
- c4dynamics/filters/luenberger.py +97 -0
- c4dynamics/rotmat/__init__.py +141 -0
- c4dynamics/rotmat/animate.py +465 -0
- c4dynamics/rotmat/rotmat.py +351 -0
- c4dynamics/sensors/__init__.py +72 -0
- c4dynamics/sensors/lineofsight.py +78 -0
- c4dynamics/sensors/radar.py +740 -0
- c4dynamics/sensors/seeker.py +1030 -0
- c4dynamics/states/__init__.py +327 -0
- c4dynamics/states/lib/__init__.py +320 -0
- c4dynamics/states/lib/datapoint.py +660 -0
- c4dynamics/states/lib/pixelpoint.py +776 -0
- c4dynamics/states/lib/rigidbody.py +677 -0
- c4dynamics/states/state.py +1486 -0
- c4dynamics/utils/__init__.py +44 -0
- c4dynamics/utils/_struct.py +6 -0
- c4dynamics/utils/const.py +130 -0
- c4dynamics/utils/cprint.py +80 -0
- c4dynamics/utils/gen_gif.py +142 -0
- c4dynamics/utils/idx2keys.py +4 -0
- c4dynamics/utils/images_loader.py +63 -0
- c4dynamics/utils/math.py +136 -0
- c4dynamics/utils/plottools.py +140 -0
- c4dynamics/utils/plottracks.py +304 -0
- c4dynamics/utils/printpts.py +36 -0
- c4dynamics/utils/slides_gen.py +64 -0
- c4dynamics/utils/tictoc.py +167 -0
- c4dynamics/utils/video_gen.py +300 -0
- c4dynamics/utils/vidgen.py +182 -0
- c4dynamics-2.0.3.dist-info/METADATA +242 -0
- c4dynamics-2.0.3.dist-info/RECORD +50 -0
- c4dynamics-2.0.3.dist-info/WHEEL +5 -0
- c4dynamics-2.0.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1071 @@
|
|
|
1
|
+
# type: ignore
|
|
2
|
+
|
|
3
|
+
from scipy.linalg import solve_discrete_are
|
|
4
|
+
from typing import Dict, Optional
|
|
5
|
+
import sys
|
|
6
|
+
sys.path.append('.')
|
|
7
|
+
import c4dynamics as c4d
|
|
8
|
+
import numpy as np
|
|
9
|
+
import warnings
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _noncontwarning(x):
|
|
13
|
+
warnings.warn(f"""The system is not continuous."""
|
|
14
|
+
f"""\nDid you mean {x}?"""
|
|
15
|
+
, c4d.c4warn)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class kalman(c4d.state):
|
|
19
|
+
'''
|
|
20
|
+
Kalman Filter.
|
|
21
|
+
|
|
22
|
+
Kalman Filter class for state estimation.
|
|
23
|
+
:class:`kalman` provides methods for prediction and update
|
|
24
|
+
phases of the Kalman filter, including both discrete and continuous systems.
|
|
25
|
+
|
|
26
|
+
For background material, implementation, and examples,
|
|
27
|
+
please refer to :mod:`filters <c4dynamics.filters>`.
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
Parameters
|
|
32
|
+
==========
|
|
33
|
+
X : dict
|
|
34
|
+
Initial state variables and their values.
|
|
35
|
+
dt : float, optional
|
|
36
|
+
Time step for the filter. Mandatory if continuous-time matrices are provided.
|
|
37
|
+
P0 : numpy.ndarray, optional
|
|
38
|
+
Covariance matrix, or standard deviations array, of the
|
|
39
|
+
initial estimation error. Mandatory if steadystate is False.
|
|
40
|
+
steadystate : bool, optional
|
|
41
|
+
Flag to indicate if the filter is in steady-state mode. Defaults to False.
|
|
42
|
+
A : numpy.ndarray, optional
|
|
43
|
+
Continuous-time state transition matrix. Defaults to None.
|
|
44
|
+
B : numpy.ndarray, optional
|
|
45
|
+
Continuous-time control matrix. Defaults to None.
|
|
46
|
+
C : numpy.ndarray, optional
|
|
47
|
+
Continuous-time measurement matrix. Defaults to None.
|
|
48
|
+
Q : numpy.ndarray, optional
|
|
49
|
+
Continuous-time process noise covariance matrix. Defaults to None.
|
|
50
|
+
R : numpy.ndarray, optional
|
|
51
|
+
Continuous-time measurement noise covariance matrix. Defaults to None.
|
|
52
|
+
F : numpy.ndarray, optional
|
|
53
|
+
Discrete-time state transition matrix. Defaults to None.
|
|
54
|
+
G : numpy.ndarray, optional
|
|
55
|
+
Discrete-time control matrix. Defaults to None.
|
|
56
|
+
H : numpy.ndarray, optional
|
|
57
|
+
Discrete-time measurement matrix. Defaults to None.
|
|
58
|
+
Qk : numpy.ndarray, optional
|
|
59
|
+
Discrete-time process noise covariance matrix. Defaults to None.
|
|
60
|
+
Rk : numpy.ndarray, optional
|
|
61
|
+
Discrete-time measurement noise covariance matrix. Defaults to None.
|
|
62
|
+
|
|
63
|
+
Notes
|
|
64
|
+
=====
|
|
65
|
+
1. `kalman` is a subclass of :class:`state <c4dynamics.states.state.state>`,
|
|
66
|
+
as such the variables provided within the parameter `X` form its state variables.
|
|
67
|
+
Hence, `X` is a dictionary of variables and their initial values, for example:
|
|
68
|
+
``X = {'x': x0, 'y': y0, 'z': z0}``.
|
|
69
|
+
|
|
70
|
+
2. The filter may be initialized with either continuous-time matrices
|
|
71
|
+
or with discrete-time matrices.
|
|
72
|
+
However, all the necessary parameters,
|
|
73
|
+
i.e. :math:`A, B, Q, R` (for continuous system) or :math:`F, G, Qk, Rk`
|
|
74
|
+
(for discrete system) must be provided consistently.
|
|
75
|
+
|
|
76
|
+
3. If continuous-time matrices are provided, then a time step parameter `dt`
|
|
77
|
+
has to be provided for the integration of the system at the
|
|
78
|
+
:meth:`predict` stage.
|
|
79
|
+
|
|
80
|
+
4. Steady-state mode: if the underlying system is linear time-invariant (LTI),
|
|
81
|
+
and also the noise covariance matrices are time-invariant,
|
|
82
|
+
then a steady-state mode of the Kalman filter can be utilized.
|
|
83
|
+
In steady-state mode the Kalman gain (`K`) and the estimation covariance matrix
|
|
84
|
+
(`P`) are computed once and are constant ('steady-state') for the entire run-time,
|
|
85
|
+
performs as well as the time-varying filter.
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
Raises
|
|
90
|
+
======
|
|
91
|
+
TypeError:
|
|
92
|
+
If X is not a dictionary.
|
|
93
|
+
ValueError:
|
|
94
|
+
If P0 is not provided when steadystate is False.
|
|
95
|
+
ValueError:
|
|
96
|
+
If neither continuous nor discrete system matrices are fully provided.
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
See Also
|
|
101
|
+
========
|
|
102
|
+
.filters
|
|
103
|
+
.ekf
|
|
104
|
+
.lowpass
|
|
105
|
+
.seeker
|
|
106
|
+
.eqm
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
Examples
|
|
112
|
+
========
|
|
113
|
+
|
|
114
|
+
The examples in the introduction to the
|
|
115
|
+
:mod:`filters <c4dynamics.filters>`
|
|
116
|
+
module demonstrate the operations of
|
|
117
|
+
the Kalman filter
|
|
118
|
+
for inputs from
|
|
119
|
+
electromagnetic devices, such as an altimeter,
|
|
120
|
+
which measures the altitude.
|
|
121
|
+
|
|
122
|
+
In the following set of examples, we run a Kalman filter
|
|
123
|
+
to demonstrate smooth and continuous tracking of vehicles
|
|
124
|
+
across video frames.
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
**1. Setup**
|
|
128
|
+
|
|
129
|
+
The process model assumes linear motion with constant velocity
|
|
130
|
+
where the system matrix
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
.. math::
|
|
134
|
+
|
|
135
|
+
A = \\begin{bmatrix}
|
|
136
|
+
0 & 0 & 0 & 0 & 1 & 0 \\\\
|
|
137
|
+
0 & 0 & 0 & 0 & 0 & 1 \\\\
|
|
138
|
+
0 & 0 & 0 & 0 & 0 & 0 \\\\
|
|
139
|
+
0 & 0 & 0 & 0 & 0 & 0 \\\\
|
|
140
|
+
0 & 0 & 0 & 0 & 0 & 0 \\\\
|
|
141
|
+
0 & 0 & 0 & 0 & 0 & 0
|
|
142
|
+
\\end{bmatrix}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
represents the linear ordinary differential equations:
|
|
146
|
+
|
|
147
|
+
.. math::
|
|
148
|
+
|
|
149
|
+
\\dot{x} = v_x \\\\
|
|
150
|
+
\\dot{y} = v_y \\\\
|
|
151
|
+
\\dot{w} = 0 \\\\
|
|
152
|
+
\\dot{h} = 0 \\\\
|
|
153
|
+
\\dot{v}_x = 0 \\\\
|
|
154
|
+
\\dot{v}_y = 0
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
It is therefore obvious that
|
|
158
|
+
the system state vector is given by:
|
|
159
|
+
|
|
160
|
+
.. math::
|
|
161
|
+
|
|
162
|
+
x = [x, y, w, h, v_x, v_y]^T
|
|
163
|
+
|
|
164
|
+
Where :math:`x, y` are pixel coordinates, :math:`w, h` are bounding box dimensions, and :math:`v_x, v_y` are velocities.
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
As measurement for the vehicle position and
|
|
168
|
+
box size we use the `YOLOv3` object detection model.
|
|
169
|
+
YOLOv3 is incorporated in the c4dynamics' class
|
|
170
|
+
:class:`yolov3 <c4dynamics.detectors.yolo3_opencv.yolov3>`.
|
|
171
|
+
|
|
172
|
+
The method :meth:`yolov3.detect <c4dynamics.detectors.yolo3_opencv.yolov3.detect>`
|
|
173
|
+
returns a :class:`pixelpoint <c4dynamics.states.lib.pixelpoint.pixelpoint>` instance
|
|
174
|
+
for each detected object:
|
|
175
|
+
|
|
176
|
+
.. code::
|
|
177
|
+
|
|
178
|
+
>>> from c4dynamics import pixelpoint
|
|
179
|
+
>>> print(pixelpoint())
|
|
180
|
+
[ x y w h ]
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
That is, the measurements fed into the Kalman filter are
|
|
184
|
+
directly the first four variables of the state.
|
|
185
|
+
|
|
186
|
+
From this, we can directly derive the
|
|
187
|
+
measurement matrix that forms the
|
|
188
|
+
relation between the measurements and the state:
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
.. math::
|
|
192
|
+
|
|
193
|
+
C = \\begin{bmatrix}
|
|
194
|
+
1 & 0 & 0 & 0 & 0 & 0 \\\\
|
|
195
|
+
0 & 1 & 0 & 0 & 0 & 0 \\\\
|
|
196
|
+
0 & 0 & 1 & 0 & 0 & 0 \\\\
|
|
197
|
+
0 & 0 & 0 & 1 & 0 & 0
|
|
198
|
+
\\end{bmatrix}
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
This also implies that the system is observable but to be on the safe side
|
|
202
|
+
let's examine the rank of the observability matrix.
|
|
203
|
+
|
|
204
|
+
First, import the required packages for the code
|
|
205
|
+
in this snippet and the ones that follow:
|
|
206
|
+
|
|
207
|
+
.. code::
|
|
208
|
+
|
|
209
|
+
>>> import c4dynamics as c4d
|
|
210
|
+
>>> from matplotlib import pyplot as plt
|
|
211
|
+
>>> from scipy.linalg import expm
|
|
212
|
+
>>> import numpy as np
|
|
213
|
+
>>> import cv2
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
Let's define the system matrices:
|
|
217
|
+
|
|
218
|
+
.. code::
|
|
219
|
+
|
|
220
|
+
>>> A = np.zeros((6, 6))
|
|
221
|
+
>>> A[0, 4] = A[1, 5] = 1
|
|
222
|
+
>>> C = np.zeros((4, 6))
|
|
223
|
+
>>> C[0, 0] = C[1, 1] = C[2, 2] = C[3, 3] = 1
|
|
224
|
+
|
|
225
|
+
Now, build the observability matrix and check the rank:
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
.. code::
|
|
229
|
+
|
|
230
|
+
>>> obsv = C
|
|
231
|
+
>>> n = len(A)
|
|
232
|
+
>>> for i in range(1, n):
|
|
233
|
+
... obsv = np.vstack((obsv, C @ np.linalg.matrix_power(A, i)))
|
|
234
|
+
>>> rank = np.linalg.matrix_rank(obsv)
|
|
235
|
+
>>> c4d.cprint(f'The system is observable (rank = n = {n}).' if rank == n else 'The system is not observable (rank = {rank), n = {n}).', 'y')
|
|
236
|
+
The system is observable
|
|
237
|
+
|
|
238
|
+
In each estimation, the `box` function converts the state coordinates to rectangle
|
|
239
|
+
corners to draw a bounding box:
|
|
240
|
+
|
|
241
|
+
.. code::
|
|
242
|
+
|
|
243
|
+
>>> def box(X):
|
|
244
|
+
... # top left
|
|
245
|
+
... xtl = int(X[0] - X[2] / 2)
|
|
246
|
+
... ytl = int(X[1] - X[3] / 2)
|
|
247
|
+
... # bottom right
|
|
248
|
+
... xbr = int(X[0] + X[2] / 2)
|
|
249
|
+
... ybr = int(X[1] + X[3] / 2)
|
|
250
|
+
... return [(xtl, ytl), (xbr, ybr)]
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
The video in the following examples is used
|
|
255
|
+
by kind permission of `Abed Ismail <https://www.pexels.com/@abed-ismail>`_
|
|
256
|
+
|
|
257
|
+
The video can be fetched using the c4dynamics'
|
|
258
|
+
datasets module (see :mod:`c4dynamics.datasets`):
|
|
259
|
+
|
|
260
|
+
.. code::
|
|
261
|
+
|
|
262
|
+
>>> vidpath = c4d.datasets.video('drifting_car')
|
|
263
|
+
Fetched successfully
|
|
264
|
+
|
|
265
|
+
Video setup:
|
|
266
|
+
|
|
267
|
+
.. code::
|
|
268
|
+
|
|
269
|
+
>>> video_cap = cv2.VideoCapture(vidpath)
|
|
270
|
+
>>> fps = video_cap.get(cv2.CAP_PROP_FPS)
|
|
271
|
+
>>> dt_frame = 1 / fps
|
|
272
|
+
|
|
273
|
+
Let's take the prediction rate to be twice the frames rate:
|
|
274
|
+
|
|
275
|
+
.. code::
|
|
276
|
+
|
|
277
|
+
>>> dt = dt_frame / 2
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
**2. Steady-state mode**
|
|
281
|
+
|
|
282
|
+
As start, let's take the noise matrices (:math:`Q_k` of the process, and
|
|
283
|
+
:math:`R_k` of the measurement) as constant. Since the system is
|
|
284
|
+
LTI (linear time invariant), the Kalman gain (`K`) and consequently the estimation covariance matrix
|
|
285
|
+
(`P`) are computed once and are constant ('steady-state') for the entire run-time.
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
Dynamics model and noise matrices:
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
# TODO im not sure anymore its needed to start with cont.
|
|
292
|
+
just add the cont matrices and explain why the covariances are the same.
|
|
293
|
+
# TODO my conclusion from all this is that it's not an example.
|
|
294
|
+
its a program. examples are short and straright froward and not
|
|
295
|
+
entail all this intro.
|
|
296
|
+
# maybe to separate between disc and cont only in the
|
|
297
|
+
sys matrices but in the covariance to leave it to the user consid.
|
|
298
|
+
# 3. instead of messing with all this maybe just show simple things.
|
|
299
|
+
things that relevant to the user and not to the fresh class studegnt of eng.
|
|
300
|
+
focus on seeing the state of the kalman. of initializaing. of storging.
|
|
301
|
+
much more important for this class. and move this example to programs==usecases.
|
|
302
|
+
|
|
303
|
+
.. code::
|
|
304
|
+
|
|
305
|
+
>>> # process dynamics
|
|
306
|
+
>>> A = np.zeros((6, 6))
|
|
307
|
+
>>> A[0, 4] = A[1, 5] = 1
|
|
308
|
+
>>> F = expm(A * dt)
|
|
309
|
+
>>> # measurement model
|
|
310
|
+
>>> H = np.zeros((4, 6))
|
|
311
|
+
>>> H[0, 0] = H[1, 1] = H[2, 2] = H[3, 3] = 1
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
From some exprerience with the objects detection model it is
|
|
315
|
+
a fair evaulation to give the model an average error of 4 pixels
|
|
316
|
+
both for position and box size.
|
|
317
|
+
Assuming that the uncertainty TODO ??
|
|
318
|
+
|
|
319
|
+
The selection of the noise errors:
|
|
320
|
+
|
|
321
|
+
.. code::
|
|
322
|
+
|
|
323
|
+
>>> # covariance matrices
|
|
324
|
+
>>> process_std = measure_std = 4
|
|
325
|
+
>>> Qk = np.eye(6) * process_std**2 # process_noise
|
|
326
|
+
>>> Rk = np.eye(4) * measure_std**2 # measure_noise
|
|
327
|
+
|
|
328
|
+
indicates that the errors associated with the process
|
|
329
|
+
and the errors associated with the measurement
|
|
330
|
+
have equal weight (a standard deviation of `4`, units depend on the
|
|
331
|
+
variable).
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
Kalman object definition.
|
|
336
|
+
The initialization includes the state variables, mode, and matrices:
|
|
337
|
+
|
|
338
|
+
.. code::
|
|
339
|
+
|
|
340
|
+
>>> kf = kalman({'x': 0, 'y': 0, 'w': 0, 'h': 0, 'vx': 0, 'vy': 0}
|
|
341
|
+
... , steadystate = True, F = F, H = H, Qk = Qk, Rk = Rk)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
Object detection model:
|
|
345
|
+
|
|
346
|
+
.. code::
|
|
347
|
+
|
|
348
|
+
>>> yolo3 = c4d.detectors.yolov3()
|
|
349
|
+
Fetched successfully
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
Main loop. The first step, prediction, occurs in every cycle.
|
|
353
|
+
The second step, update (correction), occurs when a car detection is made:
|
|
354
|
+
|
|
355
|
+
.. code::
|
|
356
|
+
|
|
357
|
+
>>> t = 0
|
|
358
|
+
>>> while video_cap.isOpened():
|
|
359
|
+
... t += dt
|
|
360
|
+
... # predict
|
|
361
|
+
... kf.predict()
|
|
362
|
+
... ret, frame = video_cap.read()
|
|
363
|
+
... if not ret: break
|
|
364
|
+
... d = yolo3.detect(frame)
|
|
365
|
+
... if d and d[0].class_id == 'car':
|
|
366
|
+
... # correct
|
|
367
|
+
... kf.update(d[0].X)
|
|
368
|
+
... kf.detect = d
|
|
369
|
+
... kf.storeparams('detect', t)
|
|
370
|
+
... kf.store(t)
|
|
371
|
+
... _ = cv2.rectangle(frame, box(kf.X)[0], box(kf.X)[1], [0, 255, 0], 2)
|
|
372
|
+
... cv2.imshow('', frame)
|
|
373
|
+
... cv2.waitKey(10)
|
|
374
|
+
>>> cv2.destroyAllWindows()
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
.. figure:: /_examples/kf/drifting_car.gif
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
**3. Plotting**
|
|
383
|
+
|
|
384
|
+
The :meth:`plot <c4dynamics.states.state.state.plot>`
|
|
385
|
+
method of the superclass :class:`state <c4dynamics.states.state.state>`
|
|
386
|
+
allows direct generation of the state variables.
|
|
387
|
+
The plot of the position `x` is given by:
|
|
388
|
+
|
|
389
|
+
.. code::
|
|
390
|
+
|
|
391
|
+
>>> kf.plot('x')
|
|
392
|
+
>>> plt.show()
|
|
393
|
+
|
|
394
|
+
.. figure:: /_examples/kf/steadystate_x.png
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
Now, since we also stored the detections (using
|
|
399
|
+
:meth:`storeparams <c4dynamics.states.state.state.storeparams>`),
|
|
400
|
+
we can add the detection marks on the state line:
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
.. code::
|
|
404
|
+
|
|
405
|
+
>>> plt.plot(*kf.data('x'), 'om', label = 'estimation')
|
|
406
|
+
>>> plt.gca().plot(kf.data('detect')[0], np.vectorize(lambda d: d.x if isinstance(d, c4d.pixelpoint) else np.nan)(kf.data('detect')[1]), 'co', label = 'detection')
|
|
407
|
+
>>> c4d.plotdefaults(plt.gca(), 'x - steady-state mode', 'Time', 'x', 8)
|
|
408
|
+
>>> plt.legend()
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
The first argument (:code:`kf.data('detect')[0]`) in the third line is
|
|
412
|
+
just the time series of the detections at the storing samples.
|
|
413
|
+
The second argument uses numpy's `vectorize` to extract the
|
|
414
|
+
`x` field from the detection data.
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
.. code::
|
|
418
|
+
|
|
419
|
+
>>> plt.show()
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
.. figure:: /_examples/kf/steadystate_detections.png
|
|
423
|
+
|
|
424
|
+
By focusing on an arbitrary region the operation of the prediction is revealed.
|
|
425
|
+
While the frame rate is 30 frames per second, the main loop runs 60 frames
|
|
426
|
+
per second.
|
|
427
|
+
For every cycle where no image is taken, the prediction
|
|
428
|
+
estimates the object's position based on the dynamics model:
|
|
429
|
+
|
|
430
|
+
.. figure:: /_examples/kf/steadystate_detections_zoom.png
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
This is true also for the edges where the object is outside the frame and
|
|
434
|
+
wherever the detection model fails to identify the object in the frame.
|
|
435
|
+
In such cases, the Kalman filter provides
|
|
436
|
+
an optimal estimation of the objects' current state.
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
By default, kalman's :meth:`store` stores also samples of the
|
|
440
|
+
main diagonal of `P`, the covariance matrix. Each element
|
|
441
|
+
is named `Pii`, where `i` is the index of the variable in
|
|
442
|
+
the state. Here `x` is the first variable:
|
|
443
|
+
|
|
444
|
+
.. code::
|
|
445
|
+
|
|
446
|
+
>>> print(kf)
|
|
447
|
+
[ x y w h vx vy ]
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
Then extracting the standard deviations of `x` from the storage
|
|
451
|
+
is possible by:
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
.. code::
|
|
455
|
+
|
|
456
|
+
>>> t_std, x_std = kf.data('P00')[0], np.sqrt(kf.data('P00')[1])
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
As before, the first argument provides the
|
|
460
|
+
time series for the samples of `P00`.
|
|
461
|
+
In the second argument, we take
|
|
462
|
+
square root of the values of `P00` to convert the variances to standard deviations.
|
|
463
|
+
|
|
464
|
+
The standard deviations represent the estimation error.
|
|
465
|
+
It is therefore convinent to plot them alongside the state variables:
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
.. code::
|
|
469
|
+
|
|
470
|
+
>>> plt.gca().plot(t_std, kf.data('x')[1] - x_std, 'w', linewidth = 1, label = 'std')
|
|
471
|
+
>>> plt.gca().plot(t_std, kf.data('x')[1] + x_std, 'w', linewidth = 1)
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
.. figure:: /_examples/kf/steadystate_std.png
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
The nature of the steady-state mode is conveyed here
|
|
478
|
+
by the constant variance, which represents the error in the variable
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
**4. Discrete system**
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
In the previous example, we ran the filter in steady-state mode.
|
|
490
|
+
That means that the estimation error (the state covariance matrix `P`)
|
|
491
|
+
is calculated once and remains
|
|
492
|
+
constant during the filter runtime.
|
|
493
|
+
|
|
494
|
+
This mode is enabled when the covariance matrices
|
|
495
|
+
that describe the process noise (:math:`Q` or :math:`Q_k`)
|
|
496
|
+
and the measurement noise (:math:`R` or :math:`R_k`) are
|
|
497
|
+
themselves constant.
|
|
498
|
+
|
|
499
|
+
However, when the noise matrices are time-varying,
|
|
500
|
+
steady-state mode is not feasible.
|
|
501
|
+
|
|
502
|
+
The previous case may be improved by adjusting
|
|
503
|
+
the process noise matrix :math:`Q_k`.
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
Let's re-examine the plot of the x-coordinate over time:
|
|
507
|
+
|
|
508
|
+
.. figure:: /_examples/kf/steadystate_x.png
|
|
509
|
+
|
|
510
|
+
The dynamics model assumes linear motion.
|
|
511
|
+
However, the actual motion in the x-coordinate
|
|
512
|
+
is approximately linear up to `4s`, but then
|
|
513
|
+
changes direction, continues linearly until
|
|
514
|
+
`7s`, and changes direction again until exit the frame.
|
|
515
|
+
|
|
516
|
+
In fact, in the vicinity of `t = 4s`, there is a
|
|
517
|
+
significant gap between the
|
|
518
|
+
estimation (magenta) and the detection measures (cyan):
|
|
519
|
+
|
|
520
|
+
.. figure:: /_examples/kf/steadystate_std_zoom.png
|
|
521
|
+
|
|
522
|
+
The reason is that the filter relies on the process model
|
|
523
|
+
just as it trusts the measurements and therefore
|
|
524
|
+
averages the predictons and the measurements.
|
|
525
|
+
|
|
526
|
+
Recall that we used :math:`Q_k, R_k` a diagonal matrices with
|
|
527
|
+
a standard deviation of `4`:
|
|
528
|
+
|
|
529
|
+
.. code::
|
|
530
|
+
|
|
531
|
+
>>> process_std = measure_std = 4
|
|
532
|
+
>>> Qk = np.eye(6) * process_std**2
|
|
533
|
+
>>> Rk = np.eye(4) * measure_std**2
|
|
534
|
+
|
|
535
|
+
To address the gap between the estimation and the detections,
|
|
536
|
+
let's make the process noise :math:`Q_k` less
|
|
537
|
+
tight around `t = 4s`:
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
.. math::
|
|
541
|
+
|
|
542
|
+
process std = \\begin{cases}
|
|
543
|
+
8 & \\text{3.9 < t < 4.15} \\\\
|
|
544
|
+
4 & \\text{otherwise}
|
|
545
|
+
\\end{cases}
|
|
546
|
+
|
|
547
|
+
Namely, at `t = 4s`, the process error is high, and the filter
|
|
548
|
+
should place less weight on the process model.
|
|
549
|
+
|
|
550
|
+
In fact, since the filter recalculates the covariance at each time step,
|
|
551
|
+
it is better to reduce :math:`R_k` and :math:`Q_k` by a factor compared
|
|
552
|
+
to the steady state mode values. Here, the factor is set to `0.5`.
|
|
553
|
+
|
|
554
|
+
.. code::
|
|
555
|
+
|
|
556
|
+
>>> noisefactor = 0.5
|
|
557
|
+
>>> Qk *= noisefactor
|
|
558
|
+
>>> Rk *= noisefactor
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
The filter initialization is similar to the previous case,
|
|
564
|
+
with the steady-state flag omitted.
|
|
565
|
+
|
|
566
|
+
Discrete system kalman initalization:
|
|
567
|
+
|
|
568
|
+
.. code::
|
|
569
|
+
|
|
570
|
+
>>> kf = c4d.filters.kalman({'x': 0, 'y': 0, 'w': 0, 'h': 0, 'vx': 0, 'vy': 0}
|
|
571
|
+
, P0 = Qk, F = F, H = H, Qk = Qk, Rk = Rk)
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
The main loop is only modified to include the change in :math:`Q_k`:
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
.. code::
|
|
580
|
+
|
|
581
|
+
>>> t = 0
|
|
582
|
+
>>> # main loop
|
|
583
|
+
>>> while video_cap.isOpened():
|
|
584
|
+
... kf.store(t)
|
|
585
|
+
... t += dt
|
|
586
|
+
... if t > 3.9 and t < 4.15:
|
|
587
|
+
... Qk = np.eye(6) * 8**2 * noisefactor
|
|
588
|
+
... else:
|
|
589
|
+
... Qk = np.eye(6) * 4**2 * noisefactor
|
|
590
|
+
... kf.predict(Qk = Qk)
|
|
591
|
+
... if round(t / dt_frame, 1) % 1 >= 1e-10: continue
|
|
592
|
+
... # camera cycle:
|
|
593
|
+
... ret, frame = video_cap.read()
|
|
594
|
+
... if not ret: break
|
|
595
|
+
... d = yolo3.detect(frame)
|
|
596
|
+
... if d and (d[0].class_id == 'car'):
|
|
597
|
+
... kf.update(d[0].X)
|
|
598
|
+
... kf.detect = d
|
|
599
|
+
... kf.storeparams('detect', t)
|
|
600
|
+
... cv2.rectangle(frame, box(kf.X)[0], box(kf.X)[1], [0, 255, 0], 2)
|
|
601
|
+
... cv2.imshow('', frame)
|
|
602
|
+
... cv2.waitKey(10)
|
|
603
|
+
>>> cv2.destroyAllWindows()
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
Now, the measures should respond stronger
|
|
607
|
+
when the car changes direction at :math:`t \\approx 4s`:
|
|
608
|
+
|
|
609
|
+
.. figure:: /_examples/kf/discrete_std_zoom.png
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
**5. Continuous system**
|
|
616
|
+
|
|
617
|
+
We can achieve the same result by running continuous system.
|
|
618
|
+
|
|
619
|
+
The respective system
|
|
620
|
+
|
|
621
|
+
Let:
|
|
622
|
+
|
|
623
|
+
.. code::
|
|
624
|
+
|
|
625
|
+
Q = Qk / dt
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
This however not suprising, as the class and its methods converts any input
|
|
629
|
+
system to a discerte represnation according to the inverse of the equations above
|
|
630
|
+
and run the filter.
|
|
631
|
+
|
|
632
|
+
'''
|
|
633
|
+
# TODO maybe change 'time histories' with 'time series' or 'time evolution'
|
|
634
|
+
|
|
635
|
+
Kinf = None
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
# def __init__(self, X, dt: Optional[float] = None, P0 = None, steadystate = False
|
|
639
|
+
# , A = None, C = None, Q = None, R = None, B = None
|
|
640
|
+
# , F = None, H = None, Qk = None, Rk = None, G = None):
|
|
641
|
+
def __init__(self, X: dict, dt: Optional[float] = None, P0: Optional[np.ndarray] = None, steadystate: bool = False
|
|
642
|
+
, A: Optional[np.ndarray] = None, B: Optional[np.ndarray] = None, C: Optional[np.ndarray] = None
|
|
643
|
+
, Q: Optional[np.ndarray] = None, R: Optional[np.ndarray] = None
|
|
644
|
+
, F: Optional[np.ndarray] = None, G: Optional[np.ndarray] = None, H: Optional[np.ndarray] = None
|
|
645
|
+
, Qk: Optional[np.ndarray] = None, Rk: Optional[np.ndarray] = None):
|
|
646
|
+
#
|
|
647
|
+
# P0 is mandatory and it is either the initial state covariance matrix itself or
|
|
648
|
+
# a vector of the diagonal standard deviations.
|
|
649
|
+
# dt is for the predict integration.
|
|
650
|
+
# F and H are linear transition matrix and linear measure matrix for
|
|
651
|
+
# a linear kalman filter.
|
|
652
|
+
# Q and R are process noise and measure noise matrices when they are time invariant.
|
|
653
|
+
##
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
if not isinstance(X, dict):
|
|
658
|
+
raise TypeError("""X must be a dictionary containig pairs of variables
|
|
659
|
+
and initial conditions, e.g.: {''x'': 0, ''y'': 0}""")
|
|
660
|
+
super().__init__(**X)
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
# initialize cont or discrete system
|
|
664
|
+
self.isdiscrete = True
|
|
665
|
+
self.G = None
|
|
666
|
+
if A is not None and C is not None:
|
|
667
|
+
# continuous system
|
|
668
|
+
#
|
|
669
|
+
self.isdiscrete = False
|
|
670
|
+
if dt is None:
|
|
671
|
+
raise ValueError("""dt is necessary for a continuous system""")
|
|
672
|
+
|
|
673
|
+
self.dt = dt
|
|
674
|
+
#
|
|
675
|
+
self.F = np.eye(len(A)) + A * dt
|
|
676
|
+
self.H = np.atleast_2d(C)
|
|
677
|
+
if B is not None:
|
|
678
|
+
self.G = np.atleast_2d(B) * dt
|
|
679
|
+
if Q is not None:
|
|
680
|
+
self.Qk = np.atleast_2d(Q) * dt
|
|
681
|
+
if R is not None:
|
|
682
|
+
self.Rk = np.atleast_2d(R) / dt
|
|
683
|
+
|
|
684
|
+
elif F is not None and H is not None:
|
|
685
|
+
# discrete
|
|
686
|
+
self.F = np.atleast_2d(F)
|
|
687
|
+
self.H = np.atleast_2d(H)
|
|
688
|
+
if G is not None:
|
|
689
|
+
self.G = np.atleast_2d(G)
|
|
690
|
+
if Qk is not None:
|
|
691
|
+
self.Qk = np.atleast_2d(Qk)
|
|
692
|
+
if Rk is not None:
|
|
693
|
+
self.Rk = np.atleast_2d(Rk)
|
|
694
|
+
|
|
695
|
+
else:
|
|
696
|
+
raise ValueError("""At least one set of matrices has to be entirely provided:
|
|
697
|
+
\nFor a continuous system: A, C (B is optional).
|
|
698
|
+
\nWhere: x'' = A*x + B*u + w, y = C*x + v, E(w*w^T) = Q*delta(t), E(v*v^T) = Q*delta(t).
|
|
699
|
+
\nFor a dicscrete system: F, H (G is optional).
|
|
700
|
+
\nWhere: x(k) = F*x(k-1) + G*u(k-1) + wk, y(k) = H*x(k), E(wk*wk^T) = Qk*delta(k), E(vk*vk^T) = Rk * delta(k)""")
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
if steadystate:
|
|
704
|
+
# in steady state mode Q and R or Qk and Rk must be provided:
|
|
705
|
+
if self.Qk is None or self.Rk is None:
|
|
706
|
+
raise ValueError("""In steady-state mode at least one set of noise matrices must be entirely provided:"""
|
|
707
|
+
"""\nFor a continuous system: Q, R. """
|
|
708
|
+
"""\nWhere: x'' = A*x + B*u + w, y = C*x + v, E(w*w^T) = Q*delta(t), E(v*v^T) = Q*delta(t). """
|
|
709
|
+
"""\nFor a dicscrete system: Qk, Rk."""
|
|
710
|
+
"""\nWhere: x(k) = F*x(k-1) + G*u(k-1) + wk, y(k) = H*x(k), E(wk*wk^T) = Qk*delta(k), E(vk*vk^T) = Rk * delta(k)""")
|
|
711
|
+
|
|
712
|
+
self.P = solve_discrete_are(self.F.T, self.H.T, self.Qk, self.Rk)
|
|
713
|
+
self.Kinf = self.P @ self.H.T @ np.linalg.inv(self.H @ self.P @ self.H.T + self.Rk)
|
|
714
|
+
|
|
715
|
+
else: # steady state is off
|
|
716
|
+
if P0 is None:
|
|
717
|
+
# NOTE maybe init with zeros and raising warning is better solution.
|
|
718
|
+
raise ValueError(r'P0 is a necessary variable (optional only in steadystate mode)')
|
|
719
|
+
|
|
720
|
+
P0 = np.atleast_2d(P0)
|
|
721
|
+
|
|
722
|
+
if P0.shape[0] == P0.shape[1]:
|
|
723
|
+
# square matrix
|
|
724
|
+
self.P = P0
|
|
725
|
+
else:
|
|
726
|
+
# only standard deviations are provided
|
|
727
|
+
# self.P = np.diag(P0.flatten()**2)
|
|
728
|
+
self.P = np.diag(P0.ravel()**2)
|
|
729
|
+
|
|
730
|
+
self._Pdata = []
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
@property
|
|
735
|
+
def A(self):
|
|
736
|
+
if self.isdiscrete:
|
|
737
|
+
_noncontwarning('F')
|
|
738
|
+
return None
|
|
739
|
+
|
|
740
|
+
a = (self.F - np.eye(len(self.F))) / self.dt
|
|
741
|
+
return a
|
|
742
|
+
|
|
743
|
+
@A.setter
|
|
744
|
+
def A(self, a):
|
|
745
|
+
if self.isdiscrete:
|
|
746
|
+
_noncontwarning('F')
|
|
747
|
+
else:
|
|
748
|
+
self.F = np.eye(len(a)) + a * self.dt
|
|
749
|
+
|
|
750
|
+
|
|
751
|
+
@property
|
|
752
|
+
def B(self):
|
|
753
|
+
if self.isdiscrete:
|
|
754
|
+
_noncontwarning('G')
|
|
755
|
+
return None
|
|
756
|
+
return self.G / self.dt
|
|
757
|
+
|
|
758
|
+
@B.setter
|
|
759
|
+
def B(self, b):
|
|
760
|
+
if self.isdiscrete:
|
|
761
|
+
_noncontwarning('G')
|
|
762
|
+
else:
|
|
763
|
+
self.G = b * self.dt
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
@property
|
|
767
|
+
def C(self):
|
|
768
|
+
if self.isdiscrete:
|
|
769
|
+
_noncontwarning('H')
|
|
770
|
+
return None
|
|
771
|
+
return self.H
|
|
772
|
+
|
|
773
|
+
@C.setter
|
|
774
|
+
def C(self, c):
|
|
775
|
+
if self.isdiscrete:
|
|
776
|
+
_noncontwarning('H')
|
|
777
|
+
else:
|
|
778
|
+
self.H = c
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
@property
|
|
782
|
+
def Q(self):
|
|
783
|
+
if self.isdiscrete:
|
|
784
|
+
_noncontwarning('Qk')
|
|
785
|
+
return None
|
|
786
|
+
return self.Qk / self.dt
|
|
787
|
+
|
|
788
|
+
@Q.setter
|
|
789
|
+
def Q(self, q):
|
|
790
|
+
if self.isdiscrete:
|
|
791
|
+
_noncontwarning('Qk')
|
|
792
|
+
else:
|
|
793
|
+
self.Qk = q * self.dt
|
|
794
|
+
|
|
795
|
+
|
|
796
|
+
@property
|
|
797
|
+
def R(self):
|
|
798
|
+
if self.isdiscrete:
|
|
799
|
+
_noncontwarning('Rk')
|
|
800
|
+
return None
|
|
801
|
+
return self.Rk * self.dt
|
|
802
|
+
|
|
803
|
+
@R.setter
|
|
804
|
+
def R(self, r):
|
|
805
|
+
if self.isdiscrete:
|
|
806
|
+
_noncontwarning('Rk')
|
|
807
|
+
else:
|
|
808
|
+
self.Rk = r / self.dt
|
|
809
|
+
|
|
810
|
+
|
|
811
|
+
# def predict(self, u = None):
|
|
812
|
+
def predict(self, u: Optional[np.ndarray] = None
|
|
813
|
+
, Q: Optional[np.ndarray] = None, Qk: Optional[np.ndarray] = None):
|
|
814
|
+
'''
|
|
815
|
+
Predicts the next state and covariance based
|
|
816
|
+
on the current state and process model.
|
|
817
|
+
|
|
818
|
+
Parameters
|
|
819
|
+
----------
|
|
820
|
+
u : numpy.ndarray, optional
|
|
821
|
+
Control input. Defaults to None.
|
|
822
|
+
|
|
823
|
+
|
|
824
|
+
FROM PIXELPOINT:
|
|
825
|
+
|
|
826
|
+
|
|
827
|
+
Gets and sets the frame size.
|
|
828
|
+
|
|
829
|
+
Parameters
|
|
830
|
+
----------
|
|
831
|
+
fsize : tuple
|
|
832
|
+
Size of the frame in pixels (width, height).
|
|
833
|
+
- (width) int : Frame width in pixels.
|
|
834
|
+
- (height) int : Frame height in pixels.
|
|
835
|
+
|
|
836
|
+
|
|
837
|
+
Returns
|
|
838
|
+
-------
|
|
839
|
+
out : tuple
|
|
840
|
+
A tuple of the frame size in pixels (width, height).
|
|
841
|
+
|
|
842
|
+
|
|
843
|
+
Raises
|
|
844
|
+
------
|
|
845
|
+
ValueError
|
|
846
|
+
If `fsize` doesn't have exactly two elements, a ValueError is raised.
|
|
847
|
+
|
|
848
|
+
|
|
849
|
+
Examples
|
|
850
|
+
--------
|
|
851
|
+
For detailed usage,
|
|
852
|
+
see the examples in the introduction to
|
|
853
|
+
the :class:`kalman` class.
|
|
854
|
+
|
|
855
|
+
define kalman:
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
run in steadys tate:
|
|
859
|
+
|
|
860
|
+
run with control input
|
|
861
|
+
|
|
862
|
+
run with varying discerte Qk
|
|
863
|
+
|
|
864
|
+
run with cont vayring Q
|
|
865
|
+
|
|
866
|
+
|
|
867
|
+
|
|
868
|
+
'''
|
|
869
|
+
# TODO test the size of the objects.
|
|
870
|
+
# test the type.
|
|
871
|
+
# make sure the user is working with c4d modules.
|
|
872
|
+
# actually only u should be tested here all the other need be tested at the init stage.
|
|
873
|
+
# this F must be linear, but it can be linearized once for the entire
|
|
874
|
+
# process (regular kalman), or linearized and delivered at each step (extended kalman)
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
if self.Kinf is None:
|
|
878
|
+
|
|
879
|
+
if Q is not None:
|
|
880
|
+
# NOTE in disc system this line raises non-existing dt error.
|
|
881
|
+
# maybe it's better idea to check always the type of the system before updating any parameter.
|
|
882
|
+
self.Qk = np.atleast_2d(Q) * self.dt
|
|
883
|
+
elif Qk is not None:
|
|
884
|
+
self.Qk = np.atleast_2d(Qk)
|
|
885
|
+
elif self.Qk is None:
|
|
886
|
+
raise ValueError(r'Q or Qk must be provided in every call to predict() (optional only in steadystate mode)')
|
|
887
|
+
|
|
888
|
+
self.P = self.F @ self.P @ self.F.T + self.Qk
|
|
889
|
+
# self.P = self.F @ self.P @ self.F.T + self.Q
|
|
890
|
+
|
|
891
|
+
# this F can be either linear or nonlinear function of x.
|
|
892
|
+
# print(f'{x=}')
|
|
893
|
+
self.X = self.F @ self.X
|
|
894
|
+
# print(f'{x=}')
|
|
895
|
+
|
|
896
|
+
if u is not None:
|
|
897
|
+
if self.G is None:
|
|
898
|
+
# c4d.cprint(f"""Warning: u={u} is introduced as control input but the input matrix
|
|
899
|
+
# is zero! (G for discrete system or B for continuous)""", 'r')
|
|
900
|
+
|
|
901
|
+
warnings.warn(f"""\nWarning: u={u} is introduced as control input but the input matrix is zero! (G for discrete system or B for continuous)."""
|
|
902
|
+
, c4d.c4warn)
|
|
903
|
+
|
|
904
|
+
else:
|
|
905
|
+
|
|
906
|
+
if len(u.ravel()) != self.G.shape[1]:
|
|
907
|
+
raise ValueError(f"""The number of elements in u must equal the number of columns of the input matrix (B or G), {len(u.ravel())} != {self.G.shape[1]}""")
|
|
908
|
+
self.X += self.G @ u.ravel()
|
|
909
|
+
|
|
910
|
+
|
|
911
|
+
|
|
912
|
+
def update(self, z: np.ndarray
|
|
913
|
+
, R: Optional[np.ndarray] = None, Rk: Optional[np.ndarray] = None):
|
|
914
|
+
'''
|
|
915
|
+
Updates the state estimate based on the given measurements.
|
|
916
|
+
|
|
917
|
+
Parameters
|
|
918
|
+
----------
|
|
919
|
+
z : numpy.ndarray
|
|
920
|
+
Measurement vector.
|
|
921
|
+
|
|
922
|
+
|
|
923
|
+
|
|
924
|
+
|
|
925
|
+
|
|
926
|
+
define kalman:
|
|
927
|
+
|
|
928
|
+
run in steadys tate:
|
|
929
|
+
|
|
930
|
+
run with varying discerte Rk
|
|
931
|
+
|
|
932
|
+
run with cont vayring R
|
|
933
|
+
|
|
934
|
+
|
|
935
|
+
'''
|
|
936
|
+
|
|
937
|
+
|
|
938
|
+
# this H must be linear, but as F may it be linearized once about an equilibrium point for
|
|
939
|
+
# the entire process (regular kalman) or at each
|
|
940
|
+
# iteration about the current state (ekf).
|
|
941
|
+
# TODO add Mahalanobis optional test
|
|
942
|
+
|
|
943
|
+
if len(z.ravel()) != self.H.shape[0]:
|
|
944
|
+
raise ValueError(f"""The number of elements in z must equal
|
|
945
|
+
the number of roww=s of the measurement matrix (C or H),
|
|
946
|
+
{len(z.ravel())} != {self.H.shape[0]}""")
|
|
947
|
+
|
|
948
|
+
if self.Kinf is None:
|
|
949
|
+
if R is not None:
|
|
950
|
+
self.Rk = np.atleast_2d(R) / self.dt
|
|
951
|
+
elif Rk is not None:
|
|
952
|
+
self.Rk = np.atleast_2d(Rk)
|
|
953
|
+
elif self.Rk is None:
|
|
954
|
+
raise ValueError(r'R or Rk must be provided in every call to update() (optional only in steadystate mode)')
|
|
955
|
+
|
|
956
|
+
K = self.P @ self.H.T @ np.linalg.inv(self.H @ self.P @ self.H.T + self.Rk)
|
|
957
|
+
self.P = self.P - K @ self.H @ self.P
|
|
958
|
+
else:
|
|
959
|
+
K = self.Kinf
|
|
960
|
+
|
|
961
|
+
|
|
962
|
+
|
|
963
|
+
# this H can be expressed as either linear or nonlinear function of x.
|
|
964
|
+
# print(f'\n correct \n')
|
|
965
|
+
# print(f'{x=} {K=} {z=} {hx=}')
|
|
966
|
+
self.X += K @ (z.ravel() - self.H @ self.X)
|
|
967
|
+
|
|
968
|
+
|
|
969
|
+
|
|
970
|
+
|
|
971
|
+
|
|
972
|
+
|
|
973
|
+
# def store(self, t = -1):
|
|
974
|
+
def store(self, t: int = -1):
|
|
975
|
+
'''
|
|
976
|
+
Stores the current state and diagonal elements of the covariance matrix.
|
|
977
|
+
|
|
978
|
+
Parameters
|
|
979
|
+
----------
|
|
980
|
+
|
|
981
|
+
t : int, optional
|
|
982
|
+
Time step for storing the state. Defaults to -1.
|
|
983
|
+
|
|
984
|
+
'''
|
|
985
|
+
|
|
986
|
+
super().store(t) # store state
|
|
987
|
+
# store covariance:
|
|
988
|
+
for i, p in enumerate(np.diag(self.P)):
|
|
989
|
+
pstr = f'P{i}{i}'
|
|
990
|
+
setattr(self, pstr, p) # set
|
|
991
|
+
self.storeparams(pstr, t) # store
|
|
992
|
+
|
|
993
|
+
|
|
994
|
+
|
|
995
|
+
|
|
996
|
+
@staticmethod
|
|
997
|
+
# def linearmodel(dt, process_noise, measure_noise):
|
|
998
|
+
def linearmodel(dt: float, process_noise: float, measure_noise: float):
|
|
999
|
+
|
|
1000
|
+
'''
|
|
1001
|
+
Defines a linear Kalman filter model for tracking position and velocity.
|
|
1002
|
+
|
|
1003
|
+
Parameters
|
|
1004
|
+
----------
|
|
1005
|
+
dt : float
|
|
1006
|
+
Time step for the system model.
|
|
1007
|
+
process_noise : float
|
|
1008
|
+
Standard deviation of the process noise.
|
|
1009
|
+
measure_noise : float
|
|
1010
|
+
Standard deviation of the measurement noise.
|
|
1011
|
+
|
|
1012
|
+
Returns
|
|
1013
|
+
-------
|
|
1014
|
+
kf : kalman
|
|
1015
|
+
A Kalman filter object initialized with the linear system model.
|
|
1016
|
+
|
|
1017
|
+
|
|
1018
|
+
|
|
1019
|
+
X = [x, y, w, h, vx, vy]
|
|
1020
|
+
# 0 1 2 3 4 5
|
|
1021
|
+
|
|
1022
|
+
x' = vx
|
|
1023
|
+
y' = vy
|
|
1024
|
+
w' = 0
|
|
1025
|
+
h' = 0
|
|
1026
|
+
vx' = 0
|
|
1027
|
+
vy' = 0
|
|
1028
|
+
|
|
1029
|
+
H = [1 0 0 0 0 0
|
|
1030
|
+
0 1 0 0 0 0
|
|
1031
|
+
0 0 1 0 0 0
|
|
1032
|
+
0 0 0 1 0 0]
|
|
1033
|
+
'''
|
|
1034
|
+
from scipy.linalg import expm
|
|
1035
|
+
|
|
1036
|
+
A = np.zeros((6, 6))
|
|
1037
|
+
A[0, 4] = A[1, 5] = 1
|
|
1038
|
+
F = expm(A * dt)
|
|
1039
|
+
H = np.zeros((4, 6))
|
|
1040
|
+
H[0, 0] = H[1, 1] = H[2, 2] = H[3, 3] = 1
|
|
1041
|
+
|
|
1042
|
+
Qk = np.eye(6) * process_noise**2
|
|
1043
|
+
Rk = np.eye(4) * measure_noise**2
|
|
1044
|
+
|
|
1045
|
+
Q = np.eye(6) * process_noise**2
|
|
1046
|
+
R = np.eye(4) * measure_noise**2
|
|
1047
|
+
|
|
1048
|
+
# kf = kalman({'x': 0, 'y': 0, 'w': 0, 'h': 0, 'vx': 0, 'vy': 0}
|
|
1049
|
+
# , steadystate = True, A = A, C = H, Q = Q, R = R, dt = dt)
|
|
1050
|
+
kf = kalman({'x': 0, 'y': 0, 'w': 0, 'h': 0, 'vx': 0, 'vy': 0}
|
|
1051
|
+
, steadystate = True, F = F, H = H, Qk = Qk, Rk = Rk)
|
|
1052
|
+
return kf
|
|
1053
|
+
|
|
1054
|
+
|
|
1055
|
+
|
|
1056
|
+
|
|
1057
|
+
|
|
1058
|
+
|
|
1059
|
+
|
|
1060
|
+
if __name__ == "__main__":
|
|
1061
|
+
import contextlib
|
|
1062
|
+
import doctest
|
|
1063
|
+
|
|
1064
|
+
# Redirect both stdout and stderr to a file within this block
|
|
1065
|
+
with open(os.path.join('tests', '_out', 'output.txt'), 'w') as f:
|
|
1066
|
+
with contextlib.redirect_stdout(f), contextlib.redirect_stderr(f):
|
|
1067
|
+
doctest.testmod()
|
|
1068
|
+
|
|
1069
|
+
|
|
1070
|
+
|
|
1071
|
+
|