PageRenderTime 479ms CodeModel.GetById 32ms RepoModel.GetById 0ms app.codeStats 0ms

/inst/findpeaks.m

https://bitbucket.org/mtmiller/octave-signal
Objective C | 361 lines | 311 code | 50 blank | 0 comment | 32 complexity | 6f5b99905b3a6d0bf59619bcdd617fb2 MD5 | raw file
Possible License(s): GPL-3.0
  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. if (nargin < 1)
  87. print_usage ();
  88. endif
  89. if (! (isvector (data) && numel (data) >= 3))
  90. error ("findpeaks:InvalidArgument",
  91. "findpeaks: DATA must be a vector of at least 3 elements");
  92. endif
  93. transpose = (rows (data) == 1);
  94. if (transpose)
  95. data = data.';
  96. endif
  97. ## --- Parse arguments --- #
  98. __data__ = abs (detrend (data, 0));
  99. posscal = @(x) isscalar (x) && x >= 0;
  100. parser = inputParser ();
  101. parser.FunctionName = "findpeaks";
  102. ## FIXME: inputParser was first implemented in the general package in the
  103. ## old @class type. This allowed for a very similar interface to
  104. ## Matlab but not quite equal. classdef was then implemented in
  105. ## Octave 4.0 release, which enabled inputParser to be implemented
  106. ## properly. However, this causes problem because we don't know
  107. ## what implementation may be running. A new version of the general
  108. ## package is being released to avoid the two implementations to
  109. ## co-exist.
  110. ##
  111. ## To keep supporting older octave versions, we have an alternative
  112. ## path that avoids inputParser. And if inputParser is available,
  113. ## we check what implementation is is, and act accordingly.
  114. ## Note that in Octave 4.0, inputParser is classdef and Octave behaves
  115. ## weird for it. which ("inputParser") will return empty (thinks its a
  116. ## builtin function).
  117. if (exist ("inputParser") == 2
  118. && isempty (strfind (which ("inputParser"),
  119. ["@inputParser" filesep "inputParser.m"])))
  120. ## making use of classdef's inputParser ..
  121. parser.addParamValue ("MinPeakHeight", 2*std (__data__),posscal);
  122. parser.addParamValue ("MinPeakDistance", 4, posscal);
  123. parser.addParamValue ("MinPeakWidth", 2, posscal);
  124. parser.addSwitch ("DoubleSided");
  125. parser.parse (varargin{:});
  126. minH = parser.Results.MinPeakHeight;
  127. minD = parser.Results.MinPeakDistance;
  128. minW = parser.Results.MinPeakWidth;
  129. dSided = parser.Results.DoubleSided;
  130. else
  131. ## either old @inputParser or no inputParser at all...
  132. lvarargin = lower (varargin);
  133. ds = strcmpi (lvarargin, "DoubleSided");
  134. if (any (ds))
  135. dSided = true;
  136. lvarargin(ds) = [];
  137. else
  138. dSided = false;
  139. endif
  140. [~, minH, minD, minW] = parseparams (lvarargin,
  141. "minpeakheight", 2 * std (__data__),
  142. "minpeakdistance", 4,
  143. "minpeakwidth", 2);
  144. if (! posscal (minH))
  145. error ("findpeaks: MinPeakHeight must be a positive scalar");
  146. elseif (! posscal (minD))
  147. error ("findpeaks: MinPeakDistance must be a positive scalar");
  148. elseif (! posscal (minW))
  149. error ("findpeaks: MinPeakWidth must be a positive scalar");
  150. endif
  151. endif
  152. if (dSided)
  153. [data, __data__] = deal (__data__, data);
  154. elseif (min (data) < 0)
  155. error ("findpeaks:InvalidArgument",
  156. 'Data contains negative values. You may want to "DoubleSided" option');
  157. endif
  158. ## Rough estimates of first and second derivative
  159. df1 = diff (data, 1)([1; (1:end)']);
  160. df2 = diff (data, 2)([1; 1; (1:end)']);
  161. ## check for changes of sign of 1st derivative and negativity of 2nd
  162. ## derivative.
  163. ## <= in 1st derivative includes the case of oversampled signals.
  164. idx = find (df1.*[df1(2:end); 0] <= 0 & [df2(2:end); 0] < 0);
  165. ## Get peaks that are beyond given height
  166. tf = data(idx) > minH;
  167. idx = idx(tf);
  168. ## sort according to magnitude
  169. [~, tmp] = sort (data(idx), "descend");
  170. idx_s = idx(tmp);
  171. ## Treat peaks separated less than minD as one
  172. D = abs (bsxfun (@minus, idx_s, idx_s'));
  173. if (any (D(:) < minD))
  174. i = 1;
  175. peak = cell ();
  176. node2visit = 1:size(D,1);
  177. visited = [];
  178. idx_pruned = idx_s;
  179. ## debug
  180. ## h = plot(1:length(data),data,"-",idx_s,data(idx_s),'.r',idx_s,data(idx_s),'.g');
  181. ## set(h(3),"visible","off");
  182. while (! isempty (node2visit))
  183. d = D(node2visit(1),:);
  184. visited = [visited node2visit(1)];
  185. node2visit(1) = [];
  186. neighs = setdiff (find (d < minD), visited);
  187. if (! isempty (neighs))
  188. ## debug
  189. ## set(h(3),"xdata",idx_s(neighs),"ydata",data(idx_s(neighs)),"visible","on")
  190. ## pause(0.2)
  191. ## set(h(3),"visible","off");
  192. idx_pruned = setdiff (idx_pruned, idx_s(neighs));
  193. visited = [visited neighs];
  194. node2visit = setdiff (node2visit, visited);
  195. ## debug
  196. ## set(h(2),"xdata",idx_pruned,"ydata",data(idx_pruned))
  197. ## pause
  198. endif
  199. endwhile
  200. idx = idx_pruned;
  201. endif
  202. extra = struct ("parabol", [], "height", [], "baseline", [], "roots", []);
  203. ## Estimate widths of peaks and filter for:
  204. ## width smaller than given.
  205. ## wrong concavity.
  206. ## not high enough
  207. ## data at peak is lower than parabola by 1%
  208. if (minW > 0)
  209. ## debug
  210. ## h = plot(1:length(data),data,"-",idx,data(idx),'.r',...
  211. ## idx,data(idx),'og',idx,data(idx),'-m');
  212. ## set(h(4),"linewidth",2)
  213. ## set(h(3:4),"visible","off");
  214. idx_pruned = idx;
  215. n = numel (idx);
  216. np = numel (data);
  217. struct_count = 0;
  218. for i=1:n
  219. ind = (round (max(idx(i)-minD/2,1)) : ...
  220. round (min(idx(i)+minD/2,np)))';
  221. pp = polyfit (ind, data(ind), 2);
  222. H = pp(3) - pp(2)^2/(4*pp(1));
  223. ## debug
  224. ## x = linspace(ind(1)-1,ind(end)+1,10);
  225. ## set(h(4),"xdata",x,"ydata",polyval(pp,x),"visible","on")
  226. ## set(h(3),"xdata",ind,"ydata",data(ind),"visible","on")
  227. ## pause(0.2)
  228. ## set(h(3:4),"visible","off");
  229. rz = roots ([pp(1:2) pp(3)-mean([H,minH])]);
  230. width = abs (diff (rz));
  231. if (width < minW || pp(1) > 0 || H < minH || data(idx(i)) < 0.99*H)
  232. idx_pruned = setdiff (idx_pruned, idx(i));
  233. elseif (nargout > 2)
  234. struct_count++;
  235. extra.parabol(struct_count).x = ind([1 end]);
  236. extra.parabol(struct_count).pp = pp;
  237. extra.roots(struct_count,1:2)= rz;
  238. extra.height(struct_count) = H;
  239. extra.baseline(struct_count) = mean ([H minH]);
  240. endif
  241. ## debug
  242. ## set(h(2),"xdata",idx_pruned,"ydata",data(idx_pruned))
  243. ## pause(0.2)
  244. endfor
  245. idx = idx_pruned;
  246. endif
  247. if (dSided)
  248. pks = __data__(idx);
  249. else
  250. pks = data(idx);
  251. endif
  252. if (transpose)
  253. pks = pks.';
  254. idx = idx.';
  255. endif
  256. if (nargout() > 2)
  257. varargout{1} = extra;
  258. endif
  259. endfunction
  260. %!demo
  261. %! t = 2*pi*linspace(0,1,1024)';
  262. %! 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);
  263. %!
  264. %! data1 = abs(y); # Positive values
  265. %! [pks idx] = findpeaks(data1);
  266. %!
  267. %! data2 = y; # Double-sided
  268. %! [pks2 idx2] = findpeaks(data2,"DoubleSided");
  269. %! [pks3 idx3] = findpeaks(data2,"DoubleSided","MinPeakHeight",0.5);
  270. %!
  271. %! subplot(1,2,1)
  272. %! plot(t,data1,t(idx),data1(idx),'.m')
  273. %! subplot(1,2,2)
  274. %! plot(t,data2,t(idx2),data2(idx2),".m;>2*std;",t(idx3),data2(idx3),"or;>0.1;")
  275. %! legend("Location","NorthOutside","Orientation","horizontal")
  276. %!
  277. %! #----------------------------------------------------------------------------
  278. %! # Finding the peaks of smooth data is not a big deal!
  279. %!demo
  280. %! t = 2*pi*linspace(0,1,1024)';
  281. %! 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);
  282. %!
  283. %! data = abs(y + 0.1*randn(length(y),1)); # Positive values + noise
  284. %! [pks idx] = findpeaks(data,"MinPeakHeight",1);
  285. %!
  286. %! dt = t(2)-t(1);
  287. %! [pks2 idx2] = findpeaks(data,"MinPeakHeight",1,...
  288. %! "MinPeakDistance",round(0.5/dt));
  289. %!
  290. %! subplot(1,2,1)
  291. %! plot(t,data,t(idx),data(idx),'.r')
  292. %! subplot(1,2,2)
  293. %! plot(t,data,t(idx2),data(idx2),'.r')
  294. %!
  295. %! #----------------------------------------------------------------------------
  296. %! # Noisy data may need tuning of the parameters. In the 2nd example,
  297. %! # MinPeakDistance is used as a smoother of the peaks.
  298. %!assert (isempty (findpeaks ([1, 1, 1])))
  299. %!assert (isempty (findpeaks ([1; 1; 1])))
  300. ## Test for bug #45056
  301. %!test
  302. %! ## Test input vector is an oversampled sinusoid with clipped peaks
  303. %! x = min (3, cos (2*pi*[0:8000] ./ 600) + 2.01);
  304. %! assert (! isempty (findpeaks (x)))
  305. %% Test input validation
  306. %!error findpeaks ()
  307. %!error findpeaks (1)
  308. %!error findpeaks ([1, 2])
  309. ## Failing test because we are not Matlab compatible
  310. %!xtest assert (findpeaks ([34 134 353 64 134 14 56 67 234 143 64 575 8657]),
  311. %! [353 134 234])