Wideband autonomous SDR analysis engine forked from sdr-visual-suite
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

366 строки
9.2KB

  1. package detector
  2. import (
  3. "math"
  4. "sort"
  5. "time"
  6. "sdr-visual-suite/internal/cfar"
  7. "sdr-visual-suite/internal/classifier"
  8. )
  9. type Event struct {
  10. ID int64 `json:"id"`
  11. Start time.Time `json:"start"`
  12. End time.Time `json:"end"`
  13. CenterHz float64 `json:"center_hz"`
  14. Bandwidth float64 `json:"bandwidth_hz"`
  15. PeakDb float64 `json:"peak_db"`
  16. SNRDb float64 `json:"snr_db"`
  17. FirstBin int `json:"first_bin"`
  18. LastBin int `json:"last_bin"`
  19. Class *classifier.Classification `json:"class,omitempty"`
  20. }
  21. type Detector struct {
  22. ThresholdDb float64
  23. MinDuration time.Duration
  24. Hold time.Duration
  25. EmaAlpha float64
  26. HysteresisDb float64
  27. MinStableFrames int
  28. GapTolerance time.Duration
  29. CFARScaleDb float64
  30. binWidth float64
  31. nbins int
  32. sampleRate int
  33. ema []float64
  34. active map[int64]*activeEvent
  35. nextID int64
  36. cfarEngine cfar.CFAR
  37. lastThresholds []float64
  38. lastNoiseFloor float64
  39. }
  40. type activeEvent struct {
  41. id int64
  42. start time.Time
  43. lastSeen time.Time
  44. centerHz float64
  45. bwHz float64
  46. peakDb float64
  47. snrDb float64
  48. firstBin int
  49. lastBin int
  50. class *classifier.Classification
  51. stableHits int
  52. }
  53. type Signal struct {
  54. FirstBin int `json:"first_bin"`
  55. LastBin int `json:"last_bin"`
  56. CenterHz float64 `json:"center_hz"`
  57. BWHz float64 `json:"bw_hz"`
  58. PeakDb float64 `json:"peak_db"`
  59. SNRDb float64 `json:"snr_db"`
  60. NoiseDb float64 `json:"noise_db,omitempty"`
  61. Class *classifier.Classification `json:"class,omitempty"`
  62. }
  63. func New(thresholdDb float64, sampleRate int, fftSize int, minDur, hold time.Duration, emaAlpha, hysteresis float64, minStable int, gapTolerance time.Duration, cfarMode string, cfarGuard, cfarTrain, cfarRank int, cfarScaleDb float64, cfarWrap bool) *Detector {
  64. if minDur <= 0 {
  65. minDur = 250 * time.Millisecond
  66. }
  67. if hold <= 0 {
  68. hold = 500 * time.Millisecond
  69. }
  70. if emaAlpha <= 0 || emaAlpha > 1 {
  71. emaAlpha = 0.2
  72. }
  73. if hysteresis <= 0 {
  74. hysteresis = 3
  75. }
  76. if minStable <= 0 {
  77. minStable = 3
  78. }
  79. if gapTolerance <= 0 {
  80. gapTolerance = hold
  81. }
  82. if cfarGuard < 0 {
  83. cfarGuard = 2
  84. }
  85. if cfarTrain <= 0 {
  86. cfarTrain = 16
  87. }
  88. if cfarScaleDb <= 0 {
  89. cfarScaleDb = 6
  90. }
  91. if cfarRank <= 0 || cfarRank > 2*cfarTrain {
  92. cfarRank = int(math.Round(0.75 * float64(2*cfarTrain)))
  93. if cfarRank <= 0 {
  94. cfarRank = 1
  95. }
  96. }
  97. var cfarEngine cfar.CFAR
  98. if cfarMode != "" && cfarMode != "OFF" {
  99. cfarEngine = cfar.New(cfar.Config{
  100. Mode: cfar.Mode(cfarMode),
  101. GuardCells: cfarGuard,
  102. TrainCells: cfarTrain,
  103. Rank: cfarRank,
  104. ScaleDb: cfarScaleDb,
  105. WrapAround: cfarWrap,
  106. })
  107. }
  108. return &Detector{
  109. ThresholdDb: thresholdDb,
  110. MinDuration: minDur,
  111. Hold: hold,
  112. EmaAlpha: emaAlpha,
  113. HysteresisDb: hysteresis,
  114. MinStableFrames: minStable,
  115. GapTolerance: gapTolerance,
  116. CFARScaleDb: cfarScaleDb,
  117. binWidth: float64(sampleRate) / float64(fftSize),
  118. nbins: fftSize,
  119. sampleRate: sampleRate,
  120. ema: make([]float64, fftSize),
  121. active: map[int64]*activeEvent{},
  122. nextID: 1,
  123. cfarEngine: cfarEngine,
  124. }
  125. }
  126. func (d *Detector) Process(now time.Time, spectrum []float64, centerHz float64) ([]Event, []Signal) {
  127. signals := d.detectSignals(spectrum, centerHz)
  128. finished := d.matchSignals(now, signals)
  129. return finished, signals
  130. }
  131. func (d *Detector) LastThresholds() []float64 {
  132. if len(d.lastThresholds) == 0 {
  133. return nil
  134. }
  135. return append([]float64(nil), d.lastThresholds...)
  136. }
  137. func (d *Detector) LastNoiseFloor() float64 {
  138. return d.lastNoiseFloor
  139. }
  140. // UpdateClasses refreshes active event classes from current signals.
  141. func (d *Detector) UpdateClasses(signals []Signal) {
  142. for _, s := range signals {
  143. for _, ev := range d.active {
  144. if overlapHz(s.CenterHz, s.BWHz, ev.centerHz, ev.bwHz) && math.Abs(s.CenterHz-ev.centerHz) < (s.BWHz+ev.bwHz)/2.0 {
  145. if s.Class != nil {
  146. if ev.class == nil || s.Class.Confidence >= ev.class.Confidence {
  147. ev.class = s.Class
  148. }
  149. }
  150. }
  151. }
  152. }
  153. }
  154. func (d *Detector) detectSignals(spectrum []float64, centerHz float64) []Signal {
  155. n := len(spectrum)
  156. if n == 0 {
  157. return nil
  158. }
  159. smooth := d.smoothSpectrum(spectrum)
  160. var thresholds []float64
  161. if d.cfarEngine != nil {
  162. thresholds = d.cfarEngine.Thresholds(smooth)
  163. }
  164. d.lastThresholds = append(d.lastThresholds[:0], thresholds...)
  165. noiseGlobal := median(smooth)
  166. d.lastNoiseFloor = noiseGlobal
  167. var signals []Signal
  168. in := false
  169. start := 0
  170. peak := -1e9
  171. peakBin := 0
  172. for i := 0; i < n; i++ {
  173. v := smooth[i]
  174. thresholdOn := d.ThresholdDb
  175. if thresholds != nil && !math.IsNaN(thresholds[i]) {
  176. thresholdOn = thresholds[i]
  177. }
  178. thresholdOff := thresholdOn - d.HysteresisDb
  179. if v >= thresholdOn {
  180. if !in {
  181. in = true
  182. start = i
  183. peak = v
  184. peakBin = i
  185. } else if v > peak {
  186. peak = v
  187. peakBin = i
  188. }
  189. } else if in && v < thresholdOff {
  190. noise := noiseGlobal
  191. if thresholds != nil && peakBin >= 0 && peakBin < len(thresholds) && !math.IsNaN(thresholds[peakBin]) {
  192. noise = thresholds[peakBin] - d.CFARScaleDb
  193. }
  194. signals = append(signals, d.makeSignal(start, i-1, peak, peakBin, noise, centerHz, smooth))
  195. in = false
  196. }
  197. }
  198. if in {
  199. noise := noiseGlobal
  200. if thresholds != nil && peakBin >= 0 && peakBin < len(thresholds) && !math.IsNaN(thresholds[peakBin]) {
  201. noise = thresholds[peakBin] - d.CFARScaleDb
  202. }
  203. signals = append(signals, d.makeSignal(start, n-1, peak, peakBin, noise, centerHz, smooth))
  204. }
  205. return signals
  206. }
  207. func (d *Detector) makeSignal(first, last int, peak float64, peakBin int, noise float64, centerHz float64, spectrum []float64) Signal {
  208. centerBin := float64(first+last) / 2.0
  209. centerFreq := centerHz + (centerBin-float64(d.nbins)/2.0)*d.binWidth
  210. bw := float64(last-first+1) * d.binWidth
  211. snr := peak - noise
  212. return Signal{
  213. FirstBin: first,
  214. LastBin: last,
  215. CenterHz: centerFreq,
  216. BWHz: bw,
  217. PeakDb: peak,
  218. SNRDb: snr,
  219. NoiseDb: noise,
  220. }
  221. }
  222. func (d *Detector) smoothSpectrum(spectrum []float64) []float64 {
  223. if d.ema == nil || len(d.ema) != len(spectrum) {
  224. d.ema = make([]float64, len(spectrum))
  225. copy(d.ema, spectrum)
  226. return d.ema
  227. }
  228. alpha := d.EmaAlpha
  229. for i := range spectrum {
  230. v := spectrum[i]
  231. d.ema[i] = alpha*v + (1-alpha)*d.ema[i]
  232. }
  233. // IMPORTANT: caller must not modify returned slice
  234. return d.ema
  235. }
  236. func (d *Detector) matchSignals(now time.Time, signals []Signal) []Event {
  237. used := make(map[int64]bool, len(d.active))
  238. for _, s := range signals {
  239. var best *activeEvent
  240. var candidates []struct {
  241. ev *activeEvent
  242. dist float64
  243. }
  244. for _, ev := range d.active {
  245. if overlapHz(s.CenterHz, s.BWHz, ev.centerHz, ev.bwHz) && math.Abs(s.CenterHz-ev.centerHz) < (s.BWHz+ev.bwHz)/2.0 {
  246. candidates = append(candidates, struct {
  247. ev *activeEvent
  248. dist float64
  249. }{ev: ev, dist: math.Abs(s.CenterHz - ev.centerHz)})
  250. }
  251. }
  252. if len(candidates) > 0 {
  253. sort.Slice(candidates, func(i, j int) bool { return candidates[i].dist < candidates[j].dist })
  254. best = candidates[0].ev
  255. }
  256. if best == nil {
  257. id := d.nextID
  258. d.nextID++
  259. d.active[id] = &activeEvent{
  260. id: id,
  261. start: now,
  262. lastSeen: now,
  263. centerHz: s.CenterHz,
  264. bwHz: s.BWHz,
  265. peakDb: s.PeakDb,
  266. snrDb: s.SNRDb,
  267. firstBin: s.FirstBin,
  268. lastBin: s.LastBin,
  269. class: s.Class,
  270. stableHits: 1,
  271. }
  272. continue
  273. }
  274. used[best.id] = true
  275. best.lastSeen = now
  276. best.stableHits++
  277. best.centerHz = (best.centerHz + s.CenterHz) / 2.0
  278. if s.BWHz > best.bwHz {
  279. best.bwHz = s.BWHz
  280. }
  281. if s.PeakDb > best.peakDb {
  282. best.peakDb = s.PeakDb
  283. }
  284. if s.SNRDb > best.snrDb {
  285. best.snrDb = s.SNRDb
  286. }
  287. if s.FirstBin < best.firstBin {
  288. best.firstBin = s.FirstBin
  289. }
  290. if s.LastBin > best.lastBin {
  291. best.lastBin = s.LastBin
  292. }
  293. if s.Class != nil {
  294. if best.class == nil || s.Class.Confidence >= best.class.Confidence {
  295. best.class = s.Class
  296. }
  297. }
  298. }
  299. var finished []Event
  300. for id, ev := range d.active {
  301. if used[id] {
  302. continue
  303. }
  304. if now.Sub(ev.lastSeen) < d.GapTolerance {
  305. continue
  306. }
  307. duration := ev.lastSeen.Sub(ev.start)
  308. if duration < d.MinDuration || ev.stableHits < d.MinStableFrames {
  309. delete(d.active, id)
  310. continue
  311. }
  312. finished = append(finished, Event{
  313. ID: ev.id,
  314. Start: ev.start,
  315. End: ev.lastSeen,
  316. CenterHz: ev.centerHz,
  317. Bandwidth: ev.bwHz,
  318. PeakDb: ev.peakDb,
  319. SNRDb: ev.snrDb,
  320. FirstBin: ev.firstBin,
  321. LastBin: ev.lastBin,
  322. Class: ev.class,
  323. })
  324. delete(d.active, id)
  325. }
  326. return finished
  327. }
  328. func overlapHz(c1, b1, c2, b2 float64) bool {
  329. l1 := c1 - b1/2.0
  330. r1 := c1 + b1/2.0
  331. l2 := c2 - b2/2.0
  332. r2 := c2 + b2/2.0
  333. return l1 <= r2 && l2 <= r1
  334. }
  335. func median(vals []float64) float64 {
  336. if len(vals) == 0 {
  337. return 0
  338. }
  339. cpy := append([]float64(nil), vals...)
  340. sort.Float64s(cpy)
  341. mid := len(cpy) / 2
  342. if len(cpy)%2 == 0 {
  343. return (cpy[mid-1] + cpy[mid]) / 2.0
  344. }
  345. return cpy[mid]
  346. }