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
|
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,
|
sweep: np.ndarray,
|
||||||
freqs: Optional[np.ndarray],
|
freqs: Optional[np.ndarray],
|
||||||
bins: int,
|
bins: int,
|
||||||
) -> np.ndarray:
|
) -> np.ndarray:
|
||||||
"""Посчитать FFT-строку, используя калиброванную частотную ось при подготовке входа."""
|
"""Посчитать линейный модуль FFT-строки, используя калиброванную частотную ось."""
|
||||||
if bins <= 0:
|
if bins <= 0:
|
||||||
return np.zeros((0,), dtype=np.float32)
|
return np.zeros((0,), dtype=np.float32)
|
||||||
|
|
||||||
@ -217,10 +224,18 @@ def _compute_fft_row(
|
|||||||
fft_in[:take_fft] = fft_seg * win
|
fft_in[:take_fft] = fft_seg * win
|
||||||
spec = np.fft.ifft(fft_in)
|
spec = np.fft.ifft(fft_in)
|
||||||
mag = np.abs(spec).astype(np.float32)
|
mag = np.abs(spec).astype(np.float32)
|
||||||
fft_row = 20.0 * np.log10(mag + 1e-9)
|
if mag.shape[0] != bins:
|
||||||
if fft_row.shape[0] != bins:
|
mag = mag[:bins]
|
||||||
fft_row = fft_row[:bins]
|
return mag
|
||||||
return fft_row
|
|
||||||
|
|
||||||
|
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:
|
def _compute_distance_axis(freqs: Optional[np.ndarray], bins: int) -> np.ndarray:
|
||||||
@ -1080,8 +1095,8 @@ def main():
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--backend",
|
"--backend",
|
||||||
choices=["auto", "pg", "mpl"],
|
choices=["auto", "pg", "mpl"],
|
||||||
default="auto",
|
default="pg",
|
||||||
help="Графический бэкенд: pyqtgraph (pg) — быстрее; matplotlib (mpl) — совместимый. По умолчанию auto",
|
help="Графический бэкенд: pyqtgraph (pg) — быстрее; matplotlib (mpl) — совместимый. По умолчанию pg",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--norm-type",
|
"--norm-type",
|
||||||
@ -1119,14 +1134,18 @@ def main():
|
|||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
# Попробуем быстрый бэкенд (pyqtgraph) при auto/pg
|
# Попробуем быстрый бэкенд (pyqtgraph) при auto/pg
|
||||||
if args.backend in ("pg"):
|
if args.backend == "pg":
|
||||||
try:
|
try:
|
||||||
return run_pyqtgraph(args)
|
return run_pyqtgraph(args)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if args.backend == "pg":
|
sys.stderr.write(f"[error] PyQtGraph бэкенд недоступен: {e}\n")
|
||||||
sys.stderr.write(f"[error] PyQtGraph бэкенд недоступен: {e}\n")
|
sys.exit(1)
|
||||||
sys.exit(1)
|
|
||||||
# При auto — тихо откатываемся на matplotlib
|
if args.backend == "auto":
|
||||||
|
try:
|
||||||
|
return run_pyqtgraph(args)
|
||||||
|
except Exception:
|
||||||
|
pass # При auto — тихо откатываемся на matplotlib
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import matplotlib
|
import matplotlib
|
||||||
@ -1186,6 +1205,7 @@ def main():
|
|||||||
ymax_slider = None
|
ymax_slider = None
|
||||||
contrast_slider = None
|
contrast_slider = None
|
||||||
calib_enabled = False
|
calib_enabled = False
|
||||||
|
bg_subtract_enabled = False
|
||||||
peak_calibrate_mode = bool(getattr(args, "calibrate", False))
|
peak_calibrate_mode = bool(getattr(args, "calibrate", False))
|
||||||
current_peak_width: Optional[float] = None
|
current_peak_width: Optional[float] = None
|
||||||
norm_type = str(getattr(args, "norm_type", "projector")).strip().lower()
|
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:
|
def _normalize_sweep(raw: np.ndarray, calib: np.ndarray) -> np.ndarray:
|
||||||
return _normalize_by_calib(raw, calib, norm_type=norm_type)
|
return _normalize_by_calib(raw, calib, norm_type=norm_type)
|
||||||
|
|
||||||
def _set_calib_enabled():
|
def _sync_checkbox_states():
|
||||||
nonlocal calib_enabled, current_sweep_norm
|
nonlocal calib_enabled, bg_subtract_enabled, current_sweep_norm
|
||||||
try:
|
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:
|
except Exception:
|
||||||
calib_enabled = False
|
calib_enabled = False
|
||||||
|
bg_subtract_enabled = False
|
||||||
if calib_enabled and current_sweep_raw is not None and last_calib_sweep is not None:
|
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)
|
current_sweep_norm = _normalize_sweep(current_sweep_raw, last_calib_sweep)
|
||||||
else:
|
else:
|
||||||
@ -1299,11 +1322,11 @@ def main():
|
|||||||
ax_smin = fig.add_axes([0.92, 0.55, 0.02, 0.35])
|
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_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_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")
|
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")
|
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")
|
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):
|
def _on_ylim_change(_val):
|
||||||
try:
|
try:
|
||||||
@ -1318,7 +1341,7 @@ def main():
|
|||||||
ymax_slider.on_changed(_on_ylim_change)
|
ymax_slider.on_changed(_on_ylim_change)
|
||||||
# Контраст влияет на верхнюю границу цветовой шкалы (процент от авто-диапазона)
|
# Контраст влияет на верхнюю границу цветовой шкалы (процент от авто-диапазона)
|
||||||
contrast_slider.on_changed(lambda _v: fig.canvas.draw_idle())
|
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:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -1449,11 +1472,12 @@ def main():
|
|||||||
if ring_time is not None:
|
if ring_time is not None:
|
||||||
ring_time[head] = time.time()
|
ring_time[head] = time.time()
|
||||||
head = (head + 1) % ring.shape[0]
|
head = (head + 1) % ring.shape[0]
|
||||||
# FFT строка (дБ)
|
# FFT строка (линейный модуль; перевод в дБ делаем при отображении)
|
||||||
if ring_fft is not None:
|
if ring_fft is not None:
|
||||||
bins = ring_fft.shape[1]
|
bins = ring_fft.shape[1]
|
||||||
fft_row = _compute_fft_row(s, freqs, bins)
|
fft_mag = _compute_fft_mag_row(s, freqs, bins)
|
||||||
ring_fft[(head - 1) % ring_fft.shape[0], :] = fft_row
|
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_min = np.nanmin(fft_row)
|
||||||
fr_max = np.nanmax(fft_row)
|
fr_max = np.nanmax(fft_row)
|
||||||
@ -1519,7 +1543,7 @@ def main():
|
|||||||
return base_t
|
return base_t
|
||||||
|
|
||||||
def _subtract_recent_mean_fft(disp_fft: np.ndarray) -> np.ndarray:
|
def _subtract_recent_mean_fft(disp_fft: np.ndarray) -> np.ndarray:
|
||||||
"""Вычесть среднее по каждой частоте за последние spec_mean_sec секунд."""
|
"""Вычесть среднее по каждой дальности за последние spec_mean_sec секунд в линейной области."""
|
||||||
if spec_mean_sec <= 0.0:
|
if spec_mean_sec <= 0.0:
|
||||||
return disp_fft
|
return disp_fft
|
||||||
disp_times = make_display_times()
|
disp_times = make_display_times()
|
||||||
@ -1536,6 +1560,33 @@ def main():
|
|||||||
mean_spec = np.nan_to_num(mean_spec, nan=0.0)
|
mean_spec = np.nan_to_num(mean_spec, nan=0.0)
|
||||||
return disp_fft - mean_spec[:, None]
|
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():
|
def make_display_ring_fft():
|
||||||
if ring_fft is None:
|
if ring_fft is None:
|
||||||
return np.zeros((1, 1), dtype=np.float32)
|
return np.zeros((1, 1), dtype=np.float32)
|
||||||
@ -1658,39 +1709,61 @@ def main():
|
|||||||
|
|
||||||
# Обновление водопада спектров
|
# Обновление водопада спектров
|
||||||
if changed and ring_fft is not None:
|
if changed and ring_fft is not None:
|
||||||
disp_fft = make_display_ring_fft()
|
disp_fft_lin = make_display_ring_fft()
|
||||||
disp_fft = _subtract_recent_mean_fft(disp_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)
|
img_fft_obj.set_data(disp_fft)
|
||||||
# Подписи времени не обновляем динамически (оставляем авто-тики)
|
# Подписи времени не обновляем динамически (оставляем авто-тики)
|
||||||
# Автодиапазон по среднему спектру за видимый интервал (как в хорошей версии)
|
# Автодиапазон по среднему спектру за видимый интервал (как в хорошей версии)
|
||||||
try:
|
if bg_spec is not None:
|
||||||
# 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:
|
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:
|
except Exception:
|
||||||
c = 1.0
|
span = float("nan")
|
||||||
vmax_eff = vmin_v + c * (vmax_v - vmin_v)
|
if np.isfinite(span) and span > 0.0:
|
||||||
img_fft_obj.set_clim(vmin=vmin_v, vmax=vmax_eff)
|
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:
|
if changed and current_info:
|
||||||
status_payload = dict(current_info)
|
status_payload = dict(current_info)
|
||||||
@ -1776,16 +1849,11 @@ def run_pyqtgraph(args):
|
|||||||
peak_calibrate_mode = bool(getattr(args, "calibrate", False))
|
peak_calibrate_mode = bool(getattr(args, "calibrate", False))
|
||||||
try:
|
try:
|
||||||
import pyqtgraph as pg
|
import pyqtgraph as pg
|
||||||
from PyQt5 import QtCore, QtWidgets # noqa: F401
|
from pyqtgraph.Qt import QtCore, QtWidgets # type: ignore
|
||||||
except Exception:
|
except Exception as e:
|
||||||
# Возможно установлена PySide6
|
raise RuntimeError(
|
||||||
try:
|
"pyqtgraph и совместимый Qt backend не найдены. Установите: pip install pyqtgraph PyQt5"
|
||||||
import pyqtgraph as pg
|
) from e
|
||||||
from PySide6 import QtCore, QtWidgets # noqa: F401
|
|
||||||
except Exception as e:
|
|
||||||
raise RuntimeError(
|
|
||||||
"pyqtgraph/PyQt5(Pyside6) не найдены. Установите: pip install pyqtgraph PyQt5"
|
|
||||||
) from e
|
|
||||||
|
|
||||||
# Очередь завершённых свипов и поток чтения
|
# Очередь завершённых свипов и поток чтения
|
||||||
q: Queue[SweepPacket] = Queue(maxsize=1000)
|
q: Queue[SweepPacket] = Queue(maxsize=1000)
|
||||||
@ -1823,8 +1891,25 @@ def run_pyqtgraph(args):
|
|||||||
app.setQuitOnLastWindowClosed(True)
|
app.setQuitOnLastWindowClosed(True)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
win = pg.GraphicsLayoutWidget(show=True, title=args.title)
|
main_window = QtWidgets.QWidget()
|
||||||
win.resize(1200, 600)
|
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="Сырые данные")
|
p_line = win.addPlot(row=0, col=0, title="Сырые данные")
|
||||||
@ -1889,20 +1974,17 @@ def run_pyqtgraph(args):
|
|||||||
spec_left_line.setVisible(False)
|
spec_left_line.setVisible(False)
|
||||||
spec_right_line.setVisible(False)
|
spec_right_line.setVisible(False)
|
||||||
|
|
||||||
# Отдельное окно контролов: GraphicsLayoutWidget не принимает обычные QWidget через addItem.
|
# Правая панель настроек внутри основного окна.
|
||||||
calib_cb = QtWidgets.QCheckBox("калибровка")
|
calib_cb = QtWidgets.QCheckBox("нормировка")
|
||||||
control_window = None
|
bg_subtract_cb = QtWidgets.QCheckBox("вычет фона")
|
||||||
control_layout = None
|
try:
|
||||||
if peak_calibrate_mode:
|
settings_title = QtWidgets.QLabel("Настройки")
|
||||||
control_window = QtWidgets.QWidget()
|
settings_layout.addWidget(settings_title)
|
||||||
try:
|
except Exception:
|
||||||
control_window.setWindowTitle(f"{args.title} controls")
|
pass
|
||||||
except Exception:
|
settings_layout.addWidget(calib_cb)
|
||||||
pass
|
settings_layout.addWidget(bg_subtract_cb)
|
||||||
control_layout = QtWidgets.QVBoxLayout(control_window)
|
calib_window = None
|
||||||
control_layout.setContentsMargins(8, 8, 8, 8)
|
|
||||||
control_layout.setSpacing(6)
|
|
||||||
control_layout.addWidget(calib_cb)
|
|
||||||
|
|
||||||
# Статусная строка (внизу окна)
|
# Статусная строка (внизу окна)
|
||||||
status = pg.LabelItem(justify="left")
|
status = pg.LabelItem(justify="left")
|
||||||
@ -1931,6 +2013,7 @@ def run_pyqtgraph(args):
|
|||||||
spec_clip = _parse_spec_clip(getattr(args, "spec_clip", None))
|
spec_clip = _parse_spec_clip(getattr(args, "spec_clip", None))
|
||||||
spec_mean_sec = float(getattr(args, "spec_mean_sec", 0.0))
|
spec_mean_sec = float(getattr(args, "spec_mean_sec", 0.0))
|
||||||
calib_enabled = False
|
calib_enabled = False
|
||||||
|
bg_subtract_enabled = False
|
||||||
current_peak_width: Optional[float] = None
|
current_peak_width: Optional[float] = None
|
||||||
norm_type = str(getattr(args, "norm_type", "projector")).strip().lower()
|
norm_type = str(getattr(args, "norm_type", "projector")).strip().lower()
|
||||||
c_edits = []
|
c_edits = []
|
||||||
@ -1959,16 +2042,31 @@ def run_pyqtgraph(args):
|
|||||||
else:
|
else:
|
||||||
current_sweep_norm = None
|
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:
|
try:
|
||||||
calib_cb.stateChanged.connect(lambda _v: _set_calib_enabled())
|
calib_cb.stateChanged.connect(lambda _v: _set_calib_enabled())
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
try:
|
||||||
|
bg_subtract_cb.stateChanged.connect(lambda _v: _set_bg_subtract_enabled())
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
if peak_calibrate_mode:
|
if peak_calibrate_mode:
|
||||||
try:
|
try:
|
||||||
c_widget = QtWidgets.QWidget()
|
calib_window = QtWidgets.QWidget()
|
||||||
c_layout = QtWidgets.QFormLayout(c_widget)
|
try:
|
||||||
c_layout.setContentsMargins(0, 0, 0, 0)
|
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):
|
def _apply_c_value(idx: int, edit):
|
||||||
global CALIBRATION_C
|
global CALIBRATION_C
|
||||||
@ -1990,17 +2088,18 @@ def run_pyqtgraph(args):
|
|||||||
edit.editingFinished.connect(lambda i=idx, e=edit: _apply_c_value(i, e))
|
edit.editingFinished.connect(lambda i=idx, e=edit: _apply_c_value(i, e))
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
c_layout.addRow(f"C{idx}", edit)
|
calib_layout.addRow(f"C{idx}", edit)
|
||||||
c_edits.append(edit)
|
c_edits.append(edit)
|
||||||
if control_layout is not None:
|
try:
|
||||||
control_layout.addWidget(c_widget)
|
calib_window.show()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
if control_window is not None:
|
|
||||||
try:
|
|
||||||
control_window.show()
|
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
try:
|
||||||
|
settings_layout.addStretch(1)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def ensure_buffer(_w: int):
|
def ensure_buffer(_w: int):
|
||||||
nonlocal ring, ring_time, head, width, x_shared, ring_fft, distance_shared
|
nonlocal ring, ring_time, head, width, x_shared, ring_fft, distance_shared
|
||||||
@ -2076,6 +2175,33 @@ def run_pyqtgraph(args):
|
|||||||
return None
|
return None
|
||||||
return (vmin, vmax)
|
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):
|
def push_sweep(s: np.ndarray, freqs: Optional[np.ndarray] = None):
|
||||||
nonlocal ring, ring_time, head, ring_fft, y_min_fft, y_max_fft
|
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:
|
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:
|
if ring_time is not None:
|
||||||
ring_time[head] = time.time()
|
ring_time[head] = time.time()
|
||||||
head = (head + 1) % ring.shape[0]
|
head = (head + 1) % ring.shape[0]
|
||||||
# FFT строка (дБ)
|
# FFT строка (линейный модуль; перевод в дБ делаем при отображении)
|
||||||
if ring_fft is not None:
|
if ring_fft is not None:
|
||||||
bins = ring_fft.shape[1]
|
bins = ring_fft.shape[1]
|
||||||
fft_row = _compute_fft_row(s, freqs, bins)
|
fft_mag = _compute_fft_mag_row(s, freqs, bins)
|
||||||
ring_fft[(head - 1) % ring_fft.shape[0], :] = fft_row
|
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_min = np.nanmin(fft_row)
|
||||||
fr_max = np.nanmax(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):
|
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
|
pass
|
||||||
|
|
||||||
if changed and ring_fft is not None:
|
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_lin = ring_fft if head == 0 else np.roll(ring_fft, -head, axis=0)
|
||||||
disp_fft = disp_fft.T
|
disp_fft_lin = disp_fft_lin.T
|
||||||
if spec_mean_sec > 0.0 and ring_time is not None:
|
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)
|
disp_times = ring_time if head == 0 else np.roll(ring_time, -head)
|
||||||
now_t = time.time()
|
now_t = time.time()
|
||||||
mask = np.isfinite(disp_times) & (disp_times >= (now_t - spec_mean_sec))
|
mask = np.isfinite(disp_times) & (disp_times >= (now_t - spec_mean_sec))
|
||||||
if np.any(mask):
|
if np.any(mask):
|
||||||
try:
|
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)
|
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:
|
except Exception:
|
||||||
pass
|
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
|
levels = None
|
||||||
try:
|
if bg_spec is not None:
|
||||||
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:
|
|
||||||
try:
|
try:
|
||||||
vmin_v = float(np.nanpercentile(disp_fft, spec_clip[0]))
|
p5 = float(np.nanpercentile(disp_fft, 5))
|
||||||
vmax_v = float(np.nanpercentile(disp_fft, spec_clip[1]))
|
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:
|
if np.isfinite(vmin_v) and np.isfinite(vmax_v) and vmin_v != vmax_v:
|
||||||
levels = (vmin_v, vmax_v)
|
levels = (vmin_v, vmax_v)
|
||||||
except Exception:
|
except Exception:
|
||||||
levels = None
|
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:
|
if levels is None and spec_clip is not None:
|
||||||
levels = (y_min_fft, y_max_fft)
|
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:
|
if levels is not None:
|
||||||
img_fft.setImage(disp_fft, autoLevels=False, levels=levels)
|
img_fft.setImage(disp_fft, autoLevels=False, levels=levels)
|
||||||
else:
|
else:
|
||||||
@ -2337,9 +2481,13 @@ def run_pyqtgraph(args):
|
|||||||
pass
|
pass
|
||||||
stop_event.set()
|
stop_event.set()
|
||||||
reader.join(timeout=1.0)
|
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:
|
try:
|
||||||
control_window.close()
|
calib_window.close()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -2352,7 +2500,7 @@ def run_pyqtgraph(args):
|
|||||||
except Exception:
|
except Exception:
|
||||||
prev_sigint = None
|
prev_sigint = None
|
||||||
|
|
||||||
orig_close_event = getattr(win, "closeEvent", None)
|
orig_close_event = getattr(main_window, "closeEvent", None)
|
||||||
|
|
||||||
def _close_event(event):
|
def _close_event(event):
|
||||||
try:
|
try:
|
||||||
@ -2371,12 +2519,16 @@ def run_pyqtgraph(args):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
win.closeEvent = _close_event # type: ignore[method-assign]
|
main_window.closeEvent = _close_event # type: ignore[method-assign]
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
app.aboutToQuit.connect(on_quit)
|
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)
|
exec_fn = getattr(app, "exec_", None) or getattr(app, "exec", None)
|
||||||
try:
|
try:
|
||||||
exec_fn()
|
exec_fn()
|
||||||
|
|||||||
Reference in New Issue
Block a user