Skip to content
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
52 changes: 52 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: Deploy Documentation

on:
push:
branches:
- master
paths:
- 'docs/**'
- 'mkdocs.yml'
- 'exact/EXACT-API.yml'
workflow_dispatch:

permissions:
contents: read
pages: write
id-token: write

concurrency:
group: pages
cancel-in-progress: false

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: '3.11'

- name: Install MkDocs dependencies
run: pip install -r docs/requirements.txt

- name: Build docs
run: mkdocs build --strict

- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v3
with:
path: site/

deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,7 @@ env.prod
env.prod.db
env.prod.aws-db
*.aws-db

# MkDocs generated files
site/
docs/api/openapi.yml
92 changes: 92 additions & 0 deletions docs/api/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# API Reference

EXACT exposes a full REST API at `/api/v1/`. The interactive reference below is generated from the OpenAPI specification.

## Authentication

```bash
# Obtain a token
curl -X POST http://your-server/api/auth/token/login/ \
-H 'Content-Type: application/json' \
-d '{"username": "exact", "password": "exact"}'
# → {"auth_token": "abc123..."}

# Use the token in subsequent requests
curl http://your-server/api/v1/images/image_sets/ \
-H 'Authorization: Token abc123...'
```

## Available Resources

| Resource | Endpoint |
|---|---|
| Users | `/api/v1/users/users/` |
| Teams | `/api/v1/users/teams/` |
| Team memberships | `/api/v1/users/team_membership/` |
| Images | `/api/v1/images/images/` |
| Image sets | `/api/v1/images/image_sets/` |
| Set tags | `/api/v1/images/set_tags/` |
| Screening modes | `/api/v1/images/screening_modes/` |
| Annotations | `/api/v1/annotations/annotations/` |
| Annotation types | `/api/v1/annotations/annotation_types/` |
| Annotation media files | `/api/v1/annotations/annotation_media_files/` |
| Verifications | `/api/v1/annotations/verifications/` |
| Log image actions | `/api/v1/annotations/log_image_actions/` |
| Products | `/api/v1/administration/products/` |

## Filtering, Expanding, and Field Selection

All list endpoints support [django-filter](https://django-filter.readthedocs.io/) for filtering and [drf-flex-fields](https://github.com/rsinger86/drf-flex-fields) for field selection.

### Filter by field value

```
GET /api/v1/images/image_sets/?name__contains=Mitosis
```

### Expand nested objects

```
GET /api/v1/images/image_sets/?expand=product_set,main_annotation_type
```

### Include only specific fields

```
GET /api/v1/images/image_sets/?fields=id,name
```

### Exclude fields

```
GET /api/v1/images/image_sets/?omit=images,product_set
```

## Python Client

```bash
pip install EXACT-Sync
```

```python
from exact_sync.v1.configuration import Configuration
from exact_sync.v1 import ApiClient
from exact_sync.v1.api import images_api

config = Configuration(host="http://localhost:8000")
config.username = "exact"
config.password = "exact"

with ApiClient(config) as client:
api = images_api.ImagesApi(client)
imagesets = api.images_image_sets_list()
print(imagesets.results)
```

See [EXACT-Sync on GitHub](https://github.com/DeepMicroscopy/EXACT-Sync) and the [example notebooks](https://nbviewer.jupyter.org/github/DeepMicroscopy/Exact/tree/master/doc/).

---

## Interactive API Explorer

<swagger-ui src="./openapi.yml"/>
151 changes: 151 additions & 0 deletions docs/developer-guide/adding-image-formats.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
# Adding Image Formats

EXACT uses an OpenSlide-compatible handler interface. Any class that implements the interface below can be registered as a format handler without touching the tile server or the viewer.

## 1. Implement the handler

Create a file in `exact/util/` (e.g., `myformat.py`):

```python
from PIL import Image
from typing import Dict, List, Tuple

class MyFormatSlide:
def __init__(self, filename: str):
# Open the file and read metadata here
self.filename = filename
self._width = ...
self._height = ...

# ── Required: OpenSlide-compatible properties ─────────────────────────

@property
def dimensions(self) -> Tuple[int, int]:
"""(width, height) at full resolution."""
return (self._width, self._height)

@property
def level_count(self) -> int:
return 1 # return >1 if you have a native image pyramid

@property
def level_dimensions(self) -> List[Tuple[int, int]]:
return [(self._width, self._height)]

@property
def level_downsamples(self) -> List[float]:
return [1.0]

def get_best_level_for_downsample(self, downsample: float) -> int:
return 0

@property
def properties(self) -> Dict[str, str]:
return {
'openslide.mpp-x': '0.5', # microns per pixel, x
'openslide.mpp-y': '0.5', # microns per pixel, y
'openslide.objective-power': '20',
'openslide.vendor': 'MyFormat',
}

# ── Required: tile reading ────────────────────────────────────────────

def read_region(
self,
location: Tuple[int, int],
level: int,
size: Tuple[int, int],
frame: int = 0,
plane: int = 0,
) -> Image.Image:
"""Return an RGBA PIL Image for the requested region."""
x, y = location
w, h = size
# ... read the pixel data and return as RGBA ...
return Image.fromarray(rgba_array, 'RGBA')

def get_thumbnail(self, size: Tuple[int, int]) -> Image.Image:
return self.read_region((0, 0), 0, size)

# ── Required: frame / z-stack support ────────────────────────────────

@property
def nFrames(self) -> int:
return 1 # number of z/t frames

@property
def frame_type(self):
from util.enums import FrameType
return FrameType.ZSTACK # or FrameType.NONE

@property
def frame_descriptors(self) -> List[str]:
return ['frame 0']

@property
def default_frame(self) -> int:
return 0
```

### Optional: Multi-Planar Reformat (MPR)

If your format is a 3D volume and you want axial/coronal/sagittal views, add these methods:

```python
def dimensions_for_plane(self, plane: int) -> Tuple[int, int]:
"""Return (width, height) for the given plane index (0=axial, 1=coronal, 2=sagittal).
Return None if this format does not support MPR."""
from util.enums import PlaneType
if plane == PlaneType.AXIAL:
return self._ax_dims
if plane == PlaneType.CORONAL:
return self._cor_dims
if plane == PlaneType.SAGITTAL:
return self._sag_dims

def nframes_for_plane(self, plane: int) -> int:
"""Return the number of slices along the normal axis of the given plane."""
...
```

## 2. Register the handler

Open `util/slide_server.py` and find the `_open_slide` function (or the handler-selection logic). Add a detection branch before the OpenSlide fallback:

```python
from util.myformat import MyFormatSlide

def _open_slide(path: str):
if path.endswith('.myext'):
return MyFormatSlide(path)
# ... existing handlers ...
return openslide.OpenSlide(path)
```

## 3. Make it picklable (for caching)

The slide cache serialises handlers via `pickle`. Implement `__reduce__` so the handler can be reconstructed from just the filename:

```python
def __reduce__(self):
return (self.__class__, (self.filename,))
```

## 4. Test the tile pipeline

Upload a sample file and check:

1. The imageset view shows a thumbnail.
2. The annotator opens and tiles load at multiple zoom levels.
3. If multi-frame: the frame slider appears and stepping through frames works.
4. If MPR: the plane selector appears and all three reformats render correctly.

## Reference: NIfTI implementation

`util/nifti.py` is a complete reference implementation for a volumetric format with MPR. It covers:

- RAS+ reorientation via nibabel
- Per-plane pixel dimension calculation with aspect-ratio correction
- Radiological display convention (Anterior/Superior at top, patient Right on left)
- Windowing from a sparse intensity sample
- `__reduce__` for cache serialisation
Loading
Loading