| 18 | } |
| 19 | |
| 20 | QVector<float> SpectrogramBuffer::extractPatch(double sigFreqMhz, |
| 21 | double freqWidthMhz) const |
| 22 | { |
| 23 | if (m_count < kMaxFrames) { return {}; } |
| 24 | |
| 25 | // Use 2× signal width as the extraction window so the patch captures |
| 26 | // enough spectral context for the CNN to detect boundary shape. |
| 27 | const double halfWin = std::max(freqWidthMhz, 0.003) * 1.0; // ± 1× width |
| 28 | |
| 29 | QVector<float> patch; |
| 30 | patch.resize(kMaxFrames * kPatchFreqBins, 0.0f); |
| 31 | |
| 32 | // Iterate frames oldest → newest |
| 33 | for (int fi = 0; fi < kMaxFrames; ++fi) { |
| 34 | const int idx = (m_head + fi) % kMaxFrames; // m_head is next-write, so oldest is m_head |
| 35 | const Frame& fr = m_frames[idx]; |
| 36 | if (fr.bins.isEmpty() || fr.bandwidthMhz <= 0.0) { continue; } |
| 37 | |
| 38 | const int N = fr.bins.size(); |
| 39 | const double startMhz = fr.centerMhz - fr.bandwidthMhz / 2.0; |
| 40 | const double hzPerBin = fr.bandwidthMhz * 1.0e6 / N; |
| 41 | |
| 42 | // Resample N input bins into kPatchFreqBins output bins |
| 43 | // covering [sigFreqMhz - halfWin, sigFreqMhz + halfWin]. |
| 44 | const double winStartMhz = sigFreqMhz - halfWin; |
| 45 | const double winEndMhz = sigFreqMhz + halfWin; |
| 46 | |
| 47 | for (int pb = 0; pb < kPatchFreqBins; ++pb) { |
| 48 | const double fMhz = winStartMhz + |
| 49 | (pb + 0.5) / kPatchFreqBins * (winEndMhz - winStartMhz); |
| 50 | const double srcBinF = (fMhz - startMhz) / fr.bandwidthMhz * N - 0.5; |
| 51 | const int b0 = static_cast<int>(std::floor(srcBinF)); |
| 52 | const int b1 = b0 + 1; |
| 53 | const float t = static_cast<float>(srcBinF - b0); |
| 54 | float val = 0.0f; |
| 55 | if (b0 >= 0 && b1 < N) { |
| 56 | val = fr.bins[b0] * (1.0f - t) + fr.bins[b1] * t; |
| 57 | } else if (b0 >= 0 && b0 < N) { |
| 58 | val = fr.bins[b0]; |
| 59 | } else if (b1 >= 0 && b1 < N) { |
| 60 | val = fr.bins[b1]; |
| 61 | } |
| 62 | patch[fi * kPatchFreqBins + pb] = val; |
| 63 | } |
| 64 | } |
| 65 | |
| 66 | // Normalize to [0, 1] using the patch min/max |
| 67 | float minV = patch[0], maxV = patch[0]; |
| 68 | for (float v : patch) { |
| 69 | minV = std::min(minV, v); |
| 70 | maxV = std::max(maxV, v); |
| 71 | } |
| 72 | const float range = maxV - minV; |
| 73 | if (range > 0.5f) { |
| 74 | for (float& v : patch) { v = (v - minV) / range; } |
| 75 | } else { |
| 76 | // Flat patch — no signal content; zero it so CNN returns ~0.5 |
| 77 | patch.fill(0.0f); |
no test coverage detected