[![GitHub Workflow Status](https://img.shields.io/github/workflow/status/instamatic-dev/pyserialem/build)](https://github.com/instamatic-dev/pyserialem/actions)
[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pyserialem)](https://pypi.org/project/pyserialem/)
[![PyPI](https://img.shields.io/pypi/v/pyserialem.svg?style=flat)](https://pypi.org/project/pyserialem/)
[![PyPI - Downloads](https://img.shields.io/pypi/dm/pyserialem)](https://pypi.org/project/pyserialem/)

# PySerialEM

A small Python library to read and write [SerialEM][serialem] navigator files (`.nav`), and process grid maps.

Install using `pip install pyserialem`.

# Usage

The main use of `pyserialem` is to manipulate coordinates in a `.nav` file written by SerialEM ([specification][serialem_nav]).

Reading a `.nav` file:

```python
import pyserialem
from pathlib import Path

p = Path('C:/path/to/data/') / 'nav.nav'
items = pyserialem.read_nav_file(p)  # list
```
You can set the `acquire_only` toggle to return only the items with the `Acquire` tag set:

```python
items = pyserialem.read_nav_file(p, acquire_only=True)  # list
```

This returns a `list` of `MapItem` and `NavItem`. A `MapItem` is associated with an image in the corresponding `.mrc` file, and a `NavItem` is a marker or point on that image.

```python
map_items = [item.kind == 'Map' for item in items]
nav_items = [item.kind == 'Marker' for item in items]
```

All of the tags associated with the `MapItem` or `NavItem` can be accessed as an attribute using the same name as in the `.nav` file, i.e. with the key defined [here][serialem_nav]. This is also how the values should be updated:

```python
nav_item = nav_items[0]
stage_position = nav_item.StageXYZ  # tuple
map_item.StageXYZ = (100, 200, 0)   # overwrite values
```

Alternatively, the stage position can be accessed directly through:

```python
x = map_item.stage_x
y = map_item.stage_y
z = map_item.stage_z
xy = map_item.stage_xy
```

## Map items

A `MapItem` has all the functions of a `NavItem`, and then some. Each `MapItem` can have a list of markers associated with it:

```python
map_item = map_items[0]
markers = map_item.markers  # list
```

To visualize them, call:

```python
map_item.plot()
```

To just load the image associated with the `MapItem`:

```python
img = map_item.load()  # np.array
```

They can be extracted as a dictionary:

```python
d = map_item.to_dict()  # dict
```

...and restored:

```python
new_map_item = pysem.from_dict(d, tag='new_mapitem')
```

This is also the easiest way of constructing a new `MapItem`, because some keys can be autogenerated. Otherwise, all the required keys have to be specified to the `MapItem` constructor. The `tag` specifies the name of the item when displayed in `SerialEM`. If omitted, one will be generated.

It is easy to add new markers to a `MapItem`. As a pixel coordinate (i.e. from segmentation) is the default. `PySerialEM` calculates the corresponding stage position. The `acquire` argument sets the acquire flag (default=`True`):

```python
pixel_position = (0, 0)
new_nav_item = map_item.add_marker(
    pixel_position,
    tag='pixel_item',
    acquire=True)  # NavItem
```

You can also add a marker as a stage coordinate (although this is a bit more tricky to calculate the corresponding pixel coordinate):

```python
stage_positionion = (1000, 1000)
new_nav_item = map_item.add_marker(
    pixel_position,
    kind='stage',
    tag='stage_item',
    acquire=False)  # NavItem
```

To add many markers:

```python
pixel_coordinates = ((0, 0), (100, 100), (200, 200))
nav_item_group = map_item.add_marker_group(pixel_coordinates)  # tuple
```

Specify `replace=True` to replace the current list of markers associated with the `MapItem`.

If the `MapItem` has a set of markers associated with it `map_item.markers`, the coordinates be retrieved as followed:

```python
map_item.markers_as_pixel_coordinates()  # np.array (Nx2)
map_item.markers_as_stage_coordinates()  # np.array (Nx2)
```

To just convert between stage and pixel coordinates:

```python
pixel_coord = (1024, 1024)
stage_coord = map_item.pixel_to_stagecoords(pixel_coord)  # tuple
new_pixel_coord = map_item.stage_to_pixelcoords(stagecoord)  # tuple
assert new_pixel_coord == pixel_coord
```

To write a new file:

```python
pyserialem.write_nav_file('out.nav', map_item, *nav_item_group)
```

Note the `*`. This function captures arguments in a list (`*args`, so they must be unpacked when supplied.

## Stitching

A basic stitching algorithm is available to get an overview of the location of all map items:

```python
map_items = [item for item in items if item.kind == 'Map']
pyserialem.stitch_map_items(map_items)
```

For more advanced stitching and montaging, use the `pyserialem.montage` module. A [demo notebook](demos/montage_processing_serialem.ipynb) is available to demonstrate its usage.

## Mdoc files

There is also a simple function to read `.mdoc` files ([link][serialem_nav]). This returns a list of python objects where each key can be accessed as an attribute.

```python
p = Path('C:/path/to/data') / 'gm.mrc.mdoc'
mdoc = pyserialem.read_mdoc_file(p)
```

[src]: https://github.com/stefsmeets/pyserialem
[serialem]: https://bio3d.colorado.edu/SerialEM/
[serialem_nav]: https://bio3d.colorado.edu/SerialEM/hlp/html/about_formats.htm
