├── README.md
├── images
├── RMSE1.png
├── RMSE2.png
├── RMSE3.png
├── RMSE4-Darknet.png
├── RMSE4-GTL.png
├── RMSE4.png
├── step1-resnet.png
├── step2-resnet.png
├── step4-darknet.png
├── step4-ground-truth.png
└── step4-resnet.png
└── student
├── association.py
├── evaluation.py
├── filter.py
├── loop_over_dataset_f4.py
├── measurements.py
├── objdet_detect.py
├── params.py
└── trackmanagement.py
/README.md:
--------------------------------------------------------------------------------
1 | # Sensor Fusion and Object Tracking
2 | Self-Driving Car Engineer Nanodegree
3 | https://www.udacity.com/course/self-driving-car-engineer-nanodegree--nd013
4 |
5 | ## Installation Instructions
6 |
7 | Download or clone this github repository with the starter code:
8 | https://github.com/udacity/nd013-c2-fusion-starter
9 |
10 | Follow all the installation instructions in the github repository:
11 | https://github.com/udacity/nd013-c2-fusion-starter#installation-instructions-for-running-locally
12 |
13 | Basically, you need to install Python 3.7 and all the requirements in the file `requirements.txt` by typing this command in the terminal: `pip3 install -r requirements.txt`.
14 |
15 | Additionally, you need to download and install the "Waymo Open Dataset Files" and the Pre-Trained Models for `darknet` and `fpn_resnet` . Once downloaded, please copy the model files into the paths `/tools/objdet_models/darknet/pretrained` and `/tools/objdet_models/fpn_resnet/pretrained` respectively.
16 |
17 | Moreover, download the precomputed results for `darknet` and `fpn_resnet` in this link . And then unzip them into the folder `/results`.
18 |
19 | Once you completed the first steps to install this project, it is time to download the following files in this github repository and to copy them in the following directories:
20 |
21 | | FILES TO COPY | DIRECTORIES |
22 | | ------------- | -------------|
23 | | [`/student/loop_over_dataset_f4.py`](/student/loop_over_dataset_f4.py) | `/` |
24 | | [`/student/association.py`](/student/association.py) | `/student/` |
25 | | [`/student/filter.py`](/student/filter.py) | `/student/` |
26 | | [`/student/measurements.py`](/student/measurements.py) | `/student/` |
27 | | [`/student/trackmanagement.py`](/student/trackmanagement.py) | `/student/` |
28 | | [`/student/objdet_detect.py`](/student/objdet_detect.py) | `/student/` |
29 | | [`/student/params.py`](/student/params.py) | `/misc/` |
30 | | [`/student/evaluation.py`](/student/evaluation.py) | `/misc/` |
31 |
32 | Note: The directory `/` is relative to this project's directory.
33 |
34 | Now that you have successfully installed this project, you can run it by using this command `python loop_over_dataset_f4.py`.
35 |
36 | ## Questions to Answer
37 |
38 | ### Write a short recap of the four tracking steps and what you implemented there (EKF, track management, data association, camera-lidar sensor fusion). Which results did you achieve?
39 |
40 | **Step 1. Extended Kalman Filter (EKF):** I implemented a complete Kalman filter with 6 dimensions: 3 dimensions for the position (x, y, z) and 3 dimensions for the velocity (vx, vy, vz). According to Prof. Thrun, this is a great achievement because it is the most important part of a tracking system with sensor fusion support. It was difficult for me to know when to use `H * x` and when to use `hx`. The later `hx` is only used when computing gamma and when computing the Mahalanobis Distance (MHD). And I spent a lot of time tuning the 6 x 6 matrixes returned by `P()` and `Q()`. I think I found an almost optimal configuration for this project.
41 |
42 | **Step 2. Track Management:** I programmed a module to create new tracks, delete old tracks, and update the states of current tracks, based on the information obtained by the measurements. In my new version, I used a dictionary whose keys are the frames and whose values are lists that accumulate the presence or absence of camera measurements and lidar measurements. This approach is more robust because it does not penalize the track when it receives measurements from only 1 sensor and when it lacks measurements from the other sensor. And this approach accelerates the confirmation of tracks when measurements from both sensors are present. Analyze my code for this part.
43 |
44 | **Step 3. Data Association:** The algorithm to associate tracks with measurements is very clever because it matches the nearest pair of track and measurement in the space formed by the Mahalanobis Distance (MHD), which is a space deformed by the statistical expectations of positions and velocities. And the algorithm continues to match the nearest pairs until there are no pairs to match.
45 |
46 | **Step 4. Camera-Lidar Sensor Fusion:** This is the final step to complete the whole sensor fusion system. Coordinates from 2 different sensors with different geometries are transformed into vehicle coordinates by using the homogeneous transformation matrices. In like manner, vehicle coordinates are transformed into the corresponding sensor coordinates in order to compute `hx` and the EKF's Jacobian. In this step, I was confused about how to activate the camera measurements. Because they were not active in spite of following the instructions correctly. I searched for all the conditional statements like `if sensor.name == 'lidar':` and I changed them to `if sensor.name in ['lidar', 'camera']:` with disastrous results. Finally, I noticed that such changes were not necessary. Because I forgot to use `hx` when computing the Mahalanobis Distance (MHD). Using `H * x` when computing MHD led to discarding all camera measurements.
47 |
48 | ### Which part of the project was most difficult for you to complete, and why?
49 |
50 | For me, the most difficult part of the project was to fully understand the overall structure of the project. In addition to completing all the TODOs, I also needed to read the source code of all the Python files in the whole project. Because the TODOs asked me to do many things that were confusing and ambiguous. I didn't know how to complete many TODOs. The devil is in the details. And by reading and understanding all the source files, I finally completed the project. The real problem is this project has many mutually-interacting synergistic parts. And if you program one part in a wrong way, the whole project fails. So, it's very demotivating to see that nothing in this project works well. But I knew that it was my fault. I knew that I programmed many parts in a wrong way. So, I printed and printed all the intermediate results, validating that everything is well programmed as intended. In this way, I got everything correct and now the project finally works.
51 |
52 | ### Do you see any benefits in camera-lidar fusion compared to lidar-only tracking (in theory and in your concrete results)?
53 |
54 | In theory, cameras have more resolution and more frames per second than lidar. Hence, cameras are better at detecting objects in the environment, resolving ambiguities.
55 |
56 | In my concrete results, I experienced a significant improvement in the accuracy when I finished implementing the Step 4, which fuses lidar measurements with camera measurements. Resolving the ambiguities of the video in Step 4 would be very difficult without camera measurements.
57 |
58 | Camera measurements are ambiguous in the lines of ray tracing. Throughout these lines, any depth is valid and ambiguous. This ambiguity is resolved by using lidar measurements which have accurate depth information. Moreover, in the project there were big gaps of false negatives, lack of some lidar measurements. So, the fusion of cameras and lidars compensate their weaknesses.
59 |
60 | ### Which challenges will a sensor fusion system face in real-life scenarios? Did you see any of these challenges in the project?
61 |
62 | Cameras are less accurate at night and at estimating distances. Hence, sensor fusion systems must deal with these inaccuracies. Fortunately, the EKF has the mathematical foundations to deal with noisy measurements properly.
63 |
64 | Yes, I saw in the project that camera measurements were less accurate than lidar measurements. Fortunately, theses cameras recorded videos in the day, not in the night, which would make this project harder.
65 |
66 | ### Can you think of ways to improve your tracking results in the future?
67 |
68 | I used precomputed detections in this project. And these precomputed detections provided by Udacity had lots of false negatives, lots of ground-truth labels without measurements. The gaps in the frames are big. I'm not exaggerating. Perhaps I should compute my own detections by using my own code. I hope to improve my results in this way.
69 |
70 | Moreover, I should try the "Stand Out Suggestions":
71 |
72 | - Fine-tune your parameterization and see how low an RMSE you can achieve! One idea would be to apply the standard deviation values for lidar that you found in the mid-term project. The parameters in `misc/params.py` should enable a first running tracking, but there is still a lot of room for improvement through parameter tuning!
73 | - Implement a more advanced data association, e.g. Global Nearest Neighbor (GNN) or Joint Probabilistic Data Association (JPDA).
74 | - Feed your camera detections from Project 1 to the tracking.
75 | - Adapt the Kalman filter to also estimate the object's width, length, and height, instead of simply using the unfiltered lidar detections as we did.
76 | - Use a non-linear motion model, e.g. a bicycle model, which is more appropriate for vehicle movement than our linear motion model, since a vehicle can only move forward or backward, not in any direction.
77 |
78 | ## Results
79 |
80 | **[SDCE ND] Sensor Fusion and Object Tracking (Step 1, Resnet) [Submission 2]**
81 | https://youtu.be/d9EZSFHPsio
82 |
83 |
84 | The tracker (EKF) follows the car smoothly.
85 |
86 | **RMSE through time (Step 1)**
87 |
88 |
89 | And the mean RMSE is 0.31, below 0.35 as the rubric requested.
90 |
91 | **[SDCE ND] Sensor Fusion and Object Tracking (Step 2, Resnet) [Submission 2]**
92 | https://youtu.be/Gc0H8hfHD-c
93 |
94 |
95 | EKF follows this fast car without problems. At the end, the tracker deletes the track once it falls outside the field-of-view. However, the tracker takes some time until it finally confirms the track. Why? Because I programmed the tracker to be skeptical enough to reject the ghosts tracks in Step 4.
96 |
97 | **RMSE through time (Step 2)**
98 |
99 |
100 | The mean RMSE of this track is 0.03, which is very good!
101 |
102 | **RMSE through time (Step 3)**
103 |
104 |
105 | Step 3 is the same experiment in Step 4. So, I didn't upload a video of Step 3. And I will explain the RMSEs of Step 3 in Step 4.
106 |
107 | **[SDCE ND] Sensor Fusion and Object Tracking (Step 4, Resnet) [Submission 2]**
108 | https://youtu.be/k-RrG9VT9Kk
109 |
110 |
111 | EKF follows the 2 important cars smoothly without problems. But in the time period from second 10th to second 13th, where there are no lidar measurements in 1 car, the mean RMSE increases slightly. In this part, EKF predicts the car movements in an imperfect way, given there are no lidar measurements. After that, the car recovers its lidar measurements and its tracking. Notice that in the video I draw the camera measurements in yellow. In spite of the lack of lidar measurements in that time period, EKF updates the track with camera measurements, preventing the track from being deleted. However, camera measurements are ambiguous in the lines of ray tracing. Throughout these lines, any depth is valid and ambiguous. And that's why, without lidar measurements, the error followed the line of ray tracing. Watch the video.
112 |
113 | This error is not my fault because the precomputed results of `fpn_resnet` that Udacity gave us lack many lidar measurements in big gaps. Fortunately, I did my best to follow the 2 important cars from the beginning to the end, without losing the track. And I obtained a decent mean RMSE for the 2 important cars: 0.40 and 0.14.
114 |
115 | I debugged extensively this part. I thought I was not using camera measurements properly. But I tracked the flow of measurements throughout all the functions and I noticed that at most 1 lidar measurement and 1 camera measurement are being matched to each track in each frame. Moreover, I changed the calculations of camera measurements slightly, changing signs and adding some quantities, producing disastrous errors. It means that my formulas without slight modifications are correct.
116 |
117 | **RMSE through time (Step 4)**
118 |
119 |
120 | Track 0 (blue) and track 1 (orange) are the 2 important cars whose tracking was never lost. Track 0 (blue) increases its RMSEs slightly from second 10th to second 13th, when there are no lidar measurements. Track 10 (green) is the black car whose precomputed detections shown no lidar measurements until second 18th.
121 |
122 | **[SDCE ND] Sensor Fusion and Object Tracking (Step 4, Ground Truth) [Submission 2]**
123 | https://youtu.be/Dl25MHOwtD8
124 |
125 |
126 | When I use ground-truth labels, my tracker works perfectly and follows the 2 important cars, Track 0 (blue) and track 1 (orange), from the begining to the end with mean RMSEs of 0.13 and 0.06, as the rubric requested. Track 10 (green) is the black car that appeared in the middle of the video and it was not fully confirmed until both lidar measurements and camera measurements intersected the fields of view. Track 15 (red) is a car parked in the right side at the end of the video.
127 |
128 | **RMSE through time (Step 4 with ground-truth labels)**
129 |
130 |
131 | **[SDCE ND] Sensor Fusion and Object Tracking (Step 4, Darknet) [Submission 2]**
132 | https://youtu.be/AiZo0Lup4XI
133 |
134 |
135 | When I used the precomputed detections of `darknet`, lidar measurements have no big gaps of false negatives. And I obtained almost perfect results as the rubric requested. My tracker works perfectly and follows the 2 important cars, Track 0 (blue) and track 1 (orange), from the begining to the end with mean RMSEs of 0.18 and 0.17. Track 27 (green) is the black car that appeared in the middle of the video. There are only 3 confirmed tracks with mean RMSEs below 0.25. And all ghosts tracks were eliminated. My graph with only 3 confirmed tracks is very similar to the graph requested by the rubric. I'm so happy with my results now.
136 |
137 | In the video, the 3D boxes of cars sometimes disappear. But the 2D tracks are never lost. I modified the visualization code to exclude 3D boxes whose vertices are outside the field of view. The problem is Darknet does not provide 3D information when detecting 2D images, making the calculation of 3D boxes more difficult. In spite of lacking 3D information, my tracker managed to obtain almost perfect results, which is amazing! That's the magic of Kalman filters and sensor fusion.
138 |
139 | **RMSE through time (Step 4 with Darknet)**
140 |
141 |
142 | ## Note for the reviewer:
143 |
144 | Notice that the precomputed detections of `fpn_resnet` given by Udacity have big gaps of false negatives in the lidar measurements. I did my best at tuning the parameters and algorithms of this project. However, the mean RMSE of Step 4 is slightly above of what the rubric asks. That's why I use the precomputed detections of `darknet` to fulfill the mean RMSE requested by the rubric. And now my results are almost perfect.
145 |
146 | Here is the github repository of my solution: https://github.com/jckuri/Sensor-Fusion-and-Object-Tracking-2
147 |
--------------------------------------------------------------------------------
/images/RMSE1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jckuri/Sensor-Fusion-and-Object-Tracking-2/ec4a17a1169078894ad26e0da9cf2ddbc4cab547/images/RMSE1.png
--------------------------------------------------------------------------------
/images/RMSE2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jckuri/Sensor-Fusion-and-Object-Tracking-2/ec4a17a1169078894ad26e0da9cf2ddbc4cab547/images/RMSE2.png
--------------------------------------------------------------------------------
/images/RMSE3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jckuri/Sensor-Fusion-and-Object-Tracking-2/ec4a17a1169078894ad26e0da9cf2ddbc4cab547/images/RMSE3.png
--------------------------------------------------------------------------------
/images/RMSE4-Darknet.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jckuri/Sensor-Fusion-and-Object-Tracking-2/ec4a17a1169078894ad26e0da9cf2ddbc4cab547/images/RMSE4-Darknet.png
--------------------------------------------------------------------------------
/images/RMSE4-GTL.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jckuri/Sensor-Fusion-and-Object-Tracking-2/ec4a17a1169078894ad26e0da9cf2ddbc4cab547/images/RMSE4-GTL.png
--------------------------------------------------------------------------------
/images/RMSE4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jckuri/Sensor-Fusion-and-Object-Tracking-2/ec4a17a1169078894ad26e0da9cf2ddbc4cab547/images/RMSE4.png
--------------------------------------------------------------------------------
/images/step1-resnet.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jckuri/Sensor-Fusion-and-Object-Tracking-2/ec4a17a1169078894ad26e0da9cf2ddbc4cab547/images/step1-resnet.png
--------------------------------------------------------------------------------
/images/step2-resnet.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jckuri/Sensor-Fusion-and-Object-Tracking-2/ec4a17a1169078894ad26e0da9cf2ddbc4cab547/images/step2-resnet.png
--------------------------------------------------------------------------------
/images/step4-darknet.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jckuri/Sensor-Fusion-and-Object-Tracking-2/ec4a17a1169078894ad26e0da9cf2ddbc4cab547/images/step4-darknet.png
--------------------------------------------------------------------------------
/images/step4-ground-truth.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jckuri/Sensor-Fusion-and-Object-Tracking-2/ec4a17a1169078894ad26e0da9cf2ddbc4cab547/images/step4-ground-truth.png
--------------------------------------------------------------------------------
/images/step4-resnet.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jckuri/Sensor-Fusion-and-Object-Tracking-2/ec4a17a1169078894ad26e0da9cf2ddbc4cab547/images/step4-resnet.png
--------------------------------------------------------------------------------
/student/association.py:
--------------------------------------------------------------------------------
1 | # ---------------------------------------------------------------------
2 | # Project "Track 3D-Objects Over Time"
3 | # Copyright (C) 2020, Dr. Antje Muntzinger / Dr. Andreas Haja.
4 | #
5 | # Purpose of this file : Data association class with single nearest neighbor association and gating based on Mahalanobis distance
6 | #
7 | # You should have received a copy of the Udacity license together with this program.
8 | #
9 | # https://www.udacity.com/course/self-driving-car-engineer-nanodegree--nd013
10 | # ----------------------------------------------------------------------
11 | #
12 |
13 | # imports
14 | import numpy as np
15 | from scipy.stats.distributions import chi2
16 |
17 | # add project directory to python path to enable relative imports
18 | import os
19 | import sys
20 | PACKAGE_PARENT = '..'
21 | SCRIPT_DIR = os.path.dirname(os.path.realpath(os.path.join(os.getcwd(), os.path.expanduser(__file__))))
22 | sys.path.append(os.path.normpath(os.path.join(SCRIPT_DIR, PACKAGE_PARENT)))
23 |
24 | import misc.params as params
25 |
26 | class Association:
27 | '''Data association class with single nearest neighbor association and gating based on Mahalanobis distance'''
28 | def __init__(self):
29 | self.association_matrix = np.matrix([])
30 | self.unassigned_tracks = []
31 | self.unassigned_meas = []
32 |
33 | def associate(self, track_list, meas_list, KF, cnt_frame):
34 |
35 | ############
36 | # TODO Step 3: association:
37 | # - replace association_matrix with the actual association matrix based on Mahalanobis distance (see below) for all tracks and all measurements
38 | # - update list of unassigned measurements and unassigned tracks
39 | ############
40 |
41 | self.association_matrix = np.matrix([])
42 | self.unassigned_tracks = []
43 | self.unassigned_meas = []
44 |
45 | N = len(track_list) # N tracks
46 | M = len(meas_list) # M measurements
47 |
48 | if N > 0: self.unassigned_tracks = list(range(N))
49 | if M > 0: self.unassigned_meas = list(range(M))
50 | if N > 0 and M > 0:
51 | self.association_matrix = np.inf * np.ones((N, M))
52 | for i in range(N):
53 | track = track_list[i]
54 | for j in range(M):
55 | meas = meas_list[j]
56 | dist = self.MHD(track, meas, KF)
57 | if self.gating(dist, sensor = meas.sensor):
58 | #if True: ############# HERE!!!!!!!
59 | self.association_matrix[i, j] = dist
60 | #if i in self.unassigned_tracks: self.unassigned_tracks.remove(i)
61 | #if j in self.unassigned_meas: self.unassigned_meas.remove(j)
62 |
63 | return
64 |
65 | # the following only works for at most one track and one measurement
66 | self.association_matrix = np.matrix([]) # reset matrix
67 | self.unassigned_tracks = [] # reset lists
68 | self.unassigned_meas = []
69 |
70 | if len(meas_list) > 0:
71 | self.unassigned_meas = [0]
72 | if len(track_list) > 0:
73 | self.unassigned_tracks = [0]
74 | if len(meas_list) > 0 and len(track_list) > 0:
75 | self.association_matrix = np.matrix([[0]])
76 |
77 | ############
78 | # END student code
79 | ############
80 |
81 | def get_closest_track_and_meas(self):
82 | ############
83 | # TODO Step 3: find closest track and measurement:
84 | # - find minimum entry in association matrix
85 | # - delete row and column
86 | # - remove corresponding track and measurement from unassigned_tracks and unassigned_meas
87 | # - return this track and measurement
88 | ############
89 |
90 | # find closest track and measurement for next update
91 | A = self.association_matrix
92 | if np.min(A) == np.inf:
93 | return np.nan, np.nan
94 |
95 | # get indices of minimum entry
96 | ij_min = np.unravel_index(np.argmin(A, axis=None), A.shape)
97 | ind_track = ij_min[0]
98 | ind_meas = ij_min[1]
99 |
100 | # delete row and column for next update
101 | A = np.delete(A, ind_track, 0)
102 | A = np.delete(A, ind_meas, 1)
103 | self.association_matrix = A
104 |
105 | # update this track with this measurement
106 | update_track = self.unassigned_tracks[ind_track]
107 | update_meas = self.unassigned_meas[ind_meas]
108 |
109 | # remove this track and measurement from list
110 | self.unassigned_tracks.remove(update_track)
111 | self.unassigned_meas.remove(update_meas)
112 |
113 | return update_track, update_meas
114 |
115 | # the following only works for at most one track and one measurement
116 | update_track = 0
117 | update_meas = 0
118 |
119 | # remove from list
120 | self.unassigned_tracks.remove(update_track)
121 | self.unassigned_meas.remove(update_meas)
122 | self.association_matrix = np.matrix([])
123 |
124 | ############
125 | # END student code
126 | ############
127 | return update_track, update_meas
128 |
129 | def gating(self, MHD, sensor):
130 | ############
131 | # TODO Step 3: return True if measurement lies inside gate, otherwise False
132 | ############
133 |
134 | p = 0.9995 #0.999 #params.gating_threshold #0.999 #0.95
135 | limit = chi2.ppf(p, df = 2)
136 | return MHD < limit
137 |
138 | ############
139 | # END student code
140 | ############
141 |
142 | def MHD(self, track, meas, KF):
143 | ############
144 | # TODO Step 3: calculate and return Mahalanobis distance
145 | ############
146 |
147 | x = track.x
148 | H = meas.sensor.get_H(x)
149 | #gamma = meas.z - H * x
150 | gamma = meas.z - meas.sensor.get_hx(x)
151 | S = H * track.P * H.transpose() + meas.R
152 | return gamma.transpose() * np.linalg.inv(S) * gamma
153 |
154 | ############
155 | # END student code
156 | ############
157 |
158 | def associate_and_update(self, manager, meas_list, KF, cnt_frame):
159 | # associate measurements and tracks
160 | self.associate(manager.track_list, meas_list, KF, cnt_frame)
161 |
162 | #print(f'BEFORE: self.association_matrix.shape={self.association_matrix.shape}')
163 |
164 | # update associated tracks with measurements
165 | while self.association_matrix.shape[0]>0 and self.association_matrix.shape[1]>0:
166 |
167 | # search for next association between a track and a measurement
168 | ind_track, ind_meas = self.get_closest_track_and_meas()
169 | if np.isnan(ind_track):
170 | print('---no more associations---')
171 | break
172 | track = manager.track_list[ind_track]
173 |
174 | # check visibility, only update tracks in fov
175 | if not meas_list[0].sensor.in_fov(track.x):
176 | continue
177 |
178 | # Kalman update
179 | print('update track', track.id, 'with', meas_list[ind_meas].sensor.name, 'measurement', ind_meas)
180 | KF.update(track, meas_list[ind_meas])
181 |
182 | # update score and track state
183 | manager.handle_updated_track(track, cnt_frame)
184 |
185 | # save updated track
186 | manager.track_list[ind_track] = track
187 |
188 | # run track management
189 | manager.manage_tracks(self.unassigned_tracks, self.unassigned_meas, meas_list, cnt_frame)
190 |
191 | for track in manager.track_list:
192 | print('track', track.id, 'score =', track.score)
193 |
194 | #print(f'AFTER: self.association_matrix.shape={self.association_matrix.shape}')
195 |
--------------------------------------------------------------------------------
/student/evaluation.py:
--------------------------------------------------------------------------------
1 | # ---------------------------------------------------------------------
2 | # Project "Track 3D-Objects Over Time"
3 | # Copyright (C) 2020, Dr. Antje Muntzinger / Dr. Andreas Haja.
4 | #
5 | # Purpose of this file : Evaluate and plot results
6 | #
7 | # You should have received a copy of the Udacity license together with this program.
8 | #
9 | # https://www.udacity.com/course/self-driving-car-engineer-nanodegree--nd013
10 | # ----------------------------------------------------------------------
11 | #
12 |
13 | # imports
14 | import numpy as np
15 | import matplotlib
16 | matplotlib.use('wxagg') # change backend so that figure maximizing works on Mac as well
17 | import matplotlib.pyplot as plt
18 | import matplotlib.patches as patches
19 | from matplotlib.path import Path
20 | from matplotlib import colors
21 | from matplotlib.transforms import Affine2D
22 | import matplotlib.ticker as ticker
23 | import os
24 | import cv2
25 |
26 | # add project directory to python path to enable relative imports
27 | import os
28 | import sys
29 | PACKAGE_PARENT = '..'
30 | SCRIPT_DIR = os.path.dirname(os.path.realpath(os.path.join(os.getcwd(), os.path.expanduser(__file__))))
31 | sys.path.append(os.path.normpath(os.path.join(SCRIPT_DIR, PACKAGE_PARENT)))
32 |
33 | from tools.waymo_reader.simple_waymo_open_dataset_reader import label_pb2
34 |
35 | def plot_tracks(fig, ax, ax2, track_list, meas_list_cam, meas_list, lidar_labels, lidar_labels_valid,
36 | image, camera, configs_det, state=None):
37 |
38 | # plot image
39 | ax.cla()
40 | ax2.cla()
41 | ax2.imshow(image)
42 |
43 | # plot tracks, measurements and ground truth in birds-eye view
44 | for track in track_list:
45 | if state == None or track.state == state: # plot e.g. only confirmed tracks
46 |
47 | # choose color according to track state
48 | if track.state == 'confirmed':
49 | col = 'green'
50 | elif track.state == 'tentative':
51 | col = 'orange'
52 | else:
53 | col = 'red'
54 |
55 | # get current state variables
56 | w = track.width
57 | h = track.height
58 | l = track.length
59 | x = track.x[0]
60 | y = track.x[1]
61 | z = track.x[2]
62 | yaw = track.yaw
63 |
64 | # plot boxes in top view
65 | point_of_rotation = np.array([w/2, l/2])
66 | rec = plt.Rectangle(-point_of_rotation, width=w, height=l,
67 | color=col, alpha=0.2,
68 | transform=Affine2D().rotate_around(*(0,0), -yaw)+Affine2D().translate(-y,x)+ax.transData)
69 | ax.add_patch(rec)
70 |
71 | # write track id for debugging
72 | ax.text(float(-track.x[1]), float(track.x[0]+1), str(track.id))
73 |
74 | if track.state =='initialized':
75 | ax.scatter(float(-track.x[1]), float(track.x[0]), color=col, s=80, marker='x', label='initialized track')
76 | elif track.state =='tentative':
77 | ax.scatter(float(-track.x[1]), float(track.x[0]), color=col, s=80, marker='x', label='tentative track')
78 | elif track.state =='confirmed':
79 | ax.scatter(float(-track.x[1]), float(track.x[0]), color=col, s=80, marker='x', label='confirmed track')
80 |
81 | # project tracks in image
82 | # transform from vehicle to camera coordinates
83 | pos_veh = np.ones((4, 1)) # homogeneous coordinates
84 | pos_veh[0:3] = track.x[0:3]
85 | pos_sens = camera.veh_to_sens*pos_veh # transform from vehicle to sensor coordinates
86 | x = pos_sens[0]
87 | y = pos_sens[1]
88 | z = pos_sens[2]
89 |
90 | # compute rotation around z axis
91 | R = np.matrix([[np.cos(yaw), np.sin(yaw), 0],
92 | [-np.sin(yaw), np.cos(yaw), 0],
93 | [0, 0, 1]])
94 |
95 | # bounding box corners
96 | x_corners = [-l/2, l/2, l/2, l/2, l/2, -l/2, -l/2, -l/2]
97 | y_corners = [-w/2, -w/2, -w/2, w/2, w/2, w/2, w/2, -w/2]
98 | z_corners = [-h/2, -h/2, h/2, h/2, -h/2, -h/2, h/2, h/2]
99 |
100 | # bounding box
101 | corners_3D = np.array([x_corners, y_corners, z_corners])
102 |
103 | # rotate
104 | corners_3D = R*corners_3D
105 |
106 | # translate
107 | corners_3D += np.array([x, y, z]).reshape((3, 1))
108 | # print ( 'corners_3d', corners_3D)
109 |
110 | # remove bounding boxes that include negative x, projection makes no sense
111 | if np.any(corners_3D[0,:] <= 0):
112 | continue
113 |
114 | # project to image
115 | in_fov = True
116 | corners_2D = np.zeros((2,8))
117 | for k in range(8):
118 | if not camera.in_fov(corners_3D[:, k]): in_fov = False
119 | corners_2D[0,k] = camera.c_i - camera.f_i * corners_3D[1,k] / corners_3D[0,k]
120 | corners_2D[1,k] = camera.c_j - camera.f_j * corners_3D[2,k] / corners_3D[0,k]
121 | # print ( 'corners_2d', corners_2D)
122 |
123 | # edges of bounding box in vertex index from above, e.g. index 0 stands for [-l/2, -w/2, -h/2]
124 | draw_line_indices = [0, 1, 2, 3, 4, 5, 6, 7, 0, 5, 4, 1, 2, 7, 6, 3]
125 |
126 | paths_2D = np.transpose(corners_2D[:, draw_line_indices])
127 | # print ( 'paths_2D', paths_2D)
128 |
129 | codes = [Path.LINETO]*paths_2D.shape[0]
130 | codes[0] = Path.MOVETO
131 | path = Path(paths_2D, codes)
132 |
133 | # plot bounding box in image
134 | p = patches.PathPatch(
135 | path, fill=False, color=col, linewidth=3)
136 | if in_fov: ax2.add_patch(p)
137 |
138 | # plot labels
139 | for label, valid in zip(lidar_labels, lidar_labels_valid):
140 | if valid:
141 | ax.scatter(-1*label.box.center_y, label.box.center_x, color='gray', s=80, marker='+', label='ground truth')
142 | # plot measurements
143 | for meas in meas_list:
144 | ax.scatter(-1*meas.z[1], meas.z[0], color='blue', marker='.', label='measurement')
145 |
146 | # PLOT MEAS LIST CAM
147 | for meas in meas_list_cam:
148 | x = meas.z[0]
149 | y = meas.z[1]
150 | ax2.scatter(x, y, color='yellow', marker='.', label='measurement')
151 |
152 | # maximize window
153 | mng = plt.get_current_fig_manager()
154 | #mng.resize(*mng.window.maxsize())
155 | mng.resize(1280, 720)
156 | #mng.window.showMaximized()
157 | #mng.frame.Maximize(True)
158 |
159 | # axis
160 | ax.set_xlabel('y [m]')
161 | ax.set_ylabel('x [m]')
162 | ax.set_aspect('equal')
163 | ax.set_ylim(configs_det.lim_x[0], configs_det.lim_x[1]) # x forward, y left in vehicle coordinates
164 | ax.set_xlim(-configs_det.lim_y[1], -configs_det.lim_y[0])
165 | # correct x ticks (positive to the left)
166 | ticks_x = ticker.FuncFormatter(lambda x, pos: '{0:g}'.format(-x) if x!=0 else '{0:g}'.format(x))
167 | ax.xaxis.set_major_formatter(ticks_x)
168 |
169 | # remove repeated labels
170 | handles, labels = ax.get_legend_handles_labels()
171 | handle_list, label_list = [], []
172 | for handle, label in zip(handles, labels):
173 | if label not in label_list:
174 | handle_list.append(handle)
175 | label_list.append(label)
176 | ax.legend(handle_list, label_list, loc='center left', shadow=True, fontsize='x-large', bbox_to_anchor=(0.8, 0.5))
177 |
178 | plt.pause(0.01)
179 |
180 | return fig, ax, ax2
181 |
182 |
183 | def plot_rmse(manager, all_labels):
184 | fig, ax = plt.subplots()
185 | plot_empty = True
186 |
187 | # loop over all tracks
188 | for track_id in range(manager.last_id+1):
189 | rmse_sum = 0
190 | cnt = 0
191 | rmse = []
192 | time = []
193 |
194 | # loop over timesteps
195 | for i, result_dict in enumerate(manager.result_list):
196 | label_list = all_labels[i]
197 | if track_id not in result_dict:
198 | continue
199 | track = result_dict[track_id]
200 | if track.state != 'confirmed':
201 | continue
202 |
203 | # find closest label and calculate error at this timestamp
204 | min_error = np.inf
205 | for label, valid in zip(label_list[0], label_list[1]):
206 | error = 0
207 | if valid:
208 | error += (label.box.center_x - float(track.x[0]))**2
209 | error += (label.box.center_y - float(track.x[1]))**2
210 | error += (label.box.center_z - float(track.x[2]))**2
211 | if error < min_error:
212 | min_error = error
213 | if min_error < np.inf:
214 | error = np.sqrt(min_error)
215 | time.append(track.t)
216 | rmse.append(error)
217 | rmse_sum += error
218 | cnt += 1
219 |
220 | # calc overall RMSE
221 | if cnt != 0:
222 | plot_empty = False
223 | rmse_sum /= cnt
224 | # plot RMSE
225 | ax.plot(time, rmse, marker='x', label='RMSE track ' + str(track_id) + '\n(mean: '
226 | + '{:.2f}'.format(rmse_sum) + ')')
227 |
228 | # maximize window
229 | mng = plt.get_current_fig_manager()
230 | #mng.window.showMaximized()
231 | mng.resize(1280, 720)
232 | #mng.frame.Maximize(True)
233 | ax.set_ylim(0,1)
234 | if plot_empty:
235 | print('No confirmed tracks found to plot RMSE!')
236 | else:
237 | plt.legend(loc='center left', shadow=True, fontsize='x-large', bbox_to_anchor=(0.9, 0.5))
238 | plt.xlabel('time [s]')
239 | plt.ylabel('RMSE [m]')
240 | plt.show()
241 |
242 |
243 | def make_movie(path):
244 | # read track plots
245 | images = sorted([img for img in os.listdir(path) if img.endswith(".png")])
246 | frame = cv2.imread(os.path.join(path, images[0]))
247 | height, width, layers = frame.shape
248 |
249 | # save with 10fps to result dir
250 | video = cv2.VideoWriter(os.path.join(path, 'my_tracking_results.avi'), 0, 10, (width,height))
251 |
252 | for image in images:
253 | fname = os.path.join(path, image)
254 | video.write(cv2.imread(fname))
255 | os.remove(fname) # clean up
256 |
257 | cv2.destroyAllWindows()
258 | video.release()
259 |
--------------------------------------------------------------------------------
/student/filter.py:
--------------------------------------------------------------------------------
1 | # ---------------------------------------------------------------------
2 | # Project "Track 3D-Objects Over Time"
3 | # Copyright (C) 2020, Dr. Antje Muntzinger / Dr. Andreas Haja.
4 | #
5 | # Purpose of this file : Kalman filter class
6 | #
7 | # You should have received a copy of the Udacity license together with this program.
8 | #
9 | # https://www.udacity.com/course/self-driving-car-engineer-nanodegree--nd013
10 | # ----------------------------------------------------------------------
11 | #
12 |
13 | # imports
14 | import numpy as np
15 |
16 | # add project directory to python path to enable relative imports
17 | import os
18 | import sys
19 | PACKAGE_PARENT = '..'
20 | SCRIPT_DIR = os.path.dirname(os.path.realpath(os.path.join(os.getcwd(), os.path.expanduser(__file__))))
21 | sys.path.append(os.path.normpath(os.path.join(SCRIPT_DIR, PACKAGE_PARENT)))
22 | import misc.params as params
23 |
24 | def kalman_matrix(q1, q2, q3, q4):
25 | return np.matrix([[q1, 0, 0, q2, 0, 0 ],
26 | [0, q1, 0, 0, q2, 0 ],
27 | [0, 0, q1, 0, 0, q2],
28 | [q3, 0, 0, q4, 0, 0 ],
29 | [0, q3, 0, 0, q4, 0 ],
30 | [0, 0, q3, 0, 0, q4]])
31 |
32 |
33 | import random
34 | np.random.seed(seed = 1234)
35 | random.seed(1234)
36 |
37 | class Filter:
38 | '''Kalman filter class'''
39 | def __init__(self):
40 |
41 | self.dim_state = params.dim_state # process model dimension
42 | self.dt = params.dt # time increment
43 | self.q = params.q # process noise variable for Kalman filter Q
44 |
45 | def F(self):
46 | ############
47 | # TODO Step 1: implement and return system matrix F
48 | ############
49 | dt = self.dt
50 | #return kalman_matrix(1, dt, 0, 1) # really good
51 | #return kalman_matrix(1, 0.1, 0, 1) # REALLY GOOD
52 | return kalman_matrix(1, 0.2, 0, 1) # GREAT
53 | #return kalman_matrix(1, 0.18, 0, 1)
54 | #return kalman_matrix(1, 0.15, 0, 1)
55 | #return kalman_matrix(1, 0, 0, 1)
56 | #return kalman_matrix(1, 0.1, 0, 1)
57 |
58 | return np.matrix([[1, 0, 0, dt, 0, 0 ],
59 | [0, 1, 0, 0, dt, 0 ],
60 | [0, 0, 1, 0, 0, dt],
61 | [0, 0, 0, 1, 0, 0 ],
62 | [0, 0, 0, 0, 1, 0 ],
63 | [0, 0, 0, 0, 0, 1 ]])
64 |
65 | ############
66 | # END student code
67 | ############
68 |
69 | def Q(self):
70 | ############
71 | # TODO Step 1: implement and return process noise covariance Q
72 | ############
73 |
74 | #return kalman_matrix(1, 1, 1, 1) # really good
75 | #return kalman_matrix(0.5, 0.5, 0.5, 0.5)
76 | #return kalman_matrix(0.1, 0.1, 0.1, 0.1)
77 | #return kalman_matrix(0.01, 0.01, 0.01, 0.01) # REALLY GOOD
78 | #return kalman_matrix(0.01, 0.001, 0.001, 0.01) # MUCH BETTER
79 | return kalman_matrix(0.01, 0, 0, 0.01) # ALMOST EXCELLENT
80 | #return kalman_matrix(0.01, 0.001, 0.001, 0.01)
81 | #return kalman_matrix(0.005, 0.001, 0.001, 0.005)
82 | #return kalman_matrix(0.02, 0.02, 0.02, 0.02)
83 | #return kalman_matrix(0.005, 0.005, 0.005, 0.005)
84 | #return kalman_matrix(3, 1, 1, 3)
85 |
86 | q = self.q
87 | dt = self.dt
88 | q1 = ((dt**3)/3) * q
89 | q2 = ((dt**2)/2) * q
90 | q3 = dt * q
91 | return np.matrix([[q1, 0, 0, q2, 0, 0 ],
92 | [0, q1, 0, 0, q2, 0 ],
93 | [0, 0, q1, 0, 0, q2],
94 | [q2, 0, 0, q3, 0, 0 ],
95 | [0, q2, 0, 0, q3, 0 ],
96 | [0, 0, q2, 0, 0, q3]])
97 |
98 | ############
99 | # END student code
100 | ############
101 |
102 | def predict(self, track):
103 | ############
104 | # TODO Step 1: predict state x and estimation error covariance P to next timestep, save x and P in track
105 | ############
106 |
107 | F = self.F()
108 | x = F * track.x # state prediction
109 | P = F * track.P * F.transpose() + self.Q() # covariance prediction
110 | track.set_x(x)
111 | track.set_P(P)
112 |
113 | ############
114 | # END student code
115 | ############
116 |
117 | def update(self, track, meas):
118 | ############
119 | # TODO Step 1: update state x and covariance P with associated measurement, save x and P in track
120 | ############
121 |
122 | #print(f'Update track {track.id}: sensor.name={meas.sensor.name}, x.shape={track.x.shape}, x={track.x.transpose()}')
123 | x = track.x
124 | P = track.P
125 | H = meas.sensor.get_H(x) # measurement matrix
126 | gamma = self.gamma(track, meas)
127 | S = self.S(track, meas, H) # covariance of residual
128 | K = P * H.transpose() * np.linalg.inv(S) # Kalman gain
129 | x = x + K * gamma # state update
130 | I = np.identity(self.dim_state)
131 | P = (I - K * H) * P # covariance update
132 | track.set_x(x)
133 | track.set_P(P)
134 |
135 | ############
136 | # END student code
137 | ############
138 | track.update_attributes(meas)
139 |
140 | def gamma(self, track, meas):
141 | ############
142 | # TODO Step 1: calculate and return residual gamma
143 | ############
144 |
145 | #x = track.x
146 | #H = meas.sensor.get_H(x) # measurement matrix
147 | #return meas.z - H * x # residual
148 | #print(f'GAMMA: meas.sensor.name={meas.sensor.name}, meas.z={meas.z.transpose()}')
149 | return meas.z - meas.sensor.get_hx(track.x)
150 |
151 | ############
152 | # END student code
153 | ############
154 |
155 | def S(self, track, meas, H):
156 | ############
157 | # TODO Step 1: calculate and return covariance of residual S
158 | ############
159 |
160 | return H * track.P * H.transpose() + meas.R # covariance of residual
161 |
162 | ############
163 | # END student code
164 | ############
165 |
--------------------------------------------------------------------------------
/student/loop_over_dataset_f4.py:
--------------------------------------------------------------------------------
1 | # ---------------------------------------------------------------------
2 | # Project "Track 3D-Objects Over Time"
3 | # Copyright (C) 2020, Dr. Antje Muntzinger / Dr. Andreas Haja.
4 | #
5 | # Purpose of this file : Loop over all frames in a Waymo Open Dataset file,
6 | # detect and track objects and visualize results
7 | #
8 | # You should have received a copy of the Udacity license together with this program.
9 | #
10 | # https://www.udacity.com/course/self-driving-car-engineer-nanodegree--nd013
11 | # ----------------------------------------------------------------------
12 | #
13 |
14 | ##################
15 | ## Imports
16 |
17 | ## general package imports
18 | import os
19 | import sys
20 | import numpy as np
21 | import math
22 | import cv2
23 | import matplotlib.pyplot as plt
24 | import copy
25 |
26 | ## Add current working directory to path
27 | sys.path.append(os.getcwd())
28 |
29 | ## Waymo open dataset reader
30 | from tools.waymo_reader.simple_waymo_open_dataset_reader import utils as waymo_utils
31 | from tools.waymo_reader.simple_waymo_open_dataset_reader import WaymoDataFileReader, dataset_pb2, label_pb2
32 |
33 | ## 3d object detection
34 | import student.objdet_pcl as pcl
35 | import student.objdet_detect as det
36 | import student.objdet_eval as eval
37 |
38 | import misc.objdet_tools as tools
39 | from misc.helpers import save_object_to_file, load_object_from_file, make_exec_list
40 |
41 | from student.filter import Filter
42 | from student.trackmanagement import Trackmanagement
43 | from student.association import Association
44 | from student.measurements import Sensor, Measurement
45 | from misc.evaluation import plot_tracks, plot_rmse, make_movie
46 | import misc.params as params
47 |
48 | ##################
49 | ## Set parameters and perform initializations
50 |
51 | ## Select Waymo Open Dataset file and frame numbers
52 | data_filename = 'training_segment-1005081002024129653_5313_150_5333_150_with_camera_labels.tfrecord' # Sequence 1
53 | # data_filename = 'training_segment-10072231702153043603_5725_000_5745_000_with_camera_labels.tfrecord' # Sequence 2
54 | # data_filename = 'training_segment-10963653239323173269_1924_000_1944_000_with_camera_labels.tfrecord' # Sequence 3
55 | show_only_frames = [0, 200] #[100, 130] #[0, 200] # show only frames in interval for debugging
56 |
57 | ## Prepare Waymo Open Dataset file for loading
58 | data_fullpath = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'dataset', data_filename) # adjustable path in case this script is called from another working directory
59 | results_fullpath = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'results')
60 | datafile = WaymoDataFileReader(data_fullpath)
61 | datafile_iter = iter(datafile) # initialize dataset iterator
62 |
63 | ## Initialize object detection
64 | configs_det = det.load_configs(model_name='darknet') # options are 'darknet', 'fpn_resnet'
65 | model_det = det.create_model(configs_det)
66 |
67 | configs_det.use_labels_as_objects = False # True = use groundtruth labels as objects, False = use model-based detection
68 | configs_det.lim_y = [-25, 25]
69 |
70 | ## Initialize tracking
71 | KF = Filter() # set up Kalman filter
72 | association = Association() # init data association
73 | manager = Trackmanagement() # init track manager
74 | lidar = None # init lidar sensor object
75 | camera = None # init camera sensor object
76 |
77 | ## Selective execution and visualization
78 | exec_detection = [] # options are 'bev_from_pcl', 'detect_objects', 'validate_object_labels', 'measure_detection_performance'; options not in the list will be loaded from file
79 | exec_tracking = ['perform_tracking'] # options are 'perform_tracking'
80 | exec_visualization = ['show_tracks', 'make_tracking_movie'] # options are 'show_range_image', 'show_bev', 'show_pcl', 'show_labels_in_image', 'show_objects_and_labels_in_bev', 'show_objects_in_bev_labels_in_camera', 'show_tracks', 'show_detection_performance', 'make_tracking_movie'
81 | exec_list = make_exec_list(exec_detection, exec_tracking, exec_visualization)
82 | vis_pause_time = 0 # set pause time between frames in ms (0 = stop between frames until key is pressed)
83 |
84 |
85 | ##################
86 | ## Perform detection & tracking over all selected frames
87 |
88 | cnt_frame = 0
89 | all_labels = []
90 | det_performance_all = []
91 | if 'show_tracks' in exec_list:
92 | fig, (ax2, ax) = plt.subplots(1,2) # init track plot
93 |
94 | while True:
95 | try:
96 | ## Get next frame from Waymo dataset
97 | frame = next(datafile_iter)
98 | if cnt_frame < show_only_frames[0]:
99 | cnt_frame = cnt_frame + 1
100 | continue
101 | elif cnt_frame > show_only_frames[1]:
102 | print('reached end of selected frames')
103 | break
104 |
105 | print('------------------------------')
106 | print('processing frame #' + str(cnt_frame))
107 |
108 | #################################
109 | ## Perform 3D object detection
110 |
111 | ## Extract calibration data and front camera image from frame
112 | lidar_name = dataset_pb2.LaserName.TOP
113 | camera_name = dataset_pb2.CameraName.FRONT
114 | lidar_calibration = waymo_utils.get(frame.context.laser_calibrations, lidar_name)
115 | camera_calibration = waymo_utils.get(frame.context.camera_calibrations, camera_name)
116 | if 'load_image' in exec_list:
117 | image = tools.extract_front_camera_image(frame)
118 |
119 | ## Compute lidar point-cloud from range image
120 | if 'pcl_from_rangeimage' in exec_list:
121 | print('computing point-cloud from lidar range image')
122 | lidar_pcl = tools.pcl_from_range_image(frame, lidar_name)
123 | else:
124 | print('loading lidar point-cloud from result file')
125 | lidar_pcl = load_object_from_file(results_fullpath, data_filename, 'lidar_pcl', cnt_frame)
126 |
127 | ## Compute lidar birds-eye view (bev)
128 | if 'bev_from_pcl' in exec_list:
129 | print('computing birds-eye view from lidar pointcloud')
130 | lidar_bev = pcl.bev_from_pcl(lidar_pcl, configs_det)
131 | else:
132 | print('loading birds-eve view from result file')
133 | lidar_bev = load_object_from_file(results_fullpath, data_filename, 'lidar_bev', cnt_frame)
134 |
135 | ## 3D object detection
136 | if (configs_det.use_labels_as_objects==True):
137 | print('using groundtruth labels as objects')
138 | detections = tools.convert_labels_into_objects(frame.laser_labels, configs_det)
139 | else:
140 | if 'detect_objects' in exec_list:
141 | print('detecting objects in lidar pointcloud')
142 | detections = det.detect_objects(lidar_bev, model_det, configs_det)
143 | else:
144 | print('loading detected objects from result file')
145 | detections = load_object_from_file(results_fullpath, data_filename, 'detections_' + configs_det.arch + '_' + str(configs_det.conf_thresh), cnt_frame)
146 |
147 | ## Validate object labels
148 | if 'validate_object_labels' in exec_list:
149 | print("validating object labels")
150 | valid_label_flags = tools.validate_object_labels(frame.laser_labels, lidar_pcl, configs_det, 0 if configs_det.use_labels_as_objects==True else 10)
151 | else:
152 | print('loading object labels and validation from result file')
153 | valid_label_flags = load_object_from_file(results_fullpath, data_filename, 'valid_labels', cnt_frame)
154 |
155 | ## Performance evaluation for object detection
156 | if 'measure_detection_performance' in exec_list:
157 | print('measuring detection performance')
158 | det_performance = eval.measure_detection_performance(detections, frame.laser_labels, valid_label_flags, configs_det.min_iou)
159 | else:
160 | print('loading detection performance measures from file')
161 | det_performance = load_object_from_file(results_fullpath, data_filename, 'det_performance_' + configs_det.arch + '_' + str(configs_det.conf_thresh), cnt_frame)
162 |
163 | det_performance_all.append(det_performance) # store all evaluation results in a list for performance assessment at the end
164 |
165 |
166 | ## Visualization for object detection
167 | if 'show_range_image' in exec_list:
168 | img_range = pcl.show_range_image(frame, lidar_name)
169 | img_range = img_range.astype(np.uint8)
170 | cv2.imshow('range_image', img_range)
171 | cv2.waitKey(vis_pause_time)
172 |
173 | if 'show_pcl' in exec_list:
174 | pcl.show_pcl(lidar_pcl)
175 |
176 | if 'show_bev' in exec_list:
177 | tools.show_bev(lidar_bev, configs_det)
178 | cv2.waitKey(vis_pause_time)
179 |
180 | if 'show_labels_in_image' in exec_list:
181 | img_labels = tools.project_labels_into_camera(camera_calibration, image, frame.laser_labels, valid_label_flags, 0.5)
182 | cv2.imshow('img_labels', img_labels)
183 | cv2.waitKey(vis_pause_time)
184 |
185 | if 'show_objects_and_labels_in_bev' in exec_list:
186 | tools.show_objects_labels_in_bev(detections, frame.laser_labels, lidar_bev, configs_det)
187 | cv2.waitKey(vis_pause_time)
188 |
189 | if 'show_objects_in_bev_labels_in_camera' in exec_list:
190 | tools.show_objects_in_bev_labels_in_camera(detections, lidar_bev, image, frame.laser_labels, valid_label_flags, camera_calibration, configs_det)
191 | cv2.waitKey(vis_pause_time)
192 |
193 |
194 | #################################
195 | ## Perform tracking
196 | if 'perform_tracking' in exec_list:
197 | # set up sensor objects
198 | if lidar is None:
199 | lidar = Sensor('lidar', lidar_calibration)
200 | if camera is None:
201 | camera = Sensor('camera', camera_calibration)
202 |
203 | # preprocess lidar detections
204 | meas_list_lidar = []
205 | for detection in detections:
206 | meas_list_lidar = lidar.generate_measurement(cnt_frame, detection[1:], meas_list_lidar)
207 |
208 | # preprocess camera detections
209 | meas_list_cam = []
210 | for label in frame.camera_labels[0].labels:
211 | if(label.type == label_pb2.Label.Type.TYPE_VEHICLE):
212 |
213 | box = label.box
214 | # use camera labels as measurements and add some random noise
215 | z = [box.center_x, box.center_y, box.width, box.length]
216 | z[0] = z[0] + np.random.normal(0, params.sigma_cam_i)
217 | z[1] = z[1] + np.random.normal(0, params.sigma_cam_j)
218 | meas_list_cam = camera.generate_measurement(cnt_frame, z, meas_list_cam)
219 |
220 | # Kalman prediction
221 | for track in manager.track_list:
222 | print('predict track', track.id)
223 | KF.predict(track)
224 | track.set_t((cnt_frame - 1)*0.1) # save next timestamp
225 |
226 | # associate all lidar measurements to all tracks
227 | association.associate_and_update(manager, meas_list_lidar, KF, cnt_frame)
228 |
229 | # associate all camera measurements to all tracks
230 | association.associate_and_update(manager, meas_list_cam, KF, cnt_frame)
231 |
232 | # save results for evaluation
233 | result_dict = {}
234 | for track in manager.track_list:
235 | result_dict[track.id] = track
236 | manager.result_list.append(copy.deepcopy(result_dict))
237 | label_list = [frame.laser_labels, valid_label_flags]
238 | all_labels.append(label_list)
239 |
240 | # visualization
241 | if 'show_tracks' in exec_list:
242 | #fig, ax, ax2 = plot_tracks(fig, ax, ax2, manager.track_list, meas_list_lidar, frame.laser_labels, valid_label_flags, image, camera, configs_det)
243 | fig, ax, ax2 = plot_tracks(fig, ax, ax2, manager.track_list, meas_list_cam, meas_list_lidar, frame.laser_labels, valid_label_flags, image, camera, configs_det)
244 | if 'make_tracking_movie' in exec_list:
245 | # save track plots to file
246 | fname = results_fullpath + '/tracking%03d.png' % cnt_frame
247 | print('Saving frame', fname)
248 | fig.savefig(fname)
249 |
250 | # increment frame counter
251 | cnt_frame = cnt_frame + 1
252 |
253 | except StopIteration:
254 | # if StopIteration is raised, break from loop
255 | print("StopIteration has been raised\n")
256 | break
257 |
258 |
259 | #################################
260 | ## Post-processing
261 |
262 | ## Evaluate object detection performance
263 | if 'show_detection_performance' in exec_list:
264 | eval.compute_performance_stats(det_performance_all, configs_det)
265 |
266 | ## Plot RMSE for all tracks
267 | if 'show_tracks' in exec_list:
268 | plot_rmse(manager, all_labels)
269 |
270 | ## Make movie from tracking results
271 | if 'make_tracking_movie' in exec_list:
272 | make_movie(results_fullpath)
273 |
--------------------------------------------------------------------------------
/student/measurements.py:
--------------------------------------------------------------------------------
1 | # ---------------------------------------------------------------------
2 | # Project "Track 3D-Objects Over Time"
3 | # Copyright (C) 2020, Dr. Antje Muntzinger / Dr. Andreas Haja.
4 | #
5 | # Purpose of this file : Classes for sensor and measurement
6 | #
7 | # You should have received a copy of the Udacity license together with this program.
8 | #
9 | # https://www.udacity.com/course/self-driving-car-engineer-nanodegree--nd013
10 | # ----------------------------------------------------------------------
11 | #
12 |
13 | # imports
14 | import numpy as np
15 |
16 | # add project directory to python path to enable relative imports
17 | import os
18 | import sys
19 | PACKAGE_PARENT = '..'
20 | SCRIPT_DIR = os.path.dirname(os.path.realpath(os.path.join(os.getcwd(), os.path.expanduser(__file__))))
21 | sys.path.append(os.path.normpath(os.path.join(SCRIPT_DIR, PACKAGE_PARENT)))
22 | import misc.params as params
23 |
24 | class Sensor:
25 | '''Sensor class including measurement matrix'''
26 | def __init__(self, name, calib):
27 | self.name = name
28 | if name == 'lidar':
29 | self.dim_meas = 3
30 | self.sens_to_veh = np.matrix(np.identity((4))) # transformation sensor to vehicle coordinates equals identity matrix because lidar detections are already in vehicle coordinates
31 | self.fov = [-np.pi/2, np.pi/2] # angle of field of view in radians
32 |
33 | elif name == 'camera':
34 | self.dim_meas = 2
35 | self.sens_to_veh = np.matrix(calib.extrinsic.transform).reshape(4,4) # transformation sensor to vehicle coordinates
36 | self.f_i = calib.intrinsic[0] # focal length i-coordinate
37 | self.f_j = calib.intrinsic[1] # focal length j-coordinate
38 | self.c_i = calib.intrinsic[2] # principal point i-coordinate
39 | self.c_j = calib.intrinsic[3] # principal point j-coordinate
40 | self.fov = [-0.35, 0.35] # angle of field of view in radians, inaccurate boundary region was removed
41 |
42 | self.veh_to_sens = np.linalg.inv(self.sens_to_veh) # transformation vehicle to sensor coordinates
43 |
44 | def in_fov(self, x):
45 | # check if an object x can be seen by this sensor
46 | ############
47 | # TODO Step 4: implement a function that returns True if x lies in the sensor's field of view,
48 | # otherwise False.
49 | ############
50 |
51 | #return True
52 | if self.name == 'lidar':
53 | if x[0] > 49: return False
54 | pos_veh = np.ones((4, 1)) # homogeneous coordinates
55 | pos_veh[0:3] = x[0:3]
56 | pos_sens = self.veh_to_sens*pos_veh # transform from vehicle to sensor coordinates
57 | alpha = np.arctan(pos_sens[1, 0]/pos_sens[0, 0]) # calc angle between object and x-axis
58 | # no normalization needed because returned alpha always lies between [-pi/2, pi/2]
59 | within_fov = alpha > self.fov[0] and alpha < self.fov[1]
60 | #print('alpha:', alpha, type(alpha), 'within_fov:', within_fov, type(within_fov))
61 | return within_fov
62 |
63 | ############
64 | # END student code
65 | ############
66 |
67 | def get_hx(self, x):
68 | # calculate nonlinear measurement expectation value h(x)
69 | if self.name == 'lidar':
70 | pos_veh = np.ones((4, 1)) # homogeneous coordinates
71 | pos_veh[0:3] = x[0:3]
72 | pos_sens = self.veh_to_sens*pos_veh # transform from vehicle to lidar coordinates
73 | return pos_sens[0:3]
74 | elif self.name == 'camera':
75 |
76 | ############
77 | # TODO Step 4: implement nonlinear camera measurement function h:
78 | # - transform position estimate from vehicle to camera coordinates
79 | # - project from camera to image coordinates
80 | # - make sure to not divide by zero, raise an error if needed
81 | # - return h(x)
82 | ############
83 |
84 | pos_veh = np.ones((4, 1)) # homogeneous coordinates
85 | pos_veh[0:3] = x[0:3]
86 | pos_sens = self.veh_to_sens * pos_veh # transform from vehicle to camera coordinates
87 | hx = np.zeros((2,1))
88 | if pos_sens[0] == 0:
89 | raise NameError('Jacobian not defined for x=0!')
90 | else:
91 | hx[0, 0] = self.c_i - self.f_i * pos_sens[1] / pos_sens[0] # project to image coordinates
92 | hx[1, 0] = self.c_j - self.f_j * pos_sens[2] / pos_sens[0]
93 | #print(f'CAMERA hx:\n{hx}')
94 | return hx
95 |
96 | ############
97 | # END student code
98 | ############
99 |
100 | def get_H(self, x):
101 | # calculate Jacobian H at current x from h(x)
102 | H = np.matrix(np.zeros((self.dim_meas, params.dim_state)))
103 | R = self.veh_to_sens[0:3, 0:3] # rotation
104 | T = self.veh_to_sens[0:3, 3] # translation
105 | if self.name == 'lidar':
106 | H[0:3, 0:3] = R
107 | elif self.name == 'camera':
108 | # check and print error message if dividing by zero
109 | if R[0,0]*x[0] + R[0,1]*x[1] + R[0,2]*x[2] + T[0] == 0:
110 | raise NameError('Jacobian not defined for this x!')
111 | else:
112 | H[0,0] = self.f_i * (-R[1,0] / (R[0,0]*x[0] + R[0,1]*x[1] + R[0,2]*x[2] + T[0])
113 | + R[0,0] * (R[1,0]*x[0] + R[1,1]*x[1] + R[1,2]*x[2] + T[1]) \
114 | / ((R[0,0]*x[0] + R[0,1]*x[1] + R[0,2]*x[2] + T[0])**2))
115 | H[1,0] = self.f_j * (-R[2,0] / (R[0,0]*x[0] + R[0,1]*x[1] + R[0,2]*x[2] + T[0])
116 | + R[0,0] * (R[2,0]*x[0] + R[2,1]*x[1] + R[2,2]*x[2] + T[2]) \
117 | / ((R[0,0]*x[0] + R[0,1]*x[1] + R[0,2]*x[2] + T[0])**2))
118 | H[0,1] = self.f_i * (-R[1,1] / (R[0,0]*x[0] + R[0,1]*x[1] + R[0,2]*x[2] + T[0])
119 | + R[0,1] * (R[1,0]*x[0] + R[1,1]*x[1] + R[1,2]*x[2] + T[1]) \
120 | / ((R[0,0]*x[0] + R[0,1]*x[1] + R[0,2]*x[2] + T[0])**2))
121 | H[1,1] = self.f_j * (-R[2,1] / (R[0,0]*x[0] + R[0,1]*x[1] + R[0,2]*x[2] + T[0])
122 | + R[0,1] * (R[2,0]*x[0] + R[2,1]*x[1] + R[2,2]*x[2] + T[2]) \
123 | / ((R[0,0]*x[0] + R[0,1]*x[1] + R[0,2]*x[2] + T[0])**2))
124 | H[0,2] = self.f_i * (-R[1,2] / (R[0,0]*x[0] + R[0,1]*x[1] + R[0,2]*x[2] + T[0])
125 | + R[0,2] * (R[1,0]*x[0] + R[1,1]*x[1] + R[1,2]*x[2] + T[1]) \
126 | / ((R[0,0]*x[0] + R[0,1]*x[1] + R[0,2]*x[2] + T[0])**2))
127 | H[1,2] = self.f_j * (-R[2,2] / (R[0,0]*x[0] + R[0,1]*x[1] + R[0,2]*x[2] + T[0])
128 | + R[0,2] * (R[2,0]*x[0] + R[2,1]*x[1] + R[2,2]*x[2] + T[2]) \
129 | / ((R[0,0]*x[0] + R[0,1]*x[1] + R[0,2]*x[2] + T[0])**2))
130 | return H
131 |
132 | def generate_measurement(self, num_frame, z, meas_list):
133 | # generate new measurement from this sensor and add to measurement list
134 | ############
135 | # TODO Step 4: remove restriction to lidar in order to include camera as well
136 | ############
137 |
138 | #if self.name == 'lidar' or self.name == 'camera':
139 | if True:
140 | meas = Measurement(num_frame, z, self)
141 | meas_list.append(meas)
142 | #print(f'sensor.name: {self.name}, num_frame={num_frame}, z={z}')
143 | return meas_list
144 |
145 | ############
146 | # END student code
147 | ############
148 |
149 |
150 | ###################
151 |
152 | class Measurement:
153 | '''Measurement class including measurement values, covariance, timestamp, sensor'''
154 | def __init__(self, num_frame, z, sensor):
155 | #print(f'New measurement: num_frame={num_frame}, sensor.name={sensor.name}, z={z}')
156 | # create measurement object
157 | self.t = (num_frame - 1) * params.dt # time
158 | if sensor.name == 'lidar':
159 | sigma_lidar_x = params.sigma_lidar_x # load params
160 | sigma_lidar_y = params.sigma_lidar_y
161 | sigma_lidar_z = params.sigma_lidar_z
162 | self.z = np.zeros((sensor.dim_meas,1)) # measurement vector
163 | self.z[0] = z[0]
164 | self.z[1] = z[1]
165 | self.z[2] = z[2]
166 | self.sensor = sensor # sensor that generated this measurement
167 | self.R = np.matrix([[sigma_lidar_x**2, 0, 0 ], # measurement noise covariance matrix
168 | [0, sigma_lidar_y**2, 0 ],
169 | [0, 0, sigma_lidar_z**2]])
170 |
171 | self.width = z[4]
172 | self.length = z[5]
173 | self.height = z[3]
174 | self.yaw = z[6]
175 | elif sensor.name == 'camera':
176 |
177 | ############
178 | # TODO Step 4: initialize camera measurement including z, R, and sensor
179 | ############
180 |
181 | sigma_cam_i = params.sigma_cam_i
182 | sigma_cam_j = params.sigma_cam_j
183 | self.z = np.zeros((sensor.dim_meas, 1)) # measurement vector
184 | self.z[0] = z[0]
185 | self.z[1] = z[1]
186 | self.sensor = sensor # sensor that generated this measurement
187 | self.R = np.matrix([[sigma_cam_i**2, 0 ], # measurement noise covariance matrix
188 | [0, sigma_cam_j**2]])
189 | # From loop_over_dataset.py
190 | # z = [box.center_x, box.center_y, box.width, box.length]
191 | # meas_list_cam = camera.generate_measurement(cnt_frame, z, meas_list_cam)
192 | self.width = z[2]
193 | self.length = z[3]
194 | ############
195 | # END student code
196 | ############
197 |
--------------------------------------------------------------------------------
/student/objdet_detect.py:
--------------------------------------------------------------------------------
1 | # ---------------------------------------------------------------------
2 | # Project "Track 3D-Objects Over Time"
3 | # Copyright (C) 2020, Dr. Antje Muntzinger / Dr. Andreas Haja.
4 | #
5 | # Purpose of this file : Detect 3D objects in lidar point clouds using deep learning
6 | #
7 | # You should have received a copy of the Udacity license together with this program.
8 | #
9 | # https://www.udacity.com/course/self-driving-car-engineer-nanodegree--nd013
10 | # ----------------------------------------------------------------------
11 | #
12 |
13 | # general package imports
14 | import numpy as np
15 | import torch
16 | from easydict import EasyDict as edict
17 |
18 | # add project directory to python path to enable relative imports
19 | import os
20 | import sys
21 | PACKAGE_PARENT = '..'
22 | SCRIPT_DIR = os.path.dirname(os.path.realpath(os.path.join(os.getcwd(), os.path.expanduser(__file__))))
23 | sys.path.append(os.path.normpath(os.path.join(SCRIPT_DIR, PACKAGE_PARENT)))
24 |
25 | # model-related
26 | from tools.objdet_models.resnet.models import fpn_resnet
27 | from tools.objdet_models.resnet.utils.evaluation_utils import decode, post_processing
28 |
29 | from tools.objdet_models.darknet.models.darknet2pytorch import Darknet as darknet
30 | from tools.objdet_models.darknet.utils.evaluation_utils import post_processing_v2
31 |
32 |
33 | # load model-related parameters into an edict
34 | def load_configs_model(model_name='darknet', configs=None):
35 |
36 | # init config file, if none has been passed
37 | if configs==None:
38 | configs = edict()
39 |
40 | # get parent directory of this file to enable relative paths
41 | curr_path = os.path.dirname(os.path.realpath(__file__))
42 | parent_path = configs.model_path = os.path.abspath(os.path.join(curr_path, os.pardir))
43 |
44 | # set parameters according to model type
45 | if model_name == "darknet":
46 | configs.model_path = os.path.join(parent_path, 'tools', 'objdet_models', 'darknet')
47 | configs.pretrained_filename = os.path.join(configs.model_path, 'pretrained', 'complex_yolov4_mse_loss.pth')
48 | configs.arch = 'darknet'
49 | configs.batch_size = 4
50 | configs.cfgfile = os.path.join(configs.model_path, 'config', 'complex_yolov4.cfg')
51 | configs.conf_thresh = 0.5
52 | configs.distributed = False
53 | configs.img_size = 608
54 | configs.nms_thresh = 0.4
55 | configs.num_samples = None
56 | configs.num_workers = 4
57 | configs.pin_memory = True
58 | configs.use_giou_loss = False
59 |
60 | elif model_name == 'fpn_resnet':
61 | ####### ID_S3_EX1-3 START #######
62 | #######
63 | print("student task ID_S3_EX1-3")
64 |
65 | configs.model_path = os.path.join(parent_path, 'tools', 'objdet_models', 'resnet')
66 | configs.pretrained_filename = os.path.join(configs.model_path, 'pretrained', 'fpn_resnet_18_epoch_300.pth')
67 | configs.arch = 'fpn_resnet'
68 | configs.batch_size = 4
69 | #configs.cfgfile = os.path.join(configs.model_path, 'config', 'complex_yolov4.cfg')
70 | #configs.conf_thresh = 0.5
71 | configs.distributed = False
72 | configs.img_size = 608
73 | #configs.nms_thresh = 0.4
74 | configs.num_samples = None
75 | configs.num_workers = 1
76 | configs.pin_memory = True
77 | #configs.use_giou_loss = False
78 |
79 | #######
80 | ####### ID_S3_EX1-3 END #######
81 |
82 | else:
83 | raise ValueError("Error: Invalid model name")
84 | print('configs.pretrained_filename:', configs.pretrained_filename)
85 |
86 | # GPU vs. CPU
87 | configs.no_cuda = True # if true, cuda is not used
88 | configs.gpu_idx = 0 # GPU index to use.
89 | configs.device = torch.device('cpu' if configs.no_cuda else 'cuda:{}'.format(configs.gpu_idx))
90 |
91 | return configs
92 |
93 |
94 | # load all object-detection parameters into an edict
95 | def load_configs(model_name='fpn_resnet', configs=None):
96 |
97 | # init config file, if none has been passed
98 | if configs==None:
99 | configs = edict()
100 |
101 | # birds-eye view (bev) parameters
102 | configs.lim_x = [0, 50] # detection range in m
103 | configs.lim_y = [-25, 25]
104 | configs.lim_z = [-1, 3]
105 | configs.lim_r = [0, 1.0] # reflected lidar intensity
106 | configs.bev_width = 608 # pixel resolution of bev image
107 | configs.bev_height = 608
108 |
109 | # add model-dependent parameters
110 | configs = load_configs_model(model_name, configs)
111 |
112 | # visualization parameters
113 | configs.output_width = 608 # width of result image (height may vary)
114 | configs.obj_colors = [[0, 255, 255], [0, 0, 255], [255, 0, 0]] # 'Pedestrian': 0, 'Car': 1, 'Cyclist': 2
115 |
116 | return configs
117 |
118 |
119 | def setup_configs_for_resnet(configs):
120 | configs.pin_memory = True
121 | configs.distributed = False # For testing on 1 GPU only
122 |
123 | configs.input_size = (608, 608)
124 | configs.hm_size = (152, 152)
125 | configs.down_ratio = 4
126 | configs.max_objects = 50
127 |
128 | configs.imagenet_pretrained = False
129 | configs.head_conv = 64
130 | configs.num_classes = 3
131 | configs.num_center_offset = 2
132 | configs.num_z = 1
133 | configs.num_dim = 3
134 | configs.num_direction = 2 # sin, cos
135 |
136 | configs.heads = {
137 | 'hm_cen': configs.num_classes,
138 | 'cen_offset': configs.num_center_offset,
139 | 'direction': configs.num_direction,
140 | 'z_coor': configs.num_z,
141 | 'dim': configs.num_dim
142 | }
143 | configs.num_input_features = 4
144 |
145 | configs.conf_thresh = 0.5
146 |
147 | def create_resnet_model(configs):
148 | """Create model based on architecture name"""
149 | try:
150 | arch_parts = configs.arch.split('_')
151 | #num_layers = int(arch_parts[-1])
152 | num_layers = 18 #18
153 | except:
154 | raise ValueError
155 | if 'fpn_resnet' in configs.arch:
156 | print('using ResNet architecture with feature pyramid')
157 | model = fpn_resnet.get_pose_net(num_layers=num_layers, heads=configs.heads, head_conv=configs.head_conv,
158 | imagenet_pretrained=configs.imagenet_pretrained)
159 | elif 'resnet' in configs.arch:
160 | print('using ResNet architecture')
161 | model = resnet.get_pose_net(num_layers=num_layers, heads=configs.heads, head_conv=configs.head_conv,
162 | imagenet_pretrained=configs.imagenet_pretrained)
163 | else:
164 | assert False, 'Undefined model backbone'
165 |
166 | return model
167 |
168 |
169 | # create model according to selected model type
170 | def create_model(configs):
171 |
172 | # check for availability of model file
173 | assert os.path.isfile(configs.pretrained_filename), "No file at {}".format(configs.pretrained_filename)
174 |
175 | # create model depending on architecture name
176 | if (configs.arch == 'darknet') and (configs.cfgfile is not None):
177 | print('using darknet')
178 | model = darknet(cfgfile=configs.cfgfile, use_giou_loss=configs.use_giou_loss)
179 |
180 | elif 'fpn_resnet' in configs.arch:
181 | print('using ResNet architecture with feature pyramid')
182 |
183 | ####### ID_S3_EX1-4 START #######
184 | #######
185 | print("student task ID_S3_EX1-4")
186 | setup_configs_for_resnet(configs)
187 | model = create_resnet_model(configs)
188 | #######
189 | ####### ID_S3_EX1-4 END #######
190 |
191 | else:
192 | assert False, 'Undefined model backbone'
193 |
194 | # load model weights
195 | model.load_state_dict(torch.load(configs.pretrained_filename, map_location='cpu'))
196 | print('Loaded weights from {}\n'.format(configs.pretrained_filename))
197 |
198 | # set model to evaluation state
199 | configs.device = torch.device('cpu' if configs.no_cuda else 'cuda:{}'.format(configs.gpu_idx))
200 | model = model.to(device=configs.device) # load model to either cpu or gpu
201 | model.eval()
202 |
203 | return model
204 |
205 |
206 | # detect trained objects in birds-eye view
207 | def detect_objects(input_bev_maps, model, configs):
208 |
209 | # deactivate autograd engine during test to reduce memory usage and speed up computations
210 | with torch.no_grad():
211 |
212 | # perform inference
213 | outputs = model(input_bev_maps)
214 |
215 | # decode model output into target object format
216 | if 'darknet' in configs.arch:
217 |
218 | # perform post-processing
219 | output_post = post_processing_v2(outputs, conf_thresh=configs.conf_thresh, nms_thresh=configs.nms_thresh)
220 | detections = []
221 | for sample_i in range(len(output_post)):
222 | if output_post[sample_i] is None:
223 | continue
224 | detection = output_post[sample_i]
225 | for obj in detection:
226 | x, y, w, l, im, re, _, _, _ = obj
227 | yaw = np.arctan2(im, re)
228 | detections.append([1, x, y, 0.0, 1.50, w, l, yaw])
229 |
230 | elif 'fpn_resnet' in configs.arch:
231 | # decode output and perform post-processing
232 |
233 | ####### ID_S3_EX1-5 START #######
234 | #######
235 | print("student task ID_S3_EX1-5")
236 |
237 | #######
238 | ####### ID_S3_EX1-5 END #######
239 |
240 |
241 |
242 | ####### ID_S3_EX2 START #######
243 | #######
244 | # Extract 3d bounding boxes from model response
245 | print("student task ID_S3_EX2")
246 | objects = []
247 |
248 | ## step 1 : check whether there are any detections
249 |
250 | ## step 2 : loop over all detections
251 |
252 | ## step 3 : perform the conversion using the limits for x, y and z set in the configs structure
253 |
254 | ## step 4 : append the current object to the 'objects' array
255 |
256 | #######
257 | ####### ID_S3_EX2 START #######
258 |
259 | return objects
260 |
261 |
--------------------------------------------------------------------------------
/student/params.py:
--------------------------------------------------------------------------------
1 | # ---------------------------------------------------------------------
2 | # Project "Track 3D-Objects Over Time"
3 | # Copyright (C) 2020, Dr. Antje Muntzinger / Dr. Andreas Haja.
4 | #
5 | # Purpose of this file : Parameter file for tracking
6 | #
7 | # You should have received a copy of the Udacity license together with this program.
8 | #
9 | # https://www.udacity.com/course/self-driving-car-engineer-nanodegree--nd013
10 | # ----------------------------------------------------------------------
11 | #
12 |
13 | # general parameters
14 | dim_state = 6 # process model dimension
15 |
16 | # Kalman filter parameters (Step 1)
17 | dt = 0.1 # time increment
18 | q = 3 #0.1 #3 # process noise variable for Kalman filter Q
19 |
20 | # track management parameters (Step 2)
21 | #confirmed_threshold = 0.8 # track score threshold to switch from 'tentative' to 'confirmed'
22 | #delete_threshold = 0.6 # track score threshold to delete confirmed tracks
23 | window = 10 # number of frames for track score calculation
24 | max_P = 3**2 # delete track if covariance of px or py bigger than this
25 | sigma_p44 = 50 # initial setting for estimation error covariance P entry for vx
26 | sigma_p55 = 50 # initial setting for estimation error covariance P entry for vy
27 | sigma_p66 = 5 # initial setting for estimation error covariance P entry for vz
28 | weight_dim = 0.1 # sliding average parameter for dimension estimation
29 |
30 | # association parameters (Step 3)
31 | gating_threshold = 0.995 # percentage of correct measurements that shall lie inside gate
32 |
33 | # measurement parameters (Step 4)
34 | sigma_lidar_x = 0.1 #0.1 # measurement noise standard deviation for lidar x position
35 | sigma_lidar_y = 0.1 #0.1 # measurement noise standard deviation for lidar y position
36 | sigma_lidar_z = 0.1 #0.1 # measurement noise standard deviation for lidar z position
37 | sigma_cam_i = 5 #5 # measurement noise standard deviation for image i coordinate
38 | sigma_cam_j = 5 #5 # measurement noise standard deviation for image j coordinate
39 |
--------------------------------------------------------------------------------
/student/trackmanagement.py:
--------------------------------------------------------------------------------
1 | # ---------------------------------------------------------------------
2 | # Project "Track 3D-Objects Over Time"
3 | # Copyright (C) 2020, Dr. Antje Muntzinger / Dr. Andreas Haja.
4 | #
5 | # Purpose of this file : Classes for track and track management
6 | #
7 | # You should have received a copy of the Udacity license together with this program.
8 | #
9 | # https://www.udacity.com/course/self-driving-car-engineer-nanodegree--nd013
10 | # ----------------------------------------------------------------------
11 | #
12 |
13 | # imports
14 | import numpy as np
15 | import collections
16 |
17 | # add project directory to python path to enable relative imports
18 | import os
19 | import sys
20 | PACKAGE_PARENT = '..'
21 | SCRIPT_DIR = os.path.dirname(os.path.realpath(os.path.join(os.getcwd(), os.path.expanduser(__file__))))
22 | sys.path.append(os.path.normpath(os.path.join(SCRIPT_DIR, PACKAGE_PARENT)))
23 | import misc.params as params
24 |
25 | class Track:
26 | '''Track class with state, covariance, id, score'''
27 | def __init__(self, meas, id, cnt_frame):
28 | print('creating track no.', id)
29 | M_rot = meas.sensor.sens_to_veh[0:3, 0:3] # rotation matrix from sensor to vehicle coordinates
30 |
31 | ############
32 | # TODO Step 2: initialization:
33 | # - replace fixed track initialization values by initialization of x and P based on
34 | # unassigned measurement transformed from sensor to vehicle coordinates
35 | # - initialize track state and track score with appropriate values
36 | ############
37 |
38 | self.x = np.matrix([[49.53980697],
39 | [ 3.41006279],
40 | [ 0.91790581],
41 | [ 0. ],
42 | [ 0. ],
43 | [ 0. ]])
44 | self.P = np.matrix([[9.0e-02, 0.0e+00, 0.0e+00, 0.0e+00, 0.0e+00, 0.0e+00],
45 | [0.0e+00, 9.0e-02, 0.0e+00, 0.0e+00, 0.0e+00, 0.0e+00],
46 | [0.0e+00, 0.0e+00, 6.4e-03, 0.0e+00, 0.0e+00, 0.0e+00],
47 | [0.0e+00, 0.0e+00, 0.0e+00, 2.5e+03, 0.0e+00, 0.0e+00],
48 | [0.0e+00, 0.0e+00, 0.0e+00, 0.0e+00, 2.5e+03, 0.0e+00],
49 | [0.0e+00, 0.0e+00, 0.0e+00, 0.0e+00, 0.0e+00, 2.5e+01]])
50 | self.state = 'confirmed'
51 | self.score = 0
52 |
53 | # PART 2
54 | z = np.ones((4, 1))
55 | z[:meas.sensor.dim_meas] = meas.z
56 | self.x = np.zeros((6, 1))
57 | self.x[:3] = (meas.sensor.sens_to_veh * z)[:3]
58 | self.P = np.matrix([[9.0e-02, 0.0e+00, 0.0e+00, 0.0e+00, 0.0e+00, 0.0e+00],
59 | [0.0e+00, 9.0e-02, 0.0e+00, 0.0e+00, 0.0e+00, 0.0e+00],
60 | [0.0e+00, 0.0e+00, 6.4e-03, 0.0e+00, 0.0e+00, 0.0e+00],
61 | [0.0e+00, 0.0e+00, 0.0e+00, params.sigma_p44, 0.0e+00, 0.0e+00],
62 | [0.0e+00, 0.0e+00, 0.0e+00, 0.0e+00, params.sigma_p55, 0.0e+00],
63 | [0.0e+00, 0.0e+00, 0.0e+00, 0.0e+00, 0.0e+00, params.sigma_p66]])
64 | self.state = 'initialized'
65 | self.score = 1. / params.window
66 | self.assignments = {cnt_frame: [1]} #[1] #[1, 0]
67 | #self.last_assignments = self.assignments
68 |
69 | ############
70 | # END student code
71 | ############
72 |
73 | # other track attributes
74 | self.id = id
75 | self.width = meas.width
76 | self.height = meas.height
77 | self.length = meas.length
78 | self.yaw = np.arccos(M_rot[0,0]*np.cos(meas.yaw) + M_rot[0,1]*np.sin(meas.yaw)) # transform rotation from sensor to vehicle coordinates
79 | self.t = meas.t
80 |
81 | def append_assignment_and_compute_score(self, assignment, cnt_frame):
82 | if cnt_frame in self.assignments:
83 | self.assignments[cnt_frame].append(assignment)
84 | else:
85 | self.assignments[cnt_frame] = [assignment]
86 | points = 0
87 | for frame in self.assignments.keys():
88 | if cnt_frame - frame < params.window:
89 | assignments = self.assignments[frame]
90 | points += any(assignments)
91 | points += len(assignments) > 1 and all(assignments)
92 | self.score = points / float(params.window)
93 | #print(f'score={self.score}')
94 |
95 | def set_x(self, x):
96 | self.x = x
97 |
98 | def set_P(self, P):
99 | self.P = P
100 |
101 | def set_t(self, t):
102 | self.t = t
103 |
104 | def update_attributes(self, meas):
105 | # use exponential sliding average to estimate dimensions and orientation
106 | if meas.sensor.name == 'lidar':
107 | c = params.weight_dim
108 | self.width = c*meas.width + (1 - c)*self.width
109 | self.length = c*meas.length + (1 - c)*self.length
110 | self.height = c*meas.height + (1 - c)*self.height
111 | M_rot = meas.sensor.sens_to_veh
112 | self.yaw = np.arccos(M_rot[0,0]*np.cos(meas.yaw) + M_rot[0,1]*np.sin(meas.yaw)) # transform rotation from sensor to vehicle coordinates
113 |
114 |
115 | ###################
116 |
117 | class Trackmanagement:
118 | '''Track manager with logic for initializing and deleting objects'''
119 | def __init__(self):
120 | self.N = 0 # current number of tracks
121 | self.track_list = []
122 | self.last_id = -1
123 | self.result_list = []
124 |
125 | def manage_tracks(self, unassigned_tracks, unassigned_meas, meas_list, cnt_frame):
126 | ############
127 | # TODO Step 2: implement track management:
128 | # - decrease the track score for unassigned tracks
129 | # - delete tracks if the score is too low or P is too big (check params.py for parameters that might be helpful, but
130 | # feel free to define your own parameters)
131 | ############
132 |
133 | #for meas in meas_list:
134 | # print(f'meas.sensor.name={meas.sensor.name}, meas.z={meas.z.transpose()}\n{meas.sensor.sens_to_veh}=sens_to_veh, meas.sensor.fov={meas.sensor.fov}')
135 |
136 | # decrease score for unassigned tracks
137 | for i in unassigned_tracks:
138 | track = self.track_list[i]
139 | # check visibility
140 | #if meas_list:
141 | # sensor = meas_list[0].sensor
142 | # if sensor.name == 'lidar' and not sensor.in_fov(track.x):
143 | # pass
144 | track.append_assignment_and_compute_score(0, cnt_frame)
145 |
146 | # delete old tracks
147 | to_be_deleted = []
148 | for i in range(len(self.track_list)):
149 | track = self.track_list[i]
150 | P = track.P
151 | if (track.state == 'initialized' and track.score < 0.3 and len(track.assignments) >= 3) or \
152 | (track.state == 'tentative' and track.score <= 0.6) or \
153 | (track.state == 'confirmed' and track.score <= 0.8) or \
154 | (P[0,0] > params.max_P or P[1,1] > params.max_P):
155 | to_be_deleted.append(track)
156 | for track in to_be_deleted:
157 | self.delete_track(track)
158 |
159 | ############
160 | # END student code
161 | ############
162 |
163 | # initialize new track with unassigned measurement
164 | for j in unassigned_meas:
165 | if meas_list[j].sensor.name == 'lidar': # only initialize with lidar measurements
166 | self.init_track(meas_list[j], cnt_frame)
167 |
168 | def addTrackToList(self, track):
169 | self.track_list.append(track)
170 | self.N += 1
171 | self.last_id = track.id
172 |
173 | def init_track(self, meas, cnt_frame):
174 | track = Track(meas, self.last_id + 1, cnt_frame)
175 | self.addTrackToList(track)
176 |
177 | def delete_track(self, track):
178 | print('deleting track no.', track.id)
179 | self.track_list.remove(track)
180 |
181 | def handle_updated_track(self, track, cnt_frame):
182 | ############
183 | # TODO Step 2: implement track management for updated tracks:
184 | # - increase track score
185 | # - set track state to 'tentative' or 'confirmed'
186 | ############
187 |
188 | track.append_assignment_and_compute_score(1, cnt_frame)
189 | if track.state == 'initialized' and track.score >= 0.8:
190 | track.state = 'tentative'
191 | if track.state == 'tentative' and track.score >= 1.5:
192 | track.state = 'confirmed'
193 |
194 | ############
195 | # END student code
196 | ############
197 |
--------------------------------------------------------------------------------