From 2b2e323fbf7b51befd7fbfb1de9b2a749f73e7e7 Mon Sep 17 00:00:00 2001 From: ivngrmk Date: Wed, 19 Nov 2025 17:47:31 +0300 Subject: [PATCH] Added normalization and detection. --- .../core/processors/configs/bscan_config.json | 8 +- .../processors/configs/magnitude_config.json | 2 +- .../implementations/bscan_processor.py | 310 +++++++++++------- 3 files changed, 203 insertions(+), 117 deletions(-) diff --git a/vna_system/core/processors/configs/bscan_config.json b/vna_system/core/processors/configs/bscan_config.json index dce72fe..6696aa4 100644 --- a/vna_system/core/processors/configs/bscan_config.json +++ b/vna_system/core/processors/configs/bscan_config.json @@ -1,12 +1,16 @@ { "open_air": true, "axis": "abs", - "cut": 0.267, + "cut": 0.23, "max": 1.5, "gain": 1.0, "start_freq": 470.0, "stop_freq": 8800.0, "clear_history": false, - "sigma": 4.28, + "sigma": 3.42, + "border_border_m": 0.26, + "if_normalize": false, + "if_draw_level": false, + "detection_level": 3.0, "data_limit": 500 } \ No newline at end of file diff --git a/vna_system/core/processors/configs/magnitude_config.json b/vna_system/core/processors/configs/magnitude_config.json index b4b8a6a..5d834fc 100644 --- a/vna_system/core/processors/configs/magnitude_config.json +++ b/vna_system/core/processors/configs/magnitude_config.json @@ -3,6 +3,6 @@ "y_max": 40, "autoscale": true, "show_magnitude": true, - "show_phase": true, + "show_phase": false, "open_air": false } \ No newline at end of file diff --git a/vna_system/core/processors/implementations/bscan_processor.py b/vna_system/core/processors/implementations/bscan_processor.py index b71a8e3..239f202 100644 --- a/vna_system/core/processors/implementations/bscan_processor.py +++ b/vna_system/core/processors/implementations/bscan_processor.py @@ -58,6 +58,10 @@ class BScanProcessor(BaseProcessor): "stop_freq": 8800.0, # Stop frequency (MHz) "clear_history": False, # UI button; not persisted "sigma" : 0.01, + "border_border_m" : 0.5, + "if_normalize" : False, + "if_draw_level" : False, + "detection_level" : 5, } def get_ui_parameters(self) -> list[UIParameter]: @@ -120,6 +124,43 @@ class BScanProcessor(BaseProcessor): value=cfg["stop_freq"], options={"min": 100.0, "max": 8800.0, "step": 10.0, "dtype": "float"}, ), + + # Modern features + UIParameter( + name="sigma", + label="Степень сглаживания в abs режиме", + type="slider", + value=0.01, + options={"min": 0.01, "max": 5.0, "step": 0.01, "dtype": "float"}, + ), + UIParameter( + name="if_normalize", + label="Нормировка", + type="toggle", + value=False, + ), + UIParameter( + name="border_border_m", + label="Глубина границы", + type="slider", + value=0.5, + options={"min": 0.0, "max": 2.5, "step": 0.01, "dtype": "float"}, + ), + UIParameter( + name="if_draw_level", + label="Детекция", + type="toggle", + value=False, + ), + UIParameter( + name="detection_level", + label="Порог детекции,%", + type="slider", + value=5, + options={"min": 0.0, "max": 100.0, "step": 0.1, "dtype": "float"}, + ), + + # Big botton UIParameter( name="clear_history", label="Очистить историю", @@ -127,13 +168,6 @@ class BScanProcessor(BaseProcessor): value=False, options={"action": "Очистить накопленную историю графика"}, ), - UIParameter( - name="sigma", - label="Степень сглаживания в abs режиме", - type="slider", - value=self._config.get("y_max", 10), - options={"min": 0.01, "max": 5.0, "step": 0.01, "dtype": "float"}, - ), ] def update_config(self, updates: dict[str, Any]) -> None: @@ -300,112 +334,167 @@ class BScanProcessor(BaseProcessor): # ------------------------------------------------------------------------- def generate_plotly_config( - self, - processed_data: dict[str, Any], - vna_config: dict[str, Any], # noqa: ARG002 - reserved for future layout tweaks - ) -> dict[str, Any]: - """ - Produce a Plotly-compatible heatmap configuration from accumulated sweeps. - """ - if "error" in processed_data: - return { - "data": [], - "layout": { - "title": "B-Scan анализ - Ошибка", - "annotations": [ - { - "text": f"Ошибка: {processed_data['error']}", - "x": 0.5, - "y": 0.5, - "xref": "paper", - "yref": "paper", - "showarrow": False, - } - ], - "template": "plotly_dark", - }, + self, + processed_data: dict[str, Any], + vna_config: dict[str, Any], # noqa: ARG002 - reserved for future layout tweaks + ) -> dict[str, Any]: + """ + Produce a Plotly-compatible heatmap configuration from accumulated sweeps. + """ + if "error" in processed_data: + return { + "data": [], + "layout": { + "title": "B-Scan анализ - Ошибка", + "annotations": [ + { + "text": f"Ошибка: {processed_data['error']}", + "x": 0.5, + "y": 0.5, + "xref": "paper", + "yref": "paper", + "showarrow": False, + } + ], + "template": "plotly_dark", + }, + } + + with self._lock: + history = list(self._plot_history) + + if not history: + return { + "data": [], + "layout": { + "title": "B-Scan анализ - Нет данных", + "xaxis": {"title": "Номер развертки"}, + "yaxis": {"title": "Глубина (м)"}, + "template": "plotly_dark", + }, + } + + + Y_VALUE = self._config["border_border_m"] + + # Build scatter-like heatmap (irregular grid) from history + x_coords: list[int] = [] + y_coords: list[float] = [] + z_values: list[float] = [] + + z_values_square = np.zeros((len(history[0]["distance_data"]),len(history)),dtype=float) + for sweep_index, item in enumerate(history, start=1): + depths = item["distance_data"] + amps = item["time_domain_data"] + + if self._config['if_normalize']: + depth_mask = np.array(depths) < Y_VALUE + normalized_ampls = np.array(amps) / np.max(np.array(amps)[depth_mask]) + else: + normalized_ampls = np.array(amps) + z_values_square[:,sweep_index-1] = normalized_ampls + + for d, a in zip(depths, normalized_ampls, strict=False): + x_coords.append(sweep_index) + y_coords.append(d) + z_values.append(float(a)) + + if self._config["if_draw_level"]: + detection_level_abs = np.percentile(z_values, 100 - self._config["detection_level"]) + detected_points_mask = (np.array(z_values) > detection_level_abs) & (np.array(y_coords) > Y_VALUE) + detected_points_x = list([float(x) for x in np.array(x_coords)[detected_points_mask]]) + detected_points_y = list([float(y) for y in np.array(y_coords)[detected_points_mask]]) + detected_trace = { + "type": "scatter", + "mode": "markers", + "x": detected_points_x, + "y": detected_points_y, + "marker": { + "size": 8, + "color": "red", + "symbol": "circle", + }, + "name": "Обнаруженные точки", + } + else: + detected_trace = None + + + # Colorscale selection + if self._config["axis"] == "abs": + colorscale = "Viridis" + heatmap_kwargs: dict[str, Any] = {} + else: + colorscale = "RdBu" + heatmap_kwargs = {"zmid": 0} + + heatmap_trace = { + "type": "heatmap", + "x": x_coords, + "y": y_coords, + "z": z_values, + "colorscale": colorscale, + "colorbar": {"title": "Амплитуда"}, + "hovertemplate": ( + "Развертка: %{x}
" + "Глубина: %{y:.3f} м
" + "Амплитуда: %{z:.3f}
" + "" + ), + **heatmap_kwargs, } - with self._lock: - history = list(self._plot_history) + freq_start, freq_stop = processed_data.get("frequency_range", [0.0, 0.0]) + config_info = ( + f"Частота: {freq_start/1e6:.1f}-{freq_stop/1e6:.1f} МГц | " + f"Усиление: {self._config['gain']:.1f} | " + f"Отсечка: {self._config['cut']:.3f} м | " + f"Макс глубина: {self._config['max']:.1f} м | " + f"Ось: {self._config['axis']} | " + f"Разверток: {len(history)}" + ) - if not history: - return { - "data": [], - "layout": { - "title": "B-Scan анализ - Нет данных", - "xaxis": {"title": "Номер развертки"}, - "yaxis": {"title": "Глубина (м)"}, - "template": "plotly_dark", - }, + if processed_data.get("reference_used", False): + config_info += " | Открытый воздух: ВКЛ" + + # if self._config["data_limitation"]: + # config_info += f" | Limit: {self._config['data_limitation']}" + + layout = { + "title": f"B-Scan тепловая карта - {config_info}", + "xaxis": {"title": "Номер развертки", "side": "bottom"}, + "yaxis": {"title": "Глубина (м)", "autorange": "reversed"}, + "hovermode": "closest", + "height": 546, + "template": "plotly_dark", + "margin": {"t": 40, "r": 50, "b": 110, "l": 50}, + "autosize": True, } - # Build scatter-like heatmap (irregular grid) from history - x_coords: list[int] = [] - y_coords: list[float] = [] - z_values: list[float] = [] + print(self._config['if_normalize'], self._config['if_draw_level']) + if self._config['if_normalize'] or self._config['if_draw_level']: + layout["shapes"] = layout.get("shapes", []) + [ + { + "type": "line", + # по X — координаты "бумаги" (0–1 по всей ширине графика) + "xref": "paper", + # по Y — координаты данных (в метрах глубины) + "yref": "y", + "x0": 0, + "x1": 1, + "y0": Y_VALUE, + "y1": Y_VALUE, + "line": { + "width": 2, + "dash": "dash", + "color": "white", + }, + } + ] - for sweep_index, item in enumerate(history, start=1): - depths = item["distance_data"] - amps = item["time_domain_data"] - - for d, a in zip(depths, amps, strict=False): - x_coords.append(sweep_index) - y_coords.append(d) - z_values.append(a) - - # Colorscale selection - if self._config["axis"] == "abs": - colorscale = "Viridis" - heatmap_kwargs: dict[str, Any] = {} - else: - colorscale = "RdBu" - heatmap_kwargs = {"zmid": 0} - - heatmap_trace = { - "type": "heatmap", - "x": x_coords, - "y": y_coords, - "z": z_values, - "colorscale": colorscale, - "colorbar": {"title": "Амплитуда"}, - "hovertemplate": ( - "Развертка: %{x}
" - "Глубина: %{y:.3f} м
" - "Амплитуда: %{z:.3f}
" - "" - ), - **heatmap_kwargs, - } - - freq_start, freq_stop = processed_data.get("frequency_range", [0.0, 0.0]) - config_info = ( - f"Частота: {freq_start/1e6:.1f}-{freq_stop/1e6:.1f} МГц | " - f"Усиление: {self._config['gain']:.1f} | " - f"Отсечка: {self._config['cut']:.3f} м | " - f"Макс глубина: {self._config['max']:.1f} м | " - f"Ось: {self._config['axis']} | " - f"Разверток: {len(history)}" - ) - - if processed_data.get("reference_used", False): - config_info += " | Открытый воздух: ВКЛ" - - # if self._config["data_limitation"]: - # config_info += f" | Limit: {self._config['data_limitation']}" - - layout = { - "title": f"B-Scan тепловая карта - {config_info}", - "xaxis": {"title": "Номер развертки", "side": "bottom"}, - "yaxis": {"title": "Глубина (м)", "autorange": "reversed"}, - "hovermode": "closest", - "height": 546, - "template": "plotly_dark", - "margin": {"t": 40, "r": 50, "b": 110, "l": 50}, - "autosize": True - } - - return {"data": [heatmap_trace], "layout": layout} + if detected_trace is not None: + return {"data": [heatmap_trace,detected_trace], "layout": layout} + return {"data": [heatmap_trace], "layout": layout} # ------------------------------------------------------------------------- # Recalculation override @@ -568,13 +657,8 @@ class BScanProcessor(BaseProcessor): # Determine sigma for smoothing if vna_config: - # calc_type = vna_config.get("axis", "abs") - # sigma = float(vna_config.get("sigma", 0.01)) - calc_type = self._config["axis"] sigma = self._config["sigma"] - - pass else: calc_type = self._config["axis"] sigma = self._config["sigma"] @@ -594,10 +678,8 @@ class BScanProcessor(BaseProcessor): depth_out, time_out = self._apply_depth_processing(depth_m, time_response) if calc_type == 'abs': - print('Filtering with sigma',sigma) filtered_time_out = gaussian_filter1d(time_out, sigma=sigma) else: - print("Not filtering") filtered_time_out = time_out return {