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

381 lines
9.6KB

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