Wideband autonomous SDR analysis engine forked from sdr-visual-suite
No puede seleccionar más de 25 temas Los temas deben comenzar con una letra o número, pueden incluir guiones ('-') y pueden tener hasta 35 caracteres de largo.

552 líneas
14KB

  1. package detector
  2. import (
  3. "math"
  4. "sort"
  5. "time"
  6. "sdr-visual-suite/internal/cfar"
  7. "sdr-visual-suite/internal/classifier"
  8. "sdr-visual-suite/internal/config"
  9. )
  10. type Event struct {
  11. ID int64 `json:"id"`
  12. Start time.Time `json:"start"`
  13. End time.Time `json:"end"`
  14. CenterHz float64 `json:"center_hz"`
  15. Bandwidth float64 `json:"bandwidth_hz"`
  16. PeakDb float64 `json:"peak_db"`
  17. SNRDb float64 `json:"snr_db"`
  18. FirstBin int `json:"first_bin"`
  19. LastBin int `json:"last_bin"`
  20. Class *classifier.Classification `json:"class,omitempty"`
  21. }
  22. type Detector struct {
  23. ThresholdDb float64
  24. MinDuration time.Duration
  25. Hold time.Duration
  26. EmaAlpha float64
  27. HysteresisDb float64
  28. MinStableFrames int
  29. GapTolerance time.Duration
  30. CFARScaleDb float64
  31. EdgeMarginDb float64
  32. MaxSignalBwHz float64
  33. MergeGapHz float64
  34. binWidth float64
  35. nbins int
  36. sampleRate int
  37. ema []float64
  38. active map[int64]*activeEvent
  39. nextID int64
  40. cfarEngine cfar.CFAR
  41. lastThresholds []float64
  42. lastNoiseFloor float64
  43. }
  44. type activeEvent struct {
  45. id int64
  46. start time.Time
  47. lastSeen time.Time
  48. centerHz float64
  49. bwHz float64
  50. peakDb float64
  51. snrDb float64
  52. firstBin int
  53. lastBin int
  54. class *classifier.Classification
  55. stableHits int
  56. }
  57. type Signal struct {
  58. FirstBin int `json:"first_bin"`
  59. LastBin int `json:"last_bin"`
  60. CenterHz float64 `json:"center_hz"`
  61. BWHz float64 `json:"bw_hz"`
  62. PeakDb float64 `json:"peak_db"`
  63. SNRDb float64 `json:"snr_db"`
  64. NoiseDb float64 `json:"noise_db,omitempty"`
  65. Class *classifier.Classification `json:"class,omitempty"`
  66. }
  67. func New(detCfg config.DetectorConfig, sampleRate int, fftSize int) *Detector {
  68. minDur := time.Duration(detCfg.MinDurationMs) * time.Millisecond
  69. hold := time.Duration(detCfg.HoldMs) * time.Millisecond
  70. gapTolerance := time.Duration(detCfg.GapToleranceMs) * time.Millisecond
  71. emaAlpha := detCfg.EmaAlpha
  72. hysteresis := detCfg.HysteresisDb
  73. minStable := detCfg.MinStableFrames
  74. cfarMode := detCfg.CFARMode
  75. binWidth := float64(sampleRate) / float64(fftSize)
  76. cfarGuard := int(math.Ceil(detCfg.CFARGuardHz / binWidth))
  77. if cfarGuard < 0 {
  78. cfarGuard = 0
  79. }
  80. cfarTrain := int(math.Ceil(detCfg.CFARTrainHz / binWidth))
  81. if cfarTrain < 1 {
  82. cfarTrain = 1
  83. }
  84. cfarRank := detCfg.CFARRank
  85. cfarScaleDb := detCfg.CFARScaleDb
  86. cfarWrap := detCfg.CFARWrapAround
  87. thresholdDb := detCfg.ThresholdDb
  88. edgeMarginDb := detCfg.EdgeMarginDb
  89. maxSignalBwHz := detCfg.MaxSignalBwHz
  90. mergeGapHz := detCfg.MergeGapHz
  91. if minDur <= 0 {
  92. minDur = 250 * time.Millisecond
  93. }
  94. if hold <= 0 {
  95. hold = 500 * time.Millisecond
  96. }
  97. if emaAlpha <= 0 || emaAlpha > 1 {
  98. emaAlpha = 0.2
  99. }
  100. if hysteresis <= 0 {
  101. hysteresis = 3
  102. }
  103. if minStable <= 0 {
  104. minStable = 3
  105. }
  106. if gapTolerance <= 0 {
  107. gapTolerance = hold
  108. }
  109. if cfarScaleDb <= 0 {
  110. cfarScaleDb = 6
  111. }
  112. if edgeMarginDb <= 0 {
  113. edgeMarginDb = 3.0
  114. }
  115. if maxSignalBwHz <= 0 {
  116. maxSignalBwHz = 150000
  117. }
  118. if mergeGapHz <= 0 {
  119. mergeGapHz = 5000
  120. }
  121. if cfarRank <= 0 || cfarRank > 2*cfarTrain {
  122. cfarRank = int(math.Round(0.75 * float64(2*cfarTrain)))
  123. if cfarRank <= 0 {
  124. cfarRank = 1
  125. }
  126. }
  127. var cfarEngine cfar.CFAR
  128. if cfarMode != "" && cfarMode != "OFF" {
  129. cfarEngine = cfar.New(cfar.Config{
  130. Mode: cfar.Mode(cfarMode),
  131. GuardCells: cfarGuard,
  132. TrainCells: cfarTrain,
  133. Rank: cfarRank,
  134. ScaleDb: cfarScaleDb,
  135. WrapAround: cfarWrap,
  136. })
  137. }
  138. return &Detector{
  139. ThresholdDb: thresholdDb,
  140. MinDuration: minDur,
  141. Hold: hold,
  142. EmaAlpha: emaAlpha,
  143. HysteresisDb: hysteresis,
  144. MinStableFrames: minStable,
  145. GapTolerance: gapTolerance,
  146. CFARScaleDb: cfarScaleDb,
  147. EdgeMarginDb: edgeMarginDb,
  148. MaxSignalBwHz: maxSignalBwHz,
  149. MergeGapHz: mergeGapHz,
  150. binWidth: float64(sampleRate) / float64(fftSize),
  151. nbins: fftSize,
  152. sampleRate: sampleRate,
  153. ema: make([]float64, fftSize),
  154. active: map[int64]*activeEvent{},
  155. nextID: 1,
  156. cfarEngine: cfarEngine,
  157. }
  158. }
  159. func (d *Detector) Process(now time.Time, spectrum []float64, centerHz float64) ([]Event, []Signal) {
  160. signals := d.detectSignals(spectrum, centerHz)
  161. finished := d.matchSignals(now, signals)
  162. return finished, signals
  163. }
  164. func (d *Detector) LastThresholds() []float64 {
  165. if len(d.lastThresholds) == 0 {
  166. return nil
  167. }
  168. return append([]float64(nil), d.lastThresholds...)
  169. }
  170. func (d *Detector) LastNoiseFloor() float64 {
  171. return d.lastNoiseFloor
  172. }
  173. func (d *Detector) UpdateClasses(signals []Signal) {
  174. for _, s := range signals {
  175. for _, ev := range d.active {
  176. if overlapHz(s.CenterHz, s.BWHz, ev.centerHz, ev.bwHz) && math.Abs(s.CenterHz-ev.centerHz) < (s.BWHz+ev.bwHz)/2.0 {
  177. if s.Class != nil {
  178. if ev.class == nil || s.Class.Confidence >= ev.class.Confidence {
  179. ev.class = s.Class
  180. }
  181. }
  182. }
  183. }
  184. }
  185. }
  186. func (d *Detector) detectSignals(spectrum []float64, centerHz float64) []Signal {
  187. n := len(spectrum)
  188. if n == 0 {
  189. return nil
  190. }
  191. smooth := d.smoothSpectrum(spectrum)
  192. var thresholds []float64
  193. if d.cfarEngine != nil {
  194. thresholds = d.cfarEngine.Thresholds(smooth)
  195. }
  196. d.lastThresholds = append(d.lastThresholds[:0], thresholds...)
  197. noiseGlobal := median(smooth)
  198. d.lastNoiseFloor = noiseGlobal
  199. var signals []Signal
  200. in := false
  201. start := 0
  202. peak := -1e9
  203. peakBin := 0
  204. for i := 0; i < n; i++ {
  205. v := smooth[i]
  206. thresholdOn := d.ThresholdDb
  207. if thresholds != nil && !math.IsNaN(thresholds[i]) {
  208. thresholdOn = thresholds[i]
  209. }
  210. thresholdOff := thresholdOn - d.HysteresisDb
  211. if v >= thresholdOn {
  212. if !in {
  213. in = true
  214. start = i
  215. peak = v
  216. peakBin = i
  217. } else if v > peak {
  218. peak = v
  219. peakBin = i
  220. }
  221. } else if in && v < thresholdOff {
  222. noise := noiseGlobal
  223. if thresholds != nil && peakBin >= 0 && peakBin < len(thresholds) && !math.IsNaN(thresholds[peakBin]) {
  224. noise = thresholds[peakBin] - d.CFARScaleDb
  225. }
  226. signals = append(signals, d.makeSignal(start, i-1, peak, peakBin, noise, centerHz, smooth))
  227. in = false
  228. }
  229. }
  230. if in {
  231. noise := noiseGlobal
  232. if thresholds != nil && peakBin >= 0 && peakBin < len(thresholds) && !math.IsNaN(thresholds[peakBin]) {
  233. noise = thresholds[peakBin] - d.CFARScaleDb
  234. }
  235. signals = append(signals, d.makeSignal(start, n-1, peak, peakBin, noise, centerHz, smooth))
  236. }
  237. signals = d.expandSignalEdges(signals, smooth, noiseGlobal, centerHz)
  238. for i := range signals {
  239. centerBin := float64(signals[i].FirstBin+signals[i].LastBin) / 2.0
  240. signals[i].CenterHz = d.centerFreqForBin(centerBin, centerHz)
  241. signals[i].BWHz = float64(signals[i].LastBin-signals[i].FirstBin+1) * d.binWidth
  242. }
  243. return signals
  244. }
  245. func (d *Detector) expandSignalEdges(signals []Signal, smooth []float64, noiseFloor float64, centerHz float64) []Signal {
  246. n := len(smooth)
  247. if n == 0 || len(signals) == 0 {
  248. return signals
  249. }
  250. margin := d.EdgeMarginDb
  251. if margin <= 0 {
  252. margin = 3.0
  253. }
  254. maxExpansionBins := int(d.MaxSignalBwHz / d.binWidth)
  255. if maxExpansionBins < 10 {
  256. maxExpansionBins = 10
  257. }
  258. for i := range signals {
  259. seed := signals[i]
  260. peakDb := seed.PeakDb
  261. localNoise := noiseFloor
  262. leftProbe := seed.FirstBin - 50
  263. rightProbe := seed.LastBin + 50
  264. if leftProbe >= 0 && rightProbe < n {
  265. leftNoise := minInRange(smooth, maxInt(0, leftProbe), maxInt(0, seed.FirstBin-5))
  266. rightNoise := minInRange(smooth, minInt(n-1, seed.LastBin+5), minInt(n-1, rightProbe))
  267. localNoise = math.Min(leftNoise, rightNoise)
  268. }
  269. edgeThreshold := localNoise + margin
  270. newFirst := seed.FirstBin
  271. prevVal := smooth[newFirst]
  272. for j := 0; j < maxExpansionBins; j++ {
  273. next := newFirst - 1
  274. if next < 0 {
  275. break
  276. }
  277. val := smooth[next]
  278. if val <= edgeThreshold {
  279. break
  280. }
  281. if val > prevVal+1.0 && val < peakDb-6.0 {
  282. break
  283. }
  284. prevVal = val
  285. newFirst = next
  286. }
  287. newLast := seed.LastBin
  288. prevVal = smooth[newLast]
  289. for j := 0; j < maxExpansionBins; j++ {
  290. next := newLast + 1
  291. if next >= n {
  292. break
  293. }
  294. val := smooth[next]
  295. if val <= edgeThreshold {
  296. break
  297. }
  298. if val > prevVal+1.0 && val < peakDb-6.0 {
  299. break
  300. }
  301. prevVal = val
  302. newLast = next
  303. }
  304. signals[i].FirstBin = newFirst
  305. signals[i].LastBin = newLast
  306. centerBin := float64(newFirst+newLast) / 2.0
  307. signals[i].CenterHz = d.centerFreqForBin(centerBin, centerHz)
  308. signals[i].BWHz = float64(newLast-newFirst+1) * d.binWidth
  309. }
  310. signals = d.mergeOverlapping(signals, centerHz)
  311. return signals
  312. }
  313. func (d *Detector) mergeOverlapping(signals []Signal, centerHz float64) []Signal {
  314. if len(signals) <= 1 {
  315. return signals
  316. }
  317. gapBins := 0
  318. if d.MergeGapHz > 0 && d.binWidth > 0 {
  319. gapBins = int(math.Ceil(d.MergeGapHz / d.binWidth))
  320. }
  321. sort.Slice(signals, func(i, j int) bool {
  322. return signals[i].FirstBin < signals[j].FirstBin
  323. })
  324. merged := []Signal{signals[0]}
  325. for i := 1; i < len(signals); i++ {
  326. last := &merged[len(merged)-1]
  327. cur := signals[i]
  328. gap := cur.FirstBin - last.LastBin - 1
  329. if gap <= gapBins {
  330. if cur.LastBin > last.LastBin {
  331. last.LastBin = cur.LastBin
  332. }
  333. if cur.PeakDb > last.PeakDb {
  334. last.PeakDb = cur.PeakDb
  335. }
  336. if cur.SNRDb > last.SNRDb {
  337. last.SNRDb = cur.SNRDb
  338. }
  339. centerBin := float64(last.FirstBin+last.LastBin) / 2.0
  340. last.BWHz = float64(last.LastBin-last.FirstBin+1) * d.binWidth
  341. last.CenterHz = d.centerFreqForBin(centerBin, centerHz)
  342. if cur.NoiseDb < last.NoiseDb || last.NoiseDb == 0 {
  343. last.NoiseDb = cur.NoiseDb
  344. }
  345. } else {
  346. merged = append(merged, cur)
  347. }
  348. }
  349. return merged
  350. }
  351. func (d *Detector) centerFreqForBin(bin float64, centerHz float64) float64 {
  352. return centerHz + (bin-float64(d.nbins)/2.0)*d.binWidth
  353. }
  354. func minInRange(s []float64, from, to int) float64 {
  355. if len(s) == 0 {
  356. return 0
  357. }
  358. if from < 0 {
  359. from = 0
  360. }
  361. if to >= len(s) {
  362. to = len(s) - 1
  363. }
  364. if from > to {
  365. return s[minInt(maxInt(from, 0), len(s)-1)]
  366. }
  367. m := s[from]
  368. for i := from + 1; i <= to; i++ {
  369. if s[i] < m {
  370. m = s[i]
  371. }
  372. }
  373. return m
  374. }
  375. func maxInt(a, b int) int {
  376. if a > b {
  377. return a
  378. }
  379. return b
  380. }
  381. func minInt(a, b int) int {
  382. if a < b {
  383. return a
  384. }
  385. return b
  386. }
  387. func overlapHz(center1, bw1, center2, bw2 float64) bool {
  388. left1 := center1 - bw1/2
  389. right1 := center1 + bw1/2
  390. left2 := center2 - bw2/2
  391. right2 := center2 + bw2/2
  392. return left1 <= right2 && left2 <= right1
  393. }
  394. func median(vals []float64) float64 {
  395. if len(vals) == 0 {
  396. return 0
  397. }
  398. cp := append([]float64(nil), vals...)
  399. sort.Float64s(cp)
  400. mid := len(cp) / 2
  401. if len(cp)%2 == 0 {
  402. return (cp[mid-1] + cp[mid]) / 2
  403. }
  404. return cp[mid]
  405. }
  406. func (d *Detector) makeSignal(first, last int, peak float64, peakBin int, noise float64, centerHz float64, spectrum []float64) Signal {
  407. centerBin := float64(first+last) / 2.0
  408. centerFreq := centerHz + (centerBin-float64(d.nbins)/2.0)*d.binWidth
  409. bw := float64(last-first+1) * d.binWidth
  410. snr := peak - noise
  411. return Signal{
  412. FirstBin: first,
  413. LastBin: last,
  414. CenterHz: centerFreq,
  415. BWHz: bw,
  416. PeakDb: peak,
  417. SNRDb: snr,
  418. NoiseDb: noise,
  419. }
  420. }
  421. func (d *Detector) smoothSpectrum(spectrum []float64) []float64 {
  422. if d.ema == nil || len(d.ema) != len(spectrum) {
  423. d.ema = make([]float64, len(spectrum))
  424. copy(d.ema, spectrum)
  425. return d.ema
  426. }
  427. alpha := d.EmaAlpha
  428. for i := range spectrum {
  429. v := spectrum[i]
  430. d.ema[i] = alpha*v + (1-alpha)*d.ema[i]
  431. }
  432. return d.ema
  433. }
  434. func (d *Detector) matchSignals(now time.Time, signals []Signal) []Event {
  435. used := make(map[int64]bool, len(d.active))
  436. for _, s := range signals {
  437. var best *activeEvent
  438. var candidates []struct {
  439. ev *activeEvent
  440. dist float64
  441. }
  442. for _, ev := range d.active {
  443. if overlapHz(s.CenterHz, s.BWHz, ev.centerHz, ev.bwHz) && math.Abs(s.CenterHz-ev.centerHz) < (s.BWHz+ev.bwHz)/2.0 {
  444. candidates = append(candidates, struct {
  445. ev *activeEvent
  446. dist float64
  447. }{ev: ev, dist: math.Abs(s.CenterHz - ev.centerHz)})
  448. }
  449. }
  450. if len(candidates) > 0 {
  451. sort.Slice(candidates, func(i, j int) bool { return candidates[i].dist < candidates[j].dist })
  452. best = candidates[0].ev
  453. }
  454. if best == nil {
  455. id := d.nextID
  456. d.nextID++
  457. d.active[id] = &activeEvent{
  458. id: id,
  459. start: now,
  460. lastSeen: now,
  461. centerHz: s.CenterHz,
  462. bwHz: s.BWHz,
  463. peakDb: s.PeakDb,
  464. snrDb: s.SNRDb,
  465. firstBin: s.FirstBin,
  466. lastBin: s.LastBin,
  467. class: s.Class,
  468. stableHits: 1,
  469. }
  470. continue
  471. }
  472. used[best.id] = true
  473. best.lastSeen = now
  474. best.stableHits++
  475. best.centerHz = (best.centerHz + s.CenterHz) / 2.0
  476. if s.BWHz > best.bwHz {
  477. best.bwHz = s.BWHz
  478. }
  479. if s.PeakDb > best.peakDb {
  480. best.peakDb = s.PeakDb
  481. }
  482. if s.SNRDb > best.snrDb {
  483. best.snrDb = s.SNRDb
  484. }
  485. if s.FirstBin < best.firstBin {
  486. best.firstBin = s.FirstBin
  487. }
  488. if s.LastBin > best.lastBin {
  489. best.lastBin = s.LastBin
  490. }
  491. if s.Class != nil {
  492. if best.class == nil || s.Class.Confidence >= best.class.Confidence {
  493. best.class = s.Class
  494. }
  495. }
  496. }
  497. var finished []Event
  498. for id, ev := range d.active {
  499. if used[id] {
  500. continue
  501. }
  502. if now.Sub(ev.lastSeen) < d.GapTolerance {
  503. continue
  504. }
  505. duration := ev.lastSeen.Sub(ev.start)
  506. if duration < d.MinDuration || ev.stableHits < d.MinStableFrames {
  507. delete(d.active, id)
  508. continue
  509. }
  510. finished = append(finished, Event{
  511. ID: ev.id,
  512. Start: ev.start,
  513. End: ev.lastSeen,
  514. CenterHz: ev.centerHz,
  515. Bandwidth: ev.bwHz,
  516. PeakDb: ev.peakDb,
  517. SNRDb: ev.snrDb,
  518. FirstBin: ev.firstBin,
  519. LastBin: ev.lastBin,
  520. Class: ev.class,
  521. })
  522. delete(d.active, id)
  523. }
  524. return finished
  525. }