Wideband autonomous SDR analysis engine forked from sdr-visual-suite
25개 이상의 토픽을 선택하실 수 없습니다. Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

349 lines
8.7KB

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