You need to sign in or sign up before continuing.
Newer
Older

Christopher Randolph Rhodes
committed
from pathlib import Path

Christopher Randolph Rhodes
committed
from typing import Dict, List

Christopher Randolph Rhodes
committed
from uuid import uuid4
import numpy as np

Christopher Randolph Rhodes
committed
import pandas as pd

Christopher Randolph Rhodes
committed
from skimage.measure import label, regionprops_table
from skimage.morphology import dilation
from sklearn.model_selection import train_test_split

Christopher Randolph Rhodes
committed

Christopher Randolph Rhodes
committed
from extensions.chaeo.accessors import MonoPatchStack

Christopher Randolph Rhodes
committed
from extensions.chaeo.annotators import draw_boxes_on_3d_image

Christopher Randolph Rhodes
committed
from extensions.chaeo.models import PatchStackObjectClassifier
from extensions.chaeo.process import mask_largest_object

Christopher Randolph Rhodes
committed
from extensions.chaeo.products import export_patches_from_zstack, export_patch_masks_from_zstack, export_multichannel_patches_from_zstack, get_patches_from_zmask_meta, get_patch_masks_from_zmask_meta
from extensions.chaeo.zmask import build_zmask_from_object_mask, project_stack_from_focal_points

Christopher Randolph Rhodes
committed
from extensions.ilastik.models import IlastikPixelClassifierModel

Christopher Randolph Rhodes
committed
from model_server.accessors import generate_file_accessor, InMemoryDataAccessor, write_accessor_data_to_file

Christopher Randolph Rhodes
committed
from model_server.models import Model

Christopher Randolph Rhodes
committed
from model_server.process import rescale

Christopher Randolph Rhodes
committed
from model_server.workflows import Timer

Christopher Randolph Rhodes
committed
def get_zmask_meta(
input_file_path: str,
ilastik_pixel_classifier: IlastikPixelClassifierModel,
segmentation_channel: int,
pxmap_threshold: float,
pxmap_foreground_channel: int = 0,
zmask_zindex: int = None,
zmask_clip: int = None,
zmask_filters: Dict = None,
zmask_type: str = 'boxes',

Christopher Randolph Rhodes
committed
**kwargs,

Christopher Randolph Rhodes
committed
) -> tuple:

Christopher Randolph Rhodes
committed
ti = Timer()
stack = generate_file_accessor(Path(input_file_path))
fstem = Path(input_file_path).stem
ti.click('file_input')
# MIP if no zmask z-index is given, then classify pixels
if isinstance(zmask_zindex, int):
assert 0 < zmask_zindex < stack.nz
zmask_data = stack.get_one_channel_data(channel=segmentation_channel).data[:, :, :, zmask_zindex]
else:
zmask_data = stack.get_one_channel_data(channel=segmentation_channel).data.max(axis=-1, keepdims=True)
if zmask_clip:
zmask_data = rescale(zmask_data, zmask_clip)
mip = InMemoryDataAccessor(
zmask_data,
)
pxmap, _ = ilastik_pixel_classifier.infer(mip)
ti.click('infer_pixel_probability')
obmask = InMemoryDataAccessor(
pxmap.data > pxmap_threshold
)
ti.click('threshold_pixel_mask')
# make zmask
zmask, zmask_meta, df, interm = build_zmask_from_object_mask(
obmask.get_one_channel_data(pxmap_foreground_channel),
stack.get_one_channel_data(segmentation_channel),
mask_type=zmask_type,
filters=zmask_filters,

Christopher Randolph Rhodes
committed
expand_box_by=kwargs['zmask_expand_box_by'],

Christopher Randolph Rhodes
committed
)
ti.click('generate_zmasks')
# record pixel scale
df['pixel_scale_in_micrometers'] = float(stack.pixel_scale_in_micrometers.get('X'))

Christopher Randolph Rhodes
committed
return ti, stack, fstem, obmask, pxmap, zmask, zmask_meta, df, interm

Christopher Randolph Rhodes
committed
def export_patches_from_multichannel_zstack(

Christopher Randolph Rhodes
committed
input_file_path: str,
output_folder_path: str,

Christopher Randolph Rhodes
committed
models: List[Model],

Christopher Randolph Rhodes
committed
pxmap_threshold: float,

Christopher Randolph Rhodes
committed
pxmap_foreground_channel: int,
segmentation_channel: int,

Christopher Randolph Rhodes
committed
patches_channel: int,

Christopher Randolph Rhodes
committed
zmask_zindex: int = None, # None for MIP,

Christopher Randolph Rhodes
committed
zmask_clip: int = None,
zmask_type: str = 'boxes',

Christopher Randolph Rhodes
committed
zmask_filters: Dict = None,
zmask_expand_box_by: int = None,
export_2d_patches_for_training=True,
export_2d_patches_for_annotation=True,
draw_bounding_box_on_2d_patch=True,
draw_contour_on_2d_patch=False,
draw_mask_on_2d_patch=False,
export_3d_patches=True,
export_annotated_zstack=True,
draw_label_on_zstack=False,

Christopher Randolph Rhodes
committed
rgb_overlay_channels=(None, None, None),
rgb_overlay_weights=(1.0, 1.0, 1.0),

Christopher Randolph Rhodes
committed
) -> Dict:

Christopher Randolph Rhodes
committed
pixel_classifier = models[0]

Christopher Randolph Rhodes
committed

Christopher Randolph Rhodes
committed
ti, stack, fstem, obmask, pxmap, zmask, zmask_meta, df, interm = get_zmask_meta(

Christopher Randolph Rhodes
committed
input_file_path,
pixel_classifier,
segmentation_channel,
pxmap_threshold,
pxmap_foreground_channel=pxmap_foreground_channel,
zmask_zindex=zmask_zindex,
zmask_clip=zmask_clip,
zmask_expand_box_by=zmask_expand_box_by,
zmask_filters=zmask_filters,
zmask_type=zmask_type,

Christopher Randolph Rhodes
committed
)

Christopher Randolph Rhodes
committed
if export_pixel_probabilities:
write_accessor_data_to_file(

Christopher Randolph Rhodes
committed
Path(output_folder_path) / 'pixel_probabilities' / (fstem + '.tif'),
pxmap
)
ti.click('export_pixel_probability')

Christopher Randolph Rhodes
committed
if export_3d_patches and len(zmask_meta) > 0:

Christopher Randolph Rhodes
committed
Path(output_folder_path) / '3d_patches',
stack.get_one_channel_data(patches_channel),
zmask_meta,
prefix=fstem,
draw_bounding_box=False,
rescale_clip=0.001,
make_3d=True,
)
ti.click('export_3d_patches')

Christopher Randolph Rhodes
committed
if export_2d_patches_for_annotation and len(zmask_meta) > 0:
files = export_multichannel_patches_from_zstack(

Christopher Randolph Rhodes
committed
Path(output_folder_path) / '2d_patches_annotation',

Christopher Randolph Rhodes
committed
rescale_clip=0.001,
make_3d=False,
focus_metric='max_sobel',

Christopher Randolph Rhodes
committed
ch_white=patches_channel,

Christopher Randolph Rhodes
committed
ch_rgb_overlay=rgb_overlay_channels,
draw_bounding_box=draw_bounding_box_on_2d_patch,

Christopher Randolph Rhodes
committed
bounding_box_linewidth=2,
draw_contour=draw_contour_on_2d_patch,
draw_mask=draw_mask_on_2d_patch,

Christopher Randolph Rhodes
committed
overlay_gain=rgb_overlay_weights,

Christopher Randolph Rhodes
committed
df_patches = pd.DataFrame(files)

Christopher Randolph Rhodes
committed
# associate 2d patches, dropping labeled objects that were not exported as patches
df = pd.merge(df, df_patches, left_index=True, right_on='df_index').drop(columns='df_index')
# prepopulate patch UUID
df['patch_id'] = df.apply(lambda _: uuid4(), axis=1)
if export_2d_patches_for_training and len(zmask_meta) > 0:
files = export_multichannel_patches_from_zstack(

Christopher Randolph Rhodes
committed
Path(output_folder_path) / '2d_patches_training',

Christopher Randolph Rhodes
committed
stack.get_one_channel_data(patches_channel),
zmask_meta,
prefix=fstem,
rescale_clip=0.001,
make_3d=False,
focus_metric='max_sobel',
)
ti.click('export_2d_patches')
if export_patch_masks and len(zmask_meta) > 0:
files = export_patch_masks_from_zstack(

Christopher Randolph Rhodes
committed
Path(output_folder_path) / 'patch_masks',
stack.get_one_channel_data(patches_channel),
prefix=fstem,

Christopher Randolph Rhodes
committed
)
if export_annotated_zstack:
annotated = InMemoryDataAccessor(
draw_boxes_on_3d_image(
stack.get_one_channel_data(patches_channel).data,
zmask_meta,
add_label=draw_label_on_zstack,
)
)
write_accessor_data_to_file(

Christopher Randolph Rhodes
committed
Path(output_folder_path) / 'annotated_zstacks' / (fstem + '.tif'),
annotated
)
ti.click('export_annotated_zstack')
# generate multichannel projection from label centroids
dff = df[df['keeper']]
if len(zmask_meta) > 0:
interm['projected'] = project_stack_from_focal_points(
dff['centroid-0'].to_numpy(),
dff['centroid-1'].to_numpy(),
dff['zi'].to_numpy(),
stack,
degree=4,
)
else: # else just return MIP
interm['projected'] = stack.data.max(axis=-1)

Christopher Randolph Rhodes
committed

Christopher Randolph Rhodes
committed
return {

Christopher Randolph Rhodes
committed
'pixel_model_id': pixel_classifier.model_id,

Christopher Randolph Rhodes
committed
'input_filepath': input_file_path,

Christopher Randolph Rhodes
committed
'number_of_objects': len(zmask_meta),
'pixeL_scale_in_micrometers': stack.pixel_scale_in_micrometers,

Christopher Randolph Rhodes
committed
'success': True,
'timer_results': ti.events,
'dataframe': df[df['keeper'] == True],

Christopher Randolph Rhodes
committed
'interm': interm,

Christopher Randolph Rhodes
committed
def infer_object_map_from_zstack(

Christopher Randolph Rhodes
committed
input_file_path: str,
output_folder_path: str,
models: List[Model],
pxmap_threshold: float,
pxmap_foreground_channel: int,
segmentation_channel: int,
patches_channel: int,
zmask_zindex: int = None, # None for MIP,
zmask_clip: int = None,
zmask_type: str = 'boxes',
zmask_filters: Dict = None,

Christopher Randolph Rhodes
committed
# zmask_expand_box_by: int = None,

Christopher Randolph Rhodes
committed
**kwargs,
) -> Dict:

Christopher Randolph Rhodes
committed
assert len(models) == 2

Christopher Randolph Rhodes
committed
pixel_classifier = models[0]

Christopher Randolph Rhodes
committed
assert isinstance(pixel_classifier, IlastikPixelClassifierModel)
object_classifier = models[1]
assert isinstance(object_classifier, PatchStackObjectClassifier)

Christopher Randolph Rhodes
committed
ti, stack, fstem, obmask, pxmap, zmask, zmask_meta, df, interm = get_zmask_meta(
input_file_path,
pixel_classifier,
segmentation_channel,
pxmap_threshold,
pxmap_foreground_channel=pxmap_foreground_channel,
zmask_zindex=zmask_zindex,
zmask_clip=zmask_clip,

Christopher Randolph Rhodes
committed
# zmask_expand_box_by=zmask_expand_box_by,

Christopher Randolph Rhodes
committed
zmask_filters=zmask_filters,
zmask_type=zmask_type,

Christopher Randolph Rhodes
committed
**kwargs

Christopher Randolph Rhodes
committed
)
# extract patches to accessor
patches_acc = get_patches_from_zmask_meta(

Christopher Randolph Rhodes
committed
stack.get_one_channel_data(patches_channel),

Christopher Randolph Rhodes
committed
zmask_meta,
rescale_clip=zmask_clip,
make_3d=False,

Christopher Randolph Rhodes
committed
focus_metric='max_sobel',

Christopher Randolph Rhodes
committed
**kwargs
)
# extract masks
patch_masks_acc = get_patch_masks_from_zmask_meta(
stack,
zmask_meta,
**kwargs
)
# send patches and mask stacks to object classifier
result_acc, _ = object_classifier.infer(patches_acc, patch_masks_acc)

Christopher Randolph Rhodes
committed

Christopher Randolph Rhodes
committed
labels_map = interm['label_map']
output_map = np.zeros(labels_map.shape, dtype=labels_map.dtype)
assert labels_map.shape == interm['label_map'].shape
assert labels_map.dtype == interm['label_map'].dtype

Christopher Randolph Rhodes
committed
# assign labels to object map:

Christopher Randolph Rhodes
committed
meta = []

Christopher Randolph Rhodes
committed
for ii in range(0, len(zmask_meta)):

Christopher Randolph Rhodes
committed
object_id = zmask_meta[ii]['info'].label
result_patch = mask_largest_object(result_acc.iat(ii))
object_class = np.unique(result_patch)[1]
output_map[labels_map == object_id] = object_class
meta.append({'object_id': ii, 'object_class': object_id})

Christopher Randolph Rhodes
committed

Christopher Randolph Rhodes
committed
output_path = Path(output_folder_path) / ('obj_classes_' + (fstem + '.tif'))

Christopher Randolph Rhodes
committed
write_accessor_data_to_file(

Christopher Randolph Rhodes
committed
output_path,

Christopher Randolph Rhodes
committed
InMemoryDataAccessor(output_map)

Christopher Randolph Rhodes
committed
)
ti.click('export_object_classes')

Christopher Randolph Rhodes
committed
return {
'timer_results': ti.events,
'dataframe': pd.DataFrame(meta),
'interm': {},

Christopher Randolph Rhodes
committed
'output_path': output_path.__str__(),

Christopher Randolph Rhodes
committed
}

Christopher Randolph Rhodes
committed

Christopher Randolph Rhodes
committed
def transfer_ecotaxa_labels_to_patch_stacks(

Christopher Randolph Rhodes
committed
where_masks: str,
where_patches: str,
object_csv: str,
ecotaxa_tsv: str,
where_output: str,
patch_size: tuple = (256, 256),
tr_split=0.6,
dilate_label_mask: bool = True, # to mitigate connected components error in ilastik
allow_multiple_objects: bool = False,
) -> Dict:
assert tr_split > 0.5 # reduce chance that low-probability objects are omitted from training
# read patch metadata
df_obj = pd.read_csv(
object_csv,
)
df_ecotaxa = pd.read_csv(
ecotaxa_tsv,
header=[0],
dtype={
('object_annotation_date', '[t]'): str,
('object_annotation_time', '[t]'): str,
('object_annotation_category_id', '[t]'): str,
}
)
df_merge = pd.merge(df_obj, df_ecotaxa, left_on='patch_id', right_on='object_id')
# assign each unique lowest-level annotation to a class index
se_unique = pd.Series(
df_merge.object_annotation_hierarchy.unique()
)
df_split = (
se_unique.str.rsplit(
pat='>', n=1, expand=True
)
)
df_labels = pd.DataFrame({
'annotation_class_id': df_split.index + 1,
'hierarchy': se_unique,
'annotation_class': df_split.loc[:, 1].str.lower()
})
# join patch filenames and annotation classes
df_pf = pd.merge(
df_merge[['patch_filename', 'object_annotation_hierarchy']],
df_labels,
left_on='object_annotation_hierarchy',
right_on='hierarchy',
)
df_pl = df_pf[df_pf['object_annotation_hierarchy'].notnull()]
# export annotation classes and their summary stats
df_tr, df_te = train_test_split(df_pl, train_size=tr_split)

Christopher Randolph Rhodes
committed
# df_labels['counts'] = df_pl['annotation_class_id'].value_counts()
df_labels = pd.merge(
df_labels,
pd.DataFrame(

Christopher Randolph Rhodes
committed
[df_pl.annotation_class_id.value_counts(), df_tr.annotation_class_id.value_counts(), df_te.annotation_class_id.value_counts()],
index=['total', 'to_train', 'to_test']
).T,
left_on='annotation_class_id',
right_index=True,
how='outer'
)
df_labels.loc[df_labels.to_train.isna(), 'to_train'] = 0
df_labels.loc[df_labels.to_test.isna(), 'to_test'] = 0

Christopher Randolph Rhodes
committed
for col in ['total', 'to_train', 'to_test']:
df_labels.loc[df_labels[col].isna(), col] = 0
df_labels.to_csv(Path(where_output) / 'labels_key.csv', index=False)
# export patches as z-stacks
for (dfk, dfv) in {'train': df_tr, 'test': df_te}.items():
zstack_keys = ['mask', 'label', 'raw']
zstacks = {f'{dfk}_{zsk}': np.zeros((*patch_size, 1, len(dfv)), dtype='uint8') for zsk in zstack_keys}
stack_meta = []
for fi, pl in enumerate(dfv.itertuples(name='PatchFile')):
fn = pl._asdict()['patch_filename']
ac = pl._asdict()['annotation_class']
aci = pl._asdict()['annotation_class_id']
stack_meta.append({'zi': fi, 'patch_filename': fn, 'annotation_class': ac, 'annotation_class_id': aci})
acc_bm = generate_file_accessor(Path(where_masks) / fn)
assert acc_bm.hw == patch_size, f'Unexpected patch size {patch_size}'
assert acc_bm.chroma == 1
assert acc_bm.nz == 1
mask = acc_bm.data[:, :, 0, 0]
if dilate_label_mask:
mask = dilation(mask)

Christopher Randolph Rhodes
committed
if not allow_multiple_objects:
ob_id = label(acc_bm.data[:, :, 0, 0])
mask = mask_largest_object(ob_id)
zstacks[dfk + '_mask'][:, :, 0, fi] = mask
zstacks[dfk + '_label'][:, :, 0, fi] = (mask == 255) * aci
acc_pa = generate_file_accessor(Path(where_patches) / fn)
zstacks[dfk + '_raw'][:, :, :, fi] = acc_pa.data[:, :, :, 0]
for k in zstacks.keys():
write_accessor_data_to_file(Path(where_output) / f'zstack_{k}.tif', InMemoryDataAccessor(zstacks[k]))
pd.DataFrame(stack_meta).to_csv(Path(where_output) / f'{dfk}_stack.csv', index=False)