Skip to content

Commit b8ad50a

Browse files
committed
Add ZarrTIFFWSIReader class.
1 parent 6b214fe commit b8ad50a

File tree

4 files changed

+714
-0
lines changed

4 files changed

+714
-0
lines changed

requirements/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# torch installation
22
--extra-index-url https://download.pytorch.org/whl/cu118; sys_platform != "darwin"
3+
aiohttp>=3.8.1
34
albumentations>=1.3.0
45
bokeh>=3.1.1, <3.6.0
56
Click>=8.1.3

tests/zarrtiff/tiff_fsspec.py

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import json
2+
from datetime import datetime
3+
from numbers import Number
4+
from typing import Union
5+
6+
from tifffile import TiffFile, TiffPages, tiff2fsspec
7+
8+
# Path to the JSON file
9+
json_file_path = "../../samples/fsspec/73c69d24-6f9e-44e2-bfe5-a608d4cf5c27_fsspec.json"
10+
svs_file_path = "../../samples/slides/TCGA-22-1017-01Z-00-DX1.9562FE79-A261-42D3-B394-F3E0E2FF7DDA.svs"
11+
url_place_holder = "https://replace.me/"
12+
url_to_replace = (
13+
url_place_holder
14+
+ "TCGA-22-1017-01Z-00-DX1.9562FE79-A261-42D3-B394-F3E0E2FF7DDA.svs"
15+
)
16+
final_url = "https://api.gdc.cancer.gov/data/73c69d24-6f9e-44e2-bfe5-a608d4cf5c27"
17+
18+
19+
def _parse_svs_metadata(pages: TiffPages) -> dict:
20+
"""Extract SVS specific metadata.
21+
22+
Returns:
23+
dict:
24+
Dictionary of kwargs for WSIMeta.
25+
26+
"""
27+
raw = {}
28+
mpp = None
29+
objective_power = None
30+
vendor = "Aperio"
31+
32+
description = pages[0].description
33+
raw["Description"] = description
34+
parts = description.split("|")
35+
description_headers, key_value_pairs = parts[0], parts[1:]
36+
description_headers = description_headers.split(";")
37+
38+
software, photometric_info = description_headers[0].splitlines()
39+
raw["Software"] = software
40+
raw["Photometric Info"] = photometric_info
41+
42+
def parse_svs_tag(string: str) -> tuple[str, Union[Number, str]]:
43+
"""Parse SVS key-value string.
44+
45+
Infers type(s) of data by trial and error with a fallback to
46+
the original string type.
47+
48+
Args:
49+
string (str):
50+
Key-value string in SVS format: "key=value".
51+
52+
Returns:
53+
tuple:
54+
Key-value pair.
55+
56+
"""
57+
pair = string.split("=")
58+
if len(pair) != 2:
59+
msg = "Invalid metadata. Expected string of the format 'key=value'."
60+
raise ValueError(
61+
msg,
62+
)
63+
key, value_string = pair
64+
key = key.strip()
65+
value_string = value_string.strip()
66+
67+
def us_date(string: str) -> datetime:
68+
"""Return datetime parsed according to US date format."""
69+
return datetime.strptime(string, r"%m/%d/%y").astimezone()
70+
71+
def time(string: str) -> datetime:
72+
"""Return datetime parsed according to HMS format."""
73+
return datetime.strptime(string, r"%H:%M:%S").astimezone()
74+
75+
casting_precedence = [us_date, time, int, float]
76+
value = value_string
77+
for cast in casting_precedence:
78+
try:
79+
value = cast(value_string)
80+
except ValueError: # noqa: PERF203
81+
continue
82+
else:
83+
return key, value
84+
85+
return key, value
86+
87+
svs_tags = dict(parse_svs_tag(string) for string in key_value_pairs)
88+
raw["SVS Tags"] = svs_tags
89+
mpp = svs_tags.get("MPP")
90+
if mpp is not None:
91+
mpp = [mpp] * 2
92+
objective_power = svs_tags.get("AppMag")
93+
94+
return {
95+
"objective_power": objective_power,
96+
"vendor": vendor,
97+
"mpp": mpp,
98+
"raw": raw,
99+
}
100+
101+
102+
tifffilePages = TiffFile(svs_file_path).pages
103+
104+
tiff2fsspec(svs_file_path, url="https://replace.me", out=json_file_path)
105+
106+
107+
metadata = _parse_svs_metadata(pages=tifffilePages)
108+
109+
110+
# Recursive function to convert datetime objects to JSON-compatible strings
111+
def convert_metadata(metadata):
112+
if isinstance(metadata, dict):
113+
return {key: convert_metadata(value) for key, value in metadata.items()}
114+
if isinstance(metadata, list):
115+
return [convert_metadata(item) for item in metadata]
116+
if isinstance(metadata, datetime):
117+
return metadata.isoformat() # Convert datetime to ISO 8601 string
118+
return metadata
119+
120+
121+
metadataSerializable = convert_metadata(metadata)
122+
123+
# Read the JSON data from the file
124+
with open(json_file_path) as file:
125+
json_data = json.load(file)
126+
127+
128+
# Decode `.zattrs` JSON string into a dictionary
129+
zattrs = json.loads(json_data[".zattrs"])
130+
131+
# Update metadata into `.zattrs`
132+
if "multiscales" in zattrs and isinstance(zattrs["multiscales"], list):
133+
zattrs["multiscales"][0]["metadata"] = metadataSerializable
134+
135+
# Convert back to a JSON string
136+
json_data[".zattrs"] = json.dumps(zattrs)
137+
138+
# Dump the updated JSON
139+
json_content = json.dumps(json_data, indent=2)
140+
141+
142+
def replace_url(data, output_path, old_url, new_url):
143+
for key, value in data.items():
144+
if isinstance(value, list) and value[0] == old_url:
145+
value[0] = new_url
146+
147+
with open(output_path, "w") as json_file:
148+
json.dump(data, json_file, indent=2)
149+
150+
151+
# Replace URLs in the JSON file
152+
replace_url(json_data, json_file_path, url_to_replace, final_url)

tests/zarrtiff/tileserver.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from flask_cors import CORS
2+
3+
from tiatoolbox.visualization import TileServer
4+
from tiatoolbox.wsicore import WSIReader
5+
6+
## Before running this script run: pip install aiohttp
7+
8+
svs = "../../samples/fsspec/73c69d24-6f9e-44e2-bfe5-a608d4cf5c27_fsspec.json"
9+
10+
reader = WSIReader.open(svs)
11+
12+
# Initialize and run the TileServer
13+
tile_server = TileServer(
14+
title="Tiatoolbox TileServer",
15+
layers={"layer": reader},
16+
)
17+
CORS(tile_server, send_wildcard=True)
18+
19+
20+
tile_server.run(host="127.0.0.1", port=5000)

0 commit comments

Comments
 (0)