findpeaks.m 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. ## Copyright (c) 2012 Juan Pablo Carbajal <carbajal@ifi.uzh.ch>
  2. ##
  3. ## This program is free software; you can redistribute it and/or modify
  4. ## it under the terms of the GNU General Public License as published by
  5. ## the Free Software Foundation; either version 3 of the License, or
  6. ## (at your option) any later version.
  7. ##
  8. ## This program is distributed in the hope that it will be useful,
  9. ## but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. ## GNU General Public License for more details.
  12. ##
  13. ## You should have received a copy of the GNU General Public License
  14. ## along with this program; if not, see <http://www.gnu.org/licenses/>.
  15. ## -*- texinfo -*-
  16. ## @deftypefn {Function File} {[@var{pks}, @var{loc}, @var{extra}] =} findpeaks (@var{data})
  17. ## @deftypefnx {Function File} {@dots{} =} findpeaks (@dots{}, @var{property}, @var{value})
  18. ## @deftypefnx {Function File} {@dots{} =} findpeaks (@dots{}, @asis{"DoubleSided"})
  19. ## Finds peaks on @var{data}.
  20. ##
  21. ## Peaks of a positive array of data are defined as local maxima. For
  22. ## double-sided data, they are maxima of the positive part and minima of
  23. ## the negative part. @var{data} is expected to be a single column
  24. ## vector.
  25. ##
  26. ## The function returns the value of @var{data} at the peaks in
  27. ## @var{pks}. The index indicating their position is returned in
  28. ## @var{loc}.
  29. ##
  30. ## The third output argument is a structure with additional information:
  31. ##
  32. ## @table @asis
  33. ## @item "parabol"
  34. ## A structure containing the parabola fitted to each returned peak. The
  35. ## structure has two fields, @asis{"x"} and @asis{"pp"}. The field
  36. ## @asis{"pp"} contains the coefficients of the 2nd degree polynomial
  37. ## and @asis{"x"} the extrema of the intercal here it was fitted.
  38. ##
  39. ## @item "height"
  40. ## The estimated height of the returned peaks (in units of @var{data}).
  41. ##
  42. ## @item "baseline"
  43. ## The height at which the roots of the returned peaks were calculated
  44. ## (in units of @var{data}).
  45. ##
  46. ## @item "roots"
  47. ## The abscissa values (in index units) at which the parabola fitted to
  48. ## each of the returned peaks crosses the @asis{"baseline"} value. The
  49. ## width of the peak is calculated by @command{diff(roots)}.
  50. ## @end table
  51. ##
  52. ## This function accepts property-value pair given in the list below:
  53. ##
  54. ## @table @asis
  55. ##
  56. ## @item "MinPeakHeight"
  57. ## Minimum peak height (positive scalar). Only peaks that exceed this
  58. ## value will be returned. For data taking positive and negative values
  59. ## use the option "DoubleSided". Default value @code{2*std (abs (detrend
  60. ## (data,0)))}.
  61. ##
  62. ## @item "MinPeakDistance"
  63. ## Minimum separation between (positive integer). Peaks separated by
  64. ## less than this distance are considered a single peak. This distance
  65. ## is also used to fit a second order polynomial to the peaks to
  66. ## estimate their width, therefore it acts as a smoothing parameter.
  67. ## Default value 4.
  68. ##
  69. ## @item "MinPeakWidth"
  70. ## Minimum width of peaks (positive integer). The width of the peaks is
  71. ## estimated using a parabola fitted to the neighborhood of each peak.
  72. ## The neighborhood size is equal to the value of
  73. ## @asis{"MinPeakDistance"}. The width is evaluated at the half height
  74. ## of the peak with baseline at "MinPeakHeight". Default value 2.
  75. ##
  76. ## @item "DoubleSided"
  77. ## Tells the function that data takes positive and negative values. The
  78. ## base-line for the peaks is taken as the mean value of the function.
  79. ## This is equivalent as passing the absolute value of the data after
  80. ## removing the mean.
  81. ## @end table
  82. ##
  83. ## Run @command{demo findpeaks} to see some examples.
  84. ## @end deftypefn
  85. function [pks idx varargout] = findpeaks (data, varargin)
  86. ## --- Parse arguments --- #
  87. __data__ = abs (detrend (data,0));
  88. posscal = @(x)isscalar (x) && x >= 0;
  89. parser = inputParser ();
  90. parser.FunctionName = "findpeaks";
  91. parser = addParamValue (parser,"MinPeakHeight", 2*std (__data__),posscal);
  92. parser = addParamValue (parser,"MinPeakDistance",4,posscal);
  93. parser = addParamValue (parser,"MinPeakWidth",2,posscal);
  94. parser = addSwitch (parser,"DoubleSided");
  95. parser = parse(parser,varargin{:});
  96. minH = parser.Results.MinPeakHeight;
  97. minD = parser.Results.MinPeakDistance;
  98. minW = parser.Results.MinPeakWidth;
  99. dSided = parser.Results.DoubleSided;
  100. clear parser posscal
  101. ## ------ #
  102. if dSided
  103. [data __data__] = deal (__data__, data);
  104. elseif min(data)<0
  105. error ("findpeaks:InvalidArgument",
  106. 'Data contains negative values. You may want to "DoubleSided" option');
  107. endif
  108. ## Rough estimates of first and second derivative
  109. df1 = diff (data,1)([1; (1:end)']);
  110. df2 = diff (data,2)([1; 1; (1:end)']);
  111. ## check for changes of sign of 1st derivative and negativity of 2nd
  112. ## derivative.
  113. idx = find (df1.*[df1(2:end); 0]<0 & [df2(2:end); 0]<0);
  114. ## Get peaks that are beyond given height
  115. tf = data(idx) > minH;
  116. idx = idx(tf);
  117. ## sort according to magnitude
  118. [~,tmp] = sort(data(idx),"descend");
  119. idx_s = idx(tmp);
  120. ## Treat peaks separated less than minD as one
  121. # http://savannah.gnu.org/bugs/?43691
  122. # D = abs (idx_s - idx_s');
  123. D = bsxfun (@minus, idx_s, idx_s');
  124. if any(D(:) < minD)
  125. i = 1;
  126. peak = cell ();
  127. node2visit = 1:size(D,1);
  128. visited = [];
  129. idx_pruned = idx_s;
  130. ## debug
  131. ## h = plot(1:length(data),data,"-",idx_s,data(idx_s),'.r',idx_s,data(idx_s),'.g');
  132. ## set(h(3),"visible","off");
  133. while ~isempty (node2visit)
  134. d = D(node2visit(1),:);
  135. visited = [visited node2visit(1)];
  136. node2visit(1) = [];
  137. neighs = setdiff (find (d < minD), visited);
  138. if ~isempty (neighs)
  139. ## debug
  140. ## set(h(3),"xdata",idx_s(neighs),"ydata",data(idx_s(neighs)),"visible","on")
  141. ## pause(0.2)
  142. ## set(h(3),"visible","off");
  143. idx_pruned = setdiff (idx_pruned,idx_s(neighs));
  144. visited = [visited neighs];
  145. node2visit = setdiff (node2visit,visited);
  146. ## debug
  147. ## set(h(2),"xdata",idx_pruned,"ydata",data(idx_pruned))
  148. ## pause
  149. endif
  150. endwhile
  151. idx = idx_pruned;
  152. endif
  153. ## Estimate widths of peaks and filter for:
  154. ## width smaller than given.
  155. ## wrong concavity.
  156. ## not high enough
  157. ## data at peak is lower than parabola by 1%
  158. if minW > 0
  159. ## debug
  160. ## h = plot(1:length(data),data,"-",idx,data(idx),'.r',...
  161. ## idx,data(idx),'og',idx,data(idx),'-m');
  162. ## set(h(4),"linewidth",2)
  163. ## set(h(3:4),"visible","off");
  164. idx_pruned = idx;
  165. n = length(idx);
  166. np = length(data);
  167. struct_count = 0;
  168. for i=1:n
  169. ind = (round (max(idx(i)-minD/2,1)) : ...
  170. round (min(idx(i)+minD/2,np)))';
  171. pp = polyfit (ind,data(ind),2);
  172. H = pp(3) - pp(2)^2/(4*pp(1));
  173. ## debug
  174. ## x = linspace(ind(1)-1,ind(end)+1,10);
  175. ## set(h(4),"xdata",x,"ydata",polyval(pp,x),"visible","on")
  176. ## set(h(3),"xdata",ind,"ydata",data(ind),"visible","on")
  177. ## pause(0.2)
  178. ## set(h(3:4),"visible","off");
  179. rz = roots ([pp(1:2) pp(3)-mean([H,minH])]);
  180. width = abs (diff (rz));
  181. if width < minW || pp(1) > 0 || H < minH || data(idx(i)) < 0.99*H
  182. idx_pruned = setdiff (idx_pruned, idx(i));
  183. elseif nargout > 2
  184. struct_count++;
  185. extra.parabol(struct_count).x = ind([1 end]);
  186. extra.parabol(struct_count).pp = pp;
  187. extra.roots(struct_count,1:2) = rz;
  188. extra.height(struct_count) = H;
  189. extra.baseline(struct_count) = mean([H,minH]);
  190. endif
  191. ## debug
  192. ## set(h(2),"xdata",idx_pruned,"ydata",data(idx_pruned))
  193. ## pause(0.2)
  194. endfor
  195. idx = idx_pruned;
  196. endif
  197. if dSided
  198. pks = __data__(idx);
  199. else
  200. pks = data(idx);
  201. endif
  202. if nargout()>2
  203. varargout{1} = extra;
  204. endif
  205. endfunction
  206. %!demo
  207. %! t = 2*pi*linspace(0,1,1024)';
  208. %! y = sin(3.14*t) + 0.5*cos(6.09*t) + 0.1*sin(10.11*t+1/6) + 0.1*sin(15.3*t+1/3);
  209. %!
  210. %! data1 = abs(y); # Positive values
  211. %! [pks idx] = findpeaks(data1);
  212. %!
  213. %! data2 = y; # Double-sided
  214. %! [pks2 idx2] = findpeaks(data2,"DoubleSided");
  215. %! [pks3 idx3] = findpeaks(data2,"DoubleSided","MinPeakHeight",0.5);
  216. %!
  217. %! subplot(1,2,1)
  218. %! plot(t,data1,t(idx),data1(idx),'.m')
  219. %! subplot(1,2,2)
  220. %! plot(t,data2,t(idx2),data2(idx2),".m;>2*std;",t(idx3),data2(idx3),"or;>0.1;")
  221. %! legend("Location","NorthOutside","Orientation","horizontal")
  222. %!
  223. %! #----------------------------------------------------------------------------
  224. %! # Finding the peaks of smooth data is not a big deal!
  225. %!demo
  226. %! t = 2*pi*linspace(0,1,1024)';
  227. %! y = sin(3.14*t) + 0.5*cos(6.09*t) + 0.1*sin(10.11*t+1/6) + 0.1*sin(15.3*t+1/3);
  228. %!
  229. %! data = abs(y + 0.1*randn(length(y),1)); # Positive values + noise
  230. %! [pks idx] = findpeaks(data,"MinPeakHeight",1);
  231. %!
  232. %! dt = t(2)-t(1);
  233. %! [pks2 idx2] = findpeaks(data,"MinPeakHeight",1,...
  234. %! "MinPeakDistance",round(0.5/dt));
  235. %!
  236. %! subplot(1,2,1)
  237. %! plot(t,data,t(idx),data(idx),'.r')
  238. %! subplot(1,2,2)
  239. %! plot(t,data,t(idx2),data(idx2),'.r')
  240. %!
  241. %! #----------------------------------------------------------------------------
  242. %! # Noisy data may need tunning of the parameters. In the 2nd example,
  243. %! # MinPeakDistance is used as a smoother of the peaks.