Chromatin Patterns
Extracting and quantifying chromatin patterns from routine Hematoxylin and Eosin (H&E) stained images offers important insights into nuclear architecture, an important indicator of cellular state that undergoes profound alterations in various disease pathologies, particularly cancer. This analysis workflow provides an example of how Histolytics can be used to extract and quantify chromatin patterns from H&E stained images, focusing on the following key steps:
- Segmentation of individual chromatin clumps within cell nuclei.
- Computation of key quantitative features, such as chromatin clump area and the proportion of chromatin clump area to the total nuclear area.
These interpretable metrics of chromatin organization can reveal significant biological insights; for example, a low chromatin clump-to-nucleus area ratio might indicate dispersed chromatin, while a higher proportion can reflect increased chromatin condensation or hyperchromatism—hallmarks often associated with malignancy.
Let's start the workflow demonstration by loading the example data:
from histolytics.data import hgsc_cancer_he, hgsc_cancer_nuclei
he = hgsc_cancer_he()
nuc = hgsc_cancer_nuclei()
nuc.plot(figsize=(10, 10), column="class_name")
<Axes: >
Convert vector data to raster format¶
First, we will need to convert the vector data of nuclei to raster format. This is because we are working with dense rgb-image data, meaning that the nuclei masks need to be in also in dense raster-format. The conversion allows us to be able to point to the nuclear regions within the image and focus the segmentation analysis within those regions. The conversion can be done using the gdf2inst
and gdf2sem
functions from the histolytics package.
from histolytics.utils.raster import gdf2inst, gdf2sem
import matplotlib.pyplot as plt
from skimage.color import label2rgb
cls_dict = {
"neoplastic": 1,
"connective": 2,
"inflammatory": 3,
}
# convert the nuclei gdf into raster mask
inst_mask = gdf2inst(nuc, width=he.shape[1], height=he.shape[0])
type_mask = gdf2sem(nuc, class_dict=cls_dict, width=he.shape[1], height=he.shape[0])
fig, ax = plt.subplots(1, 3, figsize=(18, 6))
ax[0].imshow(he)
ax[1].imshow(label2rgb(inst_mask, bg_label=0, alpha=0.5))
ax[2].imshow(label2rgb(type_mask, bg_label=0, alpha=0.5))
plt.show()
Chromatin clump segmentation¶
Next, we will segment the chromatin clumps from the nuclei. This can be done using the chromatin_clumps
function. It takes the rasterized nuclei mask and the rgb image as input, and returns a list of segmented chromatin clumps, chromatin clump area per nuclei, and the chromatin-to-nuclei (area) proportion per nuclei. We will segment the chromatins for only the neoplastic nuclei in this example.
import numpy as np
from histolytics.nuc_feats.chromatin import chromatin_clumps
neo_mask = inst_mask.copy()
neo_mask[type_mask != cls_dict["neoplastic"]] = 0
chrom_mask, chrom_areas, chrom_nuc_props = chromatin_clumps(he, neo_mask)
from histolytics.utils.plot import draw_thing_contours
from skimage.measure import label
fig, ax = plt.subplots(1, 2, figsize=(16, 8))
contours = draw_thing_contours(label(chrom_mask), he, chrom_mask)
ax[0].imshow(he)
ax[1].imshow(contours)
<matplotlib.image.AxesImage at 0x737e363bd880>
Let's take a bit of a closer look at the segmented chromatin clumps.
from histolytics.utils.plot import draw_thing_contours
from skimage.measure import label
fig, ax = plt.subplots(1, 2, figsize=(16, 8))
# Take a center crop of size 512x512 from chrom_mask and he for zoomed-in illustration
h, w = chrom_mask.shape
crop_size = 512
start_y = (h - crop_size) // 2
start_x = (w - crop_size) // 2
chrom_mask_crop = chrom_mask[
start_y : start_y + crop_size, start_x : start_x + crop_size
]
he_crop = he[start_y : start_y + crop_size, start_x : start_x + crop_size]
contours = draw_thing_contours(label(chrom_mask_crop), he_crop, chrom_mask_crop)
ax[0].imshow(he_crop)
ax[1].imshow(contours)
<matplotlib.image.AxesImage at 0x737e35fe3e00>
The results look reasonable by eyeballing. If you feel like you want to convert the chromatin masks into a geodataframe you can do it in the following way:
from histolytics.utils.raster import inst2gdf
inst2gdf(
label(chrom_mask).astype(np.int32),
chrom_mask.astype(np.int32),
class_dict={1: "chromatin"},
min_size=3,
)
id | class_name | geometry | |
---|---|---|---|
0 | 2 | chromatin | POLYGON ((655.99 154.225, 655.785 154.795, 655... |
1 | 4 | chromatin | POLYGON ((695.99 167.225, 695.785 167.795, 695... |
2 | 1 | chromatin | POLYGON ((683.99 136.225, 683.785 136.795, 683... |
3 | 5 | chromatin | POLYGON ((719.961 206.225, 719.139 206.795, 71... |
4 | 7 | chromatin | POLYGON ((624.039 226.225, 624.86 226.775, 627... |
... | ... | ... | ... |
365 | 522 | chromatin | POLYGON ((1115.99 1304.45, 1115.785 1305.589, ... |
366 | 524 | chromatin | POLYGON ((1101.99 1317.225, 1101.785 1317.795,... |
367 | 523 | chromatin | POLYGON ((1116.971 1316.225, 1116.355 1316.795... |
368 | 528 | chromatin | POLYGON ((1055.981 1372.225, 1055.57 1372.795,... |
369 | 529 | chromatin | POLYGON ((1055.01 1389.899, 1055.215 1392.13, ... |
370 rows × 3 columns
Or alternatively, you can just add the computed chromatin features to the existing geodataframe:
nuc.loc[nuc["class_name"] == "neoplastic", "chromatin_clump_area"] = chrom_areas
nuc.loc[nuc["class_name"] == "neoplastic", "chromatin_nucleus_proportion"] = (
chrom_nuc_props
)
nuc
geometry | class_name | chromatin_clump_area | chromatin_nucleus_proportion | |
---|---|---|---|---|
0 | POLYGON ((1394.01 0, 1395.01 1.99, 1398 3.99, ... | connective | NaN | NaN |
1 | POLYGON ((1391 2.01, 1387 2.01, 1384.01 3.01, ... | connective | NaN | NaN |
2 | POLYGON ((1382.99 156.01, 1380 156.01, 1376.01... | connective | NaN | NaN |
3 | POLYGON ((1321 170.01, 1317.01 174.01, 1312.01... | connective | NaN | NaN |
4 | POLYGON ((1297.01 0, 1299.01 2.99, 1302 5.99, ... | connective | NaN | NaN |
... | ... | ... | ... | ... |
1290 | POLYGON ((258 495, 258 496, 255 496, 255 497, ... | inflammatory | NaN | NaN |
1291 | POLYGON ((855.25 359, 851 360.01, 849.01 361.0... | connective | NaN | NaN |
1292 | POLYGON ((841 405, 841 406, 840 406, 840 407, ... | neoplastic | 8.0 | 0.008869 |
1293 | POLYGON ((954 506, 954 507, 952 507, 952 508, ... | neoplastic | 3.0 | 0.003783 |
1294 | POLYGON ((772 473, 772 474, 771 474, 771 475, ... | connective | NaN | NaN |
1295 rows × 4 columns
Let's visualize which neoplastic nuclei have the highest chromatin clump area to nucleus area proportion i.e. the most chromatin-rich nuclei.
ax = nuc.plot(
figsize=(10, 10),
column="chromatin_nucleus_proportion",
legend=True,
)
ax.set_axis_off()
Note: the gdf.plot appears upside down, this is because the origin in the raster data is in the top left corner, while in the geodataframe it is in the bottom left corner.
Conclusion¶
In this workflow tutorial we have demonstrated how to extract chromatin clumps from nuclei in histological images and how to compute chromatin clump area and the proportion of chromatin clump area to the total nuclear area, and visualize the results. This type of analysis can be used to quantify chromatin organization and can provide insights into the cellular state and disease pathologies.