cardio 2026.1.2__py3-none-any.whl → 2026.1.3__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.
cardio/__init__.py CHANGED
@@ -26,4 +26,4 @@ __all__ = [
26
26
  "window_level",
27
27
  ]
28
28
 
29
- __version__ = "2026.1.2"
29
+ __version__ = "2026.1.3"
cardio/logic.py CHANGED
@@ -338,10 +338,18 @@ class Logic:
338
338
 
339
339
  def _apply_current_mpr_settings(self, active_volume, frame):
340
340
  """Apply current slice positions and window/level to MPR actors."""
341
+ from .orientation import IndexOrder
342
+
341
343
  # Apply slice positions
342
344
  origin = getattr(self.server.state, "mpr_origin", [0.0, 0.0, 0.0])
343
345
  rotation_sequence, rotation_angles = self._get_visible_rotation_data()
344
346
 
347
+ # VTK needs origin in ITK convention - convert if necessary
348
+ current_convention = self.scene.mpr_rotation_sequence.metadata.index_order
349
+ if current_convention == IndexOrder.ROMA:
350
+ # Convert Roma to ITK: swap X and Z
351
+ origin = [origin[2], origin[1], origin[0]]
352
+
345
353
  active_volume.update_slice_positions(
346
354
  frame,
347
355
  origin,
@@ -659,6 +667,11 @@ class Logic:
659
667
 
660
668
  self.server.state.mpr_rotation_data = updated_data
661
669
 
670
+ # Transform mpr_origin: swap X and Z (indices 0 and 2)
671
+ mpr_origin = getattr(self.server.state, "mpr_origin", None)
672
+ if mpr_origin is not None and len(mpr_origin) == 3:
673
+ self.server.state.mpr_origin = [mpr_origin[2], mpr_origin[1], mpr_origin[0]]
674
+
662
675
  self.scene.mpr_rotation_sequence.metadata.index_order = new_convention
663
676
 
664
677
  def _initialize_clipping_state(self):
@@ -744,6 +757,8 @@ class Logic:
744
757
 
745
758
  # Initialize origin to volume center (in LPS coordinates)
746
759
  try:
760
+ from .orientation import IndexOrder
761
+
747
762
  current_frame = getattr(self.server.state, "frame", 0)
748
763
  volume_actor = active_volume.actors[current_frame]
749
764
  image_data = volume_actor.GetMapper().GetInput()
@@ -752,7 +767,16 @@ class Logic:
752
767
  # Set origin to volume center if it's at default [0,0,0]
753
768
  current_origin = getattr(self.server.state, "mpr_origin", [0.0, 0.0, 0.0])
754
769
  if current_origin == [0.0, 0.0, 0.0]:
755
- self.server.state.mpr_origin = list(center)
770
+ # VTK returns center in ITK convention (X=L, Y=P, Z=S)
771
+ # Transform to current convention if needed
772
+ center_list = list(center)
773
+ if (
774
+ self.scene.mpr_rotation_sequence.metadata.index_order
775
+ == IndexOrder.ROMA
776
+ ):
777
+ # Convert ITK -> Roma: swap X and Z
778
+ center_list = [center_list[2], center_list[1], center_list[0]]
779
+ self.server.state.mpr_origin = center_list
756
780
  except (RuntimeError, IndexError) as e:
757
781
  print(f"Error: Cannot get center for volume '{active_volume_label}': {e}")
758
782
  return
@@ -821,6 +845,7 @@ class Logic:
821
845
 
822
846
  def update_slice_positions(self, **kwargs):
823
847
  """Update MPR slice positions when sliders change."""
848
+ from .orientation import IndexOrder
824
849
 
825
850
  if not getattr(self.server.state, "mpr_enabled", False):
826
851
  return
@@ -845,6 +870,12 @@ class Logic:
845
870
 
846
871
  rotation_sequence, rotation_angles = self._get_visible_rotation_data()
847
872
 
873
+ # VTK needs origin in ITK convention - convert if necessary
874
+ current_convention = self.scene.mpr_rotation_sequence.metadata.index_order
875
+ if current_convention == IndexOrder.ROMA:
876
+ # Convert Roma to ITK: swap X and Z
877
+ origin = [origin[2], origin[1], origin[0]]
878
+
848
879
  # Update slice positions with rotation
849
880
  active_volume.update_slice_positions(
850
881
  current_frame,
@@ -921,6 +952,8 @@ class Logic:
921
952
 
922
953
  def update_mpr_rotation(self, **kwargs):
923
954
  """Update MPR views when rotation changes."""
955
+ from .orientation import IndexOrder
956
+
924
957
  if not getattr(self.server.state, "mpr_enabled", False):
925
958
  return
926
959
 
@@ -944,6 +977,12 @@ class Logic:
944
977
 
945
978
  rotation_sequence, rotation_angles = self._get_visible_rotation_data()
946
979
 
980
+ # VTK needs origin in ITK convention - convert if necessary
981
+ current_convention = self.scene.mpr_rotation_sequence.metadata.index_order
982
+ if current_convention == IndexOrder.ROMA:
983
+ # Convert Roma to ITK: swap X and Z
984
+ origin = [origin[2], origin[1], origin[0]]
985
+
947
986
  # Update slice positions with rotation
948
987
  active_volume.update_slice_positions(
949
988
  current_frame,
cardio/rotation.py CHANGED
@@ -56,11 +56,12 @@ class RotationMetadata(pc.BaseModel):
56
56
  class RotationSequence(pc.BaseModel):
57
57
  """Complete rotation sequence.
58
58
 
59
- Both angles and axes are always stored in the current convention/units:
59
+ All data (angles, axes, and origin) are stored in the current convention/units:
60
60
  - Angles: stored in units specified by metadata.angle_units
61
61
  - Axes: stored in convention specified by metadata.index_order
62
+ - Origin: stored in axis order specified by metadata.index_order
62
63
 
63
- When convention/units change in the UI, all existing rotations are converted.
64
+ When convention/units change in the UI, all existing data is converted.
64
65
  """
65
66
 
66
67
  model_config = pc.ConfigDict(frozen=False)
@@ -69,7 +70,7 @@ class RotationSequence(pc.BaseModel):
69
70
  angles_list: list[RotationStep] = pc.Field(default_factory=list)
70
71
  mpr_origin: list[float] = pc.Field(
71
72
  default_factory=lambda: [0.0, 0.0, 0.0],
72
- description="MPR origin position [x, y, z] in LPS coordinates",
73
+ description="MPR origin position [x, y, z] in current index_order convention",
73
74
  )
74
75
 
75
76
  @pc.field_validator("mpr_origin")
@@ -123,8 +124,13 @@ class RotationSequence(pc.BaseModel):
123
124
 
124
125
  def to_toml(self) -> str:
125
126
  """Serialize to TOML using stored serialization preferences."""
127
+ from . import __version__
128
+
126
129
  data = self.model_dump(mode="json")
127
- return tk.dumps(data)
130
+ toml_str = tk.dumps(data)
131
+
132
+ version_comment = f"# Generated by cardio version {__version__}\n\n"
133
+ return version_comment + toml_str
128
134
 
129
135
  @classmethod
130
136
  def from_toml(cls, toml_content: str) -> "RotationSequence":
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: cardio
3
- Version: 2026.1.2
3
+ Version: 2026.1.3
4
4
  Summary: A simple web-based viewer for 3D and 4D ('cine') medical imaging data.
5
5
  Keywords: Medical,Imaging,3D,4D,Visualization
6
6
  Author: Davis Marc Vigneault
@@ -75,7 +75,7 @@ $ uv init
75
75
  $ uv add cardio
76
76
  $ . ./.venv/bin/activate
77
77
  (project) cardio --version
78
- cardio 2026.1.2
78
+ cardio 2026.1.3
79
79
  ```
80
80
 
81
81
  ### Developing
@@ -1,4 +1,4 @@
1
- cardio/__init__.py,sha256=1g0tFk2oSQTiQO_WcuDGxiGlutHUn7H4yLY3iMyFIm8,601
1
+ cardio/__init__.py,sha256=07x-I5z0KKgiJwm-tqtY1qZTh5YKg32IScpzzFVEeno,601
2
2
  cardio/app.py,sha256=h-Xh57Txw3Hom-fDv08YxjXzovuIxY8_5qPy6MJThZs,1354
3
3
  cardio/assets/bone.toml,sha256=vv8uVYSHIoKuHkNCoBOkGe2_qoEbXMvQO6ypm3mMOtA,675
4
4
  cardio/assets/vascular_closed.toml,sha256=XtaZS_Zd6NSAtY3ZlUfiog3T86u9Ii0oSutU2wBQy78,1267
@@ -6,13 +6,13 @@ cardio/assets/vascular_open.toml,sha256=1M3sV1IGt3zh_3vviysKEk9quKfjF9xUBcIq3kxV
6
6
  cardio/assets/xray.toml,sha256=siPem0OZ2OkWH0e5pizftpItJKGJgxKJ_S2K0316ubQ,693
7
7
  cardio/blend_transfer_functions.py,sha256=fkLDYGMj_QXYs0vmXYT_B1tgTfl3YICLYimtWlxrmbQ,2958
8
8
  cardio/color_transfer_function.py,sha256=uTyPdwxi0HjR4wm418cQN9-q9SspkkeqvLNqXaF3zzg,792
9
- cardio/logic.py,sha256=XR7e5sb8vm7t3himwxOBR3OBP2QZxadb2hoGhqSTwiw,42877
9
+ cardio/logic.py,sha256=AuKQZ_ACTRrtB4oxPuaKYjajsoA3yoszw7GnZK11UdQ,44721
10
10
  cardio/mesh.py,sha256=wYdU0BU84eXrrHp0U0VLwYW7ZpRJ6GbT5Kl_-K6CBzY,9356
11
11
  cardio/object.py,sha256=98A32VpFR4UtVqW8dZsRJR13VVyUoJJ20uoOZBgN4js,6168
12
12
  cardio/orientation.py,sha256=xsWAFPwf3-toT4XFYyGiZJkcGFQ2uz3eLxpC1_i6yZ8,6266
13
13
  cardio/piecewise_function.py,sha256=X-_C-BVStufmFjfF8IbkqKk9Xw_jh00JUmjC22ZqeqQ,764
14
14
  cardio/property_config.py,sha256=YrWIyCoSAfJPigkhriIQybQpJantGnXjT4nJfrtIJco,1689
15
- cardio/rotation.py,sha256=IoIoyhAdtBB90myY2GezXMjPbQ-JLMRTgQCpFSVZKBg,5096
15
+ cardio/rotation.py,sha256=BCh3x347AdnsRMK-6-N9o12jnGiwg7vG-DuJirzn468,5341
16
16
  cardio/scene.py,sha256=FrUQIJrBR_jPrW7utOI3uDbaUxjmZEapzVTEVIgEWaA,15365
17
17
  cardio/screenshot.py,sha256=l8bLgxnU5O0FlnmsyVAzKwM9Y0401IzcdnDP0WqFSTY,640
18
18
  cardio/segmentation.py,sha256=KT1ClgultXyGpZDdpYxor38GflY7uCgRzfA8JEGQVaU,6642
@@ -24,7 +24,7 @@ cardio/volume.py,sha256=sdQ7gtX4jQDD9U8U95xD9LIV_1hpykj8NTCo1_aIKVM,15035
24
24
  cardio/volume_property.py,sha256=-EUMV9sWCaetgGjnqIWxPp39qrxEZKTNDJ5GHUgLMlk,1619
25
25
  cardio/volume_property_presets.py,sha256=4-hjo-dukm5sMMmWidbWnVXq0IN4sWpBnDITY9MqUFg,1625
26
26
  cardio/window_level.py,sha256=XMkwLAHcmsEYcI0SoHySQZvptq4VwX2gj--ps3hV8AQ,784
27
- cardio-2026.1.2.dist-info/WHEEL,sha256=4n27za1eEkOnA7dNjN6C5-O2rUiw6iapszm14Uj-Qmk,79
28
- cardio-2026.1.2.dist-info/entry_points.txt,sha256=xRd1otqKtW9xmidJ4CFRX1V9KWmsBbtxrgMtDColq3w,40
29
- cardio-2026.1.2.dist-info/METADATA,sha256=STZ2oobYKN12HQD_jH_w0HANJ_b4L-PM9bB41eIJZ0Q,3518
30
- cardio-2026.1.2.dist-info/RECORD,,
27
+ cardio-2026.1.3.dist-info/WHEEL,sha256=4n27za1eEkOnA7dNjN6C5-O2rUiw6iapszm14Uj-Qmk,79
28
+ cardio-2026.1.3.dist-info/entry_points.txt,sha256=xRd1otqKtW9xmidJ4CFRX1V9KWmsBbtxrgMtDColq3w,40
29
+ cardio-2026.1.3.dist-info/METADATA,sha256=BE_DMgEZ959_kbfIfIMTbDs-hIhjep_SGFSmoeMm3Ag,3518
30
+ cardio-2026.1.3.dist-info/RECORD,,