Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add pre-commit with ruff and typos, run --all-files #13

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: check-yaml
args: [--allow-multiple-documents]
- id: end-of-file-fixer
exclude: '.*\.nrrd$'
- id: trailing-whitespace
exclude: '.*\.nrrd$'
- id: check-executables-have-shebangs

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.5
hooks:
- id: ruff
args: [--fix]

- repo: https://github.com/crate-ci/typos
rev: v1.24.5
hooks:
- id: typos
exclude: '.*\.nrrd$'
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ print("First segment info:\n" + json.dumps(segment0, sort_keys=False, indent=4))
#### Extract segments by terminology

Example for getting a 3D [NRRD](SegmentationFileFormat.md) file that has label values assigned based on [standard terminology codes](UsingStandardTerminology.md).
Terminology is a `dict` that must specify `category` and `type` codes and may optionally also specify `typeModifier`, `anatomicRegion`, and `anatomicRegionModifier`. Each code is specifed by a triplet of "coding scheme designator", "code value", "code meaning" in a list.
Terminology is a `dict` that must specify `category` and `type` codes and may optionally also specify `typeModifier`, `anatomicRegion`, and `anatomicRegionModifier`. Each code is specified by a triplet of "coding scheme designator", "code value", "code meaning" in a list.

Coding scheme designator is typically `SCT` (SNOMED-CT) for clinical images. You can find codes in the [SNOMED-CT browser](https://browser.ihtsdotools.org/). When code exists for "entire X" and "structure of X" then always use the "structure" code ("entire" code has a very strict meaning that is rarely applicable in practice).

Expand Down Expand Up @@ -227,7 +227,7 @@ for segmentId in segments:
List of available nodes can be retrieved using `node_names` and `node_ids`functions:

```python
# Retreve node names of all images
# Retrieve node names of all images
slicerio.server.node_names(class_name="vtkMRMLVolumeNode")

# Retrieve all node IDs
Expand Down
4 changes: 2 additions & 2 deletions UsingStandardTerminology.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ Segmentation tools usually provide a labelmap volume as output, with each label

To avoid these issues, standard terms can be used to specify what each segment represents. A standard term has well-defined meaning, thus allowing aggregating data from many sources, and since they have to be selected from a predefined list, there is no risk of typos or inconsistencies that often occur when typing free text.

Terminology is a `dict` that must specify `category` and `type` codes and may optionally also specify `typeModifier`, `anatomicRegion`, and `anatomicRegionModifier`. Each code is specifed by a triplet of "coding scheme designator", "code value", "code meaning" in a list.
Terminology is a `dict` that must specify `category` and `type` codes and may optionally also specify `typeModifier`, `anatomicRegion`, and `anatomicRegionModifier`. Each code is specified by a triplet of "coding scheme designator", "code value", "code meaning" in a list.
`Coding scheme designator` is typically `SCT` (SNOMED-CT) for clinical images. You can find codes in the [SNOMED-CT browser](https://browser.ihtsdotools.org/). `Code value` is a unique string within the coding scheme, most commonly an integer number. `Code meaning` (third component of codes, such as "Anatomical Structure", "Ribs", "Right") is informational only, it can be used for troubleshooting or displayed to the user, but it is ignored in information processing (e.g., two codes match if their coding scheme designator and code value are the same even if code meaning is different).

According to the DICOM standard, content of a segment is specified by its `category` (such as "Anatomical structure", "Physical object", "Body substance", ...) and `type` (such as "Liver" or "Cardiac Pacemaker", ...). Optionally a modifier (`typeModifier`, such as "left" or "right") and anatomical location (`anatomicRegion` and `anatomicRegionModifier`) can be specified as well. Each of these are specifed by a standard code, which is a triplet of "coding scheme designator", "code value", and "code meaning":
According to the DICOM standard, content of a segment is specified by its `category` (such as "Anatomical structure", "Physical object", "Body substance", ...) and `type` (such as "Liver" or "Cardiac Pacemaker", ...). Optionally a modifier (`typeModifier`, such as "left" or "right") and anatomical location (`anatomicRegion` and `anatomicRegionModifier`) can be specified as well. Each of these are specified by a standard code, which is a triplet of "coding scheme designator", "code value", and "code meaning":
- `Coding scheme designator` is a string identifying the terminology. It is typically set to `SCT` (SNOMED-CT) for clinical images. However, Uberon (`UBERON`), Foundational Model of Anatomy (`FMA`) and [others](https://dicom.nema.org/medical/dicom/current/output/chtml/part16/chapter_8.html) may be used, too.
- `Code value` is a unique string within the coding scheme, most commonly an integer number.
- `Code meaning` is a human-readable meaning of the term (such as "Anatomical Structure", "Ribs", "Right"). It is informational only: it can be used for troubleshooting or displayed to the user, but it is ignored in information processing. For example, two codes match if and only if their coding scheme designator and code value are the same; it does not matter the if code meaning values are the same.
Expand Down
2 changes: 1 addition & 1 deletion slicerio/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""slicerio package -- Python utilities for readin and writing 3D Slicer supporte data sets.
"""slicerio package -- Python utilities for readin and writing 3D Slicer supported data sets.

-----------
Quick Start
Expand Down
29 changes: 14 additions & 15 deletions slicerio/segmentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ def read_segmentation(filename, skip_voxels=False):
Segmentation_ConversionParameters:=Collapse labelmaps|1|Merge the labelmaps into as few shared labelmaps as possible 1 = created labelmaps will be shared if possible without overwriting each other.&Compute surface normals|1|Compute surface normals. 1 (default) = surface normals are computed. 0 = surface normals are not computed (slightly faster but produces less smooth surface display).&Crop to reference image geometry|0|Crop the model to the extent of reference geometry. 0 (default) = created labelmap will contain the entire model. 1 = created labelmap extent will be within reference image extent.&Decimation factor|0.0|Desired reduction in the total number of polygons. Range: 0.0 (no decimation) to 1.0 (as much simplification as possible). Value of 0.8 typically reduces data set size by 80% without losing too much details.&Default slice thickness|0.0|Default thickness for contours if slice spacing cannot be calculated.&End capping|1|Create end cap to close surface inside contours on the top and bottom of the structure.\n0 = leave contours open on surface exterior.\n1 (default) = close surface by generating smooth end caps.\n2 = close surface by generating straight end caps.&Fractional labelmap oversampling factor|1|Determines the oversampling of the reference image geometry. All segments are oversampled with the same value (value of 1 means no oversampling).&Joint smoothing|0|Perform joint smoothing.&Oversampling factor|1|Determines the oversampling of the reference image geometry. If it's a number, then all segments are oversampled with the same value (value of 1 means no oversampling). If it has the value "A", then automatic oversampling is calculated.&Reference image geometry|3.0468759536743195;0;0;-193.0959930419922;0;3.0468759536743195;0;-216.39599609374994;0;0;9.999999999999998;-340.24999999999994;0;0;0;1;0;127;0;127;0;33;|Image geometry description string determining the geometry of the labelmap that is created in course of conversion. Can be copied from a volume, using the button.&Smoothing factor|-0.5|Smoothing factor. Range: 0.0 (no smoothing) to 1.0 (strong smoothing).&Threshold fraction|0.5|Determines the threshold that the closed surface is created at as a fractional value between 0 and 1.&
Segmentation_MasterRepresentation:=Binary labelmap
Segmentation_ReferenceImageExtentOffset:=0 0 0

Example header in case of overlapping segments:

NRRD0004
Expand Down Expand Up @@ -184,7 +184,7 @@ def read_segmentation(filename, skip_voxels=False):
{"name": "Threshold fraction", "value": "0.5", "description": "Determines the threshold that the closed surface is created at as a fractional value between 0 and 1."}
],
"masterRepresentation": "Binary labelmap",
"referenceImageExtentOffset": [0, 0, 0]
"referenceImageExtentOffset": [0, 0, 0]
"segments": [
{
"color": [0.992157, 0.909804, 0.619608],
Expand Down Expand Up @@ -281,7 +281,6 @@ def read_segmentation(filename, skip_voxels=False):

segments_fields = {} # map from segment index to key:value map

multiple_layers = False
spaceToLps = np.eye(4)
ijkToSpace = np.eye(4)

Expand All @@ -302,9 +301,11 @@ def read_segmentation(filename, skip_voxels=False):
continue
elif header_key == 'kinds':
if header[header_key] == ['domain', 'domain', 'domain']:
multiple_layers = False
# multiple_layers = False
pass
elif header[header_key] == ['list', 'domain', 'domain', 'domain']:
multiple_layers = True
# multiple_layers = True
pass
else:
raise IOError("kinds field must be 'domain domain domain' or 'list domain domain domain'")
continue
Expand Down Expand Up @@ -348,7 +349,7 @@ def read_segmentation(filename, skip_voxels=False):
# Segmentation_ReferenceImageExtentOffset:=0 0 0
segmentation["referenceImageExtentOffset"] = [int(i) for i in header[header_key].split(" ")]
continue

segment_match = re.match("^Segment([0-9]+)_(.+)", header_key)
if segment_match:
# Store in segment_fields (segmentation field)
Expand All @@ -358,7 +359,7 @@ def read_segmentation(filename, skip_voxels=False):
segments_fields[segment_index] = {}
segments_fields[segment_index][segment_key] = header[header_key]
continue

segmentation[header_key] = header[header_key]

# Compute voxel to physical transformation matrix
Expand Down Expand Up @@ -500,7 +501,6 @@ def write_segmentation(file, segmentation, compression_level=9, index_order=None
# [ 0. , 1. , 0. ],
# [ 0. , 0. , -1. ],
# [-1.29999542, 0. , 0. ]]))
nan_column = np.array()
space_directions = np.row_stack(([np.nan, np.nan, np.nan], space_directions))
elif dims != 3:
raise ValueError("Unsupported number of dimensions: " + str(dims))
Expand All @@ -520,7 +520,7 @@ def write_segmentation(file, segmentation, compression_level=9, index_order=None

# Add segments fields to the header

# Get list of segment IDs (needed if we need to genereate new ID)
# Get list of segment IDs (needed if we need to generate new ID)
segment_ids = set()
for output_segment_index, segment in enumerate(segmentation["segments"]):
if "id" in segment:
Expand All @@ -543,7 +543,7 @@ def write_segmentation(file, segmentation, compression_level=9, index_order=None
# Segment0_Name:=Segment_1
field_name = "Name"
value = segment[segment_key]
elif segment_key == "id":
elif segment_key == "id":
# Segment0_ID:=Segment_1
field_name = "ID"
value = segment[segment_key]
Expand Down Expand Up @@ -589,7 +589,7 @@ def write_segmentation(file, segmentation, compression_level=9, index_order=None
value = segment[segment_key]

output_header[f"Segment{output_segment_index}_{field_name}"] = value

if "id" not in segment:
# If user has not specified ID, generate a unique one
new_segment_id = generate_unique_segment_id(segment_ids)
Expand Down Expand Up @@ -689,8 +689,8 @@ def terminology_entry_matches(terminology1, terminology2):
def segment_id_from_name(segmentation, segment_name):
segments = segmentation["segments"]
for segment in segments:
if segment_name == segments["name"]:
return segments["id"]
if segment_name == segment["name"]:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this was probably wrong...?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, good catch

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please also change segments["id"] below?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

return segment["id"]
raise KeyError("Segment not found by name " + segment_name)


Expand Down Expand Up @@ -730,7 +730,7 @@ def extract_segments(segmentation, segment_names_to_label_values, minimalExtent=
output_segmentation = OrderedDict()

output_segmentation["voxels"] = output_voxels

for key in segmentation:
if key == "voxels":
continue
Expand All @@ -752,7 +752,6 @@ def extract_segments(segmentation, segment_names_to_label_values, minimalExtent=
segments = segments_from_terminology(segmentation, segment_name_to_label_value[0])
if not segments:
raise ValueError(f"Segment not found: {segment_name_to_label_value[0]}")
segment_id = segments[0]["id"]
output_segment = copy.deepcopy(segments[0])
output_label_value = segment_name_to_label_value[1]
output_segment["labelValue"] = output_label_value
Expand Down
8 changes: 4 additions & 4 deletions slicerio/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ def node_properties(name=None, id=None, class_name=None):
return properties

def node_ids(name=None, id=None, class_name=None):
"""Get list of ids of nodes availalbe on the local Slicer server.
"""Get list of ids of nodes available on the local Slicer server.
Nodes can be selected using name, id, and/or class_name.
"""
api_url = f"http://127.0.0.1:{SERVER_PORT}/slicer/mrml/ids"
Expand All @@ -117,7 +117,7 @@ def node_ids(name=None, id=None, class_name=None):
return response.json()

def node_names(name=None, id=None, class_name=None):
"""Get list of names of nodes availalbe on the local Slicer server.
"""Get list of names of nodes available on the local Slicer server.
Nodes can be selected using name, id, and/or class_name.
"""
api_url = f"http://127.0.0.1:{SERVER_PORT}/slicer/mrml/names"
Expand Down Expand Up @@ -193,13 +193,13 @@ def file_load(file_path, file_type=None, properties=None, auto_start=True, timeo
try:
response = requests.post(api_url)
retry_after_starting_server = False
except requests.exceptions.ConnectionError as e:
except requests.exceptions.ConnectionError:
if not auto_start:
raise

if retry_after_starting_server:
# Try again, with starting a server first
server_process = start_server(slicer_executable)
start_server(slicer_executable)
response = requests.post(api_url)

_report_error(response)
Expand Down
10 changes: 4 additions & 6 deletions slicerio/tests/test_segmentation.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# -*- coding: utf-8 -*-

import nrrd
import slicerio
import unittest

Expand Down Expand Up @@ -88,7 +87,7 @@ def test_extract_segments(self):
({"category": ["SCT", "123037004", "Anatomical Structure"], "type": ["SCT", "113197003", "Ribs"]}, 1),
({"category": ["SCT", "123037004", "Anatomical Structure"], "type": ["SCT", "39607008", "Lung"], "typeModifier": ["SCT", "24028007", "Right"]}, 3)
])

# Compare the two segmentations
self._assert_segmentations_equal(extracted_segmentation_by_name, extracted_segmentation_by_terminology)

Expand All @@ -101,12 +100,11 @@ def test_extract_segments(self):
self.assertEqual(len(np.where(extracted_segmentation_by_terminology["voxels"] == 4)[0]), 0) # unused label

def test_segmentation_write(self):
import numpy as np
import tempfile

input_segmentation_filepath = slicerio.get_testdata_file('Segmentation.seg.nrrd')
segmentation = slicerio.read_segmentation(input_segmentation_filepath)

# Get a temporary filename
output_segmentation_filepath = tempfile.mktemp() + '.seg.nrrd'

Expand Down Expand Up @@ -179,15 +177,15 @@ def _assert_segmentations_equal(self, segmentation1, segmentation2):
"""
import numpy as np
for key in segmentation1:
if type(segmentation1[key]) == np.ndarray or type(segmentation2[key]) == np.ndarray:
if isinstance(segmentation1[key], np.ndarray) or isinstance(segmentation2[key], np.ndarray):
self.assertTrue(np.allclose(segmentation1[key], segmentation2[key]), f"Failed for key {key}")
elif key == "segments":
for segment1, segment2 in zip(segmentation1[key], segmentation2[key]):
for segmentAttribute in segment1:
self.assertEqual(segment1[segmentAttribute], segment2[segmentAttribute], f"Failed for key {key}[{segmentAttribute}]")
else:
equal = (segmentation1[key] == segmentation2[key])
if type(equal) == list:
if isinstance(equal, list):
self.assertTrue(all(equal), f"Failed for key {key}")
else:
self.assertTrue(equal, f"Failed for key {key}")
Expand Down
Loading