senoquant 1.0.0b3__py3-none-any.whl → 1.0.0b4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
senoquant/__init__.py CHANGED
@@ -4,7 +4,7 @@ try:
4
4
  from importlib.metadata import version
5
5
  __version__ = version("senoquant")
6
6
  except Exception:
7
- __version__ = "1.0.0b3" # Fallback for development
7
+ __version__ = "1.0.0b4" # Fallback for development
8
8
 
9
9
  from ._widget import SenoQuantWidget
10
10
  __all__ = ["SenoQuantWidget"]
senoquant/_widget.py CHANGED
@@ -2,7 +2,14 @@
2
2
 
3
3
  from qtpy.QtWidgets import QTabWidget, QVBoxLayout, QWidget
4
4
 
5
- from .tabs import BatchTab, QuantificationTab, SegmentationTab, SettingsTab, SpotsTab
5
+ from .tabs import (
6
+ BatchTab,
7
+ QuantificationTab,
8
+ SegmentationTab,
9
+ SettingsTab,
10
+ SpotsTab,
11
+ VisualizationTab,
12
+ )
6
13
  from .tabs.settings.backend import SettingsBackend
7
14
 
8
15
 
@@ -26,6 +33,7 @@ class SenoQuantWidget(QWidget):
26
33
  )
27
34
  tabs.addTab(SpotsTab(napari_viewer=napari_viewer), "Spots")
28
35
  tabs.addTab(QuantificationTab(napari_viewer=napari_viewer), "Quantification")
36
+ tabs.addTab(VisualizationTab(napari_viewer=napari_viewer), "Visualization")
29
37
  tabs.addTab(BatchTab(napari_viewer=napari_viewer), "Batch")
30
38
  tabs.addTab(SettingsTab(backend=self._settings_backend), "Settings")
31
39
 
@@ -3,6 +3,7 @@
3
3
  from .segmentation.frontend import SegmentationTab
4
4
  from .spots.frontend import SpotsTab
5
5
  from .quantification.frontend import QuantificationTab
6
+ from .visualization.frontend import VisualizationTab
6
7
  from .settings.frontend import SettingsTab
7
8
  from .batch.frontend import BatchTab
8
9
 
@@ -10,6 +11,7 @@ __all__ = [
10
11
  "SegmentationTab",
11
12
  "SpotsTab",
12
13
  "QuantificationTab",
14
+ "VisualizationTab",
13
15
  "SettingsTab",
14
16
  "BatchTab",
15
17
  ]
@@ -208,7 +208,8 @@ class BatchBackend:
208
208
  cyto_channel : str or int or None, optional
209
209
  Channel selection for cytoplasm.
210
210
  cyto_nuclear_channel : str or int or None, optional
211
- Optional nuclear channel used by cytoplasmic models.
211
+ Optional nuclear input for cytoplasmic models. This may be a
212
+ channel selection or a generated nuclear label name.
212
213
  cyto_settings : dict or None, optional
213
214
  Model settings for cytoplasmic segmentation.
214
215
  spot_detector : str or None, optional
@@ -260,6 +261,14 @@ class BatchBackend:
260
261
  cyto_settings = cyto_settings or {}
261
262
  spot_settings = spot_settings or {}
262
263
  quant_backend = QuantificationBackend()
264
+ cyto_model_instance = None
265
+ cyto_nuclear_only = False
266
+ if cyto_model:
267
+ cyto_model_instance = self._segmentation_backend.get_model(cyto_model)
268
+ modes: list[str] = []
269
+ if hasattr(cyto_model_instance, "cytoplasmic_input_modes"):
270
+ modes = cyto_model_instance.cytoplasmic_input_modes()
271
+ cyto_nuclear_only = modes == ["nuclear"]
263
272
 
264
273
  # Count total items to process
265
274
  total_items = 0
@@ -349,36 +358,61 @@ class BatchBackend:
349
358
  item_result.outputs[label_name] = out_path
350
359
 
351
360
  if cyto_model:
352
- channel_idx = resolve_channel_index(
353
- cyto_channel, normalized_channels
354
- )
355
- cyto_image, cyto_meta = load_channel_data(
356
- path, channel_idx, scene_id
357
- )
358
- if cyto_image is None:
359
- raise RuntimeError(
360
- "Failed to read cytoplasmic image data."
361
- )
362
- cyto_layer = Image(cyto_image, "cytoplasmic", cyto_meta)
363
- cyto_nuclear_layer = None
364
- if cyto_nuclear_channel is not None:
365
- nuclear_idx = resolve_channel_index(
366
- cyto_nuclear_channel, normalized_channels
361
+ cyto_layer = None
362
+ cyto_meta: dict = {}
363
+ if not cyto_nuclear_only:
364
+ channel_idx = resolve_channel_index(
365
+ cyto_channel, normalized_channels
367
366
  )
368
- nuclear_image, nuclear_meta = load_channel_data(
369
- path, nuclear_idx, scene_id
367
+ cyto_image, cyto_meta = load_channel_data(
368
+ path, channel_idx, scene_id
370
369
  )
371
- if nuclear_image is None:
370
+ if cyto_image is None:
372
371
  raise RuntimeError(
373
- "Failed to read cytoplasmic nuclear data."
372
+ "Failed to read cytoplasmic image data."
373
+ )
374
+ cyto_layer = Image(cyto_image, "cytoplasmic", cyto_meta)
375
+ cyto_nuclear_layer = None
376
+ if cyto_nuclear_channel is not None:
377
+ cyto_nuclear_key = str(cyto_nuclear_channel)
378
+ if (
379
+ cyto_nuclear_only
380
+ and cyto_nuclear_key in labels_data
381
+ ):
382
+ nuclear_meta = labels_meta.get(cyto_nuclear_key, {})
383
+ cyto_nuclear_layer = Labels(
384
+ labels_data[cyto_nuclear_key],
385
+ cyto_nuclear_key,
386
+ nuclear_meta,
387
+ )
388
+ if not cyto_meta:
389
+ cyto_meta = dict(nuclear_meta)
390
+ else:
391
+ nuclear_idx = resolve_channel_index(
392
+ cyto_nuclear_channel, normalized_channels
374
393
  )
375
- cyto_nuclear_layer = Image(
376
- nuclear_image, "nuclear", nuclear_meta
394
+ nuclear_image, nuclear_meta = load_channel_data(
395
+ path, nuclear_idx, scene_id
396
+ )
397
+ if nuclear_image is None:
398
+ raise RuntimeError(
399
+ "Failed to read cytoplasmic nuclear data."
400
+ )
401
+ cyto_nuclear_layer = Image(
402
+ nuclear_image, "nuclear", nuclear_meta
403
+ )
404
+ if not cyto_meta:
405
+ cyto_meta = dict(nuclear_meta)
406
+ if cyto_nuclear_only and cyto_nuclear_layer is None:
407
+ raise RuntimeError(
408
+ "Selected cytoplasmic model requires nuclear labels."
377
409
  )
378
- model = self._segmentation_backend.get_model(cyto_model)
379
- seg_result = model.run(
410
+ if cyto_model_instance is None:
411
+ raise RuntimeError("Failed to load cytoplasmic model.")
412
+ seg_result = cyto_model_instance.run(
380
413
  task="cytoplasmic",
381
414
  layer=cyto_layer,
415
+ cytoplasmic_layer=cyto_layer,
382
416
  nuclear_layer=cyto_nuclear_layer,
383
417
  settings=cyto_settings,
384
418
  )
@@ -123,6 +123,11 @@ class BatchTab(QWidget):
123
123
  self._spot_min_size_spin: QSpinBox | None = None
124
124
  self._spot_max_size_spin: QSpinBox | None = None
125
125
  self._add_spot_button: QPushButton | None = None
126
+ self._cyto_nuclear_optional = False
127
+ self._cyto_nuclear_from_labels = False
128
+ self._cyto_requires_cyto_channel = True
129
+ self._cyto_supports_nuclear_input = False
130
+ self._refreshing_channel_choices = False
126
131
  self._config_viewer = BatchViewer()
127
132
 
128
133
  layout = QVBoxLayout()
@@ -281,6 +286,9 @@ class BatchTab(QWidget):
281
286
  self._nuclear_model_combo.currentTextChanged.connect(
282
287
  lambda _text: self._update_nuclear_settings()
283
288
  )
289
+ self._nuclear_channel_combo.currentTextChanged.connect(
290
+ self._on_nuclear_channel_changed
291
+ )
284
292
 
285
293
  cyto_layout = QFormLayout()
286
294
  cyto_layout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
@@ -293,7 +301,6 @@ class BatchTab(QWidget):
293
301
  self._cyto_channel_combo = QComboBox()
294
302
  self._cyto_nuclear_combo = QComboBox()
295
303
  self._cyto_nuclear_label = QLabel("Nuclear channel")
296
- self._cyto_nuclear_optional = False
297
304
  cyto_layout.addRow(self._cyto_enabled)
298
305
  cyto_layout.addRow("Cytoplasmic model", self._cyto_model_combo)
299
306
  cyto_layout.addRow("Cytoplasmic channel", self._cyto_channel_combo)
@@ -593,6 +600,9 @@ class BatchTab(QWidget):
593
600
 
594
601
  def _refresh_channel_choices(self) -> None:
595
602
  """Refresh combo boxes that depend on channel mapping."""
603
+ if self._refreshing_channel_choices:
604
+ return
605
+ self._refreshing_channel_choices = True
596
606
  names = [config.name for config in self._channel_configs]
597
607
 
598
608
  def populate_combo(
@@ -600,13 +610,16 @@ class BatchTab(QWidget):
600
610
  *,
601
611
  include_none: bool = False,
602
612
  none_label: str = "(none)",
613
+ explicit_items: list[str] | None = None,
603
614
  ) -> None:
604
615
  current = combo.currentText()
605
616
  combo.clear()
606
617
  items: list[str] = []
607
618
  if include_none:
608
619
  items.append(none_label)
609
- if names:
620
+ if explicit_items is not None:
621
+ items.extend(explicit_items)
622
+ elif names:
610
623
  items.extend(names)
611
624
  elif not include_none:
612
625
  items.append("0")
@@ -618,15 +631,40 @@ class BatchTab(QWidget):
618
631
  if index != -1:
619
632
  combo.setCurrentIndex(index)
620
633
 
621
- if getattr(self, "_nuclear_channel_combo", None) is not None:
622
- populate_combo(self._nuclear_channel_combo)
623
- if getattr(self, "_cyto_channel_combo", None) is not None:
624
- populate_combo(self._cyto_channel_combo)
625
- if getattr(self, "_cyto_nuclear_combo", None) is not None:
626
- populate_combo(
627
- self._cyto_nuclear_combo,
628
- include_none=self._cyto_nuclear_optional,
629
- )
634
+ try:
635
+ if getattr(self, "_nuclear_channel_combo", None) is not None:
636
+ populate_combo(self._nuclear_channel_combo)
637
+ if getattr(self, "_cyto_channel_combo", None) is not None:
638
+ populate_combo(self._cyto_channel_combo)
639
+ if getattr(self, "_cyto_nuclear_combo", None) is not None:
640
+ if self._cyto_nuclear_from_labels:
641
+ label_options = self._cyto_nuclear_label_options()
642
+ populate_combo(
643
+ self._cyto_nuclear_combo,
644
+ explicit_items=label_options,
645
+ none_label="(no nuclear labels)",
646
+ )
647
+ else:
648
+ populate_combo(
649
+ self._cyto_nuclear_combo,
650
+ include_none=self._cyto_nuclear_optional,
651
+ )
652
+ finally:
653
+ self._refreshing_channel_choices = False
654
+
655
+ def _cyto_nuclear_label_options(self) -> list[str]:
656
+ """Return available nuclear label names for nuclear-only cyto models."""
657
+ if getattr(self, "_nuclear_enabled", None) is None:
658
+ return []
659
+ if not self._nuclear_enabled.isChecked():
660
+ return []
661
+ nuclear_model = self._nuclear_model_combo.currentText().strip()
662
+ nuclear_channel = self._nuclear_channel_combo.currentText().strip()
663
+ if not nuclear_model or nuclear_model.startswith("("):
664
+ return []
665
+ if not nuclear_channel or nuclear_channel.startswith("("):
666
+ return []
667
+ return [f"{nuclear_channel}_{nuclear_model}_nuc_labels"]
630
668
 
631
669
  def _refresh_config_viewer(self) -> None:
632
670
  """Refresh the quantification preview viewer shim."""
@@ -663,6 +701,8 @@ class BatchTab(QWidget):
663
701
  self._nuclear_settings_widgets.clear()
664
702
  self._nuclear_settings_meta.clear()
665
703
  if not model_name or model_name.startswith("("):
704
+ self._refresh_channel_choices()
705
+ self._refresh_config_viewer()
666
706
  return
667
707
  model = self._segmentation_backend.get_model(model_name)
668
708
  settings = model.list_settings()
@@ -671,17 +711,23 @@ class BatchTab(QWidget):
671
711
  item.get("key", item.get("label", "")): item for item in settings
672
712
  }
673
713
  self._nuclear_settings_values = _defaults_from_settings(settings)
714
+ self._refresh_channel_choices()
715
+ self._refresh_config_viewer()
674
716
 
675
717
  def _update_cyto_settings(self) -> None:
676
718
  """Refresh cytoplasmic model settings from the selected model."""
677
719
  model_name = self._cyto_model_combo.currentText()
678
720
  self._cyto_settings_widgets.clear()
679
721
  self._cyto_settings_meta.clear()
722
+ self._cyto_nuclear_optional = False
723
+ self._cyto_nuclear_from_labels = False
724
+ self._cyto_requires_cyto_channel = True
725
+ self._cyto_supports_nuclear_input = False
680
726
  if not model_name or model_name.startswith("("):
681
- self._cyto_nuclear_combo.setEnabled(False)
682
727
  if hasattr(self, "_cyto_nuclear_label"):
683
728
  self._cyto_nuclear_label.setText("Nuclear channel")
684
- self._cyto_nuclear_optional = False
729
+ self._refresh_channel_choices()
730
+ self._update_processing_state()
685
731
  return
686
732
  model = self._segmentation_backend.get_model(model_name)
687
733
  settings = model.list_settings()
@@ -691,20 +737,30 @@ class BatchTab(QWidget):
691
737
  }
692
738
  self._cyto_settings_values = _defaults_from_settings(settings)
693
739
  modes = model.cytoplasmic_input_modes()
694
- supports_nuclear = "nuclear+cytoplasmic" in modes
695
- if supports_nuclear:
740
+ if modes == ["nuclear"]:
741
+ if hasattr(self, "_cyto_nuclear_label"):
742
+ self._cyto_nuclear_label.setText("Nuclear labels layer")
743
+ self._cyto_nuclear_from_labels = True
744
+ self._cyto_requires_cyto_channel = False
745
+ self._cyto_supports_nuclear_input = True
746
+ elif "nuclear+cytoplasmic" in modes:
696
747
  optional = model.cytoplasmic_nuclear_optional()
697
748
  suffix = "optional" if optional else "required"
698
749
  if hasattr(self, "_cyto_nuclear_label"):
699
750
  self._cyto_nuclear_label.setText(f"Nuclear channel ({suffix})")
700
- self._cyto_nuclear_combo.setEnabled(True)
701
751
  self._cyto_nuclear_optional = optional
752
+ self._cyto_supports_nuclear_input = True
702
753
  else:
703
754
  if hasattr(self, "_cyto_nuclear_label"):
704
755
  self._cyto_nuclear_label.setText("Nuclear channel")
705
- self._cyto_nuclear_combo.setEnabled(False)
706
- self._cyto_nuclear_optional = False
707
756
  self._refresh_channel_choices()
757
+ self._update_processing_state()
758
+
759
+ def _on_nuclear_channel_changed(self, _text: str) -> None:
760
+ """Refresh dependent combos when the nuclear channel changes."""
761
+ if self._cyto_nuclear_from_labels:
762
+ self._refresh_channel_choices()
763
+ self._refresh_config_viewer()
708
764
 
709
765
  def _update_spot_settings(self) -> None:
710
766
  """Refresh spot detector settings from the selected detector."""
@@ -832,12 +888,18 @@ class BatchTab(QWidget):
832
888
  nuclear_enabled = self._nuclear_enabled.isChecked()
833
889
  cyto_enabled = self._cyto_enabled.isChecked()
834
890
  spot_enabled = self._spots_enabled.isChecked()
891
+ if self._cyto_nuclear_from_labels:
892
+ self._refresh_channel_choices()
835
893
  self._nuclear_model_combo.setEnabled(nuclear_enabled)
836
894
  self._nuclear_channel_combo.setEnabled(nuclear_enabled)
837
895
  self._nuclear_settings_button.setEnabled(nuclear_enabled)
838
896
  self._cyto_model_combo.setEnabled(cyto_enabled)
839
- self._cyto_channel_combo.setEnabled(cyto_enabled)
840
- self._cyto_nuclear_combo.setEnabled(cyto_enabled)
897
+ self._cyto_channel_combo.setEnabled(
898
+ cyto_enabled and self._cyto_requires_cyto_channel
899
+ )
900
+ self._cyto_nuclear_combo.setEnabled(
901
+ cyto_enabled and self._cyto_supports_nuclear_input
902
+ )
841
903
  self._cyto_settings_button.setEnabled(cyto_enabled)
842
904
  self._spot_detector_combo.setEnabled(spot_enabled)
843
905
  self._spot_settings_button.setEnabled(spot_enabled)
@@ -886,10 +948,33 @@ class BatchTab(QWidget):
886
948
  spot_detector = None
887
949
 
888
950
  cyto_model = None
951
+ cyto_model_obj = None
889
952
  if self._cyto_enabled.isChecked() and self._cyto_model_combo.isEnabled():
890
953
  cyto_model = self._cyto_model_combo.currentText().strip()
891
954
  if cyto_model.startswith("("):
892
955
  cyto_model = None
956
+ elif cyto_model:
957
+ cyto_model_obj = self._segmentation_backend.get_model(cyto_model)
958
+
959
+ if cyto_model_obj is not None and self._cyto_requires_nuclear(cyto_model_obj):
960
+ cyto_nuclear_choice = self._cyto_nuclear_combo.currentText().strip()
961
+ if (
962
+ not cyto_nuclear_choice
963
+ or cyto_nuclear_choice == "(none)"
964
+ or cyto_nuclear_choice == "(no nuclear labels)"
965
+ ):
966
+ self._notify("Select the required cytoplasmic nuclear input.")
967
+ return
968
+
969
+ if (
970
+ cyto_model_obj.cytoplasmic_input_modes() == ["nuclear"]
971
+ and not nuclear_model
972
+ ):
973
+ self._notify(
974
+ "Selected cytoplasmic model requires nuclear labels. "
975
+ "Enable nuclear segmentation first."
976
+ )
977
+ return
893
978
 
894
979
  quant_features = (
895
980
  list(self._quant_tab._feature_configs)
@@ -1008,7 +1093,8 @@ class BatchTab(QWidget):
1008
1093
  channel=self._cyto_channel_combo.currentText(),
1009
1094
  nuclear_channel=(
1010
1095
  ""
1011
- if self._cyto_nuclear_combo.currentText().strip() == "(none)"
1096
+ if self._cyto_nuclear_combo.currentText().strip()
1097
+ in {"(none)", "(no nuclear labels)"}
1012
1098
  else self._cyto_nuclear_combo.currentText()
1013
1099
  ),
1014
1100
  settings=cyto_settings,
@@ -1057,6 +1143,8 @@ class BatchTab(QWidget):
1057
1143
  if not job.cytoplasmic.nuclear_channel:
1058
1144
  if self._cyto_nuclear_combo.findText("(none)") != -1:
1059
1145
  self._set_combo_value(self._cyto_nuclear_combo, "(none)")
1146
+ elif self._cyto_nuclear_combo.findText("(no nuclear labels)") != -1:
1147
+ self._set_combo_value(self._cyto_nuclear_combo, "(no nuclear labels)")
1060
1148
  else:
1061
1149
  self._set_combo_value(
1062
1150
  self._cyto_nuclear_combo, job.cytoplasmic.nuclear_channel
@@ -1082,6 +1170,16 @@ class BatchTab(QWidget):
1082
1170
  self._refresh_spot_channel_choices()
1083
1171
  self._refresh_config_viewer()
1084
1172
 
1173
+ @staticmethod
1174
+ def _cyto_requires_nuclear(model) -> bool:
1175
+ """Return whether the selected cytoplasmic model requires nuclear input."""
1176
+ modes = model.cytoplasmic_input_modes()
1177
+ if modes == ["nuclear"]:
1178
+ return True
1179
+ if "nuclear+cytoplasmic" not in modes:
1180
+ return False
1181
+ return not model.cytoplasmic_nuclear_optional()
1182
+
1085
1183
  def _save_profile(self) -> None:
1086
1184
  """Save the current configuration to a JSON profile."""
1087
1185
  path, _ = QFileDialog.getSaveFileName(
@@ -651,6 +651,9 @@ class SpotsTab(QWidget):
651
651
  if mask is not None:
652
652
  filtered_mask = self._apply_size_filter(mask)
653
653
  self._add_labels_layer(layer, filtered_mask, detector_name)
654
+ debug_images = result.get("debug_images")
655
+ if isinstance(debug_images, dict):
656
+ self._add_debug_image_layers(layer, detector_name, debug_images)
654
657
 
655
658
  def _add_labels_layer(self, source_layer, mask, detector_name: str) -> None:
656
659
  """Add a labels layer for the detector mask."""
@@ -694,6 +697,57 @@ class SpotsTab(QWidget):
694
697
  labels_layer.metadata = merged_metadata
695
698
  labels_layer.contour = 1
696
699
 
700
+ def _add_debug_image_layers(
701
+ self,
702
+ source_layer,
703
+ detector_name: str,
704
+ debug_images: dict,
705
+ ) -> None:
706
+ """Add debug image layers emitted by a detector run."""
707
+ if self._viewer is None:
708
+ return
709
+ source_name = getattr(source_layer, "name", "") if source_layer is not None else ""
710
+ source_name = source_name.strip() if isinstance(source_name, str) else ""
711
+
712
+ for debug_key, debug_image in debug_images.items():
713
+ if debug_image is None:
714
+ continue
715
+ image_data = np.asarray(debug_image)
716
+ if image_data.size == 0:
717
+ continue
718
+ layer_name = f"{detector_name}_{debug_key}"
719
+ if source_name:
720
+ layer_name = f"{source_name}_{layer_name}"
721
+ if layer_name in self._viewer.layers:
722
+ self._viewer.layers.remove(layer_name)
723
+
724
+ metadata = {"task": "spots", "debug": True}
725
+ image_layer = None
726
+ if Image is not None and hasattr(self._viewer, "add_layer"):
727
+ image_layer = Image(
728
+ image_data,
729
+ name=layer_name,
730
+ metadata=metadata,
731
+ )
732
+ added_layer = self._viewer.add_layer(image_layer)
733
+ if added_layer is not None:
734
+ image_layer = added_layer
735
+ elif hasattr(self._viewer, "add_image"):
736
+ try:
737
+ image_layer = self._viewer.add_image(
738
+ image_data,
739
+ name=layer_name,
740
+ metadata=metadata,
741
+ )
742
+ except TypeError:
743
+ image_layer = self._viewer.add_image(
744
+ image_data,
745
+ name=layer_name,
746
+ )
747
+
748
+ if image_layer is not None:
749
+ image_layer.visible = True
750
+
697
751
  def _apply_size_filter(self, mask: np.ndarray) -> np.ndarray:
698
752
  """Filter spots by size based on min/max settings.
699
753
 
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "rmp",
3
+ "description": "RMP spot detector implementation",
4
+ "version": "0.1.0",
5
+ "order": 0,
6
+ "settings": [
7
+ {
8
+ "key": "denoising_kernel_length",
9
+ "label": "Denoising kernel length",
10
+ "type": "int",
11
+ "min": 2,
12
+ "max": 9999,
13
+ "default": 2,
14
+ "enabled_by": "enable_denoising"
15
+ },
16
+ {
17
+ "key": "extraction_kernel_length",
18
+ "label": "Extraction kernel length",
19
+ "type": "int",
20
+ "min": 3,
21
+ "max": 9999,
22
+ "default": 10
23
+ },
24
+ {
25
+ "key": "angle_spacing",
26
+ "label": "Angle spacing",
27
+ "type": "int",
28
+ "min": 1,
29
+ "max": 10,
30
+ "default": 5
31
+ },
32
+ {
33
+ "key": "manual_threshold",
34
+ "label": "Manual threshold",
35
+ "type": "float",
36
+ "decimals": 2,
37
+ "min": 0.0,
38
+ "max": 1.0,
39
+ "default": 0.05,
40
+ "disabled_by": "auto_threshold"
41
+ },
42
+ {
43
+ "key": "auto_threshold",
44
+ "label": "Auto threshold",
45
+ "type": "bool",
46
+ "default": true
47
+ },
48
+ {
49
+ "key": "enable_denoising",
50
+ "label": "Enable denoising",
51
+ "type": "bool",
52
+ "default": true
53
+ }
54
+ ]
55
+ }