diff --git a/extensions/chaeo/examples/transfer_labels_to_ilastik_object_classifier.py b/extensions/chaeo/examples/transfer_labels_to_ilastik_object_classifier.py index d79127c39825cafc56866cc6c4c3711801b404a1..a02840be65598df36065a37a750eb9597a707ee2 100644 --- a/extensions/chaeo/examples/transfer_labels_to_ilastik_object_classifier.py +++ b/extensions/chaeo/examples/transfer_labels_to_ilastik_object_classifier.py @@ -13,9 +13,13 @@ from extensions.ilastik.models import IlastikObjectClassifierFromSegmentationMod from model_server.accessors import generate_file_accessor, GenericImageDataAccessor, InMemoryDataAccessor, write_accessor_data_to_file class PatchStackObjectClassifier(IlastikObjectClassifierFromSegmentationModel): + """ + Wrap ilastik object classification for inputs comprising raw image and binary segmentation masks, both represented + as time-series images where each frame contains only one object. + """ @staticmethod - def make_tczyx(acc): + def make_tczyx(acc: GenericImageDataAccessor): assert acc.chroma == 1 tyx = np.moveaxis( acc.data[:, :, 0, :], # YX(C)Z @@ -23,7 +27,6 @@ class PatchStackObjectClassifier(IlastikObjectClassifierFromSegmentationModel): [0, 1, 2] ) return np.expand_dims(tyx, (1, 2)) - # return tyx def infer(self, input_img: GenericImageDataAccessor, segmentation_img: GenericImageDataAccessor) -> (np.ndarray, dict): assert segmentation_img.is_mask() @@ -39,11 +42,11 @@ class PatchStackObjectClassifier(IlastikObjectClassifierFromSegmentationModel): } ] - obmaps = self.shell.workflow.batchProcessingApplet.run_export(dsi, export_to_array=True) # [z x h x w x n] + obmaps = self.shell.workflow.batchProcessingApplet.run_export(dsi, export_to_array=True) # [z x h x w x n] assert len(obmaps) == 1, 'ilastik generated more than one object map' - # for some reason these axes get scrambled to Z(1)YX(1) + # for some reason ilastik scrambles these axes to Z(1)YX(1) assert obmaps[0].shape == (input_img.nz, 1, input_img.hw[0], input_img.hw[1], 1) yxcz = np.moveaxis( obmaps[0][:, :, :, :, 0], @@ -54,7 +57,13 @@ class PatchStackObjectClassifier(IlastikObjectClassifierFromSegmentationModel): assert yxcz.shape == input_img.shape return InMemoryDataAccessor(data=yxcz), {'success': True} -def get_dataset_info(h5, lane=0): +def get_dataset_info(h5: h5py.File, lane : int = 0): + """ + Report out specific datasets in ilastik project file HDF5 + :param h5: handle to ilastik project file, as h5py.File object + :param lane: ilastik lane identifier + :return: (dict) selected data values from project file + """ lns = f'{lane:04d}' lane = f'Input Data/infos/lane{lns}' info = {} @@ -79,13 +88,22 @@ def get_dataset_info(h5, lane=0): return info -def generate_ilastik_object_classifier(template_ilp, where: str, lane=0): +def generate_ilastik_object_classifier(template_ilp: str, where: str, stack_name: str = 'train', lane: int = 0): + """ + Starting with a template project file, transfer input data and labels to a duplicate project file. + + :param template_ilp: absolute path to existing ilastik object classifier to use as a template + :param where: location of folder containing input data, segmentation maps, labels, and label descriptions + :poram stack_name: prefix of .tif and .csv files that contain classifier training data (e.g. train, test) + :param lane: ilastik lane identifier + :return: (str) name of new ilastik classifier project file + """ # validate z-stack input data root = Path(where) paths = { - 'Raw Data': root / 'zstack_train_raw.tif', - 'Segmentation Image': root / 'zstack_train_mask.tif', + 'Raw Data': root / f'zstack_{stack_name}_raw.tif', + 'Segmentation Image': root / f'zstack_{stack_name}_mask.tif', } accessors = {k: generate_file_accessor(pa) for k, pa in paths.items()} @@ -97,9 +115,9 @@ def generate_ilastik_object_classifier(template_ilp, where: str, lane=0): nz = accessors['Raw Data'].nz # now load CSV - csv_path = root / 'train_stack.csv' + csv_path = root / f'{stack_name}_stack.csv' assert csv_path.exists() - df_patches = pd.read_csv(root / 'train_stack.csv') + df_patches = pd.read_csv(csv_path) assert np.all( df_patches['zi'].sort_values().to_numpy() == np.arange(0, nz) ) @@ -142,7 +160,6 @@ def generate_ilastik_object_classifier(template_ilp, where: str, lane=0): # change object labels la_groupname = f'ObjectClassification/LabelInputs/{lns}' - del h5[la_groupname] lag = h5.create_group(la_groupname) for zi in range(0, nz): @@ -151,6 +168,12 @@ def generate_ilastik_object_classifier(template_ilp, where: str, lane=0): return new_ilp def compare_object_maps(truth: GenericImageDataAccessor, inferred: GenericImageDataAccessor) -> pd.DataFrame: + """ + Compare two object maps to assess classification results + :param truth: t-stack of truth objects + :param inferred: t-stack of inferred objects, presumably with same segmentation boundaries as truth + :return: DataFrame comparing results for each frame in truth and inferred stacks + """ assert truth.shape == inferred.shape assert np.all((truth.data == 0) == (inferred.data == 0)) assert inferred.chroma == 1 @@ -180,25 +203,43 @@ def compare_object_maps(truth: GenericImageDataAccessor, inferred: GenericImageD if __name__ == '__main__': root = Path('c:/Users/rhodes/projects/proj0011-plankton-seg/') template_ilp = root / 'exp0014/template_obj.ilp' - # template_ilp = root / 'exp0014/test_obj_from_seg.ilp' - where_patch_stack = root / 'exp0009/output/labeled_patches-20231016-0002' + where_patch_stack = root / 'exp0009/output/labeled_patches-20231018-0000' + # auto-populate an object classifier new_ilp = generate_ilastik_object_classifier( template_ilp, where_patch_stack, + stack_name='train' ) - train_zstack_raw = generate_file_accessor(where_patch_stack / 'zstack_train_raw.tif') - train_zstack_mask = generate_file_accessor(where_patch_stack / 'zstack_train_mask.tif') + def infer_and_compare(suffix): + # infer object labels from the same data used to train the classifier + train_zstack_raw = generate_file_accessor(where_patch_stack / 'zstack_train_raw.tif') + train_zstack_mask = generate_file_accessor(where_patch_stack / 'zstack_train_mask.tif') + mod = PatchStackObjectClassifier({'project_file': where_patch_stack / new_ilp}) + result_acc, _ = mod.infer(train_zstack_raw, train_zstack_mask) + write_accessor_data_to_file(where_patch_stack / f'zstack_train_result_{suffix}.tif', result_acc) + + # write comparison tables + train_truth_labels = generate_file_accessor(where_patch_stack / f'zstack_train_label.tif') + df_comp = compare_object_maps(train_truth_labels, result_acc) + df_comp.to_csv( + where_patch_stack / autonumber_new_file( + where_patch_stack, f'compare_train_result_{suffix}', 'csv' + ), + index=False + ) + print('Truth and inferred labels match?') + print(pd.value_counts(df_comp['truth_label'] == df_comp['inferred_label'])) + + # infer object labels from the same data used to train the classifier + infer_and_compare('before') - mod = PatchStackObjectClassifier({'project_file': new_ilp}) + # prompt user input when ilastik file has been modified in-app + print(f'Press enter when project file {new_ilp} has been updated in ilastik') + input() - result_acc, _ = mod.infer(train_zstack_raw, train_zstack_mask) - write_accessor_data_to_file(where_patch_stack / 'result.tif', result_acc) - print(where_patch_stack / 'result.tif') + # repeat inference with the same project file, but a fresh model handle + infer_and_compare('after') - # write comparison - train_labels = generate_file_accessor(where_patch_stack / 'zstack_train_label.tif') - df_comp = compare_object_maps(train_labels, result_acc) - df_comp.to_csv(where_patch_stack / autonumber_new_file(where_patch_stack, 'comp', 'csv'), index=False)