Wideband autonomous SDR analysis engine forked from sdr-visual-suite
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

537 lignes
13KB

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