Есть несколько проблем, которые вызывают неправильное количественное определение. Я go расскажу о том, как я бы порекомендовал вам заняться этими слайдами.
Я использую DIPlib , потому что я с ним наиболее знаком ( автор). У него есть Python привязок, которые я здесь использую, но их нельзя установить с pip
, библиотеку нужно устанавливать отдельно. Тем не менее, ничто из этого не является сложной обработкой изображения, и вы должны иметь возможность выполнять аналогичную обработку с другими библиотеками.
Загрузка изображения
Здесь нет ничего особенного, кроме того, что изображение имеет сильный JPEG артефакты сжатия, которые могут помешать размешиванию пятен. Мы немного помогаем процессу, сглаживая изображение с помощью небольшого фильтра Гаусса.
import PyDIP as dip
import numpy as np
image = dip.ImageRead('example.png')
image = dip.Gauss(image, [1]) # because of the severe JPEG compression artifacts
Несмешивание пятен
[Личное примечание: мне жаль, что Руифрок и Джонстон, авторы Бумага, представляющая метод размешивания пятен , назвала его «деконволюция», поскольку этот термин уже имел установленное значение при обработке изображений, особенно в сочетании с микроскопией. Я всегда называю это «размешиванием пятен», а не «деконволюцией».]
Это всегда должно быть первым шагом в любой попытке количественной оценки изображения байтфилда. Здесь необходимо определить три важных триплета RGB: значение RGB фона (яркость источника света) и значение RGB каждого из пятен. Процесс размешивания состоит из двух компонентов:
Сначала мы применяем отображение Пиво-Ламберта . Это отображение нелинейное. Он преобразует прошедший свет (как записано микроскопом) в значения поглощения. Абсорбция показывает, насколько сильно каждая точка на слайде поглощает свет с различными длинами волн. Пятна поглощают свет и различаются по относительной оптической плотности в каждом из каналов камеры R, G и B.
background_intensity = [209, 208, 215]
image = dip.BeerLambertMapping(image, background_intensity)
Я вручную определил интенсивность фона, но вы можете довольно хорошо автоматизировать этот процесс, если у вас есть целые слайд-изображения: в целых слайд-изображениях края изображения всегда соответствуют фону, поэтому вы можете искать там интенсивности.
Второй шаг - фактическое смешение. Смешивание оптических характеристик является линейным процессом, поэтому смешивание представляет собой решение системы линейных уравнений для каждого пикселя. Для этого нам нужно знать значения поглощения для каждого из пятен в каждом из каналов. Использование стандартных значений (как в skimage.color.hax_from_rgb
) может дать хорошее первое приближение, но редко дает наилучшую количественную оценку.
Цвета окраски меняются от анализа к анализу (например, гематоксилин имеет различный цвет в зависимости от того, кто Сделано это, какие ткани окрашены, и т. д. c.), и изменить также в зависимости от камеры, используемой для изображения слайда (каждая модель имеет различные фильтры RGB). Лучший способ определить эти цвета - подготовить слайд для каждого пятна, используя тот же протокол, но не нанося другие красители. Из этих слайдов вы можете легко получить цвета пятен, которые подходят для вашего анализа и вашего сканера слайдов. Однако на практике это происходит редко, если вообще когда-либо это делается.
Более практичное решение включает оценку цветов на самом слайде. Найдя место на слайде, где вы видите каждое из пятен по отдельности (где пятна не смешаны), можно вручную определить достаточно хорошие значения. Можно автоматически определить соответствующие значения, но это гораздо сложнее, и будет сложно найти существующую реализацию. Есть несколько работ, в которых показано, как сделать это с помощью неотрицательной матричной факторизации с ограничением разреженности, что является лучшим подходом IMO.
hematoxylin_color = np.array([0.2712, 0.2448, 0.1674])
hematoxylin_color = (hematoxylin_color/np.linalg.norm(hematoxylin_color)).tolist()
aec_color = np.array([0.2129, 0.2806, 0.4348])
aec_color = (aec_color/np.linalg.norm(aec_color)).tolist()
stains = dip.UnmixStains(image, [hematoxylin_color, aec_color])
stains = dip.ClipLow(stains, 0) # set negative values to 0
hematoxylin = stains.TensorElement(0)
aec = stains.TensorElement(1)
Обратите внимание, как линейное размешивание может привести к отрицательные значения. Это результат неправильных цветовых векторов, шума, JPEG-артефактов и объектов на слайде, которые поглощают свет, который не является двумя пятнами, которые мы определили.
Определение площади ткани
У вас уже есть хороший метод для этого, который применяется к исходному изображению RGB. Тем не менее, не применяйте маску к исходному изображению перед выполнением рассмешивания выше, сохраняйте маску как отдельное изображение. Я написал следующий фрагмент кода, который находит область ткани на основе окраски гематоксилином. Это не очень хорошо, и это не сложно улучшить, но я не хотел тратить здесь слишком много времени.
tissue = dip.MedianFilter(hematoxylin, dip.Kernel(5))
tissue = dip.Dilation(tissue, [20])
tissue = dip.Closing(tissue, [50])
area = tissue > 0.2
Определение складок ткани
Вы спрашивали об этом шаге тоже. Складки ткани обычно выглядят как более темные области на изображении. Нетрудно найти автоматический метод c для их идентификации, поскольку многие другие вещи могут также создавать более темные области на изображении. Ручная аннотация - это хорошее начало, если вы соберете достаточно аннотированных вручную примеров, вы можете обучить модели глубокого обучения, чтобы помочь вам. Я сделал это просто как заполнитель, опять же это не очень хорошо, и идентифицирует некоторые положительные области как сгибы. Складки вычитаются из маски области ткани.
folds = dip.Gauss(hematoxylin - aec, [20])
area -= folds > 0.2
Определение положительных пикселей
Для этого важно использовать фиксированный порог. Только патолог может сказать вам, каким должен быть порог, они являются золотым стандартом для того, что является положительным и отрицательным.
Обратите внимание, что все слайды должны быть подготовлены в соответствии с одним и тем же протоколом. В клинических условиях это относительно просто, потому что используемые анализы стандартизированы и валидированы и дают известные, ограниченные вариации в окрашивании. В экспериментальной обстановке, где анализы менее строго контролируются, вы можете увидеть больше различий в качестве окрашивания. Вы даже увидите изменения в цвете окраски, к сожалению. Вы можете использовать автоматические методы определения порога, чтобы хотя бы получить некоторые данные, но будут предубеждения, которые вы не сможете контролировать. Я не думаю, что есть выход: непоследовательное пятно, непоследовательные данные.
Использование основанного на содержании изображения метода, такого как Otsu, пороговое значение изменяется от образца к образцу. Например, в образцах с небольшим количеством положительных пикселей порог будет ниже, чем в других образцах, что приведет к относительному завышению положительного процента.
positive = aec > 0.1 # pick a threshold according to pathologist's idea what is positive and what is not
pp = 100 * dip.Count(dip.And(positive, area)) / dip.Count(area)
print("Percent positive:", pp)
Я получу 1,35% в этом образце. Обратите внимание, что% положительных пикселей не обязательно связан с% положительных ячеек и не должен использоваться в качестве замены.