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.
Files changed (50) hide show
  1. c4dynamics/__init__.py +240 -0
  2. c4dynamics/datasets/__init__.py +95 -0
  3. c4dynamics/datasets/_manager.py +596 -0
  4. c4dynamics/datasets/_registry.py +80 -0
  5. c4dynamics/detectors/__init__.py +37 -0
  6. c4dynamics/detectors/yolo3_opencv.py +686 -0
  7. c4dynamics/detectors/yolo3_tf.py +124 -0
  8. c4dynamics/eqm/__init__.py +324 -0
  9. c4dynamics/eqm/derivs.py +212 -0
  10. c4dynamics/eqm/integrate.py +359 -0
  11. c4dynamics/filters/__init__.py +1373 -0
  12. c4dynamics/filters/a.py +48 -0
  13. c4dynamics/filters/ekf.py +320 -0
  14. c4dynamics/filters/kalman.py +725 -0
  15. c4dynamics/filters/kalman_v0.py +1071 -0
  16. c4dynamics/filters/kalman_v1.py +821 -0
  17. c4dynamics/filters/lowpass.py +123 -0
  18. c4dynamics/filters/luenberger.py +97 -0
  19. c4dynamics/rotmat/__init__.py +141 -0
  20. c4dynamics/rotmat/animate.py +465 -0
  21. c4dynamics/rotmat/rotmat.py +351 -0
  22. c4dynamics/sensors/__init__.py +72 -0
  23. c4dynamics/sensors/lineofsight.py +78 -0
  24. c4dynamics/sensors/radar.py +740 -0
  25. c4dynamics/sensors/seeker.py +1030 -0
  26. c4dynamics/states/__init__.py +327 -0
  27. c4dynamics/states/lib/__init__.py +320 -0
  28. c4dynamics/states/lib/datapoint.py +660 -0
  29. c4dynamics/states/lib/pixelpoint.py +776 -0
  30. c4dynamics/states/lib/rigidbody.py +677 -0
  31. c4dynamics/states/state.py +1486 -0
  32. c4dynamics/utils/__init__.py +44 -0
  33. c4dynamics/utils/_struct.py +6 -0
  34. c4dynamics/utils/const.py +130 -0
  35. c4dynamics/utils/cprint.py +80 -0
  36. c4dynamics/utils/gen_gif.py +142 -0
  37. c4dynamics/utils/idx2keys.py +4 -0
  38. c4dynamics/utils/images_loader.py +63 -0
  39. c4dynamics/utils/math.py +136 -0
  40. c4dynamics/utils/plottools.py +140 -0
  41. c4dynamics/utils/plottracks.py +304 -0
  42. c4dynamics/utils/printpts.py +36 -0
  43. c4dynamics/utils/slides_gen.py +64 -0
  44. c4dynamics/utils/tictoc.py +167 -0
  45. c4dynamics/utils/video_gen.py +300 -0
  46. c4dynamics/utils/vidgen.py +182 -0
  47. c4dynamics-2.0.3.dist-info/METADATA +242 -0
  48. c4dynamics-2.0.3.dist-info/RECORD +50 -0
  49. c4dynamics-2.0.3.dist-info/WHEEL +5 -0
  50. 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
+