Code source de palm_tracer.UI.Viewer3DWidget
"""
Widget d'affichage 3D pour Napari permettant de charger un fichier CSV et de visualiser les points en 3D
avec ajustements interactifs des échelles et de la taille des points.
Ce widget ajoute dans le dock de Napari :
- un bouton de chargement de fichier CSV,
- trois champs pour contrôler les échelles en XY et Z et la taille des points,
- une option permettant d'exclure les points avec intensité nulle,
- un calque Napari Points mis à jour dynamiquement.
Le CSV doit contenir les colonnes ``"X"``, ``"Y"``, ``"Z"`` et ``"Integrated Intensity"``.
"""
from pathlib import Path
import napari
import numpy as np
import pandas as pd
from napari.utils.notifications import show_warning
from qtpy.QtCore import Qt
from qtpy.QtWidgets import QFileDialog, QFormLayout, QPushButton, QWidget
from palm_tracer.Settings.Types import CheckBox, SpinFloat
[docs]
class Viewer3DWidget(QWidget):
"""
Widget d'affichage 3D pour un viewer Napari.
Ce widget permet :
- de charger un fichier CSV contenant des coordonnées 3D
- d'ajuster l'échelle XY et Z
- de modifier la taille des points
- d'activer ou non la suppression des points d'intensité nulle
- de créer ou mettre à jour un calque de type :class:`napari.layers.Points`.
**Remarque** : peut être lancé directement avec la commande ``napari -w palm-tracer "Viewer 3D"``
:param viewer: Instance du viewer Napari où sera ajouté le calque 3D.
:type viewer: :class:`napari.Viewer`
"""
##################################################
def __init__(self, viewer: napari.Viewer):
"""
Initialise le widget et configure l'interface graphique (boutons, champs numériques, checkbox).
La création du calque Napari se fait plus tard dans :meth:`update_layer` lorsqu'un fichier CSV est chargé.
:param viewer: Viewer Napari cible.
:type viewer: :class:`napari.Viewer`
"""
super().__init__()
self.viewer = viewer
self.points_layer = None
self.data = pd.DataFrame() # DataFrame d'origine
self.z_scale = 1.0
self._widget = QWidget()
layout = QFormLayout(self._widget)
layout.setAlignment(Qt.AlignmentFlag.AlignTop) # Définir l'alignement du calque en haut.
self.setLayout(layout)
# Bouton de chargement CSV
btn = QPushButton("Load CSV")
btn.clicked.connect(self.load_csv)
layout.addWidget(btn)
# Spinbox taille des points
self.size_spin = SpinFloat("Point Size", "", 0.5, [0.1, 10], 0.1, 1)
self.size_spin.attach_to_form(layout)
self.size_spin.connect(self.update_layer)
# Spinbox échelle Z
self.xy_scale_spin = SpinFloat("XY Scale", "", 1.0, [0.0, 1000], 1.0, 1)
self.xy_scale_spin.attach_to_form(layout)
self.xy_scale_spin.connect(self.update_layer)
# Spinbox échelle Z
self.z_scale_spin = SpinFloat("Z Scale", "", 1.0, [0.0, 1000], 1.0, 1)
self.z_scale_spin.attach_to_form(layout)
self.z_scale_spin.connect(self.update_layer)
self.outliers = CheckBox("Remove Outliers", "", False)
self.outliers.attach_to_form(layout)
self.outliers.connect(self.update_layer)
##################################################
[docs]
def load_csv(self):
"""
Ouvre une boîte de dialogue pour sélectionner un fichier ``.csv`` et charge les données associées dans un :class:`pandas.DataFrame`.
Le fichier doit contenir les colonnes : ``"X"``, ``"Y"``, ``"Z"``, ``"Integrated Intensity"``
Si un calque existe déjà, il est supprimé avant la création du nouveau.
Cette méthode déclenche ensuite :meth:`update_layer` pour créer le calque 3D.
"""
path, _ = QFileDialog.getOpenFileName(self, "Load CSV", ".", "Fichiers CSV (*.csv)")
if not path or not Path(path).is_file(): return
df = pd.read_csv(path)
if not all(col in df.columns for col in ["X", "Y", "Z", "Integrated Intensity"]):
show_warning("The file must contain the columns X, Y, Z, and Integrated Intensity.")
return
self.data = df.copy()
# Supprimer le calque précédent s'il existe, (le nombre de points peu changer)
if self.points_layer is not None:
try: self.viewer.layers.remove(self.points_layer)
except Exception as e: show_warning(F"Error when deleting the old layer: {e}")
self.points_layer = None
self.update_layer()
##################################################
[docs]
def update_layer(self):
"""
Crée ou mets à jour le calque de points 3D dans le viewer Napari.
Transformations appliquées :
- réorganisation des coordonnées sous la forme ``(Z, Y, X)``
- mise à l'échelle par les valeurs choisies dans les widgets
- suppression éventuelle des points dont ``Integrated Intensity == 0``
- mise à jour dynamique du calque existant ou création d'un nouveau
Si aucune donnée n'est encore chargée, la méthode ne fait rien.
"""
if self.data.empty: return
scale_xy = self.xy_scale_spin.value
scale_z = self.z_scale_spin.value
coords = self.data[["Z", "Y", "X"]].to_numpy(dtype=float, copy=True)
coords *= np.array([scale_z, scale_xy, scale_xy], dtype=coords.dtype)
if self.outliers.value: coords = coords[self.data["Integrated Intensity"] != 0]
# Ajout ou mise à jour du calque
if self.points_layer is None:
self.points_layer = self.viewer.add_points(coords, size=self.size_spin.value, name="Points 3D", ndim=3)
else:
self.points_layer.data = coords
self.points_layer.size = self.size_spin.value
##################################################
[docs]
def create_viewer3d() -> napari.Viewer: # pragma: no cover — Aucun lancement de fenêtre sans controle en CI
"""
Crée une nouvelle fenêtre Napari 3D, sans menu,
et y ajoute le Viewer3DWidget docké à droite.
Cette fonction NE lance PAS napari.run() : elle est faite
pour être appelée depuis un plugin, donc dans une appli Qt déjà active.
"""
viewer = napari.Viewer(ndisplay=3) # . Crée le viewer 3D napari
viewer.title = "3D Viewer" # . Modifier le titre de la fenêtre
viewer.window.main_menu.setVisible(False) # . Cacher la barre de menu
widget = Viewer3DWidget(viewer) # . Crée le widget en lui passant le viewer
viewer.window.add_dock_widget(widget, name="Viewer 3D", area="right") # L'ajoute comme dock widget dans la fenêtre napari
return viewer
##################################################
[docs]
def open_viewer3d(_viewer: "napari.viewer.Viewer" = None, ) -> QWidget: # pragma: no cover — Aucun lancement de fenêtre sans controle en CI
"""
Callable utilisé par Napari pour le menu Plugins > PALM Tracer > Viewer 3D.
- Ignore le viewer courant.
- Crée une nouvelle fenêtre Napari 3D dédiée.
- Retourne un QWidget stub (caché) juste pour satisfaire
l'API "widget plugin" de Napari.
"""
# Crée la nouvelle fenêtre 3D
create_viewer3d()
# Stub minimal pour Napari (sera docké, mais caché)
stub = QWidget()
stub.hide()
return stub
##################################################
if __name__ == "__main__":
import napari
_v = create_viewer3d()
napari.run() # Lance la boucle Qt gérée par Napari