diff --git a/.gitignore b/.gitignore index 72364f99..7e78d1e1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] -*$py.class # C extensions *.so @@ -12,9 +11,7 @@ env/ build/ develop-eggs/ dist/ -downloads/ eggs/ -.eggs/ lib/ lib64/ parts/ @@ -24,12 +21,6 @@ var/ .installed.cfg *.egg -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - # Installer logs pip-log.txt pip-delete-this-directory.txt @@ -38,12 +29,9 @@ pip-delete-this-directory.txt htmlcov/ .tox/ .coverage -.coverage.* .cache nosetests.xml coverage.xml -*,cover -.hypothesis/ # Translations *.mo @@ -51,39 +39,11 @@ coverage.xml # Django stuff: *.log -local_settings.py - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy # Sphinx documentation docs/_build/ -# PyBuilder -target/ - -# IPython Notebook -.ipynb_checkpoints - -# pyenv -.python-version - -# celery beat schedule file -celerybeat-schedule - -# dotenv -.env - -# virtualenv -venv/ -ENV/ - -# Spyder project settings -.spyderproject +model_data +model_data/ +.ipynb_checkpoints/ -# Rope project settings -.ropeproject diff --git a/MyReadMe.txt b/MyReadMe.txt new file mode 100644 index 00000000..4fb1f21d --- /dev/null +++ b/MyReadMe.txt @@ -0,0 +1,6 @@ +python my_deep_sort_app.py \ + --sequence_dir=$HOME/e/dataset_tiptical/MOT16/MOT16-06 \ + --detection_file=$HOME/e/dataset_tiptical/MOT16/MOT16-06/MOT16-06.npy \ + --min_confidence=0.3 \ + --nn_budget=100 \ + --display=True diff --git a/deep_sort/linear_assignment.py b/deep_sort/linear_assignment.py index 178456cf..d432a08c 100644 --- a/deep_sort/linear_assignment.py +++ b/deep_sort/linear_assignment.py @@ -1,7 +1,13 @@ # vim: expandtab:ts=4:sw=4 from __future__ import absolute_import import numpy as np -from sklearn.utils.linear_assignment_ import linear_assignment + +import sklearn +#FutureWarning: The linear_assignment function is deprecated in 0.21 and will be removed from 0.23. Use scipy.optimize.linear_sum_assignment instead. +if sklearn.__version__>'0.22': + from scipy.optimize import linear_sum_assignment as linear_assignment # tf1.15, sklearn=>0.23 +else: + from sklearn.utils.linear_assignment_ import linear_assignment # tf1.14, sklearn=>0.22 from . import kalman_filter @@ -57,6 +63,10 @@ def min_cost_matching( cost_matrix[cost_matrix > max_distance] = max_distance + 1e-5 indices = linear_assignment(cost_matrix) + # Fix for tf1.15 envirement when use scipy.optimize.linear_sum_assignment instead from sklearn.utils.linear_assignment_ + if type(indices)==tuple: + indices = np.array([[x,y] for x,y in zip(*indices)]) + matches, unmatched_tracks, unmatched_detections = [], [], [] for col, detection_idx in enumerate(detection_indices): if col not in indices[:, 1]: diff --git a/deep_sort/nn_matching.py b/deep_sort/nn_matching.py index 2e7bfea4..c1f20b5e 100644 --- a/deep_sort/nn_matching.py +++ b/deep_sort/nn_matching.py @@ -49,8 +49,8 @@ def _cosine_distance(a, b, data_is_normalized=False): """ if not data_is_normalized: - a = np.asarray(a) / np.linalg.norm(a, axis=1, keepdims=True) - b = np.asarray(b) / np.linalg.norm(b, axis=1, keepdims=True) + a = np.asarray(a) / (np.linalg.norm(a, axis=1, keepdims=True)+1e-5) + b = np.asarray(b) / (np.linalg.norm(b, axis=1, keepdims=True)+1e-5) return 1. - np.dot(a, b.T) diff --git a/deep_sort_ex/__init__.py b/deep_sort_ex/__init__.py new file mode 100644 index 00000000..43e08fb8 --- /dev/null +++ b/deep_sort_ex/__init__.py @@ -0,0 +1 @@ +# vim: expandtab:ts=4:sw=4 diff --git a/deep_sort_ex/detection.py b/deep_sort_ex/detection.py new file mode 100644 index 00000000..95c48d56 --- /dev/null +++ b/deep_sort_ex/detection.py @@ -0,0 +1,67 @@ +# vim: expandtab:ts=4:sw=4 +import numpy as np + + +class Detection(object): + """ + This class represents a bounding box detection in a single image. + + Parameters + ---------- + tlwh : array_like + Bounding box in format `(x, y, w, h)`. + confidence : float + Detector confidence score. + feature : array_like + A feature vector that describes the object contained in this image. + + Attributes + ---------- + tlwh : ndarray + Bounding box in format `(top left x, top left y, width, height)`. + confidence : ndarray + Detector confidence score. + feature : ndarray | NoneType + A feature vector that describes the object contained in this image. + + """ + + def __init__(self, tlwh, confidence, feature, t=None, exts1=None, exts2=None, binding_obj=None, flag=None): + ''' + @param tlwh - bbox: top, left, width, height + @param confidence - 目标检测置信度 + @param ffeature - 目标图像特征码 + @param t - 检测时间(秒) + @param exts1 - 扩展属性: 扩展卡尔曼滤波器的向量(mean), 需要与 tracker,track的n_extend参数配合使用 + @param exts2 - 扩展属性: 独立于卡尔曼滤波器,对扩展通道单独处理 + @param binding_obj - 绑定原始目标检测的序号(或结构体对象),方便数据源跟踪 + @param flag - 跟踪目标标记, 如用于目标类别,由外部解析 + ''' + self.tlwh = np.asarray(tlwh, dtype=np.float) + self.t = t + self.exts1 = exts1 + self.exts2 = exts2 + self.binding_obj = binding_obj + self.flag = flag + self.confidence = float(confidence) + self.feature = np.asarray(feature, dtype=np.float32) + + def to_tlbr(self): + """Convert bounding box to format `(min x, min y, max x, max y)`, i.e., + `(top left, bottom right)`. + """ + ret = self.tlwh.copy() + ret[2:] += ret[:2] + return ret + + def to_xyah(self): + """Convert bounding box to format `(center x, center y, aspect ratio, + height)`, where the aspect ratio is `width / height`. + """ + ret = self.tlwh.copy() + ret[:2] += ret[2:] / 2 + ret[2] /= ret[3] + if self.exts1 is None: + return ret + else: + return np.hstack([ret, self.exts1]) diff --git a/deep_sort_ex/iou_matching.py b/deep_sort_ex/iou_matching.py new file mode 100644 index 00000000..c4dd0b88 --- /dev/null +++ b/deep_sort_ex/iou_matching.py @@ -0,0 +1,81 @@ +# vim: expandtab:ts=4:sw=4 +from __future__ import absolute_import +import numpy as np +from . import linear_assignment + + +def iou(bbox, candidates): + """Computer intersection over union. + + Parameters + ---------- + bbox : ndarray + A bounding box in format `(top left x, top left y, width, height)`. + candidates : ndarray + A matrix of candidate bounding boxes (one per row) in the same format + as `bbox`. + + Returns + ------- + ndarray + The intersection over union in [0, 1] between the `bbox` and each + candidate. A higher score means a larger fraction of the `bbox` is + occluded by the candidate. + + """ + bbox_tl, bbox_br = bbox[:2], bbox[:2] + bbox[2:] + candidates_tl = candidates[:, :2] + candidates_br = candidates[:, :2] + candidates[:, 2:] + + tl = np.c_[np.maximum(bbox_tl[0], candidates_tl[:, 0])[:, np.newaxis], + np.maximum(bbox_tl[1], candidates_tl[:, 1])[:, np.newaxis]] + br = np.c_[np.minimum(bbox_br[0], candidates_br[:, 0])[:, np.newaxis], + np.minimum(bbox_br[1], candidates_br[:, 1])[:, np.newaxis]] + wh = np.maximum(0., br - tl) + + area_intersection = wh.prod(axis=1) + area_bbox = bbox[2:].prod() + area_candidates = candidates[:, 2:].prod(axis=1) + return area_intersection / (area_bbox + area_candidates - area_intersection) + + +def iou_cost(tracks, detections, track_indices=None, + detection_indices=None): + """An intersection over union distance metric. + + Parameters + ---------- + tracks : List[deep_sort.track.Track] + A list of tracks. + detections : List[deep_sort.detection.Detection] + A list of detections. + track_indices : Optional[List[int]] + A list of indices to tracks that should be matched. Defaults to + all `tracks`. + detection_indices : Optional[List[int]] + A list of indices to detections that should be matched. Defaults + to all `detections`. + + Returns + ------- + ndarray + Returns a cost matrix of shape + len(track_indices), len(detection_indices) where entry (i, j) is + `1 - iou(tracks[track_indices[i]], detections[detection_indices[j]])`. + + """ + if track_indices is None: + track_indices = np.arange(len(tracks)) + if detection_indices is None: + detection_indices = np.arange(len(detections)) + + cost_matrix = np.zeros((len(track_indices), len(detection_indices))) + for row, track_idx in enumerate(track_indices): + if tracks[track_idx].time_since_update > 1: + cost_matrix[row, :] = linear_assignment.INFTY_COST + continue + + bbox = tracks[track_idx].to_tlwh() + candidates = np.asarray([detections[i].tlwh for i in detection_indices]) + cost_matrix[row, :] = 1. - iou(bbox, candidates) + return cost_matrix diff --git a/deep_sort_ex/kalman_filter.py b/deep_sort_ex/kalman_filter.py new file mode 100644 index 00000000..90d05470 --- /dev/null +++ b/deep_sort_ex/kalman_filter.py @@ -0,0 +1,250 @@ +# vim: expandtab:ts=4:sw=4 +import numpy as np +import scipy.linalg + + +""" +Table for the 0.95 quantile of the chi-square distribution with N degrees of +freedom (contains values for N=1, ..., 9). Taken from MATLAB/Octave's chi2inv +function and used as Mahalanobis gating threshold. +""" +chi2inv95 = { + 1: 3.8415, + 2: 5.9915, + 3: 7.8147, + 4: 9.4877, + 5: 11.070, + 6: 12.592, + 7: 14.067, + 8: 15.507, + 9: 16.919} + + +class KalmanFilter(object): + """ + A simple Kalman filter for tracking bounding boxes in image space. + + The 8-dimensional state space + + x, y, a, h, vx, vy, va, vh + + contains the bounding box center position (x, y), aspect ratio a, height h, + and their respective velocities. + + Object motion follows a constant velocity model. The bounding box location + (x, y, a, h) is taken as direct observation of the state space (linear + observation model). + + """ + + def __init__(self, n_extend=0): + ''' + @param n_extend - 扩展通道数 + ''' + ndim, dt = 4, 1. + ndim += n_extend # 扩展通道数 + self.n_extend = n_extend + + # Create Kalman filter model matrices. + self._motion_mat = np.eye(2 * ndim, 2 * ndim) # 预测矩阵 F: 匀速模型 + for i in range(ndim): + self._motion_mat[i, ndim + i] = dt + self._update_mat = np.eye(ndim, 2 * ndim) # 传感器读数矩阵(测量矩阵) H + + # Motion and observation uncertainty are chosen relative to the current + # state estimate. These weights control the amount of uncertainty in + # the model. This is a bit hacky. + self._std_weight_position = 1. / 20 # 系统误差权重 + self._std_weight_velocity = 1. / 160 # 系统误差权重 + + def initiate(self, measurement): + """Create track from unassociated measurement. + + Parameters + ---------- + measurement : ndarray + Bounding box coordinates (x, y, a, h) with center position (x, y), + aspect ratio a, and height h. + + Returns + ------- + (ndarray, ndarray) + Returns the mean vector (8 dimensional) and covariance matrix (8x8 + dimensional) of the new track. Unobserved velocities are initialized + to 0 mean. + + """ + mean_pos = measurement + mean_vel = np.zeros_like(mean_pos) + mean = np.r_[mean_pos, mean_vel] + + std = [ + 2 * self._std_weight_position * measurement[3], + 2 * self._std_weight_position * measurement[3], + 1e-2, + 2 * self._std_weight_position * measurement[3], + + 10 * self._std_weight_velocity * measurement[3], + 10 * self._std_weight_velocity * measurement[3], + 1e-5, + 10 * self._std_weight_velocity * measurement[3]] + + for i in range(self.n_extend): + std.insert(4, 2 * self._std_weight_position * measurement[3]) + std.append(10 * self._std_weight_velocity * measurement[3]) + + covariance = np.diag(np.square(std)) + return mean, covariance + + def predict(self, mean, covariance): + """Run Kalman filter prediction step. + + Parameters + ---------- + mean : ndarray + The 8 dimensional mean vector of the object state at the previous + time step. + covariance : ndarray + The 8x8 dimensional covariance matrix of the object state at the + previous time step. + + Returns + ------- + (ndarray, ndarray) + Returns the mean vector and covariance matrix of the predicted + state. Unobserved velocities are initialized to 0 mean. + + """ + std_pos = [ + self._std_weight_position * mean[3], + self._std_weight_position * mean[3], + 1e-2, + self._std_weight_position * mean[3]] + for i in range(self.n_extend): + std_pos.append(self._std_weight_position * mean[3]) + + std_vel = [ + self._std_weight_velocity * mean[3], + self._std_weight_velocity * mean[3], + 1e-5, + self._std_weight_velocity * mean[3]] + for i in range(self.n_extend): + std_vel.append(self._std_weight_velocity * mean[3]) + + motion_cov = np.diag(np.square(np.r_[std_pos, std_vel])) # 系统误差 Q + + mean = np.dot(self._motion_mat, mean) # 状态预测: x = F*x + covariance = np.linalg.multi_dot(( + self._motion_mat, covariance, self._motion_mat.T)) + motion_cov # 更新协方差矩阵 P = F*P*F' + Q + + return mean, covariance + + def project(self, mean, covariance): + """Project state distribution to measurement space. + + Parameters + ---------- + mean : ndarray + The state's mean vector (8 dimensional array). + covariance : ndarray + The state's covariance matrix (8x8 dimensional). + + Returns + ------- + (ndarray, ndarray) + Returns the projected mean and covariance matrix of the given state + estimate. + + """ + std = [ + self._std_weight_position * mean[3], + self._std_weight_position * mean[3], + 1e-1, + self._std_weight_position * mean[3]] + for i in range(self.n_extend): + std.append(self._std_weight_position * mean[3]) + innovation_cov = np.diag(np.square(std)) # 传感器噪声 R + + mean = np.dot(self._update_mat, mean) # 提取前面4个分量: x = H * x + covariance = np.linalg.multi_dot(( + self._update_mat, covariance, self._update_mat.T)) # P = H*P*H' + return mean, covariance + innovation_cov # P = H*P*H' + R + + def update(self, mean, covariance, measurement): + """Run Kalman filter correction step. + + Parameters + ---------- + mean : ndarray + The predicted state's mean vector (8 dimensional). + covariance : ndarray + The state's covariance matrix (8x8 dimensional). + measurement : ndarray + The 4 dimensional measurement vector (x, y, a, h), where (x, y) + is the center position, a the aspect ratio, and h the height of the + bounding box. + + Returns + ------- + (ndarray, ndarray) + Returns the measurement-corrected state distribution. + + """ + projected_mean, projected_cov = self.project(mean, covariance) # 投影到测量空间 + # Choresky分解: A=>L*L', projected_cov: P = H*P*H' + R + chol_factor, lower = scipy.linalg.cho_factor( + projected_cov, lower=True, check_finite=False) + # 求卡尔曼增益K: (H*P*H'+R)*K=P*H' + kalman_gain = scipy.linalg.cho_solve( + (chol_factor, lower), np.dot(covariance, self._update_mat.T).T, + check_finite=False).T + innovation = measurement - projected_mean # 变化值 + + new_mean = mean + np.dot(innovation, kalman_gain.T) # 新的均值向量 mean + # 新的协方差矩阵 P + new_covariance = covariance - np.linalg.multi_dot(( + kalman_gain, projected_cov, kalman_gain.T)) + return new_mean, new_covariance + + def gating_distance(self, mean, covariance, measurements, + only_position=False): + """Compute gating distance between state distribution and measurements. + + A suitable distance threshold can be obtained from `chi2inv95`. If + `only_position` is False, the chi-square distribution has 4 degrees of + freedom, otherwise 2. + + Parameters + ---------- + mean : ndarray + Mean vector over the state distribution (8 dimensional). + covariance : ndarray + Covariance of the state distribution (8x8 dimensional). + measurements : ndarray + An Nx4 dimensional matrix of N measurements, each in + format (x, y, a, h) where (x, y) is the bounding box center + position, a the aspect ratio, and h the height. + only_position : Optional[bool] + If True, distance computation is done with respect to the bounding + box center position only. + + Returns + ------- + ndarray + Returns an array of length N, where the i-th element contains the + squared Mahalanobis distance between (mean, covariance) and + `measurements[i]`. + + """ + mean, covariance = self.project(mean, covariance) + if only_position: + mean, covariance = mean[:2], covariance[:2, :2] + measurements = measurements[:, :2] + + cholesky_factor = np.linalg.cholesky(covariance) + d = measurements - mean + z = scipy.linalg.solve_triangular( + cholesky_factor, d.T, lower=True, check_finite=False, + overwrite_b=True) + squared_maha = np.sum(z * z, axis=0) + return squared_maha diff --git a/deep_sort_ex/linear_assignment.py b/deep_sort_ex/linear_assignment.py new file mode 100644 index 00000000..d432a08c --- /dev/null +++ b/deep_sort_ex/linear_assignment.py @@ -0,0 +1,200 @@ +# vim: expandtab:ts=4:sw=4 +from __future__ import absolute_import +import numpy as np + +import sklearn +#FutureWarning: The linear_assignment function is deprecated in 0.21 and will be removed from 0.23. Use scipy.optimize.linear_sum_assignment instead. +if sklearn.__version__>'0.22': + from scipy.optimize import linear_sum_assignment as linear_assignment # tf1.15, sklearn=>0.23 +else: + from sklearn.utils.linear_assignment_ import linear_assignment # tf1.14, sklearn=>0.22 +from . import kalman_filter + + +INFTY_COST = 1e+5 + + +def min_cost_matching( + distance_metric, max_distance, tracks, detections, track_indices=None, + detection_indices=None): + """Solve linear assignment problem. + + Parameters + ---------- + distance_metric : Callable[List[Track], List[Detection], List[int], List[int]) -> ndarray + The distance metric is given a list of tracks and detections as well as + a list of N track indices and M detection indices. The metric should + return the NxM dimensional cost matrix, where element (i, j) is the + association cost between the i-th track in the given track indices and + the j-th detection in the given detection_indices. + max_distance : float + Gating threshold. Associations with cost larger than this value are + disregarded. + tracks : List[track.Track] + A list of predicted tracks at the current time step. + detections : List[detection.Detection] + A list of detections at the current time step. + track_indices : List[int] + List of track indices that maps rows in `cost_matrix` to tracks in + `tracks` (see description above). + detection_indices : List[int] + List of detection indices that maps columns in `cost_matrix` to + detections in `detections` (see description above). + + Returns + ------- + (List[(int, int)], List[int], List[int]) + Returns a tuple with the following three entries: + * A list of matched track and detection indices. + * A list of unmatched track indices. + * A list of unmatched detection indices. + + """ + if track_indices is None: + track_indices = np.arange(len(tracks)) + if detection_indices is None: + detection_indices = np.arange(len(detections)) + + if len(detection_indices) == 0 or len(track_indices) == 0: + return [], track_indices, detection_indices # Nothing to match. + + cost_matrix = distance_metric( + tracks, detections, track_indices, detection_indices) + cost_matrix[cost_matrix > max_distance] = max_distance + 1e-5 + indices = linear_assignment(cost_matrix) + + # Fix for tf1.15 envirement when use scipy.optimize.linear_sum_assignment instead from sklearn.utils.linear_assignment_ + if type(indices)==tuple: + indices = np.array([[x,y] for x,y in zip(*indices)]) + + matches, unmatched_tracks, unmatched_detections = [], [], [] + for col, detection_idx in enumerate(detection_indices): + if col not in indices[:, 1]: + unmatched_detections.append(detection_idx) + for row, track_idx in enumerate(track_indices): + if row not in indices[:, 0]: + unmatched_tracks.append(track_idx) + for row, col in indices: + track_idx = track_indices[row] + detection_idx = detection_indices[col] + if cost_matrix[row, col] > max_distance: + unmatched_tracks.append(track_idx) + unmatched_detections.append(detection_idx) + else: + matches.append((track_idx, detection_idx)) + return matches, unmatched_tracks, unmatched_detections + + +def matching_cascade( + distance_metric, max_distance, cascade_depth, tracks, detections, + track_indices=None, detection_indices=None): + """Run matching cascade. + + Parameters + ---------- + distance_metric : Callable[List[Track], List[Detection], List[int], List[int]) -> ndarray + The distance metric is given a list of tracks and detections as well as + a list of N track indices and M detection indices. The metric should + return the NxM dimensional cost matrix, where element (i, j) is the + association cost between the i-th track in the given track indices and + the j-th detection in the given detection indices. + max_distance : float + Gating threshold. Associations with cost larger than this value are + disregarded. + cascade_depth: int + The cascade depth, should be se to the maximum track age. + tracks : List[track.Track] + A list of predicted tracks at the current time step. + detections : List[detection.Detection] + A list of detections at the current time step. + track_indices : Optional[List[int]] + List of track indices that maps rows in `cost_matrix` to tracks in + `tracks` (see description above). Defaults to all tracks. + detection_indices : Optional[List[int]] + List of detection indices that maps columns in `cost_matrix` to + detections in `detections` (see description above). Defaults to all + detections. + + Returns + ------- + (List[(int, int)], List[int], List[int]) + Returns a tuple with the following three entries: + * A list of matched track and detection indices. + * A list of unmatched track indices. + * A list of unmatched detection indices. + + """ + if track_indices is None: + track_indices = list(range(len(tracks))) + if detection_indices is None: + detection_indices = list(range(len(detections))) + + unmatched_detections = detection_indices + matches = [] + for level in range(cascade_depth): + if len(unmatched_detections) == 0: # No detections left + break + + track_indices_l = [ + k for k in track_indices + if tracks[k].time_since_update == 1 + level + ] + if len(track_indices_l) == 0: # Nothing to match at this level + continue + + matches_l, _, unmatched_detections = \ + min_cost_matching( + distance_metric, max_distance, tracks, detections, + track_indices_l, unmatched_detections) + matches += matches_l + unmatched_tracks = list(set(track_indices) - set(k for k, _ in matches)) + return matches, unmatched_tracks, unmatched_detections + + +def gate_cost_matrix( + kf, cost_matrix, tracks, detections, track_indices, detection_indices, + gated_cost=INFTY_COST, only_position=False): + """Invalidate infeasible entries in cost matrix based on the state + distributions obtained by Kalman filtering. + + Parameters + ---------- + kf : The Kalman filter. + cost_matrix : ndarray + The NxM dimensional cost matrix, where N is the number of track indices + and M is the number of detection indices, such that entry (i, j) is the + association cost between `tracks[track_indices[i]]` and + `detections[detection_indices[j]]`. + tracks : List[track.Track] + A list of predicted tracks at the current time step. + detections : List[detection.Detection] + A list of detections at the current time step. + track_indices : List[int] + List of track indices that maps rows in `cost_matrix` to tracks in + `tracks` (see description above). + detection_indices : List[int] + List of detection indices that maps columns in `cost_matrix` to + detections in `detections` (see description above). + gated_cost : Optional[float] + Entries in the cost matrix corresponding to infeasible associations are + set this value. Defaults to a very large value. + only_position : Optional[bool] + If True, only the x, y position of the state distribution is considered + during gating. Defaults to False. + + Returns + ------- + ndarray + Returns the modified cost matrix. + + """ + gating_dim = 2 if only_position else 4 + gating_threshold = kalman_filter.chi2inv95[gating_dim] + measurements = np.asarray( + [detections[i].to_xyah() for i in detection_indices]) + for row, track_idx in enumerate(track_indices): + track = tracks[track_idx] + gating_distance = kf.gating_distance( + track.mean, track.covariance, measurements, only_position) + cost_matrix[row, gating_distance > gating_threshold] = gated_cost + return cost_matrix diff --git a/deep_sort_ex/nn_matching.py b/deep_sort_ex/nn_matching.py new file mode 100644 index 00000000..918c0a19 --- /dev/null +++ b/deep_sort_ex/nn_matching.py @@ -0,0 +1,183 @@ +# vim: expandtab:ts=4:sw=4 +import numpy as np +import sys + +def _pdist(a, b): + """Compute pair-wise squared distance between points in `a` and `b`. + + Parameters + ---------- + a : array_like + An NxM matrix of N samples of dimensionality M. + b : array_like + An LxM matrix of L samples of dimensionality M. + + Returns + ------- + ndarray + Returns a matrix of size len(a), len(b) such that eleement (i, j) + contains the squared distance between `a[i]` and `b[j]`. + + """ + a, b = np.asarray(a), np.asarray(b) + if len(a) == 0 or len(b) == 0: + return np.zeros((len(a), len(b))) + a2, b2 = np.square(a).sum(axis=1), np.square(b).sum(axis=1) + r2 = -2. * np.dot(a, b.T) + a2[:, None] + b2[None, :] + r2 = np.clip(r2, 0., float(np.inf)) + return r2 + + +def _cosine_distance(a, b, data_is_normalized=False): + """Compute pair-wise cosine distance between points in `a` and `b`. + + Parameters + ---------- + a : array_like + An NxM matrix of N samples of dimensionality M. + b : array_like + An LxM matrix of L samples of dimensionality M. + data_is_normalized : Optional[bool] + If True, assumes rows in a and b are unit length vectors. + Otherwise, a and b are explicitly normalized to lenght 1. + + Returns + ------- + ndarray + Returns a matrix of size len(a), len(b) such that eleement (i, j) + contains the squared distance between `a[i]` and `b[j]`. + + """ + if not data_is_normalized: + a = np.asarray(a) / (np.linalg.norm(a, axis=1, keepdims=True)+1e-5) + b = np.asarray(b) / (np.linalg.norm(b, axis=1, keepdims=True)+1e-5) + return 1. - np.dot(a, b.T) + + +def _nn_euclidean_distance(x, y): + """ Helper function for nearest neighbor distance metric (Euclidean). + + Parameters + ---------- + x : ndarray + A matrix of N row-vectors (sample points). + y : ndarray + A matrix of M row-vectors (query points). + + Returns + ------- + ndarray + A vector of length M that contains for each entry in `y` the + smallest Euclidean distance to a sample in `x`. + + """ + distances = _pdist(x, y) + return np.maximum(0.0, distances.min(axis=0)) + + +def _nn_cosine_distance(x, y): + """ Helper function for nearest neighbor distance metric (cosine). + + Parameters + ---------- + x : ndarray + A matrix of N row-vectors (sample points). + y : ndarray + A matrix of M row-vectors (query points). + + Returns + ------- + ndarray + A vector of length M that contains for each entry in `y` the + smallest cosine distance to a sample in `x`. + + """ + distances = _cosine_distance(x, y) + return distances.min(axis=0) + + +class NearestNeighborDistanceMetric(object): + """ + A nearest neighbor distance metric that, for each target, returns + the closest distance to any sample that has been observed so far. + + Parameters + ---------- + metric : str + Either "euclidean" or "cosine". + matching_threshold: float + The matching threshold. Samples with larger distance are considered an + invalid match. + budget : Optional[int] + If not None, fix samples per class to at most this number. Removes + the oldest samples when the budget is reached. + + Attributes + ---------- + samples : Dict[int -> List[ndarray]] + A dictionary that maps from target identities to the list of samples + that have been observed so far. + + """ + + def __init__(self, metric, matching_threshold, budget=None): + + + if metric == "euclidean": + self._metric = _nn_euclidean_distance + elif metric == "cosine": + self._metric = _nn_cosine_distance + else: + raise ValueError( + "Invalid metric; must be either 'euclidean' or 'cosine'") + self.matching_threshold = matching_threshold + self.budget = budget + self.samples = {} + + def partial_fit(self, features, targets, active_targets): + """Update the distance metric with new data. + + Parameters + ---------- + features : ndarray + An NxM matrix of N features of dimensionality M. + targets : ndarray + An integer array of associated target identities. + active_targets : List[int] + A list of targets that are currently present in the scene. + + """ + for feature, target in zip(features, targets): + self.samples.setdefault(target, []).append(feature) + if self.budget is not None: + self.samples[target] = self.samples[target][-self.budget:] + # self.samples = {k: self.samples[k] for k in active_targets} + self.samples = {k: self.samples.get(k) for k in active_targets} # Modify + + def distance(self, features, targets): + """Compute distance between features and targets. + + Parameters + ---------- + features : ndarray + An NxM matrix of N features of dimensionality M. + targets : List[int] + A list of targets to match the given `features` against. + + Returns + ------- + ndarray + Returns a cost matrix of shape len(targets), len(features), where + element (i, j) contains the closest squared distance between + `targets[i]` and `features[j]`. + + """ + cost_matrix = np.zeros((len(targets), len(features))) + for i, target in enumerate(targets): + # modefy begin + if self.samples.get(target) is None: + print('[WARNING] self.samples.get(%d) is None, %s: %s here.' %(target, __file__, str(sys._getframe().f_lineno))) + continue + # modefy end + cost_matrix[i, :] = self._metric(self.samples[target], features) + return cost_matrix diff --git a/deep_sort_ex/track.py b/deep_sort_ex/track.py new file mode 100644 index 00000000..25c5f9d1 --- /dev/null +++ b/deep_sort_ex/track.py @@ -0,0 +1,480 @@ +# vim: expandtab:ts=4:sw=4 + +import os +import queue +import numpy as np +from scipy.signal import butter, lfilter, lfilter_zi, freqz +# 滤波方案0 +class Filter0(object): + '''均值滤波,设定队列长度,取队列均值作为输出 + ''' + def __init__(self, N=3): + ''' + @param N - 队列长度 + ''' + self.q = queue.Queue() + self.N = N + + def filter(self, data): + ''' + @param data - np.array: channels x data_len + ''' + for i in range(data.shape[1]): + if np.isnan(data[:, i]).any() or (data[:, i]==9999.0).any(): + if self.q.qsize()>0: + data[:, i] = np.array(self.q.queue).mean(axis=0) + self.q.put(data[:, i]) + else: + self.q.put(data[:, i]) + data[:, i] = np.array(self.q.queue).mean(axis=0) + if self.q.qsize()>=self.N: + self.q.get() + +# 滤波方案1 +class Filter1(object): + '''依据方差剔除奇点数据 + 根据方差统计动态调整percent值 + 设置队列记录过去N个数据值 queue[N] + 计算标准差 std(queue) => std_val + 动态计算percnet: 标准差换算 + 相对误差 err = (det-mean)/mean + 设定sigmod曲线表 tbl_percent = np.exp(range(-100, 1)) + 设定相对误差最大阈值 std_val = 0.1 + 相对误差换算曲线表序号 tbl_index = int(err*(100/std_val)) + tbl_index修正处理: >100 => 设置为100 + percent = tbl_percent[tbl_index] + + ''' + def __init__(self, N=4, std_th=0.1, percent=0.8): + ''' + @param N - 队列长度 + @param std_th - 方差阈值 + @param percent - [弃用,用sigmod自适应替代]奇异点保留前置能量比,当设为1.0即为完全用前置点替换奇异点 + ''' + self.q_size = N + self.std_th = std_th + self.percent = percent + self.q = queue.Queue() + self.tbl_percent = np.exp(range(-100, 1)) # + self.max_val = 100/std_th + + def filter(self, data): + ''' + @param data - np.array: channels x data_len + ''' + for i in range(data.shape[1]): + exist_nan = False + if np.isnan(data[:, i]).any() or (data[:, i]==9999.0).any(): + if self.q.qsize()>0: + data[:, i] = np.array(self.q.queue).mean(axis=0) + exist_nan = True + if self.q.qsize()==self.q_size: + q_data = np.array(self.q.queue) + mean_vals = np.array(q_data).mean(axis=0) + err_vals = abs(data[:, i]-mean_vals)/abs(mean_vals) + percent_ids = (err_vals*self.max_val).astype(np.int) # 相对误差截止点 0.1 + percent_ids[percent_ids>100]=100 # sigmod曲线 + percents = self.tbl_percent[percent_ids] + data[:, i] = mean_vals*percents + data[:, i]*(1.0-percents) + + if not exist_nan: + self.q.put(data[:, i]) + if self.q.qsize()>self.q_size: + self.q.get() + +# 滤波方案2 +class Filter2(object): + '''简易的卡尔曼滤波 + ''' + def __init__(self, Q=1e-6, R=4e-4): + ''' + @param Q - Q参数, channel x 1 + @param R - R参数, channel x 1 + ''' + self.Q = Q + self.R = R + self.K_prev = np.zeros_like(Q) + self.X_prev = np.zeros_like(Q) + self.P_prev = np.zeros_like(Q) + self.b_first = True + + def filter(self, data): + ''' + @param data - [InPlace], channel x data_len + ''' + if self.b_first: + # 查找第一个非 NaN数据 + next_i = None + for i in range(data.shape[1]): + if not np.isnan(data[:, i]).any() and not (data[:, i]==9999.0).any() : + next_i=i+1 + break + if not next_i is None: + self.b_first = False + self.X_prev = data[:, next_i-1] + self.P_prev = np.zeros_like(self.X_prev) + else: + next_i = data.shape[1]+1 + else: + if np.isnan(data[:, 0]).any() or (data[:, 0]==9999.0).any(): + data[:, 0] = self.X_prev + self.K_prev = self.P_prev / (self.P_prev + self.R) + data[:, 0] = self.X_prev + self.K_prev * (data[:, 0] - self.X_prev) + self.P_prev = self.P_prev - self.K_prev * self.P_prev + self.Q + next_i = 1 + for i in range(next_i, data.shape[1]): + if np.isnan(data[:, i]).any() or (data[:, i]==9999.0).any(): + data[:, i] = self.X_prev + K = self.P_prev / (self.P_prev + self.R) + data[:, i] = data[:, i-1] + K * (data[:, i] - data[:, i-1]) + P = self.P_prev - K * self.P_prev + self.Q + self.P_prev = P + self.K_prev = K + self.X_prev = data[:, i] + + +# 滤波方案3 +class Filter3(object): + '''Buffer低通滤波器 + ''' + def __init__(self, fs, cutoff=2.0, order=5): + ''' + @param fs - 采样率 + @param cutoff - 截止频率, Hz + @param order - 滤波器阶数 + ''' + self.order = order + b, a = self.butter_lowpass(cutoff, fs, order) + self.b = b + self.a = a + self.zi = lfilter_zi(b, a) + self.data_first_len = int(order*2) + self.data_first = np.zeros((self.data_first_len,), dtype=np.float32) + self.prev_val = None + self.index = 0 + + def butter_lowpass(self, cutoff, fs, order=5): + nyq = 0.5 * fs + normal_cutoff = cutoff / nyq + b, a = butter(order, normal_cutoff, btype='low', analog=False) + return b, a + + def filter(self, data): + ''' + @param data - [InPlace], channel x data_len + ''' + if self.prev_val is None: + self.prev_val = np.zeros((data.shape[0],)) + index = self.index + for chl, data_chl in enumerate(data): + for i in range(len(data_chl)): + if np.isnan(data_chl[i]) or data_chl[i]==9999.0: + data_chl[i] = self.prev_val[chl] + z, self.zi = lfilter(self.b, self.a, [data_chl[i]], zi=self.zi) + #if index+i= self._n_init: + self.state = TrackState.Confirmed + + def mark_missed(self): + """Mark this track as missed (no association at the current time step). + """ + if self.state == TrackState.Tentative: + self.state = TrackState.Deleted + elif self.time_since_update > self._max_age: + self.state = TrackState.Deleted + + def is_tentative(self): + """Returns True if this track is tentative (unconfirmed). + """ + return self.state == TrackState.Tentative + + def is_confirmed(self): + """Returns True if this track is confirmed.""" + return self.state == TrackState.Confirmed + + def is_deleted(self): + """Returns True if this track is dead and should be deleted.""" + return self.state == TrackState.Deleted diff --git a/deep_sort_ex/tracker.py b/deep_sort_ex/tracker.py new file mode 100644 index 00000000..a73ac917 --- /dev/null +++ b/deep_sort_ex/tracker.py @@ -0,0 +1,173 @@ +# vim: expandtab:ts=4:sw=4 +from __future__ import absolute_import +import numpy as np +from . import kalman_filter +from . import linear_assignment +from . import iou_matching +from .track import Track + + +class Tracker: + """ + This is the multi-target tracker. + + Parameters + ---------- + metric : nn_matching.NearestNeighborDistanceMetric + A distance metric for measurement-to-track association. + max_age : int + Maximum number of missed misses before a track is deleted. + n_init : int + Number of consecutive detections before the track is confirmed. The + track state is set to `Deleted` if a miss occurs within the first + `n_init` frames. + + Attributes + ---------- + metric : nn_matching.NearestNeighborDistanceMetric + The distance metric used for measurement to track association. + max_age : int + Maximum number of missed misses before a track is deleted. + n_init : int + Number of frames that a track remains in initialization phase. + kf : kalman_filter.KalmanFilter + A Kalman filter to filter target trajectories in image space. + tracks : List[Track] + The list of active tracks at the current time step. + + """ + + def __init__(self, metric, max_iou_distance=0.7, max_age=30, n_init=3, n_extend=0, filter_type=0, q_size=4, std_th=0.05, percent=0.8, Q=1e-6, R=4e-4, fs=5., cutoff=1., order=5): + ''' + 扩展属性 + ----- + @param n_extend - mean扩展属性数目 + @param filter_type - [Track] exts2滤波器类型 + @param q_size - [Track] 队列长度 + @param std_th - [Track] 相对误差域值 + @param percent - [Track] 奇异点保留前置能量比,当设为1.0即为完全用前置点替换奇异点 + @param Q - [Track] 卡尔曼滤波器参数 + @param R - [Track] 卡尔曼滤波器参数 + @param fs - [Track] 采样率 + @param cutoff - [Track] 截止频率, Hz + @param order - [Track] 滤波器阶数 + ''' + self.metric = metric + self.max_iou_distance = max_iou_distance + self.max_age = max_age + self.n_init = n_init + + self.kf = kalman_filter.KalmanFilter(n_extend=n_extend) + self.filter_type = filter_type # exts2 滤波器类型 + self.q_size = q_size # 队列长度 + self.std_th = std_th # 方差域值 + self.percent = percent # 奇异点保留前置能量比,当设为1.0即为完全用前置点替换奇异点 + self.Q = Q # 卡尔曼参数 + self.R = R # 卡尔曼参数 + self.fs = fs + self.cutoff = cutoff + self.order = order + self.tracks = [] + self._next_id = 1 + + + def reset(self): + # reset DeepSORT + del self.tracks[:] + self._next_id = 1 + + + def predict(self): + """Propagate track state distributions one time step forward. + + This function should be called once every time step, before `update`. + """ + for track in self.tracks: + track.predict(self.kf) + + def update(self, detections, save_to=None): + """Perform measurement update and track management. + + Parameters + ---------- + detections : List[deep_sort.detection.Detection] + A list of detections at the current time step. + + """ + # Run matching cascade. + matches, unmatched_tracks, unmatched_detections = \ + self._match(detections) + + # Update track set. + for track_idx, detection_idx in matches: + self.tracks[track_idx].update( + self.kf, detections[detection_idx], save_to=save_to) + # update track.binding_obj + self.tracks[track_idx].binding_obj = detections[detection_idx].binding_obj + + for track_idx in unmatched_tracks: + self.tracks[track_idx].mark_missed() + + for detection_idx in unmatched_detections: + self._initiate_track(detections[detection_idx]) + + self.tracks = [t for t in self.tracks if not t.is_deleted()] + + # Update distance metric. + active_targets = [t.track_id for t in self.tracks if t.is_confirmed()] + features, targets = [], [] + for track in self.tracks: + if not track.is_confirmed(): + continue + features += track.features + targets += [track.track_id for _ in track.features] + track.features = [] + self.metric.partial_fit( + np.asarray(features), np.asarray(targets), active_targets) + + + def _match(self, detections): + def gated_metric(tracks, dets, track_indices, detection_indices): + features = np.array([dets[i].feature for i in detection_indices]) + targets = np.array([tracks[i].track_id for i in track_indices]) + cost_matrix = self.metric.distance(features, targets) + cost_matrix = linear_assignment.gate_cost_matrix( + self.kf, cost_matrix, tracks, dets, track_indices, + detection_indices) + + return cost_matrix + + # Split track set into confirmed and unconfirmed tracks. + confirmed_tracks = [ + i for i, t in enumerate(self.tracks) if t.is_confirmed()] + unconfirmed_tracks = [ + i for i, t in enumerate(self.tracks) if not t.is_confirmed()] + + # Associate confirmed tracks using appearance features. + matches_a, unmatched_tracks_a, unmatched_detections = \ + linear_assignment.matching_cascade( + gated_metric, self.metric.matching_threshold, self.max_age, + self.tracks, detections, confirmed_tracks) + + # Associate remaining tracks together with unconfirmed tracks using IOU. + iou_track_candidates = unconfirmed_tracks + [ + k for k in unmatched_tracks_a if + self.tracks[k].time_since_update == 1] + unmatched_tracks_a = [ + k for k in unmatched_tracks_a if + self.tracks[k].time_since_update != 1] + matches_b, unmatched_tracks_b, unmatched_detections = \ + linear_assignment.min_cost_matching( + iou_matching.iou_cost, self.max_iou_distance, self.tracks, + detections, iou_track_candidates, unmatched_detections) + + matches = matches_a + matches_b + unmatched_tracks = list(set(unmatched_tracks_a + unmatched_tracks_b)) + return matches, unmatched_tracks, unmatched_detections + + def _initiate_track(self, detection): + mean, covariance = self.kf.initiate(detection.to_xyah()) + self.tracks.append(Track( + mean, covariance, self._next_id, self.n_init, self.max_age, + feature=detection.feature, binding_obj=detection.binding_obj, filter_type=self.filter_type, q_size=self.q_size, std_th=self.std_th, percent=self.percent, Q=self.Q, R=self.R, fs=self.fs, cutoff=self.cutoff, order=self.order)) + self._next_id += 1 diff --git a/env.bashrc b/env.bashrc new file mode 100644 index 00000000..ab569f85 --- /dev/null +++ b/env.bashrc @@ -0,0 +1,9 @@ +tf="tf1.15" +if [[ $# -gt 0 ]]; then + tf=$1 +fi + +source ~/.bashrc +#source ~/miniconda3/bin/activate tf1.14 +source ~/miniconda3/bin/activate $tf + diff --git a/my_deep_sort_app.py b/my_deep_sort_app.py new file mode 100644 index 00000000..33deb2a2 --- /dev/null +++ b/my_deep_sort_app.py @@ -0,0 +1,308 @@ +# vim: expandtab:ts=4:sw=4 +from __future__ import division, print_function, absolute_import + +import argparse +import os + +import cv2 +import numpy as np + +from application_util import preprocessing +from application_util import visualization +from deep_sort_ex import nn_matching +from deep_sort_ex.detection import Detection +from deep_sort_ex.tracker import Tracker + +def get_feature1_dim(): + return 20 + +def get_feature1(sequence_dir, frame_idx, bbox): + '''提取检测目标特征 + 通过直方图描述目标特征 + @sequence_dir - 测试图像序列存放目录 + @frame_idx - 图像序号 + @bbox - 检测目标bbox + @feature_dim - 特征数 + @return feature - 特征向量 + ''' + image_dir = os.path.join(sequence_dir, "img1") + img_file= os.path.join(image_dir, ('000000'+str(frame_idx))[-6:]+'.jpg') + img = cv2.imread(img_file, cv2.IMREAD_GRAYSCALE) + t, l, w, h = [int(x) for x in bbox] + #print('[DEBUG] img.shape: %s,bbox: %s' %(str(img.shape), bbox)) + roi = img[t:t+h,l:l+w] # tlwh + hists, bins = np.histogram(roi, bins=get_feature1_dim()) + return hists + + +def gather_sequence_info(sequence_dir, detection_file, feature_type=0): + """Gather sequence information, such as image filenames, detections, + groundtruth (if available). + + Parameters + ---------- + sequence_dir : str + Path to the MOTChallenge sequence directory. + detection_file : str + Path to the detection file. + + Returns + ------- + Dict + A dictionary of the following sequence information: + + * sequence_name: Name of the sequence + * image_filenames: A dictionary that maps frame indices to image + filenames. + * detections: A numpy array of detections in MOTChallenge format. + * groundtruth: A numpy array of ground truth in MOTChallenge format. + * image_size: Image size (height, width). + * min_frame_idx: Index of the first frame. + * max_frame_idx: Index of the last frame. + + """ + image_dir = os.path.join(sequence_dir, "img1") + image_filenames = { + int(os.path.splitext(f)[0]): os.path.join(image_dir, f) + for f in os.listdir(image_dir)} + groundtruth_file = os.path.join(sequence_dir, "gt/gt.txt") + + detections = None + if detection_file is not None: + detections = np.load(detection_file) + groundtruth = None + if os.path.exists(groundtruth_file): + groundtruth = np.loadtxt(groundtruth_file, delimiter=',') + + if len(image_filenames) > 0: + image = cv2.imread(next(iter(image_filenames.values())), + cv2.IMREAD_GRAYSCALE) + image_size = image.shape + else: + image_size = None + + if len(image_filenames) > 0: + min_frame_idx = min(image_filenames.keys()) + max_frame_idx = max(image_filenames.keys()) + else: + min_frame_idx = int(detections[:, 0].min()) + max_frame_idx = int(detections[:, 0].max()) + + info_filename = os.path.join(sequence_dir, "seqinfo.ini") + if os.path.exists(info_filename): + with open(info_filename, "r") as f: + line_splits = [l.split('=') for l in f.read().splitlines()[1:]] + info_dict = dict( + s for s in line_splits if isinstance(s, list) and len(s) == 2) + + update_ms = 1000 / int(info_dict["frameRate"]) + else: + update_ms = None + + if feature_type==0: + feature_dim = detections.shape[1] - 10 if detections is not None else 0 + elif feature_type==1: + feature_dim = get_feature1_dim() + seq_info = { + "sequence_name": os.path.basename(sequence_dir), + "image_filenames": image_filenames, + "detections": detections, + "groundtruth": groundtruth, + "image_size": image_size, + "min_frame_idx": min_frame_idx, + "max_frame_idx": max_frame_idx, + "feature_dim": feature_dim, + "update_ms": update_ms + } + return seq_info + + +def create_detections(detection_mat, frame_idx, min_height=0, feature_type=0, sequence_dir='model_data'): + """Create detections for given frame index from the raw detection matrix. + + Parameters + ---------- + detection_mat : ndarray + Matrix of detections. The first 10 columns of the detection matrix are + in the standard MOTChallenge detection format. In the remaining columns + store the feature vector associated with each detection. + frame_idx : int + The frame index. + min_height : Optional[int] + A minimum detection bounding box height. Detections that are smaller + than this value are disregarded. + + Returns + ------- + List[tracker.Detection] + Returns detection responses at given frame index. + + """ + frame_indices = detection_mat[:, 0].astype(np.int) + mask = frame_indices == frame_idx + + detection_list = [] + for row in detection_mat[mask]: + bbox, confidence, feature = row[2:6], row[6], row[10:] + if feature_type==1: + feature = get_feature1(sequence_dir, frame_idx, bbox) + if bbox[3] < min_height: + continue + detection_list.append(Detection(bbox, confidence, feature, exts1=[0.5], exts2=[0.6])) + return detection_list + + +def run(sequence_dir, detection_file, output_file, min_confidence, + nms_max_overlap, min_detection_height, max_cosine_distance, + nn_budget, display, feature_type): + """Run multi-target tracker on a particular sequence. + + Parameters + ---------- + sequence_dir : str + Path to the MOTChallenge sequence directory. + detection_file : str + Path to the detections file. + output_file : str + Path to the tracking output file. This file will contain the tracking + results on completion. + min_confidence : float + Detection confidence threshold. Disregard all detections that have + a confidence lower than this value. + nms_max_overlap: float + Maximum detection overlap (non-maxima suppression threshold). + min_detection_height : int + Detection height threshold. Disregard all detections that have + a height lower than this value. + max_cosine_distance : float + Gating threshold for cosine distance metric (object appearance). + nn_budget : Optional[int] + Maximum size of the appearance descriptor gallery. If None, no budget + is enforced. + display : bool + If True, show visualization of intermediate tracking results. + feature_type: int + Indicate how to get feature from bbox area. + + """ + seq_info = gather_sequence_info(sequence_dir, detection_file, feature_type) + metric = nn_matching.NearestNeighborDistanceMetric( + "cosine", max_cosine_distance, nn_budget) + tracker = Tracker(metric, n_extend=1, filter_type=2) + results = [] + + def frame_callback(vis, frame_idx): + print("Processing frame %05d" % frame_idx) + + # Load image and generate detections. + detections = create_detections( + seq_info["detections"], frame_idx, min_detection_height, feature_type, sequence_dir) + detections = [d for d in detections if d.confidence >= min_confidence] + + # Run non-maxima suppression. + boxes = np.array([d.tlwh for d in detections]) + scores = np.array([d.confidence for d in detections]) + indices = preprocessing.non_max_suppression( + boxes, nms_max_overlap, scores) + detections = [detections[i] for i in indices] + + # Update tracker. + tracker.predict() + tracker.update(detections) + + # Update visualization. + if display: + image = cv2.imread( + seq_info["image_filenames"][frame_idx], cv2.IMREAD_COLOR) + vis.set_image(image.copy()) + vis.draw_detections(detections) + vis.draw_trackers(tracker.tracks) + + # Store results. + for track in tracker.tracks: + if not track.is_confirmed() or track.time_since_update > 1: + continue + bbox = track.to_tlwh() + results.append([ + frame_idx, track.track_id, bbox[0], bbox[1], bbox[2], bbox[3]]) + + # Run tracker. + if display: + visualizer = visualization.Visualization(seq_info, update_ms=5) + else: + visualizer = visualization.NoVisualization(seq_info) + visualizer.run(frame_callback) + + # Store results. + f = open(output_file, 'w') + for row in results: + print('%d,%d,%.2f,%.2f,%.2f,%.2f,1,-1,-1,-1' % ( + row[0], row[1], row[2], row[3], row[4], row[5]),file=f) + + +def bool_string(input_string): + if input_string not in {"True","False"}: + raise ValueError("Please Enter a valid Ture/False choice") + else: + return (input_string == "True") + +def parse_args(): + """ Parse command line arguments. + """ + parser = argparse.ArgumentParser(description="Deep SORT") + parser.add_argument( + "--sequence_dir", help="Path to MOTChallenge sequence directory", + default='model_data') + parser.add_argument( + "--detection_file", help="Path to custom detections.", default='model_data/MOT16-06.npy') + parser.add_argument( + "--output_file", help="Path to the tracking output file. This file will" + " contain the tracking results on completion.", + default="/tmp/hypotheses.txt") + parser.add_argument( + "--min_confidence", help="Detection confidence threshold. Disregard " + "all detections that have a confidence lower than this value.", + default=0.8, type=float) + parser.add_argument( + "--min_detection_height", help="Threshold on the detection bounding " + "box height. Detections with height smaller than this value are " + "disregarded", default=0, type=int) + parser.add_argument( + "--nms_max_overlap", help="Non-maxima suppression threshold: Maximum " + "detection overlap.", default=1.0, type=float) + parser.add_argument( + "--max_cosine_distance", help="Gating threshold for cosine distance " + "metric (object appearance).", type=float, default=0.2) + parser.add_argument( + "--nn_budget", help="Maximum size of the appearance descriptors " + "gallery. If None, no budget is enforced.", type=int, default=None) + parser.add_argument( + "--display", help="Show intermediate tracking results", + default=True, type=bool_string) + parser.add_argument( + "--feature_type", help="feature type", + default=0, type=int) + + return parser.parse_args() + + +if __name__ == "__main__": + args = parse_args() + print('Parameters') + print('==========') + print('sequence_dir: ', args.sequence_dir) + print('detection_file: ', args.detection_file) + print('output_file: ', args.output_file) + print('min_confidence: ', args.min_confidence) + print('min_detection_height: ', args.min_detection_height) + print('nms_max_overlap: ', args.nms_max_overlap) + print('max_cosine_distance: ', args.max_cosine_distance) + print('nn_budget: ', args.nn_budget) + print('display: ', args.display) + print('feature_type: ', args.feature_type) + print('') + + run( + args.sequence_dir, args.detection_file, args.output_file, + args.min_confidence, args.nms_max_overlap, args.min_detection_height, + args.max_cosine_distance, args.nn_budget, args.display, args.feature_type) diff --git a/unit_test.sh b/unit_test.sh new file mode 100644 index 00000000..189df59d --- /dev/null +++ b/unit_test.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env sh +# -*- coding: utf-8 -*- + +tf="tf1.15" +if [[ $# -gt 0 ]]; then + tf=$1 +fi + +# 单元测试 +source env.bashrc $tf +python3 my_deep_sort_app.py --feature_type 1 --display False