saccades.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  1. import numpy as _np
  2. from scipy.signal import find_peaks as _find_peaks
  3. import sliding1d as _sliding
  4. def get_sign(x):
  5. return x / _np.abs(x)
  6. def sliding_diff(x):
  7. x = _np.array(x)
  8. ret = _np.empty(x.size, dtype=_np.float64)
  9. ret[1:-1] = (x[2:] - x[:-2])/2
  10. ret[0] = x[1] - x[0]
  11. ret[-1] = x[-1] - x[-2]
  12. return ret
  13. def smoothing_diff(x, rad=5, num=3):
  14. return sliding_diff(_sliding.nanmean(x, rad, num))
  15. def threshold_values(x, threshold):
  16. ret = _np.array(x, copy=True)
  17. ret[_np.abs(x) < threshold] = 0
  18. return ret
  19. def detect(time, left_pupil, right_pupil,
  20. std_period=None, std_threshold=5, min_distance_seconds=0.2,
  21. smoothing_radius_seconds=0.05, smoothing_number=3):
  22. """detect saccade events.
  23. Parameters
  24. ----------
  25. time -- the zero-starting trial time array.
  26. left_pupil -- the left-pupil position at each time point.
  27. right_pupil -- the right-pupil position at each time point.
  28. For the keyword arguments, refer to the "Procedures" section below.
  29. Returns
  30. -------
  31. a numpy.ndarray object whose size equals to the size of the `time` parameter.
  32. the returned array has all-zero values, except for the positions of the detected saccades.
  33. the values at the time points of detected saccades represent the average velocity of the two eyes.
  34. Procedures
  35. ----------
  36. 1. smoothing_diff() is used to compute velocity from position.
  37. - `smoothing_radius_seconds` and `smoothing_number` is used here.
  38. 2. the velocity is thresholded by its abosolute value
  39. with the criterion: `abs(v) > v[std_period].std(ddof=1)*std_threshold`
  40. - if `std_period` is None, then the initial 1 second of the recording is used.
  41. 3. "average velocity" is calculated as the geometric mean
  42. of the (thresholded) velocities of the left and the right pupils.
  43. 4. `scipy.signal.find_peaks` is used to detect peaks from the average velocity.
  44. - `min_distance_seconds` is used to set the minimum distance between the peaks.
  45. 5. the returned array is formatted. the value of each event is computed based on:
  46. - amplitude: the amplitude in the average velocity
  47. - signature: the signature in the left-pupil velocity.
  48. """
  49. dt = _np.diff(time).mean()
  50. if std_period is None:
  51. std_period = time < 1 # the initial 1 second
  52. smoothing_radius = int(round(smoothing_radius_seconds / dt))
  53. min_distance_samples = int(round(min_distance_seconds / dt))
  54. vleft = smoothing_diff(left_pupil, rad=smoothing_radius, num=smoothing_number) / dt
  55. vright = smoothing_diff(right_pupil, rad=smoothing_radius, num=smoothing_number) / dt
  56. vleft_thresholded = threshold_values(vleft, vleft[std_period].std(ddof=1)*std_threshold)
  57. vright_thresholded = threshold_values(vright, vright[std_period].std(ddof=1)*std_threshold)
  58. # there are some "negative spikes", possibly due to small unsynchronization of pupil motions
  59. # we take both negative and positive spikes here
  60. vamp = _np.sqrt(_np.abs(vleft_thresholded*vright_thresholded))
  61. peaks = _find_peaks(vamp, distance=min_distance_samples)[0]
  62. ret = _np.zeros(left_pupil.size, dtype=_np.float64)
  63. ret[peaks] = vamp[peaks] * get_sign(vleft_thresholded[peaks])
  64. return ret
  65. def annotate(events):
  66. """splits the saccade events array into three traces for the plotting purpose.
  67. Parameter
  68. ---------
  69. events -- as it is returned from saccades.detect()
  70. Returns
  71. -------
  72. a `dict` object, which contains the following keys and values:
  73. - none: no-event period trace, for plotting "baseline" period.
  74. - left: leftward saccade event trace
  75. - right: rightward saccade event trace
  76. For event traces, NaNs are used to represent values at irrelevant time points.
  77. """
  78. noevents = _np.zeros(events.size, dtype=_np.float64)
  79. leftevents = _np.empty(events.size, dtype=_np.float64); leftevents[:] = _np.nan
  80. rightevents = _np.empty(events.size, dtype=_np.float64); rightevents[:] = _np.nan
  81. for levt in _np.where(events > 0)[0]:
  82. rng = slice(levt-1, levt+2)
  83. leftevents[rng] = events[rng]
  84. for revt in _np.where(events < 0)[0]:
  85. rng = slice(revt-1, revt+2)
  86. rightevents[rng] = events[rng]
  87. return dict(none=noevents, left=leftevents, right=rightevents)