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

Slan to ImodModel attribute and IMAT to ImodModel.object attribute + testing & documentation #20

Merged
merged 16 commits into from
Jun 13, 2024
Merged
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
102 changes: 98 additions & 4 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,26 +30,120 @@ Out[3]:

```

Annotations made in the [slicer window](https://bio3d.colorado.edu/imod/doc/3dmodHelp/slicer.html) are stored in the IMOD binary file with both centerpoints and angles.
Slicer angles saved in the [slicer window](https://bio3d.colorado.edu/imod/doc/3dmodHelp/slicer.html)
are stored in the IMOD binary file with both centerpoints and angles.

These annotations can be read in by setting `annotation='slan'` when calling `imodmodel.read()`
These annotations can be read in by setting `annotation='slicer_angle'` when calling `imodmodel.read()`

```python
import imodmodel

df = imodmodel.read('file_with_slicer_angles.mod', annotation='slan')
df = imodmodel.read('file_with_slicer_angles.mod', annotation='slicer_angles')
```

```ipython
In [3]: df.head()
Out[3]:
object_id slan_id time x_rot y_rot z_rot center_x center_y center_z label
object_id slicer_angle_id time x_rot y_rot z_rot center_x center_y center_z label
0 0 0 1 13.100000 0.0 -30.200001 235.519577 682.744141 302.0
0 0 1 1 -41.400002 0.0 -47.700001 221.942444 661.193237 327.0
0 0 2 1 -41.400002 0.0 -41.799999 232.790726 671.332031 327.0
0 0 3 1 -35.500000 0.0 -36.000000 240.129181 679.927795 324.0
```

## ImodModel

The resulting dataframe from `imodmodel.read()` contains only information about the contours or slicer angles.
The full set of information from the imod model file can be parsed using `ImodModel`

```python
from imodmodel import ImodModel

my_model = ImodModel.from_file("my_model_file.mod")
```

```ipython
in [3]: my_model.model_field_set
out[3]:
{'id', 'extra', 'objects', 'slicer_angles', 'header'}
```

### my_model.id

`my_model.id` contains the IMOD file id and the version id

```ipython
in [4]: my_model.id
out[4]:
ID(IMOD_file_id='IMOD', version_id='V1.2')
```

### my_model.header

`my_model.header` is contains the model structure data mainly used by IMOD.

```ipython
in [5]: my_model.header
out[5]:
ModelHeader(name='IMOD-NewModel', xmax=956, ymax=924, zmax=300, objsize=3, flags=62976, drawmode=1,
mousemode=1, blacklevel=145, whitelevel=173, xoffset=0.0, yoffset=0.0, zoffset=0.0, xscale=1.0, yscale=10,
zscale=1.0, object=2, contour=-1, point=-1, res=3, thresh=128, pixelsize=1.9733333587646484, units=-9,
csum=704518946, alpha=0.0, beta=0.0, gamma=0.0)
```

### my_model.objects

`my_model.objects` is a `list` IMOD objects.

```ipython
in [6]: my_model.objects[0].header
out[6]:
ObjectHeader(name='', extra_data=[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], contsize=0,
flags=402653184, axis=0, drawmode=1, red=0.0, green=1.0, blue=0.0, pdrawsize=0, symbol=1, symsize=3,
linewidth2=1, linewidth=1, linesty=0, symflags=0, sympad=0, trans=0, meshsize=0, surfsize=0)
```

This is where object values like contours, meshes, and IMAT information are located.

```ipython
in [7]: my_model.objects[1].meshes[0].indices
out[7]:
array([[38, 40, 52],
[38, 52, 50],
[50, 52, 64],
[50, 64, 60],
...,
[ 4, 10, 26],
[ 4, 26, 20],
[20, 26, 38],
[20, 38, 32]])
```

```ipython
in [8]: my_model.objects[1].imat
out[8]:
IMAT(ambient=102, diffuse=255, specular=127, shininess=4, fillred=0, fillgreen=0, fillblue=0,
quality=0, mat2=0, valblack=0, valwhite=255, matflags2=0, mat3b3=0)
```

```ipython
in [9]: my_model.objects[1].contours[0].points
out[9]:
array([[367.00006104, 661.83343506, 134. ],
[415.66674805, 667.83343506, 134. ],
[474.33340454, 662.50012207, 134. ]])
```

### my_model.slicer_angles

`my_model.slicer_angles` is a `list` of slicer angles.

```ipython
in [10]: my_model.slicer_angles[0]
out[10]:
SLAN(time=1, angles=(0.0, 0.0, 0.0), center=(533.5, 717.0, 126.0), label='\x00')
```

That's it!

## Installation
Expand Down
53 changes: 26 additions & 27 deletions src/imodmodel/dataframe.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,19 @@
def model_to_dataframe(model: ImodModel, annotation: str = 'contour') -> pd.DataFrame:
"""Convert ImodModel model into a pandas DataFrame."""
object_dfs: List[pd.DataFrame] = []
for object_idx, object in enumerate(model.objects):
if annotation == 'slan':
if len(object.slans) == 0:
raise ValueError("Model has no SLANs.")
for slan_idx, slan in enumerate(object.slans):
slan_df = slan_to_dataframe(slan, object_idx, slan_idx)
object_dfs.append(slan_df)
elif annotation == 'contour':
for contour_idx, contour in enumerate(object.contours):
contour_df = contour_to_dataframe(contour, object_idx, contour_idx)
object_dfs.append(contour_df)
else:
raise ValueError(f"Unknown annotation type: {annotation}")
if annotation == 'slicer_angles':
if len(model.slicer_angles) == 0:
raise ValueError("Model has no slicer angles.")
for slicer_angle_idx, slicer_angle in enumerate(model.slicer_angles):
slicer_angle_df = slicer_angle_to_dataframe(slicer_angle, slicer_angle_idx)
object_dfs.append(slicer_angle_df)
elif annotation == 'contour':
for object_idx, object in enumerate(model.objects):
for contour_idx, contour in enumerate(object.contours):
contour_df = contour_to_dataframe(contour, object_idx, contour_idx)
object_dfs.append(contour_df)
else:
raise ValueError(f"Unknown annotation type: {annotation}")
return pd.concat(object_dfs)


Expand All @@ -39,18 +39,17 @@ def contour_to_dataframe(
return pd.DataFrame(contour_data)


def slan_to_dataframe(slan: SLAN, object_id: int, slan_id: int) -> pd.DataFrame:
"""Convert SLAN model into a pandas DataFrame."""
slan_data = {
"object_id": [object_id],
"slan_id": [slan_id],
"time": [slan.time],
"x_rot": [slan.angles[0]],
"y_rot": [slan.angles[1]],
"z_rot": [slan.angles[2]],
"center_x": [slan.center[0]],
"center_y": [slan.center[1]],
"center_z": [slan.center[2]],
"label": [slan.label],
def slicer_angle_to_dataframe(slicer_angle: SLAN, slicer_angle_id: int) -> pd.DataFrame:
"""Convert slicer angle model into a pandas DataFrame."""
slicer_angle_data = {
"slicer_angle_id": [slicer_angle_id],
"time": [slicer_angle.time],
"x_rot": [slicer_angle.angles[0]],
"y_rot": [slicer_angle.angles[1]],
"z_rot": [slicer_angle.angles[2]],
"center_x": [slicer_angle.center[0]],
"center_y": [slicer_angle.center[1]],
"center_z": [slicer_angle.center[2]],
"label": [slicer_angle.label],
}
return pd.DataFrame(slan_data)
return pd.DataFrame(slicer_angle_data)
2 changes: 1 addition & 1 deletion src/imodmodel/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ def read(filename: os.PathLike, annotation: str = 'contour') -> pd.DataFrame:
Parameters
----------
filename : filename to read
annotation: which annotation of the model to return ['contour', 'slan'] (default: 'contour')
annotation: which annotation of the model to return ['contour', 'slicer_angles'] (default: 'contour')
"""
model = ImodModel.from_file(filename)
return model_to_dataframe(model,annotation)
4 changes: 2 additions & 2 deletions src/imodmodel/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,8 +233,8 @@ class Object(BaseModel):
header: ObjectHeader
contours: List[Contour] = []
meshes: List[Mesh] = []
slans: List[SLAN] = []
extra: List[GeneralStorage] = []
imat: Optional[IMAT] = None


class ImodModel(BaseModel):
Expand All @@ -245,7 +245,7 @@ class ImodModel(BaseModel):
id: ID
header: ModelHeader
objects: List[Object]
imat: Optional[IMAT] = None
slicer_angles: List[SLAN] = []
extra: List[GeneralStorage] = []

@classmethod
Expand Down
10 changes: 5 additions & 5 deletions src/imodmodel/parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ def _parse_general_storage(file: BinaryIO) -> List[GeneralStorage]:
storages.append(GeneralStorage(type=type, flags=flags, index=index, value=value))
return storages

def _parse_slan(file: BinaryIO) -> SLAN:
def _parse_slicer_angle(file: BinaryIO) -> SLAN:
_parse_chunk_size(file)
data = _parse_from_specification(file, ModFileSpecification.SLAN)
return SLAN(**data)
Expand All @@ -158,15 +158,15 @@ def parse_model(file: BinaryIO) -> ImodModel:
id = _parse_id(file)
header = _parse_model_header(file)
control_sequence = _parse_control_sequence(file)
imat = None
slicer_angles = []
extra = list()

objects = []
while control_sequence != "IEOF":
if control_sequence == "OBJT":
objects.append(_parse_object(file))
elif control_sequence == "IMAT":
imat = _parse_imat(file)
objects[-1].imat = _parse_imat(file)
elif control_sequence == "CONT":
objects[-1].contours.append(_parse_contour(file))
elif control_sequence == "MESH":
Expand All @@ -180,8 +180,8 @@ def parse_model(file: BinaryIO) -> ImodModel:
elif control_sequence == "MEST":
objects[-1].meshes[-1].extra += _parse_general_storage(file)
elif control_sequence == "SLAN":
objects[-1].slans.append(_parse_slan(file))
slicer_angles.append(_parse_slicer_angle(file))
else:
_parse_unknown(file)
control_sequence = _parse_control_sequence(file)
return ImodModel(id=id, header=header, objects=objects, imat=imat, extra=extra)
return ImodModel(id=id, header=header, objects=objects, slicer_angles=slicer_angles, extra=extra)
18 changes: 14 additions & 4 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,21 @@ def meshed_curvature_model_file_handle(meshed_curvature_model_file):
return open(meshed_curvature_model_file, mode='rb')

@pytest.fixture
def slan_model_file() -> Path:
def slicer_angle_model_file() -> Path:
"""A model file from Mohammed's slicerangle measurements."""
return TEST_DATA_DIRECTORY / 'slan_example.mod'
return TEST_DATA_DIRECTORY / 'slicer_angle_example.mod'

@pytest.fixture
def slan_model_file_handle(slan_model_file):
def slicer_angle_model_file_handle(slicer_angle_model_file):
"""A file handle with Mohammed's slicerangle measurements."""
return open(slan_model_file, mode='rb')
return open(slicer_angle_model_file, mode='rb')

@pytest.fixture
def multiple_objects_model_file() -> Path:
"""A model file containing multiple object data."""
return TEST_DATA_DIRECTORY / 'multiple_objects_example.mod'

@pytest.fixture
def multiple_objects_model_file_handle(slicer_angle_model_file):
"""A file handle with multiple object data."""
return open(slicer_angle_model_file, mode='rb')
Binary file added tests/test_data/multiple_objects_example.mod
Binary file not shown.
29 changes: 15 additions & 14 deletions tests/test_functional_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,33 @@
import imodmodel


def test_read(two_contour_model_file, meshed_contour_model_file, slan_model_file):
def test_read(two_contour_model_file, meshed_contour_model_file, slicer_angle_model_file):
"""Check that model files can be read."""
example_contour_files = [two_contour_model_file, meshed_contour_model_file, slan_model_file]
example_slan_files = [slan_model_file]
example_contour_files = [two_contour_model_file, meshed_contour_model_file, slicer_angle_model_file]
example_slicer_angle_files = [slicer_angle_model_file]
expected_contour_columns = ['object_id', 'contour_id', 'x', 'y', 'z']
expected_slan_columns = [
'object_id', 'slan_id', 'time', 'x_rot', 'y_rot', 'z_rot', 'center_x',
expected_slicer_angle_columns = [
'object_id', 'slicer_angle_id', 'time', 'x_rot', 'y_rot', 'z_rot', 'center_x',
'center_y', 'center_z', 'label']

for file in example_contour_files:
contour_df = imodmodel.read(file)
assert isinstance(contour_df, pd.DataFrame)
assert all(col in expected_contour_columns for col in contour_df.columns)

for file in example_slicer_angle_files:
slicer_angle_df = imodmodel.read(file, annotation='slicer_angles')
assert isinstance(slicer_angle_df, pd.DataFrame)
assert all(col in expected_slicer_angle_columns for col in slicer_angle_df.columns)

for file in example_slan_files:
slan_df = imodmodel.read(file,annotation='slan')
assert isinstance(slan_df, pd.DataFrame)
assert all(col in expected_slan_columns for col in slan_df.columns)

def test_no_slan(two_contour_model_file):
"""Check that an error is raised if a model with no SLANs is read with the 'slan' annotation."""
with pytest.raises(ValueError,match="Model has no SLANs."):
df = imodmodel.read(two_contour_model_file, annotation='slan')
def test_no_slicer_angles(two_contour_model_file):
"""Check that an error is raised if a model with no slicer_angles is read with the 'slicer_angle' annotation."""
with pytest.raises(ValueError, match="Model has no slicer angles."):
df = imodmodel.read(two_contour_model_file, annotation='slicer_angles')


def test_unknown_annotation(two_contour_model_file):
"""Check that an error is raised if an unknown annotation is requested."""
with pytest.raises(ValueError,match="Unknown annotation type: unknown"):
with pytest.raises(ValueError, match="Unknown annotation type: unknown"):
df = imodmodel.read(two_contour_model_file, annotation='unknown')
33 changes: 27 additions & 6 deletions tests/test_model_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
Contour,
ContourHeader,
Mesh,
IMAT,
SLAN,
MeshHeader,
Object,
Expand Down Expand Up @@ -35,16 +36,36 @@ def test_read_contour(file_fixture_contour, meshes_expected, request):
assert isinstance(model.objects[0].meshes[0].header, MeshHeader)

@pytest.mark.parametrize(
"file_fixture_slan",
"file_fixture_slicer_angle",
[
('slan_model_file'),
('slicer_angle_model_file'),
]
)
def test_read_slan(file_fixture_slan, request):
def test_read_slicer_angle(file_fixture_slicer_angle, request):
"""Check the model based API"""
file = request.getfixturevalue(file_fixture_slan)
file = request.getfixturevalue(file_fixture_slicer_angle)
model = ImodModel.from_file(file)
assert isinstance(model, ImodModel)
assert len(model.objects) == 1
assert len(model.slicer_angles) == 4
assert isinstance(model.slicer_angles, list)
assert isinstance(model.slicer_angles[0], SLAN)

@pytest.mark.parametrize(
"file_fixture_multiple_objects, objects_expected",
[
('multiple_objects_model_file', 3),
]
)
def test_multiple_objects(file_fixture_multiple_objects, objects_expected, request):
file = request.getfixturevalue(file_fixture_multiple_objects)
model = ImodModel.from_file(file)
assert isinstance(model, ImodModel)
assert len(model.objects) == objects_expected
assert isinstance(model.objects[0], Object)
assert isinstance(model.objects[0].slans[0], SLAN)
assert isinstance(model.objects[0].imat, IMAT)
assert isinstance(model.objects[1].contours[0], Contour)
assert isinstance(model.objects[1], Object)
assert isinstance(model.objects[1].imat, IMAT)
assert isinstance(model.objects[2].contours[0], Contour)


Loading
Loading