Wideband autonomous SDR analysis engine forked from sdr-visual-suite
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

378 行
9.3KB

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