Przeglądaj źródła

feat: improve classifier confidence and GPU RDS path

master
Jan Svabenik 2 dni temu
rodzic
commit
5b2eafc11a
4 zmienionych plików z 83 dodań i 14 usunięć
  1. +19
    -0
      internal/classifier/classifier_test.go
  2. +31
    -7
      internal/classifier/rules.go
  3. +2
    -2
      internal/recorder/demod.go
  4. +31
    -5
      internal/recorder/wfm_hybrid.go

+ 19
- 0
internal/classifier/classifier_test.go Wyświetl plik

@@ -19,3 +19,22 @@ func TestRuleClassifyWFM(t *testing.T) {
t.Fatalf("expected WFM, got %+v", cls)
}
}

func TestSoftmaxConfidence(t *testing.T) {
scores1 := map[SignalClass]float64{ClassNFM: 2.0, ClassAM: 0.3, ClassNoise: 0.1}
c1 := softmaxConfidence(scores1, ClassNFM)
if c1 < 0.7 {
t.Fatalf("clear winner should have high confidence: %f", c1)
}

scores2 := map[SignalClass]float64{ClassSSBUSB: 1.0, ClassSSBLSB: 0.9, ClassAM: 0.8}
c2 := softmaxConfidence(scores2, ClassSSBUSB)
if c2 > 0.5 {
t.Fatalf("ambiguous should have low confidence: %f", c2)
}

c3 := softmaxConfidence(map[SignalClass]float64{}, ClassNFM)
if c3 != 0.1 {
t.Fatalf("empty should return 0.1: %f", c3)
}
}

+ 31
- 7
internal/classifier/rules.go Wyświetl plik

@@ -69,7 +69,7 @@ func RuleClassify(feat Features) Classification {
add(ClassDMR, 0.7)
}

best, bestScore, second, secondScore := top2(scores)
best, _, second, _ := top2(scores)
if best == "" {
best = ClassUnknown
}
@@ -77,11 +77,7 @@ func RuleClassify(feat Features) Classification {
second = ClassUnknown
}

conf := 0.3
if best != ClassUnknown {
sum := bestScore + secondScore + 1e-6
conf = 0.3 + 0.7*(bestScore/sum)
}
conf := softmaxConfidence(scores, best)
if best == ClassNFM || best == ClassWFM {
conf = conf * (0.8 + 0.2*clamp01(1-flat))
}
@@ -89,7 +85,7 @@ func RuleClassify(feat Features) Classification {
conf = conf * (0.7 + 0.3*clamp01(p2a/6.0))
}
if math.IsNaN(conf) || conf <= 0 {
conf = 0.3
conf = 0.1
}

if (best == ClassSSBUSB || best == ClassSSBLSB) && second == ClassUnknown {
@@ -110,6 +106,34 @@ func RuleClassify(feat Features) Classification {
}
}

func softmaxConfidence(scores map[SignalClass]float64, best SignalClass) float64 {
if len(scores) == 0 || best == "" || best == ClassUnknown {
return 0.1
}
maxScore := math.Inf(-1)
for _, v := range scores {
if v > maxScore {
maxScore = v
}
}
if math.IsInf(maxScore, -1) {
return 0.1
}
var expSum float64
var expBest float64
for k, v := range scores {
e := math.Exp(v - maxScore)
expSum += e
if k == best {
expBest = e
}
}
if expSum <= 0 {
return 0.1
}
return expBest / expSum
}

func top2(scores map[SignalClass]float64) (SignalClass, float64, SignalClass, float64) {
var best, second SignalClass
bestScore := 0.0


+ 2
- 2
internal/recorder/demod.go Wyświetl plik

@@ -39,8 +39,8 @@ func (m *Manager) demodAndWrite(dir string, ev detector.Event, iq []complex64, f
var stereoHybrid *wfmHybridResult
if audio == nil {
if name == "WFM_STEREO" {
log.Printf("gpudemod: WFM_STEREO using CPU stereo/RDS post-process for event %d", ev.ID)
res := demodWFMStereoHybrid(iq, m.sampleRate, offset, bw)
log.Printf("gpudemod: WFM_STEREO using hybrid stereo/RDS post-process for event %d", ev.ID)
res := demodWFMStereoHybrid(m.gpuEngine(), iq, m.sampleRate, offset, bw)
stereoHybrid = &res
audio = res.Audio
inputRate = res.AudioRate


+ 31
- 5
internal/recorder/wfm_hybrid.go Wyświetl plik

@@ -1,6 +1,11 @@
package recorder

import "sdr-visual-suite/internal/demod"
import (
"log"

"sdr-visual-suite/internal/demod"
"sdr-visual-suite/internal/demod/gpudemod"
)

type wfmHybridResult struct {
Audio []float32
@@ -10,14 +15,35 @@ type wfmHybridResult struct {
RDSRate int
}

func demodWFMStereoHybrid(iq []complex64, sampleRate int, offset float64, bw float64) wfmHybridResult {
func demodWFMStereoHybrid(gpu *gpudemod.Engine, iq []complex64, sampleRate int, offset float64, bw float64) wfmHybridResult {
audio, rate := demodAudioCPU(demod.Get("WFM_STEREO"), iq, sampleRate, offset, bw)
rds := demod.RDSBasebandDecimated(iq, sampleRate)

var rdsSamples []float32
var rdsRate int
if gpu != nil {
rdsIQ, gpuRate, err := gpu.ShiftFilterDecimate(iq, 57000, 4800, 4800)
if err == nil && len(rdsIQ) > 0 {
rdsSamples = make([]float32, len(rdsIQ))
for i, v := range rdsIQ {
rdsSamples[i] = real(v)
}
rdsRate = gpuRate
log.Printf("gpudemod: GPU RDS extraction used (%d samples at %d Hz)", len(rdsSamples), rdsRate)
} else if err != nil {
log.Printf("gpudemod: GPU RDS extraction failed: %v - CPU fallback", err)
}
}
if rdsSamples == nil {
rds := demod.RDSBasebandDecimated(iq, sampleRate)
rdsSamples = rds.Samples
rdsRate = rds.SampleRate
}

return wfmHybridResult{
Audio: audio,
AudioRate: rate,
Channels: 2,
RDS: rds.Samples,
RDSRate: rds.SampleRate,
RDS: rdsSamples,
RDSRate: rdsRate,
}
}

Ładowanie…
Anuluj
Zapisz