Skip to content

Commit 4a4e0d5

Browse files
matejpekarJiriStipekgemini-code-assist[bot]coderabbitai[bot]
authored
docs: initial SDK documentation (#3)
* feat: 1st docs iteration * feat: add dependency group + CI pipelines * fix: remove rules and pages * fix: gitlab template * Delete .gitlab-ci.yml * feat: mkdocs workflow * fix: dir * fix: update lock * fix: update lock * fix: update type Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * feat: add init file * fix: change mkdocs site/repo url to github * fix: syntax error in docs * fix: add RGB convert into README * Update mkdocs.yml Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Apply suggestion from @matejpekar * Apply suggestion from @matejpekar --------- Co-authored-by: JiriStipek <567776@muni.cz> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
1 parent 077fd32 commit 4a4e0d5

13 files changed

Lines changed: 1276 additions & 242 deletions

File tree

.github/workflows/mkdocs-build.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
name: MkDocs Build (RationAI Standard)
2+
on:
3+
push:
4+
branches:
5+
- main
6+
pull_request:
7+
types: [opened, synchronize, reopened]
8+
9+
jobs:
10+
run:
11+
uses: RationAI/.github/.github/workflows/mkdocs-build.yml@main

README.md

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ from PIL import Image
2121
client = rationai.Client()
2222

2323
# Load an image
24-
image = Image.open("path/to/image.jpg")
24+
image = Image.open("path/to/image.jpg").convert("RGB")
2525

2626
# Classify the image
2727
result = client.models.classify_image("model-name", image)
@@ -58,6 +58,7 @@ asyncio.run(main())
5858
Classify an image using the specified model.
5959

6060
**Parameters:**
61+
6162
- `model`: The name of the model to use for classification
6263
- `image`: The image to classify (must be uint8 RGB image)
6364
- `timeout`: Optional timeout for the request (defaults to 100 seconds)
@@ -69,20 +70,21 @@ Classify an image using the specified model.
6970
Segment an image using the specified model.
7071

7172
**Parameters:**
73+
7274
- `model`: The name of the model to use for segmentation
7375
- `image`: The image to segment (must be uint8 RGB image)
7476
- `timeout`: Optional timeout for the request (defaults to 100 seconds)
7577

7678
**Returns:** Segmentation mask as numpy array with shape `(num_classes, height, width)`
7779

78-
7980
### Slide (`client.slide`)
8081

8182
#### `heatmap(model: str, slide_path: str, tissue_mask_path: str, output_path: str, stride_fraction: float = 0.5, output_bigtiff_tile_height: int = 512, output_bigtiff_tile_width: int = 512, timeout: int = 1000) -> str`
8283

8384
Generate a heatmap for a whole slide image using the specified model.
8485

8586
**Parameters:**
87+
8688
- `model`: The name of the model to use for heatmap generation
8789
- `slide_path`: Path to the whole slide image
8890
- `tissue_mask_path`: Path to the tissue mask for the slide
@@ -101,6 +103,7 @@ Generate a heatmap for a whole slide image using the specified model.
101103
Check quality of a whole slide image.
102104

103105
**Parameters:**
106+
104107
- `wsi_path`: Path to the whole slide image
105108
- `output_path`: Directory to save output masks
106109
- `config`: Optional `SlideCheckConfig` for the quality check
@@ -113,6 +116,7 @@ Check quality of a whole slide image.
113116
Check quality of multiple slides concurrently.
114117

115118
**Parameters:**
119+
116120
- `wsi_paths`: List of paths to whole slide images
117121
- `output_path`: Directory to save output masks
118122
- `config`: Optional `SlideCheckConfig` for the quality check
@@ -126,6 +130,7 @@ Check quality of multiple slides concurrently.
126130
Generate a QC report from processed slides.
127131

128132
**Parameters:**
133+
129134
- `backgrounds`: List of paths to background (slide) images
130135
- `mask_dir`: Directory containing generated masks
131136
- `save_location`: Path where the report HTML will be saved
@@ -148,16 +153,16 @@ import rationai
148153

149154
async def process_images_with_semaphore(image_paths, model_name, max_concurrent):
150155
semaphore = asyncio.Semaphore(max_concurrent)
151-
156+
152157
async def bounded_segment(client, path):
153158
async with semaphore:
154-
image = load_image(path)
159+
image = Image.open(path).convert("RGB")
155160
return await client.models.segment_image(model_name, image)
156-
161+
157162
async with rationai.AsyncClient() as client:
158163
tasks = [bounded_segment(client, path) for path in image_paths]
159164
results = await asyncio.gather(*tasks)
160-
165+
161166
return results
162167

163168
# Process up to 16 images concurrently
@@ -174,16 +179,16 @@ from rationai import AsyncClient
174179

175180
async def process_with_as_completed(image_paths, model_name, max_concurrent):
176181
semaphore = asyncio.Semaphore(max_concurrent)
177-
182+
178183
async def bounded_request(client, path):
179184
async with semaphore:
180-
image = load_image(path)
185+
image = Image.open(path).convert("RGB")
181186
return path, await client.models.segment_image(model_name, image)
182-
187+
183188
async with AsyncClient(models_base_url="http://localhost:8000") as client:
184-
tasks = {asyncio.create_task(bounded_request(client, path)): path
189+
tasks = {asyncio.create_task(bounded_request(client, path)): path
185190
for path in image_paths}
186-
191+
187192
for future in asyncio.as_completed(tasks):
188193
path, result = await future
189194
print(f"Processed {path}")
@@ -192,7 +197,6 @@ async def process_with_as_completed(image_paths, model_name, max_concurrent):
192197
asyncio.run(process_with_as_completed(image_paths, "model-name", max_concurrent=16))
193198
```
194199

195-
196200
Start with a conservative limit and monitor server resources to find the optimal value for your setup.
197201

198202
## Configuration

docs/index.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# RationAI Python SDK
2+
3+
Python SDK for interacting with RationAI pathology image analysis services (classification, segmentation, and QC).
4+
5+
[Quick start](learn/get-started/quick-start.md)
6+
7+
[How it works](learn/how-it-works.md)
8+
9+
[API reference](reference/client.md)
10+
11+
## What you can do
12+
13+
- Run image classification and segmentation via `client.models`.
14+
- Run quality-control workflows via `client.qc`.
15+
- Choose sync (`Client`) or async (`AsyncClient`) depending on your app.
16+
17+
## Minimal examples
18+
19+
### Model example
20+
21+
```python
22+
from PIL import Image
23+
24+
import rationai
25+
26+
image = Image.open("path/to/image.jpg").convert("RGB")
27+
28+
with rationai.Client() as client:
29+
result = client.models.classify_image("model-name", image)
30+
print(result)
31+
```
32+
33+
### QC example
34+
35+
```python
36+
import rationai
37+
38+
with rationai.Client() as client:
39+
xopat_url = client.qc.check_slide(
40+
wsi_path="/data/slides/slide.svs",
41+
output_path="/data/qc-output/slide-001",
42+
)
43+
print(xopat_url)
44+
```
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
# Quick start
2+
3+
## Sync vs Async clients
4+
5+
This SDK provides two clients:
6+
7+
- `rationai.Client` (sync): Uses blocking HTTP requests. Best for scripts, notebooks, CLIs, or when your code is already synchronous.
8+
- `rationai.AsyncClient` (async): Uses non-blocking HTTP requests (`await`). Best when you already have an `asyncio` event loop (FastAPI, async workers) or you want to run many requests concurrently.
9+
10+
Both clients expose the same high-level resources:
11+
12+
- `client.models` for image classification/segmentation
13+
- `client.qc` for quality control endpoints
14+
15+
### What’s the actual difference?
16+
17+
- **Sync** calls (e.g. `client.models.classify_image(...)`) block the current thread until the request completes.
18+
- **Async** calls (e.g. `await client.models.classify_image(...)`) yield control back to the event loop while the network request is in flight, so other tasks can run.
19+
20+
### Lifecycle (important)
21+
22+
- Prefer using context managers so connections are closed:
23+
- sync: `with rationai.Client(...) as client: ...`
24+
- async: `async with rationai.AsyncClient(...) as client: ...`
25+
- If you don’t use `with`, call `client.close()` (sync) / `await client.aclose()` (async).
26+
27+
For details on what is sent over the wire (compression, payloads), see: [How it works](../how-it-works.md).
28+
29+
## API at a glance
30+
31+
### Models
32+
33+
#### `client.models.classify_image`
34+
35+
Signature:
36+
37+
`classify_image(model: str, image: PIL.Image.Image | numpy.typing.NDArray[numpy.uint8], timeout=...) -> float | dict[str, float]`
38+
39+
- `model`: Model name / path appended to `models_base_url`.
40+
- `image`: **uint8 RGB** image (PIL or NumPy array of shape `(H, W, 3)`).
41+
- `timeout`: Optional request timeout (defaults to the client’s timeout).
42+
- Returns: classification result from JSON (often `float` for binary, or `dict[class, prob]`).
43+
44+
#### `client.models.segment_image`
45+
46+
Signature:
47+
48+
`segment_image(model: str, image: PIL.Image.Image | numpy.typing.NDArray[numpy.uint8], timeout=...) -> numpy.typing.NDArray[numpy.float16]`
49+
50+
- `model`: Model name / path appended to `models_base_url`.
51+
- `image`: **uint8 RGB** image (PIL or NumPy array of shape `(H, W, 3)`).
52+
- `timeout`: Optional request timeout (defaults to the client’s timeout).
53+
- Returns: `float16` NumPy array with shape `(num_classes, height, width)`.
54+
55+
### Quality control (QC)
56+
57+
#### `client.qc.check_slide`
58+
59+
Signature:
60+
61+
`check_slide(wsi_path: os.PathLike[str] | str, output_path: os.PathLike[str] | str, config: SlideCheckConfig | None = None, timeout=3600) -> str`
62+
63+
- `wsi_path`: Path to a whole-slide image (evaluated by the QC service).
64+
- `output_path`: Directory where the QC service should write masks (evaluated by the QC service).
65+
- `config`: Optional `SlideCheckConfig` (see reference types).
66+
- `timeout`: Request timeout (default is 3600 seconds).
67+
- Returns: xOpat URL as plain text.
68+
69+
#### `client.qc.generate_report`
70+
71+
Signature:
72+
73+
`generate_report(backgrounds: Iterable[os.PathLike[str] | str], mask_dir: os.PathLike[str] | str, save_location: os.PathLike[str] | str, compute_metrics: bool = True, timeout=...) -> None`
74+
75+
- `backgrounds`: Iterable of slide/background image paths.
76+
- `mask_dir`: Directory containing generated masks.
77+
- `save_location`: Path where the report HTML should be written.
78+
- `compute_metrics`: Whether to compute aggregated metrics (default: `True`).
79+
- Returns: nothing.
80+
81+
## Synchronous client
82+
83+
```python
84+
from PIL import Image
85+
import rationai
86+
87+
image = Image.open("path/to/image.jpg").convert("RGB")
88+
89+
with rationai.Client() as client:
90+
result = client.models.classify_image("model-name", image)
91+
print(result)
92+
```
93+
94+
## Asynchronous client
95+
96+
```python
97+
import asyncio
98+
from PIL import Image
99+
import rationai
100+
101+
image = Image.open("path/to/image.jpg").convert("RGB")
102+
103+
async def main():
104+
async with rationai.AsyncClient() as client:
105+
result = await client.models.classify_image("model-name", image)
106+
print(result)
107+
108+
asyncio.run(main())
109+
```
110+
111+
### Concurrency with the async client
112+
113+
Use `asyncio` concurrency when you need to process many images. A semaphore is the simplest way to cap concurrency so you don’t overload the server.
114+
115+
```python
116+
import asyncio
117+
from PIL import Image
118+
import rationai
119+
120+
async def classify_many(paths: list[str], model: str, *, max_concurrent: int = 8) -> list[float | dict[str, float]]:
121+
sem = asyncio.Semaphore(max_concurrent)
122+
123+
async def one(client: rationai.AsyncClient, path: str) -> float | dict[str, float]:
124+
async with sem:
125+
image = Image.open(path).convert("RGB")
126+
return await client.models.classify_image(model, image)
127+
128+
async with rationai.AsyncClient() as client:
129+
return await asyncio.gather(*(one(client, p) for p in paths))
130+
```
131+
132+
## Common pitfalls
133+
134+
- **PIL image mode**: ensure RGB.
135+
136+
```python
137+
image = Image.open(path).convert("RGB")
138+
```
139+
140+
- **NumPy dtype/shape**: the services expect `uint8` RGB images.
141+
142+
```python
143+
import numpy as np
144+
145+
assert arr.dtype == np.uint8
146+
assert arr.ndim == 3 and arr.shape[2] == 3
147+
```
148+
149+
- **Forgetting to close clients**: prefer `with ...` / `async with ...`.
150+
151+
- **Too much async concurrency**: cap with a semaphore (start small like 4–16) to avoid server overload/timeouts.
152+
153+
- **Timeouts**: segmentation/QC can take longer. Increase per-request timeout if needed.
154+
155+
```python
156+
result = client.models.segment_image("model", image, timeout=300)
157+
```
158+
159+
- **QC paths are server-side**: `wsi_path` / `output_path` must exist where the QC service runs.
160+
161+
## Configuration
162+
163+
You can override service URLs and timeouts:
164+
165+
```python
166+
from rationai import Client
167+
168+
client = Client(
169+
models_base_url="http://localhost:8000",
170+
qc_base_url="http://localhost:8001",
171+
timeout=300,
172+
)
173+
```

0 commit comments

Comments
 (0)