implemented reference subtraction from B_scan. Reference is average from all visible B-scan.
This commit is contained in:
@ -198,12 +198,19 @@ def _prepare_fft_segment(
|
||||
return resampled, take_fft
|
||||
|
||||
|
||||
def _compute_fft_row(
|
||||
def _fft_mag_to_db(mag: np.ndarray) -> np.ndarray:
|
||||
"""Перевод модуля спектра в дБ с отсечкой отрицательных значений после вычитания фона."""
|
||||
mag_arr = np.asarray(mag, dtype=np.float32)
|
||||
safe_mag = np.maximum(mag_arr, 0.0)
|
||||
return (20.0 * np.log10(safe_mag + 1e-9)).astype(np.float32, copy=False)
|
||||
|
||||
|
||||
def _compute_fft_mag_row(
|
||||
sweep: np.ndarray,
|
||||
freqs: Optional[np.ndarray],
|
||||
bins: int,
|
||||
) -> np.ndarray:
|
||||
"""Посчитать FFT-строку, используя калиброванную частотную ось при подготовке входа."""
|
||||
"""Посчитать линейный модуль FFT-строки, используя калиброванную частотную ось."""
|
||||
if bins <= 0:
|
||||
return np.zeros((0,), dtype=np.float32)
|
||||
|
||||
@ -217,10 +224,18 @@ def _compute_fft_row(
|
||||
fft_in[:take_fft] = fft_seg * win
|
||||
spec = np.fft.ifft(fft_in)
|
||||
mag = np.abs(spec).astype(np.float32)
|
||||
fft_row = 20.0 * np.log10(mag + 1e-9)
|
||||
if fft_row.shape[0] != bins:
|
||||
fft_row = fft_row[:bins]
|
||||
return fft_row
|
||||
if mag.shape[0] != bins:
|
||||
mag = mag[:bins]
|
||||
return mag
|
||||
|
||||
|
||||
def _compute_fft_row(
|
||||
sweep: np.ndarray,
|
||||
freqs: Optional[np.ndarray],
|
||||
bins: int,
|
||||
) -> np.ndarray:
|
||||
"""Посчитать FFT-строку в дБ."""
|
||||
return _fft_mag_to_db(_compute_fft_mag_row(sweep, freqs, bins))
|
||||
|
||||
|
||||
def _compute_distance_axis(freqs: Optional[np.ndarray], bins: int) -> np.ndarray:
|
||||
@ -1080,8 +1095,8 @@ def main():
|
||||
parser.add_argument(
|
||||
"--backend",
|
||||
choices=["auto", "pg", "mpl"],
|
||||
default="auto",
|
||||
help="Графический бэкенд: pyqtgraph (pg) — быстрее; matplotlib (mpl) — совместимый. По умолчанию auto",
|
||||
default="pg",
|
||||
help="Графический бэкенд: pyqtgraph (pg) — быстрее; matplotlib (mpl) — совместимый. По умолчанию pg",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--norm-type",
|
||||
@ -1119,14 +1134,18 @@ def main():
|
||||
args = parser.parse_args()
|
||||
|
||||
# Попробуем быстрый бэкенд (pyqtgraph) при auto/pg
|
||||
if args.backend in ("pg"):
|
||||
if args.backend == "pg":
|
||||
try:
|
||||
return run_pyqtgraph(args)
|
||||
except Exception as e:
|
||||
if args.backend == "pg":
|
||||
sys.stderr.write(f"[error] PyQtGraph бэкенд недоступен: {e}\n")
|
||||
sys.exit(1)
|
||||
# При auto — тихо откатываемся на matplotlib
|
||||
sys.stderr.write(f"[error] PyQtGraph бэкенд недоступен: {e}\n")
|
||||
sys.exit(1)
|
||||
|
||||
if args.backend == "auto":
|
||||
try:
|
||||
return run_pyqtgraph(args)
|
||||
except Exception:
|
||||
pass # При auto — тихо откатываемся на matplotlib
|
||||
|
||||
try:
|
||||
import matplotlib
|
||||
@ -1186,6 +1205,7 @@ def main():
|
||||
ymax_slider = None
|
||||
contrast_slider = None
|
||||
calib_enabled = False
|
||||
bg_subtract_enabled = False
|
||||
peak_calibrate_mode = bool(getattr(args, "calibrate", False))
|
||||
current_peak_width: Optional[float] = None
|
||||
norm_type = str(getattr(args, "norm_type", "projector")).strip().lower()
|
||||
@ -1283,12 +1303,15 @@ def main():
|
||||
def _normalize_sweep(raw: np.ndarray, calib: np.ndarray) -> np.ndarray:
|
||||
return _normalize_by_calib(raw, calib, norm_type=norm_type)
|
||||
|
||||
def _set_calib_enabled():
|
||||
nonlocal calib_enabled, current_sweep_norm
|
||||
def _sync_checkbox_states():
|
||||
nonlocal calib_enabled, bg_subtract_enabled, current_sweep_norm
|
||||
try:
|
||||
calib_enabled = bool(cb.get_status()[0]) if cb is not None else False
|
||||
states = cb.get_status() if cb is not None else ()
|
||||
calib_enabled = bool(states[0]) if len(states) > 0 else False
|
||||
bg_subtract_enabled = bool(states[1]) if len(states) > 1 else False
|
||||
except Exception:
|
||||
calib_enabled = False
|
||||
bg_subtract_enabled = False
|
||||
if calib_enabled and current_sweep_raw is not None and last_calib_sweep is not None:
|
||||
current_sweep_norm = _normalize_sweep(current_sweep_raw, last_calib_sweep)
|
||||
else:
|
||||
@ -1299,11 +1322,11 @@ def main():
|
||||
ax_smin = fig.add_axes([0.92, 0.55, 0.02, 0.35])
|
||||
ax_smax = fig.add_axes([0.95, 0.55, 0.02, 0.35])
|
||||
ax_sctr = fig.add_axes([0.98, 0.55, 0.02, 0.35])
|
||||
ax_cb = fig.add_axes([0.92, 0.45, 0.08, 0.08])
|
||||
ax_cb = fig.add_axes([0.90, 0.40, 0.10, 0.14])
|
||||
ymin_slider = Slider(ax_smin, "R min", 0.0, 1.0, valinit=0.0, orientation="vertical")
|
||||
ymax_slider = Slider(ax_smax, "R max", 0.0, 1.0, valinit=1.0, orientation="vertical")
|
||||
contrast_slider = Slider(ax_sctr, "Int max", 0, 100, valinit=100, valstep=1, orientation="vertical")
|
||||
cb = CheckButtons(ax_cb, ["калибровка"], [False])
|
||||
cb = CheckButtons(ax_cb, ["нормировка", "вычет фона"], [False, False])
|
||||
|
||||
def _on_ylim_change(_val):
|
||||
try:
|
||||
@ -1318,7 +1341,7 @@ def main():
|
||||
ymax_slider.on_changed(_on_ylim_change)
|
||||
# Контраст влияет на верхнюю границу цветовой шкалы (процент от авто-диапазона)
|
||||
contrast_slider.on_changed(lambda _v: fig.canvas.draw_idle())
|
||||
cb.on_clicked(lambda _v: _set_calib_enabled())
|
||||
cb.on_clicked(lambda _v: _sync_checkbox_states())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@ -1449,11 +1472,12 @@ def main():
|
||||
if ring_time is not None:
|
||||
ring_time[head] = time.time()
|
||||
head = (head + 1) % ring.shape[0]
|
||||
# FFT строка (дБ)
|
||||
# FFT строка (линейный модуль; перевод в дБ делаем при отображении)
|
||||
if ring_fft is not None:
|
||||
bins = ring_fft.shape[1]
|
||||
fft_row = _compute_fft_row(s, freqs, bins)
|
||||
ring_fft[(head - 1) % ring_fft.shape[0], :] = fft_row
|
||||
fft_mag = _compute_fft_mag_row(s, freqs, bins)
|
||||
ring_fft[(head - 1) % ring_fft.shape[0], :] = fft_mag
|
||||
fft_row = _fft_mag_to_db(fft_mag)
|
||||
# Экстремумы для цветовой шкалы
|
||||
fr_min = np.nanmin(fft_row)
|
||||
fr_max = np.nanmax(fft_row)
|
||||
@ -1519,7 +1543,7 @@ def main():
|
||||
return base_t
|
||||
|
||||
def _subtract_recent_mean_fft(disp_fft: np.ndarray) -> np.ndarray:
|
||||
"""Вычесть среднее по каждой частоте за последние spec_mean_sec секунд."""
|
||||
"""Вычесть среднее по каждой дальности за последние spec_mean_sec секунд в линейной области."""
|
||||
if spec_mean_sec <= 0.0:
|
||||
return disp_fft
|
||||
disp_times = make_display_times()
|
||||
@ -1536,6 +1560,33 @@ def main():
|
||||
mean_spec = np.nan_to_num(mean_spec, nan=0.0)
|
||||
return disp_fft - mean_spec[:, None]
|
||||
|
||||
def _visible_bg_fft(disp_fft: np.ndarray) -> Optional[np.ndarray]:
|
||||
"""Оценка фона по медиане в текущем видимом по времени окне B-scan."""
|
||||
if not bg_subtract_enabled or disp_fft.size == 0:
|
||||
return None
|
||||
ny, nx = disp_fft.shape
|
||||
if ny <= 0 or nx <= 0:
|
||||
return None
|
||||
try:
|
||||
x0, x1 = ax_spec.get_xlim()
|
||||
except Exception:
|
||||
x0, x1 = 0.0, float(nx - 1)
|
||||
xmin, xmax = sorted((float(x0), float(x1)))
|
||||
ix0 = max(0, min(nx - 1, int(np.floor(xmin))))
|
||||
ix1 = max(0, min(nx - 1, int(np.ceil(xmax))))
|
||||
if ix1 < ix0:
|
||||
ix1 = ix0
|
||||
window = disp_fft[:, ix0 : ix1 + 1]
|
||||
if window.size == 0:
|
||||
return None
|
||||
try:
|
||||
bg_spec = np.nanmedian(window, axis=1)
|
||||
except Exception:
|
||||
return None
|
||||
if not np.any(np.isfinite(bg_spec)):
|
||||
return None
|
||||
return np.nan_to_num(bg_spec, nan=0.0).astype(np.float32, copy=False)
|
||||
|
||||
def make_display_ring_fft():
|
||||
if ring_fft is None:
|
||||
return np.zeros((1, 1), dtype=np.float32)
|
||||
@ -1658,39 +1709,61 @@ def main():
|
||||
|
||||
# Обновление водопада спектров
|
||||
if changed and ring_fft is not None:
|
||||
disp_fft = make_display_ring_fft()
|
||||
disp_fft = _subtract_recent_mean_fft(disp_fft)
|
||||
disp_fft_lin = make_display_ring_fft()
|
||||
disp_fft_lin = _subtract_recent_mean_fft(disp_fft_lin)
|
||||
bg_spec = _visible_bg_fft(disp_fft_lin)
|
||||
if bg_spec is not None:
|
||||
num = np.maximum(disp_fft_lin, 0.0).astype(np.float32, copy=False) + 1e-9
|
||||
den = bg_spec[:, None] + 1e-9
|
||||
disp_fft = (20.0 * np.log10(num / den)).astype(np.float32, copy=False)
|
||||
else:
|
||||
disp_fft = _fft_mag_to_db(disp_fft_lin)
|
||||
# Новые данные справа: без реверса
|
||||
img_fft_obj.set_data(disp_fft)
|
||||
# Подписи времени не обновляем динамически (оставляем авто-тики)
|
||||
# Автодиапазон по среднему спектру за видимый интервал (как в хорошей версии)
|
||||
try:
|
||||
# disp_fft имеет форму (bins, time); берём среднее по времени
|
||||
mean_spec = np.nanmean(disp_fft, axis=1)
|
||||
vmin_v = float(np.nanmin(mean_spec))
|
||||
vmax_v = float(np.nanmax(mean_spec))
|
||||
except Exception:
|
||||
vmin_v = vmax_v = None
|
||||
# Если средние не дают валидный диапазон — используем процентильную обрезку (если задана)
|
||||
if (vmin_v is None or not np.isfinite(vmin_v)) or (vmax_v is None or not np.isfinite(vmax_v)) or vmin_v == vmax_v:
|
||||
if spec_clip is not None:
|
||||
try:
|
||||
vmin_v = float(np.nanpercentile(disp_fft, spec_clip[0]))
|
||||
vmax_v = float(np.nanpercentile(disp_fft, spec_clip[1]))
|
||||
except Exception:
|
||||
vmin_v = vmax_v = None
|
||||
# Фолбэк к отслеживаемым минимум/максимумам
|
||||
if (vmin_v is None or not np.isfinite(vmin_v)) or (vmax_v is None or not np.isfinite(vmax_v)) or vmin_v == vmax_v:
|
||||
if y_min_fft is not None and y_max_fft is not None and np.isfinite(y_min_fft) and np.isfinite(y_max_fft) and y_min_fft != y_max_fft:
|
||||
vmin_v, vmax_v = y_min_fft, y_max_fft
|
||||
if vmin_v is not None and vmax_v is not None and vmin_v != vmax_v:
|
||||
# Применим скалирование контрастом (верхняя граница)
|
||||
if bg_spec is not None:
|
||||
try:
|
||||
c = float(contrast_slider.val) / 100.0 if contrast_slider is not None else 1.0
|
||||
p5 = float(np.nanpercentile(disp_fft, 5))
|
||||
p95 = float(np.nanpercentile(disp_fft, 95))
|
||||
span = max(abs(p5), abs(p95))
|
||||
except Exception:
|
||||
c = 1.0
|
||||
vmax_eff = vmin_v + c * (vmax_v - vmin_v)
|
||||
img_fft_obj.set_clim(vmin=vmin_v, vmax=vmax_eff)
|
||||
span = float("nan")
|
||||
if np.isfinite(span) and span > 0.0:
|
||||
try:
|
||||
c = float(contrast_slider.val) / 100.0 if contrast_slider is not None else 1.0
|
||||
except Exception:
|
||||
c = 1.0
|
||||
span_eff = max(span * c, 1e-6)
|
||||
img_fft_obj.set_clim(vmin=-span_eff, vmax=span_eff)
|
||||
else:
|
||||
try:
|
||||
# disp_fft имеет форму (bins, time); берём среднее по времени
|
||||
mean_spec = np.nanmean(disp_fft, axis=1)
|
||||
vmin_v = float(np.nanmin(mean_spec))
|
||||
vmax_v = float(np.nanmax(mean_spec))
|
||||
except Exception:
|
||||
vmin_v = vmax_v = None
|
||||
# Если средние не дают валидный диапазон — используем процентильную обрезку (если задана)
|
||||
if (vmin_v is None or not np.isfinite(vmin_v)) or (vmax_v is None or not np.isfinite(vmax_v)) or vmin_v == vmax_v:
|
||||
if spec_clip is not None:
|
||||
try:
|
||||
vmin_v = float(np.nanpercentile(disp_fft, spec_clip[0]))
|
||||
vmax_v = float(np.nanpercentile(disp_fft, spec_clip[1]))
|
||||
except Exception:
|
||||
vmin_v = vmax_v = None
|
||||
# Фолбэк к отслеживаемым минимум/максимумам
|
||||
if (vmin_v is None or not np.isfinite(vmin_v)) or (vmax_v is None or not np.isfinite(vmax_v)) or vmin_v == vmax_v:
|
||||
if y_min_fft is not None and y_max_fft is not None and np.isfinite(y_min_fft) and np.isfinite(y_max_fft) and y_min_fft != y_max_fft:
|
||||
vmin_v, vmax_v = y_min_fft, y_max_fft
|
||||
if vmin_v is not None and vmax_v is not None and vmin_v != vmax_v:
|
||||
# Применим скалирование контрастом (верхняя граница)
|
||||
try:
|
||||
c = float(contrast_slider.val) / 100.0 if contrast_slider is not None else 1.0
|
||||
except Exception:
|
||||
c = 1.0
|
||||
vmax_eff = vmin_v + c * (vmax_v - vmin_v)
|
||||
img_fft_obj.set_clim(vmin=vmin_v, vmax=vmax_eff)
|
||||
|
||||
if changed and current_info:
|
||||
status_payload = dict(current_info)
|
||||
@ -1776,16 +1849,11 @@ def run_pyqtgraph(args):
|
||||
peak_calibrate_mode = bool(getattr(args, "calibrate", False))
|
||||
try:
|
||||
import pyqtgraph as pg
|
||||
from PyQt5 import QtCore, QtWidgets # noqa: F401
|
||||
except Exception:
|
||||
# Возможно установлена PySide6
|
||||
try:
|
||||
import pyqtgraph as pg
|
||||
from PySide6 import QtCore, QtWidgets # noqa: F401
|
||||
except Exception as e:
|
||||
raise RuntimeError(
|
||||
"pyqtgraph/PyQt5(Pyside6) не найдены. Установите: pip install pyqtgraph PyQt5"
|
||||
) from e
|
||||
from pyqtgraph.Qt import QtCore, QtWidgets # type: ignore
|
||||
except Exception as e:
|
||||
raise RuntimeError(
|
||||
"pyqtgraph и совместимый Qt backend не найдены. Установите: pip install pyqtgraph PyQt5"
|
||||
) from e
|
||||
|
||||
# Очередь завершённых свипов и поток чтения
|
||||
q: Queue[SweepPacket] = Queue(maxsize=1000)
|
||||
@ -1823,8 +1891,25 @@ def run_pyqtgraph(args):
|
||||
app.setQuitOnLastWindowClosed(True)
|
||||
except Exception:
|
||||
pass
|
||||
win = pg.GraphicsLayoutWidget(show=True, title=args.title)
|
||||
win.resize(1200, 600)
|
||||
main_window = QtWidgets.QWidget()
|
||||
try:
|
||||
main_window.setWindowTitle(str(args.title))
|
||||
except Exception:
|
||||
pass
|
||||
main_layout = QtWidgets.QHBoxLayout(main_window)
|
||||
main_layout.setContentsMargins(6, 6, 6, 6)
|
||||
main_layout.setSpacing(6)
|
||||
win = pg.GraphicsLayoutWidget(show=False, title=args.title)
|
||||
main_layout.addWidget(win)
|
||||
settings_widget = QtWidgets.QWidget()
|
||||
settings_layout = QtWidgets.QVBoxLayout(settings_widget)
|
||||
settings_layout.setContentsMargins(6, 6, 6, 6)
|
||||
settings_layout.setSpacing(8)
|
||||
try:
|
||||
settings_widget.setMinimumWidth(170)
|
||||
except Exception:
|
||||
pass
|
||||
main_layout.addWidget(settings_widget)
|
||||
|
||||
# Плот последнего свипа (слева-сверху)
|
||||
p_line = win.addPlot(row=0, col=0, title="Сырые данные")
|
||||
@ -1889,20 +1974,17 @@ def run_pyqtgraph(args):
|
||||
spec_left_line.setVisible(False)
|
||||
spec_right_line.setVisible(False)
|
||||
|
||||
# Отдельное окно контролов: GraphicsLayoutWidget не принимает обычные QWidget через addItem.
|
||||
calib_cb = QtWidgets.QCheckBox("калибровка")
|
||||
control_window = None
|
||||
control_layout = None
|
||||
if peak_calibrate_mode:
|
||||
control_window = QtWidgets.QWidget()
|
||||
try:
|
||||
control_window.setWindowTitle(f"{args.title} controls")
|
||||
except Exception:
|
||||
pass
|
||||
control_layout = QtWidgets.QVBoxLayout(control_window)
|
||||
control_layout.setContentsMargins(8, 8, 8, 8)
|
||||
control_layout.setSpacing(6)
|
||||
control_layout.addWidget(calib_cb)
|
||||
# Правая панель настроек внутри основного окна.
|
||||
calib_cb = QtWidgets.QCheckBox("нормировка")
|
||||
bg_subtract_cb = QtWidgets.QCheckBox("вычет фона")
|
||||
try:
|
||||
settings_title = QtWidgets.QLabel("Настройки")
|
||||
settings_layout.addWidget(settings_title)
|
||||
except Exception:
|
||||
pass
|
||||
settings_layout.addWidget(calib_cb)
|
||||
settings_layout.addWidget(bg_subtract_cb)
|
||||
calib_window = None
|
||||
|
||||
# Статусная строка (внизу окна)
|
||||
status = pg.LabelItem(justify="left")
|
||||
@ -1931,6 +2013,7 @@ def run_pyqtgraph(args):
|
||||
spec_clip = _parse_spec_clip(getattr(args, "spec_clip", None))
|
||||
spec_mean_sec = float(getattr(args, "spec_mean_sec", 0.0))
|
||||
calib_enabled = False
|
||||
bg_subtract_enabled = False
|
||||
current_peak_width: Optional[float] = None
|
||||
norm_type = str(getattr(args, "norm_type", "projector")).strip().lower()
|
||||
c_edits = []
|
||||
@ -1959,16 +2042,31 @@ def run_pyqtgraph(args):
|
||||
else:
|
||||
current_sweep_norm = None
|
||||
|
||||
def _set_bg_subtract_enabled():
|
||||
nonlocal bg_subtract_enabled
|
||||
try:
|
||||
bg_subtract_enabled = bool(bg_subtract_cb.isChecked())
|
||||
except Exception:
|
||||
bg_subtract_enabled = False
|
||||
|
||||
try:
|
||||
calib_cb.stateChanged.connect(lambda _v: _set_calib_enabled())
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
bg_subtract_cb.stateChanged.connect(lambda _v: _set_bg_subtract_enabled())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if peak_calibrate_mode:
|
||||
try:
|
||||
c_widget = QtWidgets.QWidget()
|
||||
c_layout = QtWidgets.QFormLayout(c_widget)
|
||||
c_layout.setContentsMargins(0, 0, 0, 0)
|
||||
calib_window = QtWidgets.QWidget()
|
||||
try:
|
||||
calib_window.setWindowTitle(f"{args.title} freq calibration")
|
||||
except Exception:
|
||||
pass
|
||||
calib_layout = QtWidgets.QFormLayout(calib_window)
|
||||
calib_layout.setContentsMargins(8, 8, 8, 8)
|
||||
|
||||
def _apply_c_value(idx: int, edit):
|
||||
global CALIBRATION_C
|
||||
@ -1990,17 +2088,18 @@ def run_pyqtgraph(args):
|
||||
edit.editingFinished.connect(lambda i=idx, e=edit: _apply_c_value(i, e))
|
||||
except Exception:
|
||||
pass
|
||||
c_layout.addRow(f"C{idx}", edit)
|
||||
calib_layout.addRow(f"C{idx}", edit)
|
||||
c_edits.append(edit)
|
||||
if control_layout is not None:
|
||||
control_layout.addWidget(c_widget)
|
||||
except Exception:
|
||||
pass
|
||||
if control_window is not None:
|
||||
try:
|
||||
control_window.show()
|
||||
try:
|
||||
calib_window.show()
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
settings_layout.addStretch(1)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def ensure_buffer(_w: int):
|
||||
nonlocal ring, ring_time, head, width, x_shared, ring_fft, distance_shared
|
||||
@ -2076,6 +2175,33 @@ def run_pyqtgraph(args):
|
||||
return None
|
||||
return (vmin, vmax)
|
||||
|
||||
def _visible_bg_fft(disp_fft: np.ndarray) -> Optional[np.ndarray]:
|
||||
"""Оценка фона по медиане в текущем видимом по времени окне B-scan."""
|
||||
if not bg_subtract_enabled or disp_fft.size == 0:
|
||||
return None
|
||||
ny, nx = disp_fft.shape
|
||||
if ny <= 0 or nx <= 0:
|
||||
return None
|
||||
try:
|
||||
(x0, x1), _ = p_spec.viewRange()
|
||||
except Exception:
|
||||
x0, x1 = 0.0, float(nx - 1)
|
||||
xmin, xmax = sorted((float(x0), float(x1)))
|
||||
ix0 = max(0, min(nx - 1, int(np.floor(xmin))))
|
||||
ix1 = max(0, min(nx - 1, int(np.ceil(xmax))))
|
||||
if ix1 < ix0:
|
||||
ix1 = ix0
|
||||
window = disp_fft[:, ix0 : ix1 + 1]
|
||||
if window.size == 0:
|
||||
return None
|
||||
try:
|
||||
bg_spec = np.nanmedian(window, axis=1)
|
||||
except Exception:
|
||||
return None
|
||||
if not np.any(np.isfinite(bg_spec)):
|
||||
return None
|
||||
return np.nan_to_num(bg_spec, nan=0.0).astype(np.float32, copy=False)
|
||||
|
||||
def push_sweep(s: np.ndarray, freqs: Optional[np.ndarray] = None):
|
||||
nonlocal ring, ring_time, head, ring_fft, y_min_fft, y_max_fft
|
||||
if s is None or s.size == 0 or ring is None:
|
||||
@ -2088,11 +2214,12 @@ def run_pyqtgraph(args):
|
||||
if ring_time is not None:
|
||||
ring_time[head] = time.time()
|
||||
head = (head + 1) % ring.shape[0]
|
||||
# FFT строка (дБ)
|
||||
# FFT строка (линейный модуль; перевод в дБ делаем при отображении)
|
||||
if ring_fft is not None:
|
||||
bins = ring_fft.shape[1]
|
||||
fft_row = _compute_fft_row(s, freqs, bins)
|
||||
ring_fft[(head - 1) % ring_fft.shape[0], :] = fft_row
|
||||
fft_mag = _compute_fft_mag_row(s, freqs, bins)
|
||||
ring_fft[(head - 1) % ring_fft.shape[0], :] = fft_mag
|
||||
fft_row = _fft_mag_to_db(fft_mag)
|
||||
fr_min = np.nanmin(fft_row)
|
||||
fr_max = np.nanmax(fft_row)
|
||||
if y_min_fft is None or (not np.isnan(fr_min) and fr_min < y_min_fft):
|
||||
@ -2272,41 +2399,58 @@ def run_pyqtgraph(args):
|
||||
pass
|
||||
|
||||
if changed and ring_fft is not None:
|
||||
disp_fft = ring_fft if head == 0 else np.roll(ring_fft, -head, axis=0)
|
||||
disp_fft = disp_fft.T
|
||||
disp_fft_lin = ring_fft if head == 0 else np.roll(ring_fft, -head, axis=0)
|
||||
disp_fft_lin = disp_fft_lin.T
|
||||
if spec_mean_sec > 0.0 and ring_time is not None:
|
||||
disp_times = ring_time if head == 0 else np.roll(ring_time, -head)
|
||||
now_t = time.time()
|
||||
mask = np.isfinite(disp_times) & (disp_times >= (now_t - spec_mean_sec))
|
||||
if np.any(mask):
|
||||
try:
|
||||
mean_spec = np.nanmean(disp_fft[:, mask], axis=1)
|
||||
mean_spec = np.nanmean(disp_fft_lin[:, mask], axis=1)
|
||||
mean_spec = np.nan_to_num(mean_spec, nan=0.0)
|
||||
disp_fft = disp_fft - mean_spec[:, None]
|
||||
disp_fft_lin = disp_fft_lin - mean_spec[:, None]
|
||||
except Exception:
|
||||
pass
|
||||
bg_spec = _visible_bg_fft(disp_fft_lin)
|
||||
if bg_spec is not None:
|
||||
num = np.maximum(disp_fft_lin, 0.0).astype(np.float32, copy=False) + 1e-9
|
||||
den = bg_spec[:, None] + 1e-9
|
||||
disp_fft = (20.0 * np.log10(num / den)).astype(np.float32, copy=False)
|
||||
else:
|
||||
disp_fft = _fft_mag_to_db(disp_fft_lin)
|
||||
# Автодиапазон по среднему спектру за видимый интервал (как в хорошей версии)
|
||||
levels = None
|
||||
try:
|
||||
mean_spec = np.nanmean(disp_fft, axis=1)
|
||||
vmin_v = float(np.nanmin(mean_spec))
|
||||
vmax_v = float(np.nanmax(mean_spec))
|
||||
if np.isfinite(vmin_v) and np.isfinite(vmax_v) and vmin_v != vmax_v:
|
||||
levels = (vmin_v, vmax_v)
|
||||
except Exception:
|
||||
levels = None
|
||||
# Процентильная обрезка как запасной вариант
|
||||
if levels is None and spec_clip is not None:
|
||||
if bg_spec is not None:
|
||||
try:
|
||||
vmin_v = float(np.nanpercentile(disp_fft, spec_clip[0]))
|
||||
vmax_v = float(np.nanpercentile(disp_fft, spec_clip[1]))
|
||||
p5 = float(np.nanpercentile(disp_fft, 5))
|
||||
p95 = float(np.nanpercentile(disp_fft, 95))
|
||||
span = max(abs(p5), abs(p95))
|
||||
if np.isfinite(span) and span > 0.0:
|
||||
levels = (-span, span)
|
||||
except Exception:
|
||||
levels = None
|
||||
else:
|
||||
try:
|
||||
mean_spec = np.nanmean(disp_fft, axis=1)
|
||||
vmin_v = float(np.nanmin(mean_spec))
|
||||
vmax_v = float(np.nanmax(mean_spec))
|
||||
if np.isfinite(vmin_v) and np.isfinite(vmax_v) and vmin_v != vmax_v:
|
||||
levels = (vmin_v, vmax_v)
|
||||
except Exception:
|
||||
levels = None
|
||||
# Ещё один фолбэк — глобальные накопленные мин/макс
|
||||
if levels is None and y_min_fft is not None and y_max_fft is not None and np.isfinite(y_min_fft) and np.isfinite(y_max_fft) and y_min_fft != y_max_fft:
|
||||
levels = (y_min_fft, y_max_fft)
|
||||
# Процентильная обрезка как запасной вариант
|
||||
if levels is None and spec_clip is not None:
|
||||
try:
|
||||
vmin_v = float(np.nanpercentile(disp_fft, spec_clip[0]))
|
||||
vmax_v = float(np.nanpercentile(disp_fft, spec_clip[1]))
|
||||
if np.isfinite(vmin_v) and np.isfinite(vmax_v) and vmin_v != vmax_v:
|
||||
levels = (vmin_v, vmax_v)
|
||||
except Exception:
|
||||
levels = None
|
||||
# Ещё один фолбэк — глобальные накопленные мин/макс
|
||||
if levels is None and y_min_fft is not None and y_max_fft is not None and np.isfinite(y_min_fft) and np.isfinite(y_max_fft) and y_min_fft != y_max_fft:
|
||||
levels = (y_min_fft, y_max_fft)
|
||||
if levels is not None:
|
||||
img_fft.setImage(disp_fft, autoLevels=False, levels=levels)
|
||||
else:
|
||||
@ -2337,9 +2481,13 @@ def run_pyqtgraph(args):
|
||||
pass
|
||||
stop_event.set()
|
||||
reader.join(timeout=1.0)
|
||||
if control_window is not None:
|
||||
try:
|
||||
main_window.close()
|
||||
except Exception:
|
||||
pass
|
||||
if calib_window is not None:
|
||||
try:
|
||||
control_window.close()
|
||||
calib_window.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@ -2352,7 +2500,7 @@ def run_pyqtgraph(args):
|
||||
except Exception:
|
||||
prev_sigint = None
|
||||
|
||||
orig_close_event = getattr(win, "closeEvent", None)
|
||||
orig_close_event = getattr(main_window, "closeEvent", None)
|
||||
|
||||
def _close_event(event):
|
||||
try:
|
||||
@ -2371,12 +2519,16 @@ def run_pyqtgraph(args):
|
||||
pass
|
||||
|
||||
try:
|
||||
win.closeEvent = _close_event # type: ignore[method-assign]
|
||||
main_window.closeEvent = _close_event # type: ignore[method-assign]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
app.aboutToQuit.connect(on_quit)
|
||||
win.show()
|
||||
try:
|
||||
main_window.resize(1200, 680)
|
||||
except Exception:
|
||||
pass
|
||||
main_window.show()
|
||||
exec_fn = getattr(app, "exec_", None) or getattr(app, "exec", None)
|
||||
try:
|
||||
exec_fn()
|
||||
|
||||
Reference in New Issue
Block a user