/labtools/peakdetect.py

https://bitbucket.org/ibab/powertools · Python · 238 lines · 230 code · 1 blank · 7 comment · 1 complexity · d536a8a31885b65303cc77fb2f2dec06 MD5 · raw file

  1. import numpy as np
  2. def peakdetect(y_axis, x_axis=None, lookahead=500, delta=0):
  3. """
  4. Converted from/based on a MATLAB script at http://billauer.co.il/peakdet.html
  5. Algorithm for detecting local maximas and minmias in a signal.
  6. Discovers peaks by searching for values which are surrounded by lower
  7. or larger values for maximas and minimas respectively
  8. keyword arguments:
  9. y_axis -- A list containg the signal over which to find peaks
  10. x_axis -- A x-axis whose values correspond to the 'y_axis' list and is used
  11. in the return to specify the postion of the peaks. If omitted the index
  12. of the y_axis is used. (default: None)
  13. lookahead -- (optional) distance to look ahead from a peak candidate to
  14. determine if it is the actual peak (default: 500)
  15. '(sample / period) / f' where '4 >= f >= 1.25' might be a good value
  16. delta -- (optional) this specifies a minimum difference between a peak and
  17. the following points, before a peak may be considered a peak. Useful
  18. to hinder the algorithm from picking up false peaks towards to end of
  19. the signal. To work well delta should be set to 'delta >= RMSnoise * 5'.
  20. (default: 0)
  21. Delta function causes a 20% decrease in speed, when omitted
  22. Correctly used it can double the speed of the algorithm
  23. return -- two lists [maxtab, mintab] containing the positive and negative
  24. peaks respectively. Each cell of the lists contains a tupple of:
  25. (position, peak_value)
  26. to get the average peak value do 'np.mean(maxtab, 0)[1]' on the results
  27. """
  28. maxtab = []
  29. mintab = []
  30. dump = [] # Used to pop the first hit which always if false
  31. length = len(y_axis)
  32. if x_axis is None:
  33. x_axis = range(length)
  34. # perform some checks
  35. # if length != len(x_axis):
  36. # raise ValueError, "Input vectors y_axis and x_axis must have same length"
  37. # if lookahead < 1:
  38. # raise ValueError, "Lookahead must be above '1' in value"
  39. # if not (np.isscalar(delta) and delta >= 0):
  40. # raise ValueError, "delta must be a positive number"
  41. # needs to be a numpy array
  42. y_axis = np.asarray(y_axis)
  43. # maxima and minima candidates are temporarily stored in
  44. # mx and mn respectively
  45. mn, mx = np.Inf, -np.Inf
  46. # Only detect peak if there is 'lookahead' amount of points after it
  47. for index, (x, y) in enumerate(zip(x_axis[:-lookahead], y_axis[:-lookahead])):
  48. if y > mx:
  49. mx = y
  50. mxpos = x
  51. if y < mn:
  52. mn = y
  53. mnpos = x
  54. #### look for max ####
  55. if y < mx - delta and mx != np.Inf:
  56. # Maxima peak candidate found
  57. # look ahead in signal to ensure that this is a peak and not jitter
  58. if y_axis[index:index + lookahead].max() < mx:
  59. maxtab.append((mxpos, mx))
  60. dump.append(True)
  61. # set algorithm to only find minima now
  62. mx = np.Inf
  63. mn = np.Inf
  64. #### look for min ####
  65. if y > mn + delta and mn != -np.Inf:
  66. # Minima peak candidate found
  67. # look ahead in signal to ensure that this is a peak and not jitter
  68. if y_axis[index:index + lookahead].min() > mn:
  69. mintab.append((mnpos, mn))
  70. dump.append(False)
  71. # set algorithm to only find maxima now
  72. mn = -np.Inf
  73. mx = -np.Inf
  74. # Remove the false hit on the first value of the y_axis
  75. try:
  76. if dump[0]:
  77. maxtab.pop(0)
  78. # print("pop max")
  79. else:
  80. mintab.pop(0)
  81. # print("pop min")
  82. del dump
  83. except IndexError:
  84. # no peaks were found, should the function return empty lists?
  85. pass
  86. return maxtab, mintab
  87. def peakdetect_zero_crossing(y_axis, x_axis=None, window=49):
  88. """
  89. Algorithm for detecting local maximas and minmias in a signal.
  90. Discovers peaks by dividing the signal into bins and retrieving the
  91. maximum and minimum value of each the even and odd bins respectively.
  92. Division into bins is performed by smoothing the curve and finding the
  93. zero crossings.
  94. Suitable for repeatable sinusoidal signals with some amount of RMS noise
  95. tolerable. Excecutes faster than 'peakdetect', although this function will
  96. break if the offset of the signal is too large. It should also be noted
  97. that the first and last peak will probably not be found, as this algorithm
  98. only can find peaks between the first and last zero crossing.
  99. keyword arguments:
  100. y_axis -- A list containg the signal over which to find peaks
  101. x_axis -- A x-axis whose values correspond to the 'y_axis' list and is used
  102. in the return to specify the postion of the peaks. If omitted the index
  103. of the y_axis is used. (default: None)
  104. window -- the dimension of the smoothing window; should be an odd integer
  105. (default: 49)
  106. return -- two lists [maxtab, mintab] containing the positive and negative
  107. peaks respectively. Each cell of the lists contains a tupple of:
  108. (position, peak_value)
  109. to get the average peak value do 'np.mean(maxtab, 0)[1]' on the results
  110. """
  111. if x_axis is None:
  112. x_axis = range(len(y_axis))
  113. length = len(y_axis)
  114. # if length != len(x_axis):
  115. # raise ValueError, 'Input vectors y_axis and x_axis must have same length'
  116. # needs to be a numpy array
  117. y_axis = np.asarray(y_axis)
  118. zero_indices = zero_crossings(y_axis, window=window)
  119. period_lengths = np.diff(zero_indices)
  120. bins = [y_axis[indice:indice + diff] for indice, diff in
  121. zip(zero_indices, period_lengths)]
  122. even_bins = bins[::2]
  123. odd_bins = bins[1::2]
  124. # check if even bin contains maxima
  125. if even_bins[0].max() > abs(even_bins[0].min()):
  126. hi_peaks = [bin.max() for bin in even_bins]
  127. lo_peaks = [bin.min() for bin in odd_bins]
  128. else:
  129. hi_peaks = [bin.max() for bin in odd_bins]
  130. lo_peaks = [bin.min() for bin in even_bins]
  131. hi_peaks_x = [x_axis[np.where(y_axis == peak)[0]] for peak in hi_peaks]
  132. lo_peaks_x = [x_axis[np.where(y_axis == peak)[0]] for peak in lo_peaks]
  133. maxtab = [(x, y) for x, y in zip(hi_peaks, hi_peaks_x)]
  134. mintab = [(x, y) for x, y in zip(lo_peaks, lo_peaks_x)]
  135. return maxtab, mintab
  136. def smooth(x, window_len=11, window='hanning'):
  137. """
  138. smooth the data using a window with requested size.
  139. This method is based on the convolution of a scaled window with the signal.
  140. The signal is prepared by introducing reflected copies of the signal
  141. (with the window size) in both ends so that transient parts are minimized
  142. in the begining and end part of the output signal.
  143. input:
  144. x: the input signal
  145. window_len: the dimension of the smoothing window; should be an odd integer
  146. window: the type of window from 'flat', 'hanning', 'hamming', 'bartlett', 'blackman'
  147. flat window will produce a moving average smoothing.
  148. output:
  149. the smoothed signal
  150. example:
  151. t=linspace(-2,2,0.1)
  152. x=sin(t)+randn(len(t))*0.1
  153. y=smooth(x)
  154. see also:
  155. numpy.hanning, numpy.hamming, numpy.bartlett, numpy.blackman, numpy.convolve
  156. scipy.signal.lfilter
  157. TODO: the window parameter could be the window itself if an array instead of a string
  158. """
  159. # if x.ndim != 1:
  160. # raise ValueError, "smooth only accepts 1 dimension arrays."
  161. # if x.size < window_len:
  162. # raise ValueError, "Input vector needs to be bigger than window size."
  163. if window_len<3:
  164. return x
  165. # if not window in ['flat', 'hanning', 'hamming', 'bartlett', 'blackman']:
  166. # raise ValueError, "Window is on of 'flat', 'hanning', 'hamming', 'bartlett', 'blackman'"
  167. s = np.r_[x[window_len - 1:0:-1], x, x[-1:-window_len:-1]]
  168. # print(len(s))
  169. if window == 'flat': # moving average
  170. w = np.ones(window_len, 'd')
  171. else:
  172. w = eval('np.' + window + '(window_len)')
  173. y = np.convolve(w / w.sum(), s, mode='valid')
  174. return y
  175. def zero_crossings(y_axis, x_axis=None, window=49):
  176. """
  177. Algorithm to find zero crossings. Smoothens the curve and finds the
  178. zero-crossings by looking for a sign change.
  179. keyword arguments:
  180. y_axis -- A list containg the signal over which to find zero-crossings
  181. x_axis -- A x-axis whose values correspond to the 'y_axis' list and is used
  182. in the return to specify the postion of the zero-crossings. If omitted
  183. then the indice of the y_axis is used. (default: None)
  184. window -- the dimension of the smoothing window; should be an odd integer
  185. (default: 49)
  186. return -- the x_axis value or the indice for each zero-crossing
  187. """
  188. # smooth the curve
  189. length = len(y_axis)
  190. if x_axis == None:
  191. x_axis = range(length)
  192. x_axis = np.asarray(x_axis)
  193. y_axis = smooth(y_axis, window)[:length]
  194. zero_crossings = np.where(np.diff(np.sign(y_axis)))[0]
  195. times = [x_axis[indice] for indice in zero_crossings]
  196. # check if zero-crossings are valid
  197. diff = np.diff(times)
  198. # if diff.std() / diff.mean() > 0.1:
  199. # raise ValueError, "smoothing window too small, false zero-crossings found"
  200. return times