Source code for flyqma.measurement.measure
from warnings import filterwarnings, catch_warnings
import numpy as np
import pandas as pd
from scipy.ndimage.measurements import mean, standard_deviation, center_of_mass
[docs]class Measurements:
"""
Object measures properties of labeled segments within an image.
Attributes:
colordepth (int) - number of color channels
segment_ids (np.ndarray[float]) - ordered segment labels
levels (dict) - {channel: np.ndarray[float]} - expression levels
std (dict) - {channel: np.ndarray[float]} - expression std. deviation
xpos (np.ndarray[float]) - segment centroid x-positions
ypos (np.ndarray[float]) - segment centroid y-positions
voxel_size (np.ndarray[float]) - segment voxel size
"""
def __init__(self, im, labels):
"""
Measure properties of labeled segments within an image.
Args:
im (np.ndarray[float]) - 3D array of pixel values
labels (np.ndarray[int]) - cell segment labels
"""
self.colordepth = im.shape[-1]
# set segment ids (ordered)
self.segment_ids = np.unique(labels[labels.nonzero()])
# measure expression levels
self.measure_expression(im, labels, self.segment_ids)
# measure segment centroids
self.measure_centroids(labels, self.segment_ids)
# measure segment voxel sizes
self.measure_segment_size(labels, self.segment_ids)
[docs] def measure_expression(self, im, labels, segment_ids):
"""
Measure expression levels.
Args:
im (np.ndarray[float]) - 3D array of pixel values
labels (np.ndarray[int]) - cell segment labels
segment_ids (np.ndarray[int]) - ordered segment IDs
"""
# split R/G/B image channels
drop = lambda x: x.reshape(*x.shape[:2])
channels = [drop(x) for x in np.split(im, self.colordepth, axis=-1)]
# compute means
means = [mean(channel, labels, segment_ids) for channel in channels]
# compute std
with catch_warnings():
filterwarnings('ignore')
evaluate_std = lambda x: standard_deviation(x, labels, segment_ids)
stds = [evaluate_std(channel) for channel in channels]
# compile dictionaries
self.levels = dict(enumerate(means))
self.std = dict(enumerate(stds))
[docs] def measure_centroids(self, labels, segment_ids):
"""
Measure the centroid of each segment.
Args:
labels (np.ndarray[int]) - cell segment labels
segment_ids (np.ndarray[int]) - ordered segment IDs
"""
centroid_dict = self.evaluate_centroids(labels)
centroids = [centroid_dict[seg_id] for seg_id in segment_ids]
xpos, ypos = list(zip(*centroids))
self.xpos = xpos
self.ypos = ypos
[docs] @staticmethod
def evaluate_centroids(labels):
"""
Evaluate center of mass of each label.
* Note: scipy returns centroids as (y, x) which are flipped to (x, y)
Args:
labels (np.ndarray[int]) - segment label mask
Returns:
center_of_mass (dict) - {segment_id: [xpos, ypos]} pairs
"""
seg_ids = np.unique(labels[labels!=0])
coms = center_of_mass(labels, labels, seg_ids)
return {seg_id: com[::-1] for seg_id, com in zip(seg_ids, coms)}
[docs] def measure_segment_size(self, labels, segment_ids):
"""
Measure the voxel size of each segment.
Args:
labels (np.ndarray[int]) - cell segment labels
segment_ids (np.ndarray[int]) - ordered segment IDs
"""
voxels = labels[labels!=0]
bins = np.arange(0, segment_ids.max()+3, 1)
counts, _ = np.histogram(voxels, bins=bins)
self.voxel_counts = counts[segment_ids]
[docs] def build_dataframe(self):
"""
Build and return dataframe containing all measurements.
Returns:
measurements (pd.DataFrame) - measurement data
"""
# construct dataframe
measurement_data = dict(
segment_id=self.segment_ids,
centroid_x=self.xpos,
centroid_y=self.ypos,
pixel_count=self.voxel_counts)
# add intensity measurements
for channel_id in range(self.colordepth):
# define keys
key = 'ch{:d}'.format(channel_id)
key_std = key + '_std'
# store measured levels
measurement_data[key] = self.levels[channel_id]
measurement_data[key_std] = self.std[channel_id]
return pd.DataFrame(measurement_data)