Как реализовать загрузку и отображение больших полнослайдовых гистологических изображений с фронтенда
Что проверяют
Понимаете ли вы, что полнослайдовое изображение нельзя отдать браузеру целиком: это гигапиксельный пирамидальный файл, и показывают его тайлами, как онлайн-карту. Следом спрашивают, где резать тайлы, заранее или на лету, и как их кэшировать.
Как отвечать
Разложите задачу на загрузку, подготовку и показ:
- загрузка: файлы весят гигабайты, через память приложения их не гоняют. Клиент получает от бэкенда presigned URL и грузит прямо в S3-совместимое хранилище через multipart upload, либо используется протокол докачки tus. Бэкенд на FastAPI или Django выдаёт ссылки и регистрирует слайд;
- чтение: слайды приходят в SVS, NDPI, MRXS, пирамидальном TIFF или DICOM. Их читает
openslide-python:level_count,level_dimensions,read_region(); - тайлы на лету:
openslide.deepzoom.DeepZoomGeneratorотдаёт тайлы в формате Deep Zoom (DZI) через эндпоинт вида/slides/{id}/{level}/{col}_{row}.jpeg. Открытые слайды держат в LRU-кэше, открывать файл на каждый тайл дорого; - тайлы заранее:
pyvipsсdzsaveстроит пирамиду в фоновой задаче Celery. Готовые тайлы лежат в хранилище и раздаются через CDN или nginx с долгимCache-Control; - показ: на фронтенде OpenSeadragon. Он читает DZI или IIIF, грузит только видимые тайлы нужного уровня и даёт плавный зум. Разметку рисуют отдельным слоем, координаты хранят в пикселях нулевого уровня.
Назовите и компромисс: JPEG-тайлы с качеством около 80-90 сильно экономят трафик, но для диагностики степень сжатия согласуют с врачами.
Где спотыкаются
Открывают слайд через Pillow или рисуют на один canvas. Сервер упирается в память, Pillow бросает DecompressionBombError из-за Image.MAX_IMAGE_PIXELS, браузер упирается в лимит размера холста.
Путают координаты read_region(): позиция задаётся в пикселях нулевого уровня при любом уровне, а размер области в пикселях выбранного. Отсюда смещённые тайлы.
Генерируют тайлы на лету без кэша. Один пользователь, листающий слайд, даёт сотни запросов, и каждый заново декодирует кусок исходника.