Skip to content

Commit 868c089

Browse files
committed
feat: Integrate OpenSeadragon with scalebar functionality
- Added OpenSeadragon library to the project dependencies. - Implemented a new scalebar feature for the OpenSeadragon viewer to display physical dimensions. - Enhanced the viewer setup to fetch microns-per-pixel (MPP) from DZI files or URL parameters. - Created a scalebar extension to manage the display and updates of the scalebar in the viewer. - Added viewport information display to show dimensions in physical units or pixels. - Refactored existing image viewer code to accommodate the new features and improve error handling. - Introduced TypeScript definitions for OpenSeadragon to enhance type safety.
1 parent 5d99ed7 commit 868c089

File tree

12 files changed

+1746
-295
lines changed

12 files changed

+1746
-295
lines changed

label_studio/core/deepzoom_util.py

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,22 @@
11
import os
22
import io
3+
import logging
34
from pathlib import Path
45
from typing import Any, Callable, Optional, Tuple, Union
6+
from xml.etree.ElementTree import fromstring, tostring, SubElement
7+
8+
logger = logging.getLogger(__name__)
59

610
import openslide
7-
import opensdpc
11+
from openslide import OpenSlide
12+
slide_openers = [OpenSlide]
13+
try:
14+
import opensdpc
15+
slide_openers.append(opensdpc.OpenSdpc)
16+
17+
except ImportError:
18+
pass
19+
820
from PIL import Image
921

1022
from .annotated_deepzoom_generator import AnnotatedDeepZoomGenerator
@@ -23,11 +35,17 @@ def __init__(self, full_path, tile_size:int = 254, overlap:int = 1, limit_bounds
2335
self.is_sdpc = ext.lower() == ".sdpc"
2436

2537
# Load the appropriate slide object
26-
if self.is_sdpc:
27-
self._osr = opensdpc.OpenSdpc(full_path_str)
28-
else:
29-
self._osr = openslide.OpenSlide(full_path_str)
30-
38+
self._osr: Optional[OpenSlide] = None
39+
for opener in slide_openers:
40+
try:
41+
self._osr = opener(full_path_str)
42+
break
43+
except Exception:
44+
continue
45+
46+
if self._osr is None:
47+
raise RuntimeError(f'Could not open slide: {full_path_str}')
48+
3149
# Setup the deep zoom generator
3250
self._dzg = AnnotatedDeepZoomGenerator(
3351
self._osr,
@@ -63,8 +81,34 @@ def get_tile(self, level: int, tile: tuple[int, int]) -> Image.Image:
6381
return tile_img
6482

6583
def get_dzi(self, format:str = "jpeg") -> str:
66-
"""Get the DZI XML for this slide"""
67-
return self._dzg.get_dzi(format)
84+
"""Get the DZI XML for this slide with MPP metadata"""
85+
dzi_xml = self._dzg.get_dzi(format)
86+
87+
# If we have MPP data, inject it into the DZI XML using proper XML parsing
88+
if self.mpp > 0:
89+
try:
90+
# Parse the XML
91+
root = fromstring(dzi_xml)
92+
93+
# Add Property elements as children of Image
94+
prop_x = SubElement(root, 'Property', Name='openslide.mpp-x')
95+
prop_x.text = str(self.mpp)
96+
97+
prop_y = SubElement(root, 'Property', Name='openslide.mpp-y')
98+
prop_y.text = str(self.mpp)
99+
100+
# Convert back to string
101+
dzi_xml = tostring(root, encoding='unicode')
102+
103+
logger.debug(f"DZI with MPP metadata for {self.filename}:\n{dzi_xml}")
104+
except Exception as e:
105+
logger.error(f"Failed to add MPP metadata to DZI: {e}")
106+
# Return original DZI if modification fails
107+
logger.debug(f"Original DZI for {self.filename}:\n{dzi_xml}")
108+
else:
109+
logger.debug(f"DZI without MPP (mpp={self.mpp}) for {self.filename}:\n{dzi_xml}")
110+
111+
return dzi_xml
68112

69113
def get_tile_bytes(self, level: int, tile: tuple[int, int], format:str = "jpeg", quality:int = 75) -> bytes:
70114
"""Get a tile as bytes, optimized for HTTP response"""

0 commit comments

Comments
 (0)