|
| 1 | +""" |
| 2 | +Collection of functions for doppler centroid estimation. |
| 3 | +""" |
| 4 | +import functools |
| 5 | +import numbers |
| 6 | +import collections as cl |
| 7 | +import numpy as np |
| 8 | +from scipy import fft |
| 9 | + |
| 10 | + |
| 11 | +def corr_doppler_est(echo, prf, lag=1, axis=None): |
| 12 | + """Estimate Doppler centroid based on complex correlator. |
| 13 | +
|
| 14 | + It uses the Correlation Doppler Estimator (CDE) approach |
| 15 | + proposed by [MADSEN1989]_ |
| 16 | +
|
| 17 | + Parameters |
| 18 | + ---------- |
| 19 | + echo : np.ndarray(complex) |
| 20 | + 1-D or 2-D numpy complex array |
| 21 | + prf : float |
| 22 | + Pulse-repetition frequency or sampling rate in the azimuth |
| 23 | + direction in (Hz). |
| 24 | + lag : int, default=1 |
| 25 | + Lag of the correlator, a positive value. |
| 26 | + axis : None or int, optional |
| 27 | + Axis along which the correlator is performed. |
| 28 | + If None it will be the first axis. |
| 29 | +
|
| 30 | + Returns |
| 31 | + ------- |
| 32 | + float |
| 33 | + Ambiguous Doppler centroid within [-0.5*prf, 0.5*prf] |
| 34 | + float |
| 35 | + Correlation coefficient, a value within [0, 1] |
| 36 | +
|
| 37 | + Raises |
| 38 | + ------ |
| 39 | + ValueError |
| 40 | + For bad input arguments |
| 41 | + TypeError |
| 42 | + If echo is not numpy array |
| 43 | + RuntimeError: |
| 44 | + Mismtach between lag and number of elements of echo used in correlator |
| 45 | + np.AxisError: |
| 46 | + Mismtach between axis value and echo dimension |
| 47 | +
|
| 48 | + See Also |
| 49 | + -------- |
| 50 | + sign_doppler_est : Sign-Doppler estimator |
| 51 | + wavelen_diversity_doppler_est |
| 52 | +
|
| 53 | + References |
| 54 | + ---------- |
| 55 | + .. [MADSEN1989] S. Madsen, 'Estimating The Doppler Centroid of SAR Data', |
| 56 | + IEEE Transaction On Aerospace and Elect Sys, March 1989 |
| 57 | +
|
| 58 | + """ |
| 59 | + if prf <= 0.0: |
| 60 | + raise ValueError('prf must be a positive value') |
| 61 | + if not isinstance(echo, np.ndarray): |
| 62 | + raise TypeError('echo must be a numpy array') |
| 63 | + if echo.ndim > 2: |
| 64 | + raise ValueError('Max dimension of echo must be 2') |
| 65 | + if lag < 1: |
| 66 | + raise ValueError('Lag must be equal or larger than 1') |
| 67 | + if axis is None: |
| 68 | + axis = 0 |
| 69 | + else: |
| 70 | + if axis > (echo.ndim - 1): |
| 71 | + raise np.AxisError( |
| 72 | + f'axis {axis} is out of bound for dimenion {echo.ndim}') |
| 73 | + |
| 74 | + if axis == 0: |
| 75 | + if echo.shape[0] < (lag + 1): |
| 76 | + raise RuntimeError( |
| 77 | + f'Not enough samples for correlator along axis {axis}') |
| 78 | + xcor_cmp = (echo[lag:] * echo[:-lag].conj()).mean() |
| 79 | + # get mag of product of auto correlations |
| 80 | + acor_mag = np.sqrt((abs(echo[lag:])**2).mean()) |
| 81 | + acor_mag *= np.sqrt((abs(echo[:-lag])**2).mean()) |
| 82 | + else: |
| 83 | + if echo.shape[1] < (lag + 1): |
| 84 | + raise RuntimeError( |
| 85 | + f'Not enough samples for correlator along axis {axis}') |
| 86 | + xcor_cmp = (echo[:, lag:] * echo[:, :-lag].conj()).mean() |
| 87 | + # get mag of product of auto correlations |
| 88 | + acor_mag = np.sqrt((abs(echo[:, lag:])**2).mean()) |
| 89 | + acor_mag *= np.sqrt((abs(echo[:, :-lag])**2).mean()) |
| 90 | + |
| 91 | + # calculate correlation coefficient |
| 92 | + if acor_mag > 0: |
| 93 | + corr_coef = abs(xcor_cmp) / acor_mag |
| 94 | + else: |
| 95 | + corr_coef = 0.0 |
| 96 | + |
| 97 | + return prf / (2.0 * np.pi * lag) * np.angle(xcor_cmp), corr_coef |
| 98 | + |
| 99 | + |
| 100 | +def sign_doppler_est(echo, prf, lag=1, axis=None): |
| 101 | + """Estimate Doppler centroid based on sign of correlator coeffs. |
| 102 | +
|
| 103 | + It uses Sign-Doppler estimator (SDE) approach proposed by [MADSEN1989]_ |
| 104 | +
|
| 105 | + Parameters |
| 106 | + ---------- |
| 107 | + echo : np.ndarray(complex) |
| 108 | + 1-D or 2-D numpy complex array |
| 109 | + prf : float |
| 110 | + Pulse-repetition frequency or sampling rate in the azimuth |
| 111 | + direction in (Hz). |
| 112 | + lag : int, default=1 |
| 113 | + Lag of the correlator, a positive value. |
| 114 | + axis : None or int, optional |
| 115 | + Axis along which the correlator is perform. |
| 116 | + If None it will be the firsr axis. |
| 117 | +
|
| 118 | + Returns |
| 119 | + ------- |
| 120 | + float |
| 121 | + Ambiguous Doppler centroid within [-0.5*prf, 0.5*prf] |
| 122 | +
|
| 123 | + Raises |
| 124 | + ------ |
| 125 | + ValueError |
| 126 | + For bad input arguments |
| 127 | + TypeError |
| 128 | + If echo is not numpy array |
| 129 | + RuntimeError: |
| 130 | + Mismtach between lag and number of elements of echo used in correlator |
| 131 | + np.AxisError: |
| 132 | + Mismtach between Axis value and echo dimension |
| 133 | +
|
| 134 | + See Also |
| 135 | + -------- |
| 136 | + corr_doppler_est : Correlation Doppler Estimator (CDE) |
| 137 | + wavelen_diversity_doppler_est |
| 138 | +
|
| 139 | + References |
| 140 | + ---------- |
| 141 | + .. [MADSEN1989] S. Madsen, 'Estimating The Doppler Centroid of SAR Data', |
| 142 | + IEEE Transaction On Aerospace and Elect Sys, March 1989 |
| 143 | +
|
| 144 | + """ |
| 145 | + if prf <= 0.0: |
| 146 | + raise ValueError('prf must be a positive value') |
| 147 | + if not isinstance(echo, np.ndarray): |
| 148 | + raise TypeError('echo must be a numpy array') |
| 149 | + if echo.ndim > 2: |
| 150 | + raise ValueError('Max dimension of echo must be 2') |
| 151 | + if lag < 1: |
| 152 | + raise ValueError('Lag must be equal or larger than 1') |
| 153 | + if axis is None: |
| 154 | + axis = 0 |
| 155 | + else: |
| 156 | + if axis > (echo.ndim - 1): |
| 157 | + raise np.AxisError( |
| 158 | + f'axis {axis} is out of bound for dimenion {echo.ndim}') |
| 159 | + |
| 160 | + sgn_i = _sgn(echo.real) |
| 161 | + sgn_q = _sgn(echo.imag) |
| 162 | + |
| 163 | + if axis == 0: |
| 164 | + if echo.shape[0] < (lag + 1): |
| 165 | + raise RuntimeError( |
| 166 | + f'Not enough samples for correlator along axis {axis}') |
| 167 | + xcor_ii = (sgn_i[lag:] * sgn_i[:-lag]).mean() |
| 168 | + xcor_qq = (sgn_q[lag:] * sgn_q[:-lag]).mean() |
| 169 | + xcor_iq = (sgn_i[lag:] * sgn_q[:-lag]).mean() |
| 170 | + xcor_qi = (sgn_q[lag:] * sgn_i[:-lag]).mean() |
| 171 | + else: |
| 172 | + if echo.shape[1] < (lag + 1): |
| 173 | + raise RuntimeError( |
| 174 | + f'Not enough samples for correlator along axis {axis}') |
| 175 | + xcor_ii = (sgn_i[:, lag:] * sgn_i[:, :-lag]).mean() |
| 176 | + xcor_qq = (sgn_q[:, lag:] * sgn_q[:, :-lag]).mean() |
| 177 | + xcor_iq = (sgn_i[:, lag:] * sgn_q[:, :-lag]).mean() |
| 178 | + xcor_qi = (sgn_q[:, lag:] * sgn_i[:, :-lag]).mean() |
| 179 | + |
| 180 | + r_sinlaw = np.sin(0.5 * np.pi * np.asarray([xcor_ii, xcor_qq, |
| 181 | + xcor_qi, -xcor_iq])) |
| 182 | + xcor_cmp = 0.5 * complex(r_sinlaw[:2].sum(), r_sinlaw[2:].sum()) |
| 183 | + |
| 184 | + return prf / (2.0 * np.pi * lag) * np.angle(xcor_cmp) |
| 185 | + |
| 186 | + |
| 187 | +def wavelen_diversity_doppler_est(echo, prf, samprate, bandwidth, |
| 188 | + centerfreq): |
| 189 | + """Estimate Doppler based on wavelength diversity. |
| 190 | +
|
| 191 | + It uses slope of phase of range frequency along with single-lag |
| 192 | + time-domain correlator approach proposed by [BAMLER1991]_. |
| 193 | +
|
| 194 | + Parameters |
| 195 | + ---------- |
| 196 | + echo : np.ndarray(complex) |
| 197 | + 2-D complex basebanded echo, azimuth by range in time domain. |
| 198 | + prf : float |
| 199 | + Pulse repetition frequency in (Hz) |
| 200 | + samprate : float |
| 201 | + Sampling rate in range , second dim, in (Hz) |
| 202 | + bandwidth : float |
| 203 | + RF/chirp bandiwdth in (Hz) |
| 204 | + centerfreq : float |
| 205 | + RF center frequency of chirp in (Hz) |
| 206 | +
|
| 207 | + Returns |
| 208 | + ------- |
| 209 | + float |
| 210 | + Unambiguous Doppler centroid at center frequency in (Hz) |
| 211 | +
|
| 212 | + Raises |
| 213 | + ------ |
| 214 | + ValueError |
| 215 | + For bad input |
| 216 | + TypeError |
| 217 | + If echo is not numpy array |
| 218 | +
|
| 219 | + See Also |
| 220 | + -------- |
| 221 | + corr_doppler_est : Correlation Doppler Estimator (CDE) |
| 222 | + sign_doppler_est : Sign-Doppler estimator (SDE) |
| 223 | +
|
| 224 | + References |
| 225 | + ---------- |
| 226 | + .. [BAMLER1991] R. Bamler and H. Runge, 'PRF-Ambiguity Resolving by |
| 227 | + Wavelength Diversity', IEEE Transaction on GeoSci and Remote Sensing, |
| 228 | + November 1991. |
| 229 | +
|
| 230 | + """ |
| 231 | + if prf <= 0: |
| 232 | + raise ValueError('PRF must be positive value!') |
| 233 | + if samprate <= 0: |
| 234 | + raise ValueError('samprate must be positive value!') |
| 235 | + if bandwidth <= 0 or bandwidth >= samprate: |
| 236 | + raise ValueError('badnwidth must be positive less than samprate!') |
| 237 | + if centerfreq <= 0.0: |
| 238 | + raise ValueError('centerfreq must be positive value!') |
| 239 | + if not isinstance(echo, np.ndarray): |
| 240 | + raise TypeError('echo must be a numpy array') |
| 241 | + if echo.ndim != 2: |
| 242 | + raise ValueError('echo must have two dimensions') |
| 243 | + num_azb, num_rgb = echo.shape |
| 244 | + if num_azb <= 2: |
| 245 | + raise ValueError('The first dimension of echo must be larger than 2') |
| 246 | + if num_rgb > 2: |
| 247 | + raise ValueError('The second dimension of echo must be larger than 2!') |
| 248 | + |
| 249 | + # FFT along range |
| 250 | + nfft = fft.next_fast_len(num_rgb) |
| 251 | + echo_fft = fft.fft(echo, nfft, axis=1) |
| 252 | + |
| 253 | + # one-lag correlator along azimuth |
| 254 | + az_corr = (echo_fft[1:] * echo_fft[:-1].conj()).mean(axis=0) |
| 255 | + |
| 256 | + # Get the unwrapped phase of range spectrum within +/-bandwidth/2. |
| 257 | + df = samprate / nfft |
| 258 | + half_bw = 0.5 * bandwidth |
| 259 | + idx_hbw = nfft // 2 - int(half_bw / df) |
| 260 | + unwrap_phs_rg = np.unwrap(np.angle(fft.fftshift(az_corr) |
| 261 | + [idx_hbw: -idx_hbw])) # (rad) |
| 262 | + |
| 263 | + # perform linear regression in range freq within bandwidth |
| 264 | + freq_bw = -half_bw + df * np.arange(nfft - 2 * idx_hbw) |
| 265 | + pf_coef = np.polyfit(freq_bw, unwrap_phs_rg, deg=1) |
| 266 | + |
| 267 | + # get the doppler centroid at center freq based on slope |
| 268 | + dop_slope = prf / (2. * np.pi) * pf_coef[0] |
| 269 | + |
| 270 | + return centerfreq * dop_slope |
| 271 | + |
| 272 | + |
| 273 | +@functools.singledispatch |
| 274 | +def unwrap_doppler(dop, prf): |
| 275 | + """Unwrap doppler value(s) |
| 276 | +
|
| 277 | + Parameters |
| 278 | + ---------- |
| 279 | + dop : float or np.ndarray(float) or Sequence[float] |
| 280 | + Doppler centroid value(s) in (Hz) |
| 281 | + prf : float |
| 282 | + Pulse repetition frequency in (Hz). |
| 283 | +
|
| 284 | + Returns |
| 285 | + ------- |
| 286 | + float or np.ndarray(float) |
| 287 | + Unwrapped Doppler values the same format as input in (Hz) |
| 288 | +
|
| 289 | + Raises |
| 290 | + ------ |
| 291 | + ValueError |
| 292 | + For non-positive prf |
| 293 | + TypeError: |
| 294 | + Bad data stype for dop |
| 295 | +
|
| 296 | + """ |
| 297 | + raise TypeError('Unsupported data type for doppler') |
| 298 | + |
| 299 | + |
| 300 | +@unwrap_doppler.register(numbers.Real) |
| 301 | +def _unwrap_doppler_scalar(dop: float, prf: float) -> float: |
| 302 | + """Returns single doppler as it is""" |
| 303 | + if prf <= 0.0: |
| 304 | + raise ValueError('prf must be a positive value') |
| 305 | + return dop |
| 306 | + |
| 307 | + |
| 308 | +@unwrap_doppler.register(np.ndarray) |
| 309 | +def _unwrap_doppler_array(dop: np.ndarray, prf: float) -> np.ndarray: |
| 310 | + """Unwrap doppler values stored as numpy array""" |
| 311 | + if prf <= 0.0: |
| 312 | + raise ValueError('prf must be a positive value') |
| 313 | + freq2phs = 2 * np.pi / prf |
| 314 | + phs2freq = 1.0 / freq2phs |
| 315 | + return phs2freq*np.unwrap(freq2phs * dop) |
| 316 | + |
| 317 | + |
| 318 | +@unwrap_doppler.register(cl.abc.Sequence) |
| 319 | +def _unwrap_doppler_sequence(dop: cl.abc.Sequence, prf: float) -> np.ndarray: |
| 320 | + """Unwrap doppler values stored as Sequence """ |
| 321 | + if prf <= 0.0: |
| 322 | + raise ValueError('prf must be a positive value') |
| 323 | + freq2phs = 2 * np.pi / prf |
| 324 | + phs2freq = 1.0 / freq2phs |
| 325 | + return phs2freq*np.unwrap(freq2phs * np.asarray(dop)) |
| 326 | + |
| 327 | +# List of helper functions |
| 328 | + |
| 329 | + |
| 330 | +def _sgn(x: np.ndarray) -> np.ndarray: |
| 331 | + """Wrapper around numpy.sign. |
| 332 | +
|
| 333 | + It replaces zero values with one. |
| 334 | +
|
| 335 | + """ |
| 336 | + s = np.sign(x) |
| 337 | + s[s == 0] = 1 |
| 338 | + return s |
0 commit comments