diff --git a/.github/workflows/run_tests.yaml b/.github/workflows/run_tests.yaml index 10d2048d..da34a091 100644 --- a/.github/workflows/run_tests.yaml +++ b/.github/workflows/run_tests.yaml @@ -48,7 +48,7 @@ jobs: fail-fast: false matrix: os: [ "ubuntu-latest", "macos-latest", "windows-latest" ] - python-version: [ "3.7" ] + python-version: [ "3.8" ] # Main steps for the test to be reproduced across OS x Python steps: diff --git a/AxonDeepSeg/__init__.py b/AxonDeepSeg/__init__.py index 88c513ea..ce1305bf 100644 --- a/AxonDeepSeg/__init__.py +++ b/AxonDeepSeg/__init__.py @@ -1 +1 @@ -__version__ = "3.3.0" +__version__ = "4.0.0" diff --git a/AxonDeepSeg/apply_model.py b/AxonDeepSeg/apply_model.py index f9dfb587..279d8053 100644 --- a/AxonDeepSeg/apply_model.py +++ b/AxonDeepSeg/apply_model.py @@ -1,493 +1,60 @@ -# -*- coding: utf-8 -*- -import numpy as np -from skimage.transform import rescale, resize +from pathlib import Path # AxonDeepSeg imports import AxonDeepSeg.ads_utils as ads -from AxonDeepSeg.ads_utils import convert_path -from AxonDeepSeg.network_construction import uconv_net -from AxonDeepSeg.visualization.get_masks import get_masks -from AxonDeepSeg.patch_management_tools import im2patches_overlap, patches2im_overlap -from AxonDeepSeg.config_tools import update_config, default_configuration -from config import axonmyelin_suffix - -# Keras import -from keras import backend as K - -# TensorFlow import -import tensorflow as tf - - -def apply_convnet(path_acquisitions, acquisitions_resolutions, path_model_folder, config_dict, ckpt_name='model', - inference_batch_size=1, overlap_value=25, resampled_resolutions=[0.1], - prediction_proba_activate=False, gpu_per=1.0, verbosity_level=0): - """ - Preprocesses the images, transform them into patches, applies the network, stitches the predictions and return them. - :param path_acquisitions: List of path to the acquisitions. - :param acquisitions_resolutions: List of the acquisitions resolutions (floats). - :param path_model_folder: Path to the model folder. - :param config_dict: Dictionary containing the model's parameters. - :param ckpt_name: String, checkpoint to use. - :param inference_batch_size: Int, batch size to use when doing inference. - :param overlap_value: Int, number of pixels to use when overlapping the predictions of the network. - :param resampled_resolutions: List of resolutions (flaots) to resample to before performing inference. - :param prediction_proba_activate: Boolean, whether to compute the probability maps or not. - :param gpu_per: Float, percentage of GPU to use if we use it. - :param verbosity_level: Int, how much information to display. - :return: List of segmentations, and list of probability maps if requested. - """ - - # If string, convert to Path objects - path_acquisitions = convert_path(path_acquisitions) - path_model_folder = convert_path(path_model_folder) - - # We set the logging from python and Tensorflow to a high level, to avoid messages - # in the console when performing segmentation. - from logging import ERROR - tf.logging.set_verbosity(ERROR) - import warnings - warnings.filterwarnings('ignore') - - # Network Parameters - patch_size = config_dict["trainingset_patchsize"] - n_classes = config_dict["n_classes"] - - # STEP 1: Load and rescale the acquisitions, and transform them into patches. - - rs_acquisitions, rs_coeffs, original_acquisitions_shapes = load_acquisitions( - path_acquisitions, acquisitions_resolutions, resampled_resolutions, verbose_mode=verbosity_level) - - # If we are unable to load the model, we return an error message - if not path_model_folder.exists(): - print('Error: unable to find the requested model.') - return [None] * len(path_acquisitions) - - L_data, L_n_patches, L_positions = prepare_patches(rs_acquisitions, patch_size, overlap_value) - - # STEP 2: Construct Tensorflow's computing graph and restoration of the session - - # Construction of the graph - if verbosity_level >= 2: - print("Graph construction ...") - - x = tf.placeholder(tf.float32, shape=(None, patch_size, patch_size, 1)) - - model = uconv_net(config_dict, bn_updated_decay=None, verbose=True) # inference - pred = model.output - - - saver = tf.train.Saver() # Load previous model - - # We limit the amount of GPU for inference - config_gpu = tf.ConfigProto(log_device_placement=False) - config_gpu.gpu_options.per_process_gpu_memory_fraction = gpu_per - - # Launch the session (this part takes time). All images will be processed by loading the session just once. - - sess = tf.Session(config=config_gpu) - K.set_session(sess) - - model_previous_path = path_model_folder.joinpath(ckpt_name).with_suffix('.ckpt') - saver.restore(sess, str(model_previous_path)) - - # STEP 3: Inference - - if verbosity_level >= 2: - print("Beginning inference ...") - - n_patches = len(L_data) - it, rem = divmod(n_patches, inference_batch_size) - - predictions_list = [] - predictions_proba_list = [] - - # Inference of complete batches - for i in range(it): - - if verbosity_level >= 3: - print(('processing patch %s of %s' % (i + 1, it))) - - batch_x = np.array(L_data[i * inference_batch_size:(i + 1) * inference_batch_size], dtype=np.uint8) - - if prediction_proba_activate: - - # First we perform inference on the input. - current_batch_prediction, current_batch_prediction_proba = perform_batch_inference( - model, sess, pred, x, batch_x, inference_batch_size, patch_size, - n_classes, prediction_proba_activate=prediction_proba_activate) - - # Update of the predictions lists. - predictions_list.extend(current_batch_prediction) - predictions_proba_list.extend(current_batch_prediction_proba) - - else: - current_batch_prediction = perform_batch_inference(model, sess, pred, x, batch_x, inference_batch_size, - patch_size, n_classes, - prediction_proba_activate=prediction_proba_activate) - # Update of the predictions lists. - predictions_list.extend(current_batch_prediction) - - # Last batch if needed - - if rem != 0: - - if verbosity_level >= 4: - print('processing last patch') - - batch_x = np.asarray(L_data[it * inference_batch_size:]) - - if prediction_proba_activate: - - # First we perform inference on the input. - current_batch_prediction, current_batch_prediction_proba = perform_batch_inference(model, - sess, pred, batch_x, rem, - patch_size, - n_classes, - prediction_proba_activate=prediction_proba_activate) - - # Update of the predictions lists. - predictions_list.extend(current_batch_prediction) - predictions_proba_list.extend(current_batch_prediction_proba) - - else: - current_batch_prediction = perform_batch_inference(model, sess, pred, batch_x, rem, - patch_size, n_classes, - prediction_proba_activate=prediction_proba_activate) - # Update of the predictions lists. - predictions_list.extend(current_batch_prediction) - - # End of the inference step. - tf.reset_default_graph() - - # Now we have to transform the list of predictions in list of lists, - # one for each full image : we put in each sublist the patches corresponding to a full image. - - ########### STEP 4: Reconstruction of the segmented patches into segmentations of acquisitions and - # resampling to the original size - - if prediction_proba_activate: - - predictions, predictions_proba = process_segmented_patches(predictions_list, L_n_patches, L_positions, - original_acquisitions_shapes, - overlap_value, n_classes, - predictions_proba_list=predictions_proba_list, - prediction_proba_activate=prediction_proba_activate, - verbose_mode=0) - - return predictions, predictions_proba - - else: - predictions = process_segmented_patches(predictions_list, L_n_patches, L_positions, - original_acquisitions_shapes, - overlap_value, n_classes, - predictions_proba_list=None, - prediction_proba_activate=prediction_proba_activate, - verbose_mode=0) - - return predictions - - ####################################################################################################################### - - -def axon_segmentation(path_acquisitions_folders, acquisitions_filenames, path_model_folder, config_dict, - ckpt_name='model', - segmentations_filenames=[str(axonmyelin_suffix)], inference_batch_size=1, - overlap_value=25, resampled_resolutions=0.1, acquired_resolution=None, - prediction_proba_activate=False, write_mode=True, gpu_per=1.0, verbosity_level=0): - """ - Wrapper performing the segmentation of all the requested acquisitions and generates (if requested) the segmentation - images. - :param path_acquisitions_folders: List of folders where the acquisitions to segment are located. - :param acquisitions_filenames: List of names of acquisitions to segment. - :param path_model_folder: Path to the folder where the model is located. - :param config_dict: Dictionary containing the configuration of the training parameters of the model. - :param ckpt_name: String, name of the checkpoint to use. - :param segmentations_filenames: List of the names of the segmentations files, to be used when creating the files. - :param inference_batch_size: Size of the batches fed to the network. - :param overlap_value: Int, number of pixels to use for overlapping the predictions. - :param resampled_resolutions: List of the resolutions (in µm) to resample to. - :param acquired_resolution: List of the resolutions (in µm) for native images. - :param prediction_proba_activate: Boolean, whether to compute probability maps or not. - :param write_mode: Boolean, whether to create segmentation images or not. - :param gpu_per: Percentage of the GPU to use, if we use it. - :param verbosity_level: Int, level of verbosity. The higher, the more information is displayed. - :return: List of predictions, and optionally of probability maps. - """ - - # If string, convert to Path objects - path_acquisitions_folders = convert_path(path_acquisitions_folders) - path_model_folder = convert_path(path_model_folder) - # Processing input so they are lists in every situation - path_acquisitions_folders, acquisitions_filenames, resampled_resolutions, segmentations_filenames = \ - list(map(ensure_list_type, [path_acquisitions_folders, acquisitions_filenames, resampled_resolutions, - segmentations_filenames])) - - if len(segmentations_filenames) != len(path_acquisitions_folders): - segmentations_filenames = [str(axonmyelin_suffix)] * len(path_acquisitions_folders) - - if len(acquisitions_filenames) != len(path_acquisitions_folders): - acquisitions_filenames = ['image.png'] * len(path_acquisitions_folders) - - if len(resampled_resolutions) != len(path_acquisitions_folders): - resampled_resolutions = [resampled_resolutions[0]] * len(path_acquisitions_folders) - - # Generating the patch to acquisitions and loading the acquisitions resolutions. - path_acquisitions = [path_acquisitions_folders[i] / e for i, e in enumerate(acquisitions_filenames)] +from AxonDeepSeg.visualization.merge_masks import merge_masks +from config import axon_suffix, myelin_suffix, axonmyelin_suffix + +from ivadomed import inference as imed_inference + +def axon_segmentation( + path_acquisitions_folders, + acquisitions_filenames, + path_model_folder, + overlap_value=[48,48], + acquired_resolution=None, + verbosity_level = 0 + ): + ''' + Segment images using IVADOMED. + :param path_acquisitions_folders: the directory containing the images to segment. + :param acquisitions_filenames: filenames of the images to segment. + :param path_model_folder: path to the folder of the IVADOMED-trained model. + :param overlap_value: the number of pixels to be used for overlap when doing prediction. Higher value means less + border effects but more time to perform the segmentation. + :param acquired_resolution: isotropic pixel size of the acquired images. + :param verbosity_level: Level of verbosity. The higher, the more information is given about the segmentation + process. + :return: Nothing. + ''' # If we did not receive any resolution we read the pixel size in micrometer from each pixel. if acquired_resolution == None: - if (path_acquisitions_folders[0] / 'pixel_size_in_micrometer.txt').exists(): - resolutions_files = [open(path_acquisition_folder / 'pixel_size_in_micrometer.txt', 'r') - for path_acquisition_folder in path_acquisitions_folders] - acquisitions_resolutions = [float(file_.read()) for file_ in resolutions_files] + if (path_acquisitions_folders / 'pixel_size_in_micrometer.txt').exists(): + resolutions_file = open(path_acquisitions_folders / 'pixel_size_in_micrometer.txt', 'r') + str_resolution = [float(file_.read()) for file_ in resolutions_file] + acquired_resolution = float(str_resolution[0]) else: exception_msg = "ERROR: No pixel size is provided, and there is no pixel_size_in_micrometer.txt file in image folder. " \ "Please provide a pixel size (using argument -s), or add a pixel_size_in_micrometer.txt file " \ "containing the pixel size value." raise Exception(exception_msg) - # If resolution is specified as input argument, use it - else: - acquisitions_resolutions = [acquired_resolution] * len(path_acquisitions_folders) - - # Ensuring that the config file is valid - config_dict = update_config(default_configuration(), config_dict) - - # Perform the segmentation of all the requested images. - if prediction_proba_activate: - prediction, prediction_proba = apply_convnet(path_acquisitions, acquisitions_resolutions, path_model_folder, - config_dict, ckpt_name=ckpt_name, - inference_batch_size=inference_batch_size, - overlap_value=overlap_value, - resampled_resolutions=resampled_resolutions, - prediction_proba_activate=prediction_proba_activate, - gpu_per=gpu_per, verbosity_level=verbosity_level) - # Predictions are shape of image, value = class of pixel - else: - prediction = apply_convnet(path_acquisitions, acquisitions_resolutions, path_model_folder, config_dict, - ckpt_name=ckpt_name, inference_batch_size=inference_batch_size, - overlap_value=overlap_value, resampled_resolutions=resampled_resolutions, - prediction_proba_activate=prediction_proba_activate, gpu_per=gpu_per, - verbosity_level=verbosity_level) - # Predictions are shape of image, value = class of pixel - - # Final part of the function : generating the image if needed/ returning values - if write_mode: - for i, pred in enumerate(prediction): - # Transform the prediction to an image - n_classes = config_dict['n_classes'] - paint_vals = [int(255 * float(j) / (n_classes - 1)) for j in range(n_classes)] - - # Create the mask with values in range 0-255 - mask = np.zeros_like(pred) - for j in range(n_classes): - mask[pred == j] = paint_vals[j] - - # Then we save the image - image_name = convert_path(acquisitions_filenames[i]).stem - ads.imwrite(path_acquisitions_folders[i] / (image_name + segmentations_filenames[i]), mask, 'png') - - axon_prediction, myelin_prediction = get_masks(path_acquisitions_folders[i] / (image_name + segmentations_filenames[i])) - - if prediction_proba_activate: - return prediction, prediction_proba - else: - return prediction - - -def ensure_list_type(elem): - """ - Transforms the argument elem into a list if it's not already its type. - :param elem: Element to transform into a list. - :return: A list containing the element, or the element if it is already a list. - """ - if type(elem) != list: - elem = [elem] - return elem - - -def load_acquisitions(path_acquisitions, acquisitions_resolutions, resampled_resolutions, verbose_mode=0): - """ - Load and resamples acquisitions located in the indicated folders' paths. - :param path_acquisitions: List of paths to the acquisitions images. - :param acquisitions_resolutions: List of float containing the resolutions the acquisitions were acquired with. - :param resampled_resolutions: List of resolutions (floats) to resample to. - :param verbose_mode: Int, how much information to display. - :return: - """ - # If string, convert to Path objects - path_acquisitions = convert_path(path_acquisitions) - - path_acquisitions, acquisitions_resolutions, resampled_resolutions = list(map( - ensure_list_type, [path_acquisitions, acquisitions_resolutions, resampled_resolutions])) - - if verbose_mode >= 2: - print("Loading acquisitions ...") - - # Reading acquisitions images and loading them in the RAM, with their respective acquisition resolution. - # Then resampling the acquisitions images to the target resolution that the network uses. - - original_acquisitions, resampled_acquisitions, original_acquisitions_shapes = [], [], [] - - for path_img in path_acquisitions: - - original_acquisitions.append(ads.imread(path_img)) - original_acquisitions_shapes.append(original_acquisitions[-1].shape) - - # Resampling acquisitions to the target resolution - - if verbose_mode >= 2: - print("Rescaling acquisitions to the target resolution ...") - - resampling_coeffs = [current_acquisition_resolution / resampled_resolutions[i] - for i, current_acquisition_resolution in enumerate(acquisitions_resolutions)] - - for i, current_original_acquisition in enumerate(original_acquisitions): - resampled_acquisitions.append(rescale(current_original_acquisition, resampling_coeffs[i], - preserve_range=True).astype(int)) - - return resampled_acquisitions, resampling_coeffs, original_acquisitions_shapes - - -def prepare_patches(resampled_acquisitions, patch_size, overlap_value=25): - """ - Transform resampled acquisitions into patches. Each patch is also preprocessed during this step. - :param resampled_acquisitions: List of acquisitions images that have been resampled - :param patch_size: Input size of the network. - :param overlap_value: How much overlap to include when doing the inference. - :return: List of 512x512 patches ready to be fed to the network. - """ - - # Handle case when image is too small after resampling to target resolution of the model - # test_patch=resampled_acquisitions[0] - - # dims = test_patch - # height = dims[0] - # width = dims[1] - - # print "height = ",height,"***" - - # if (height<=512) or (width<=512) - # print " *** Image size error. The software requires an image input with size of at least 512x512 pixels in the target resolution of the model. *** " - - L_data, L_positions, L_n_patches = [], [], [] - - for current_acquisition in resampled_acquisitions: - image_init, data, positions = im2patches_overlap(current_acquisition, overlap_value, patch_size) - L_data.append(data) - L_positions.append(positions) - L_n_patches.append(len(data)) - - # Now we concatenate the list of patches to process them all together. - L_data = [e for sublist in L_data for e in sublist] - - return L_data, L_n_patches, L_positions - - -def process_segmented_patches(predictions_list, L_n_patches, L_positions, L_original_acquisitions_shapes, - overlap_value, n_classes, - predictions_proba_list=None, prediction_proba_activate=False, verbose_mode=0): - """ - Gathers the segmented patches into lists corresponding to each acquisition, stitches them and resamples them. - :param predictions_list: List of all segmented patches. - :param L_n_patches: List containing the number of patches related to each acquisition. - :param L_positions: List of positions in the original acquisition (in the original resolution) of each patch. - :param L_original_acquisitions_shapes: List of the shapes of the original acquisitions. - :param overlap_value: Int, number of pixels to overlap. - :param n_classes: Int, number of classes. - :param predictions_proba_list: List of the prediction probabilities for all patches. Optional. - :param prediction_proba_activate: Boolean, whether to activate or not the prediction of probabilities. - :param verbose_mode: Int, the level of verbosity. - :return: the reconstructed list of segmentations, as well as the list of probability maps for each acquisition, - if requested. - """ - patch_size = predictions_list[0].shape[0] - L_predictions = [] - L_predictions_proba = [] - L_n_patches_cum = np.cumsum([0] + L_n_patches) - - if verbose_mode >= 2: - print("Resampling predictions to their original size...") - - # Gathering segmented patches belonging to the same acquisition - for i, e in enumerate(L_n_patches_cum[:-1]): - i0 = e - i1 = L_n_patches_cum[i + 1] - L_predictions.append(predictions_list[i0:i1]) - - if prediction_proba_activate: - L_predictions_proba.append(predictions_proba_list[i0:i1]) - - # We stitch and resample each segmented patch to reconstruct the total segmentation - prediction_stitcheds = [patches2im_overlap(pred_list, L_positions[i], overlap_value, patch_size) for i, pred_list in - enumerate(L_predictions)] - predictions = [resize(prediction_stitched, L_original_acquisitions_shapes[i]) for i, prediction_stitched in - enumerate(prediction_stitcheds)] - predictions = [prediction.astype(np.uint8) for prediction in - predictions] # Rescaling operation can change the value of the pixels to float. - - # Performing the same steps for the probability maps - - if prediction_proba_activate: - - # First we create an empty list that will store all processed prediction_proba - # (meaning reshaped so that each element of the list corresponds to a predicted image, - # each element being of shape (patch_size, patch_size, n_classes) - - predictions_proba = [] - - for i, prediction_proba_list in enumerate(L_predictions_proba): - # We generate the predict proba matrix - tmp = np.split(np.stack(prediction_proba_list, axis=0), n_classes, axis=-1) - predictions_proba_list = [list(map(np.squeeze, np.split(e, L_n_patches[i], axis=0))) for e in - tmp] # We now have a list (n_classes elements) of list (n_patches elements) - # [ class0:[ patch0:[], patch1:[], ...], class1:[ patch0:[], patch1:[],... ] ... ] - - # Stitching each class - prediction_proba_stitched = [patches2im_overlap(e, L_positions[i], overlap_value, patch_size) for j, e in - enumerate(predictions_proba_list)] # for each class, we have a list of patches - - # Stacking in order to have juste one image with a depth of 3, one for each class - prediction_proba = np.stack( - [resize(e, L_original_acquisitions_shapes[i]) for e in prediction_proba_stitched], axis=-1) - predictions_proba.append(prediction_proba) - - return predictions, predictions_proba - else: - - return predictions - - -def perform_batch_inference(model, tf_session, tf_prediction_op, tf_input, batch_x, size_batch, input_size, n_classes, - prediction_proba_activate=False): - """ - Performs the segmentation of all the patches in the batch. - :param tf_session: Current Tensorflow session. - :param tf_prediction_op: Tensorflow prediction operator. - :param batch_x: List, batch of patches to segment. - :param size_batch: Int, size of the current batch. - :param input_size: Int, size of a patch. - :param n_classes: Int, number of classes. - :param prediction_proba_activate: Boolean, whether to compute the probability maps or not. - :return: List of segmentation of the patches, and optionally list of the probabilty maps for each patch. - """ - - batch_x = np.reshape(batch_x,(size_batch, input_size, input_size, 1)) - - p = model.predict(batch_x) - - Mask = np.argmax(p, axis=3) # Now Mask is a 256*256 mask with Mask[i,j] = pixel_class - - batch_predictions_list = [np.squeeze(e) for e in np.split(Mask, size_batch, axis=0)] - - if prediction_proba_activate: - # Generating the probas for each element of the batch (basically changing the shape of the prediction) - p = p.reshape(size_batch, input_size, input_size, n_classes) - - # Reshaping and adding to the preivous list (each patch is now of size (patch_size, patch_size, n_classes) ) - batch_predictions_proba_list = [np.squeeze(e) for e in np.split(p, size_batch, axis=0)] - - return batch_predictions_list, batch_predictions_proba_list - - else: - return batch_predictions_list + path_model=path_model_folder + input_filenames = acquisitions_filenames + options = {"pixel_size": [acquired_resolution, acquired_resolution], "pixel_size_units": "um", "overlap_2D": overlap_value, "binarize_maxpooling": True} + + # IVADOMED automated segmentation + nii_lst, _ = imed_inference.segment_volume(str(path_model), input_filenames, options=options) + + target_lst = [str(axon_suffix), str(myelin_suffix)] + + imed_inference.pred_to_png(nii_lst, target_lst, str(Path(input_filenames[0]).parent / Path(input_filenames[0]).stem)) + if verbosity_level >= 1: + print(Path(path_acquisitions_folders) / (Path(input_filenames[0]).stem + str(axonmyelin_suffix))) + + merge_masks( + Path(path_acquisitions_folders) / (Path(input_filenames[0]).stem + str(axon_suffix)), + Path(path_acquisitions_folders) / (Path(input_filenames[0]).stem + str(myelin_suffix)), + Path(path_acquisitions_folders) / (Path(input_filenames[0]).stem + str(axonmyelin_suffix)) + ) diff --git a/AxonDeepSeg/config_tools.py b/AxonDeepSeg/config_tools.py deleted file mode 100644 index 5af03491..00000000 --- a/AxonDeepSeg/config_tools.py +++ /dev/null @@ -1,274 +0,0 @@ -''' -Set the config variable. -''' - -from pathlib import Path -import json -import collections -import time -import copy -import AxonDeepSeg.ads_utils - -#### Network config file management. #### - -def validate_config(config): - - """ Check the config file keys - :param config: Dictionary containing the parameters of the network. - :return: Boolean. True if the configuration is valid compared to the default configuration, else False. - """ - - keys = list(default_configuration().keys()) - for key in list(config.keys()): - if not key in keys: - return False - return True - - -def default_configuration(): - """ - Generate the default configuration for the training parameters. - :return: Dictionary, the default configuration parameters. - """ - - tmp = {'batch_norm_decay_decay_activate': True, - 'batch_norm_decay_decay_period': 24000, - 'batch_norm_decay_starting_decay': 0.7, - 'batch_norm_decay_ending_decay': 0.9, - 'learning_rate_decay_activate': True, - 'learning_rate_decay_period': 16000, - 'learning_rate_decay_rate': 0.99, # Only used for exponential decay - 'learning_rate_decay_type': 'polynomial', - 'batch_norm_activate': True, - 'batch_size': 8, - 'convolution_per_layer': [3, 3, 3, 3], - 'da-3-elastic-activate': True, - 'da-elastic-alpha_max': 9, - 'da-elastic-order': 3, - 'da-4-flipping-activate': True, - 'da-flipping-order': 4, - 'da-gaussian_blur-activate': True, - 'da-gaussian_blur-order': 6, - 'da-gaussian_blur-sigma_max': 1.5, - 'da-5-noise_addition-activate': False, - 'da-6-reflection_border-activate': False, - 'da-noise_addition-order': 5, - 'da-2-random_rotation-activate': False, - 'da-random_rotation-high_bound': 89, - 'da-random_rotation-low_bound': 5, - 'da-random_rotation-order': 2, - 'da-1-rescaling-activate': False, - 'da-rescaling-factor_max': 1.2, - 'da-rescaling-order': 1, - 'da-0-shifting-activate': True, - 'da-shifting-order': 0, - 'da-shifting-percentage_max': 0.1, - 'da-type': 'all', - 'depth': 4, - 'downsampling': 'convolution', - 'dropout': 0.75, - 'features_per_convolution': [[[1, 16], [16, 16], [16, 16]], - [[16, 32], [32, 32], [32, 32]], - [[32, 64], [64, 64], [64, 64]], - [[64, 128], [128, 128], [128, 128]]], - 'learning_rate': 0.001, - 'n_classes': 3, - 'size_of_convolutions_per_layer': [[5, 5, 5], - [3, 3, 3], - [3, 3, 3], - [3, 3, 3]], - 'weighted_cost-activate': True, - 'weighted_cost-balanced_activate': True, - 'weighted_cost-balanced_weights': [1.1, 1, 1.3], - 'weighted_cost-boundaries_activate': False, - 'weighted_cost-boundaries_sigma': 2, - 'thresholds': [0, 0.2, 0.8], - 'trainingset': 'SEM_3c_512', - 'trainingset_patchsize': 512, - 'balanced_weights': [1.1, 1, 1.3], - 'dataset_mean': 120.95, # Not used right now for preprocessing, we do it on a per image basis. - 'dataset_variance': 60.23 # Not used right now for preprocessing, we do it on a per image basis. THIS SHOULD BE STD - } - - return tmp - - -def update_config(d, u): - for k, v in list(u.items()): - if isinstance(v, collections.Mapping): - r = update_config(d.get(k, {}), v) - d[k] = r - else: - d[k] = u[k] - return d - - -def generate_config(config_path=None): - """ Generate the config file - Input : - config_path : string : path to a json config file. If None, using the default configuration. - Output : - config : dict : the network config file. - """ - config = default_configuration() - if config_path != None: - with open(config_path) as conf_file: - user_conf = json.load(conf_file) - config.update(user_conf) - if not validate_config(config): - raise ValueError('Invalid configuration file') - - return config - -############################################ For submission generator - -def rec_update(elem, update_dict): - if type(elem) == dict: - return update_config(elem,update_dict) - elif type(elem) == list: - return [rec_update(e, update_dict) for e in elem] - else: - return None - -def flatten(container): - for i in container: - if isinstance(i, (list,tuple)): - for j in flatten(i): - yield j - else: - yield i - - -def grid_config(L_struct, dict_params, base_config = default_configuration()): - ''' - L_struct is a list of structures parameters in dictionnaries for the configuration file. It must contain at least the number of convolution per layer, the size of each kernel, and the nested list of number of features per layer. - ''' - # First we create the different structures from the list - base_config = update_config(default_configuration(), base_config) # We complete the base configuration if needed. - - L_configs = [] - - for structure in L_struct: - tmp = copy.deepcopy(base_config) - tmp.update(generate_struct(structure)) - L_configs.append(tmp) - - - # Then we create the grid thanks to the params. - for param, L_values in list(dict_params.items()): - temp_config = L_configs - L_configs = [] - if isinstance(L_values, collections.Iterable): - for v in L_values: - rec_update(temp_config, {param:v}) - L_configs.append(copy.deepcopy(temp_config)) - # If it's just a value we just take this value - else: - rec_update(temp_config, {param:L_values}) - L_configs.append(copy.deepcopy(temp_config)) - - # Finally we flatten the resulting nested list and we return a dictionnary with each key being the name of a model and the value being the configuration dictionnary - L_configs = list(flatten(L_configs)) - - #config_names = [generate_name_config(config)+'_'+str(i)+'-'+str(int(time.time()))[-3:] for i,config in enumerate(L_configs)] - #print L_configs - return {generate_name_config(config)+'_'+str(i)+'-'+str(int(time.time()))[-4:]:config for i,config in enumerate(L_configs)} - - -## ---------------------------------------------------------------------------------------------------------------- - -def generate_features(depth,network_first_num_features,features_augmentation,network_convolution_per_layer): - - increment = int(float(features_augmentation[1:])) - - if str(features_augmentation[0]) == 'p': - # Add N features at each convolution layer. - first_conv = [[1,network_first_num_features]] - temp = [[network_first_num_features+i*increment,network_first_num_features+(i+1)*increment] - for i in range(network_convolution_per_layer[0])[1:]] - first_layer = first_conv + temp - last_layer = first_layer - network_features_per_convolution = [first_layer] - - for cur_depth in range(depth)[1:]: - - first_conv = [[last_layer[-1][-1],last_layer[-1][-1]+increment]] - temp = [[last_layer[-1][-1]+i*increment,last_layer[-1][-1]+(i+1)*increment] for i in range(network_convolution_per_layer[cur_depth])[1:]] - current_layer = first_conv+temp - network_features_per_convolution = network_features_per_convolution + [current_layer] - - last_layer = current_layer - - elif str(features_augmentation[0]) == 'x': - # Multiply the number of features by N at each "big layer". - - first_conv = [[1,network_first_num_features]] - temp = [[network_first_num_features,network_first_num_features] - for i in range(network_convolution_per_layer[0]-1)] - first_layer = first_conv + temp - last_layer = first_layer - network_features_per_convolution = [first_layer] - for cur_depth in range(depth)[1:]: - first_conv = [[last_layer[-1][-1],last_layer[-1][-1]*increment]] - temp = [[last_layer[-1][-1]*increment,last_layer[-1][-1]*increment] for i in range(network_convolution_per_layer[cur_depth]-1)] - current_layer = first_conv+temp - network_features_per_convolution = network_features_per_convolution + [current_layer] - - last_layer = current_layer - - else: - raise ValueError('Invalid features_augmentation value. Must begin with x or p, and be followed by an integer.' ) - - - return network_features_per_convolution - -## ---------------------------------------------------------------------------------------------------------------- - -def generate_name_config(config): - - name = '' - - # Downsampling - if config['downsampling'] == 'convolution': - name += 'cv_' - elif config['downsampling'] == 'maxpooling': - name += 'mp_' - - # Number of classes - - name += str(config['n_classes']) + 'c_' - - # Depth - name += 'd' + str(config['depth']) + '_' - - # Number of convolutions per layer - # Here we make the supposition that the number of convo per layer is the same for every layer - name += 'c' + str(config['convolution_per_layer'][1]) + '_' - - # Size of convolutions per layer - # Here we make the supposition that the size of convo is the same for every layer - name += 'k' + str(config['size_of_convolutions_per_layer'][1][0]) + '_' - - # We don't mention the batch size anymore as we are doing 8 by default - - # Channels augmentation - #name += str(L_struct['features_augmentation']) + '-' - #name += str(L_struct['network_first_num_features']) - - # We return a tuple - return name - -def generate_struct(dict_struct): - - network_feature_per_convolution = generate_features(depth=len(dict_struct['structure']), - network_first_num_features=dict_struct['first_num_features'], - features_augmentation=dict_struct['features_augmentation'], - network_convolution_per_layer=[len(e) for e in dict_struct['structure']] - ) - - - return {'depth':len(dict_struct['structure']), - 'features_per_convolution':network_feature_per_convolution, - 'size_of_convolutions_per_layer':dict_struct['structure'], - 'convolution_per_layer':[len(e) for e in dict_struct['structure']] - } diff --git a/AxonDeepSeg/data_management/__init__.py b/AxonDeepSeg/data_management/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/AxonDeepSeg/data_management/dataset_building.py b/AxonDeepSeg/data_management/dataset_building.py deleted file mode 100644 index d3a744bf..00000000 --- a/AxonDeepSeg/data_management/dataset_building.py +++ /dev/null @@ -1,297 +0,0 @@ - - - -from skimage.transform import rescale -import numpy as np -from tqdm import tqdm -import shutil - -import AxonDeepSeg.ads_utils as ads -from AxonDeepSeg.data_management.input_data import labellize_mask_2d -from AxonDeepSeg.data_management.patch_extraction import extract_patch -from AxonDeepSeg.ads_utils import convert_path - -def raw_img_to_patches(path_raw_data, path_patched_data, thresh_indices = [0, 0.2, 0.8], - patch_size=512, resampling_resolution=0.1): - """ - Transform a raw acquisition to a folder of patches of size indicated in the arguments. Also performs resampling. - Note: this functions needs to be run as many times as there are different general pixel size - (thus different acquisition types / resolutions). - :param path_raw_data: Path to where the raw image folders are located. - :param path_patched_data: Path to where we will store the patched acquisitions. - :param thresh_indices: List of float, determining the thresholds separating the classes. - :param patch_size: Int, size of the patches to generate (and consequently input size of the network). - :param resampling_resolution: Float, the resolution we need to resample to so that each sample - has the same resolution in a dataset. - :return: Nothing. - """ - - # If string, convert to Path objects - path_raw_data = convert_path(path_raw_data) - path_patched_data = convert_path(path_patched_data) - - # First we define where we are going to store the patched data and we create the directory if it does not exist. - if not path_patched_data.exists(): - path_patched_data.mkdir(parents=True) - - # Loop over each raw image folder - img_folder_names = [im.name for im in path_raw_data.iterdir()] - for img_folder in tqdm(img_folder_names): - path_img_folder = path_raw_data / img_folder - if path_img_folder.is_dir(): - - # We are now in the image folder. - file = open(path_img_folder / 'pixel_size_in_micrometer.txt', 'r') - pixel_size = float(file.read()) - resample_coeff = float(pixel_size) / resampling_resolution # Used to set the resolution to the general_pixel_size - - # We go through every file in the image folder - data_names = [d.name for d in path_img_folder.iterdir()] - for data in data_names: - if 'image' in data: # If it's the raw image. - - img = ads.imread(path_img_folder / data) - img = rescale(img, resample_coeff, preserve_range=True, mode='constant').astype(int) - - elif 'mask' in data: - mask_init = ads.imread(path_img_folder / data) - mask = rescale(mask_init, resample_coeff, preserve_range=True, mode='constant', order=0) - - # Set the mask values to the classes' values - mask = labellize_mask_2d(mask, thresh_indices) # shape (size, size), values float 0.0-1.0 - - to_extract = [img, mask] - patches = extract_patch(to_extract, patch_size) - # The patch extraction is done, now we put the new patches in the corresponding folders - - # We create it if it does not exist - path_patched_folder = path_patched_data / img_folder - if not path_patched_folder.exists(): - path_patched_folder.mkdir(parents=True) - - for j, patch in enumerate(patches): - ads.imwrite(path_patched_folder.joinpath('image_%s.png'%j), patch[0]) - ads.imwrite(path_patched_folder.joinpath('mask_%s.png'%j), patch[1]) - -def patched_to_dataset(path_patched_data, path_dataset, type_, random_seed=None): - """ - Creates a dataset using already created patches. - :param path_patched_data: Path to where to find the folders where the patches folders are located. - :param path_dataset: Path to where to create the newly formed dataset. - :param type_: String, either 'unique' or 'mixed'. Unique means that we create a dataset with only TEM or only SEM - data. "Mixed" means that we are creating a dataset with both type of images. - :param random_seed: Int, the random seed to use to be able to consistenly recreate generated datasets. - :return: None. - """ - - # If string, convert to Path objects - path_patched_data = convert_path(path_patched_data) - path_dataset = convert_path(path_dataset) - - # Using the randomseed fed so that given a fixed input, the generation of the datasets is always the same. - np.random.seed(random_seed) - - # First we define where we are going to store the patched data - if not path_dataset.exists(): - path_dataset.mkdir(parents=True) - - # First case: there is only one type of acquisition to use. - if type_ == 'unique': - - i = 0 # Total patches index - - # We loop through all folders containing patches - patches_folder_names = [f for f in path_patched_data.iterdir()] - for patches_folder in tqdm(patches_folder_names): - - path_patches_folder = path_patched_data / patches_folder.name - if path_patches_folder.is_dir(): - - # We are now in the patches folder - L_img, L_mask = [], [] - filenames = [f for f in path_patches_folder.iterdir()] - for data in filenames: - root, index = data.stem.split('_') - - if 'image' in data.name: - img = ads.imread(path_patches_folder / data.name) - L_img.append((img, int(index))) - - elif 'mask' in data.name: - mask = ads.imread(path_patches_folder / data.name) - L_mask.append((mask, int(index))) - - # Now we sort the patches to be sure we get them in the right order - L_img_sorted, L_mask_sorted = sort_list_files(L_img, L_mask) - - # Saving the images in the new folder - for img,k in L_img_sorted: - ads.imwrite(path_dataset.joinpath('image_%s.png'%i), img) - ads.imwrite(path_dataset.joinpath('mask_%s.png'%i), L_mask_sorted[k][0]) - i = i+1 # Using the global i here. - - # Else we are using different types of acquisitions. It's important to have them separated in a SEM folder - # and in a TEM folder. - elif type_ == 'mixed': - # We determine which acquisition type we are going to upsample (understand : take the same images multiple times) - SEM_patches_folder = path_patched_data / 'SEM' - TEM_patches_folder = path_patched_data / 'TEM' - - minority_patches_folder, len_minority, majority_patches_folder, len_majority = find_minority_type( - SEM_patches_folder, TEM_patches_folder) - - # First we move all patches from the majority acquisition type to the new dataset - foldernames = [folder.name for folder in majority_patches_folder.iterdir()] - i = 0 - for patches_folder in tqdm(foldernames): - - path_patches_folder = majority_patches_folder / patches_folder - if path_patches_folder.is_dir(): - # We are now in the patches folder - L_img, L_mask = [], [] - filenames = [f for f in path_patches_folder.iterdir()] - for data in path_patches_folder.iterdir(): - root, index = data.stem.split('_') - - if 'image' in data.name: - img = ads.imread(path_patches_folder / data.name) - L_img.append((img, int(index))) - - elif 'mask' in data.name: - mask = ads.imread(path_patches_folder / data.name) - L_mask.append((mask, int(index))) - # Now we sort the patches to be sure we get them in the right order - L_img_sorted, L_mask_sorted = sort_list_files(L_img, L_mask) - - # Saving the images in the new folder - for img,k in L_img_sorted: - ads.imwrite(path_dataset.joinpath('image_%s.png'%i), img) - ads.imwrite(path_dataset.joinpath('mask_%s.png'%i), L_mask_sorted[k][0]) - i = i+1 - # Then we stratify - oversample the minority acquisition to the new dataset - - # We determine the ratio to take - ratio_oversampling = float(len_majority)/len_minority - - # We go through each image folder in the minorty patches - foldernames = [folder.name for folder in minority_patches_folder.iterdir()] - for patches_folder in tqdm(foldernames): - - path_patches_folder = minority_patches_folder / patches_folder - if path_patches_folder.is_dir(): - - # We are now in the patches folder - # We load every image - filenames = [f for f in path_patches_folder.iterdir()] - n_img = np.floor(len(filenames)/2) - - for data in filenames: - root, index = data.stem.split('_') - if 'image' in data.name: - img = ads.imread(path_patches_folder / data.name) - L_img.append((img, int(index))) - - elif 'mask' in data.name: - mask = ads.imread(path_patches_folder / data.name) - L_mask.append((mask, int(index))) - - # Now we sort the patches to be sure we get them in the right order - L_img_sorted, L_mask_sorted = sort_list_files(L_img, L_mask) - L_merged_sorted = np.asarray([L_img_sorted[j] + L_mask_sorted[j] for j in range(len(L_img_sorted))]) - - # We create a new array composed of enough elements so that the two types of acquisitions are balanced - # (oversampling) - L_elements_to_save = L_merged_sorted[np.random.choice( - int(L_merged_sorted.shape[0]),int(np.ceil(ratio_oversampling*n_img)), replace=True),:] - - # Finally we save all the images in order at the new dataset path. - for j in range(L_elements_to_save.shape[0]): - img = L_elements_to_save[j][0] - mask = L_elements_to_save[j][2] - ads.imwrite(path_dataset.joinpath('image_%s.png'%i), img) - ads.imwrite(path_dataset.joinpath('mask_%s.png'%i), mask) - i = i+1 - - -def sort_list_files(list_patches, list_masks): - """ - Sorts a list of patches and masks depending on their id. - :param list_patches: List of name of patches in the folder, that we want to sort. - :param list_masks: List of name of masks in the folder, that we want to sort. - :return: List of sorted lists, respectively of patches and masks. - """ - - return sorted(list_patches, key=lambda x: int(x[1])), sorted(list_masks, key=lambda x: int(x[1])) - - -def find_minority_type(SEM_patches_folder, TEM_patches_folder): - """ - Identifies the type of acquisition that has the least number of patches in order to manage oversampling after. - :param SEM_patches_folder: Path to the SEM patches. - :param TEM_patches_folder: Path to the TEM patches. - :return: The path to the minority patches folder, the number of patches of the minority patches folder, - the path to the majority path folder and the number of patches in this folder. - """ - - SEM_len = len([f for f in SEM_patches_folder.rglob("*") if f.is_file()]) - TEM_len = len([f for f in TEM_patches_folder.rglob("*") if f.is_file()]) - - if SEM_len < TEM_len: - minority_patches_folder = SEM_patches_folder - majority_patches_folder = TEM_patches_folder - len_minority = SEM_len - len_majority = TEM_len - else: - minority_patches_folder = TEM_patches_folder - majority_patches_folder = SEM_patches_folder - len_minority = TEM_len - len_majority = SEM_len - - return minority_patches_folder, len_minority, majority_patches_folder, len_majority - -def split_data(raw_dir, out_dir, seed=2019, split = [0.8, 0.2], override=False): - """ - Splits a dataset into training and validation folders. - :param raw_dir: Raw dataset folder containing a set of subdirectories to be split. - :param out_dir: Output directory of splitted dataset - :param seed: Random number generator seed. - :param split: Split fractions ([Train, Validation]). - :param override: Bool. If out_dir exists and is True, the directory will be overwritten. - """ - - # If string, convert to Path objects - raw_dir = convert_path(raw_dir) - out_dir = convert_path(out_dir) - - # Handle case if output dir already exists - if out_dir.is_dir(): - if override == True: - shutil.rmtree(out_dir) - else: - raise IOError("Directory " + str(out_dir) + " already exist. Delete or change override option.") - - - # Get splits for Training and Validation - split_train = split[0] - split_valid = split[1] - - # get sorted list of image directories - dirs=sorted([x for x in raw_dir.iterdir() if x.is_dir()]) - - # Create directories for splitted datasets - train_dir = out_dir / "Train" - train_dir.mkdir(parents=True, exist_ok=True) - - valid_dir = out_dir / "Validation" - valid_dir.mkdir(parents=True, exist_ok=True) - - # Randomly assign a number to each image folder for splitting - np.random.seed(seed=seed) - sorted_indices=np.random.choice(len(dirs), size=len(dirs), replace=False) - - # Move the image dirs from the raw folder to the split folder - for index in sorted_indices[:round(len(sorted_indices)*split_train)]: - dirs[index].rename(train_dir / dirs[index].parts[-1]) - - for index in sorted_indices[-round(len(sorted_indices)*split_valid):]: - dirs[index].rename(valid_dir / dirs[index].parts[-1]) diff --git a/AxonDeepSeg/data_management/input_data.py b/AxonDeepSeg/data_management/input_data.py deleted file mode 100644 index 913375dc..00000000 --- a/AxonDeepSeg/data_management/input_data.py +++ /dev/null @@ -1,147 +0,0 @@ -import keras -import numpy as np -import AxonDeepSeg.ads_utils as ads - -from scipy import ndimage -from skimage import exposure - -import AxonDeepSeg.ads_utils - -from AxonDeepSeg.ads_utils import convert_path - - -class DataGen(keras.utils.Sequence): - """Generates data for Keras""" - - def __init__( - self, - ids, - path, - augmentations, - batch_size=8, - image_size=512, - thresh_indices=[0, 0.2, 0.8], - ): - """ - Initalization for the DataGen class - :param ids: List of strings, ids of all the images/masks in the training set. - :param batch_size: Int, the batch size used for training. - :param image_size: Int, input image size. - :param image_size: Int, input image size. - :param augmentations: Compose object, a set of data augmentation operations to be applied. - :return: the original image, a list of patches, and their positions. - """ - - # If string, convert to Path object - path = convert_path(path) - - self.ids = ids - self.path = path - self.batch_size = batch_size - self.image_size = image_size - self.on_epoch_end() - self.thresh_indices = thresh_indices - self.augment = augmentations - - def __load__(self, id_name): - """ - Loads images and masks - :param ids_name: String, id name of a particular image/mask. - """ - - ## Path - image_path = self.path / ("image_" + id_name + ".png") - mask_path = self.path / ("mask_" + id_name + ".png") - ## Reading Image - image = ads.imread(str(image_path)) - image = np.reshape(image, (self.image_size, self.image_size, 1)) - - # -----Mask PreProcessing -------- - mask = ads.imread(str(mask_path)) - mask = descritize_mask(mask, self.thresh_indices) - # --------------------------- - return (image, mask) - - def __getitem__(self, index): - """ - Generates a batch of images/masks - :param ids_name: String, id name of a particular image/mask.. - """ - files_batch = self.ids[ - index * self.batch_size : (index + 1) * self.batch_size - ] - - image = [] - mask = [] - - for id_name in files_batch: - _img, _mask = self.__load__(id_name) - image.append(_img) - mask.append(_mask) - - images = np.array(image) - masks = np.array(mask) - - image_aug = [] - mask_aug = [] - for x, y in zip(images, masks): - aug = self.augment(image=x, mask=y) - image_aug.append(aug["image"]) - mask_aug.append(aug["mask"]) - image_aug = np.array(image_aug) - mask_aug = np.array(mask_aug) - return (image_aug, mask_aug) - - def on_epoch_end(self): - pass - - def __len__(self): - return int(np.ceil(len(self.ids) / float(self.batch_size))) - - -def labellize_mask_2d(patch, thresh_indices=[0, 0.2, 0.8]): - """ - Process a patch with 8 bit pixels ([0-255]) so that the pixels between two threshold values are set to the closest threshold, effectively - enabling the creation of a mask with as many different values as there are thresholds. - - Returns mask in [0-1] domain - """ - mask = np.zeros_like(patch) - for indice in range(len(thresh_indices) - 1): - thresh_inf_8bit = 255 * thresh_indices[indice] - thresh_sup_8bit = 255 * thresh_indices[indice + 1] - - idx = np.where( - (patch >= thresh_inf_8bit) & (patch < thresh_sup_8bit) - ) # returns (x, y) of the corresponding indices - mask[idx] = np.mean([thresh_inf_8bit / 255, thresh_sup_8bit / 255]) - - mask[(patch >= 255 * thresh_indices[-1])] = 1 - - return patch - - -def descritize_mask(mask, thresh_indices): - """ - Process a mask with 8 bit pixels ([0-255]) such that it get discretizes into 3 different channels ( background, myelin, axon) . - Returns mask composed of 3 different channels ( background, myelin, axon ) - """ - - # Discretization of the mask - mask = labellize_mask_2d( - mask, thresh_indices - ) # mask intensity float between 0-1 - - # Working out the real mask (sparse cube with n depth layer, one for each class) - n = len(thresh_indices) # number of classes - thresh_indices = [255 * x for x in thresh_indices] - real_mask = np.zeros([mask.shape[0], mask.shape[1], n]) - - for class_ in range(n - 1): - real_mask[:, :, class_] = (mask[:, :] >= thresh_indices[class_]) * ( - mask[:, :] < thresh_indices[class_ + 1] - ) - real_mask[:, :, -1] = mask[:, :] >= thresh_indices[-1] - real_mask = real_mask.astype(np.uint8) - - return real_mask diff --git a/AxonDeepSeg/data_management/patch_extraction.py b/AxonDeepSeg/data_management/patch_extraction.py deleted file mode 100644 index 6640fc9c..00000000 --- a/AxonDeepSeg/data_management/patch_extraction.py +++ /dev/null @@ -1,54 +0,0 @@ -import AxonDeepSeg.ads_utils - - - -def extract_patch(patch, size): - """ - :param patch: List of 2 or 3 ndarrays, [image, mask, (weights)]. image and mask are numpy arrays, and mask is the groundtruth segmentation. - :param size: size of the patches to extract - :return: a list of pairs [patch, ground_truth] with a very low overlapping. - """ - try: - img = patch[0] - mask = patch[1] - if len(patch) == 3: - weights = patch[2] - except: - raise ValueError('\nError: First argument of extract_patch must be a list of 2 or 3 ndarrays: [image, mask, (weights)]') - - if size < 3: - raise ValueError('\nError: patch size must be 3 or greater.') - elif size >= min(img.shape): - raise ValueError('\nError: patch size must be smaller than dimensions of image.') - - h, w = img.shape - - q_h, r_h = divmod(h, size) - q_w, r_w = divmod(w, size) - - r2_h = size-r_h - r2_w = size-r_w - - q3_h, r3_h = divmod(r2_h,q_h) - q3_w, r3_w = divmod(r2_w,q_w) - - dataset = [] - pos = 0 - while pos+size<=h: - pos2 = 0 - while pos2+size<=w: - patch_im = img[pos:pos+size, pos2:pos2+size] - patch_gt = mask[pos:pos+size, pos2:pos2+size] - if len(patch) == 3: - patch_weights = weights[pos:pos+size, pos2:pos2+size] - dataset.append([patch_im, patch_gt, patch_weights]) - else: - dataset.append([patch_im, patch_gt]) - pos2 = size + pos2 - q3_w - if pos2 + size > w : - pos2 = pos2 - r3_w - - pos = size + pos - q3_h - if pos + size > h: - pos = pos - r3_h - return dataset \ No newline at end of file diff --git a/AxonDeepSeg/download_model.py b/AxonDeepSeg/download_model.py index 4ec4b661..e0c928ab 100644 --- a/AxonDeepSeg/download_model.py +++ b/AxonDeepSeg/download_model.py @@ -4,24 +4,27 @@ def download_model(destination = None): + sem_release_version = 'r20211209v2' + tem_release_version = 'r20211111v3' + bf_release_version = 'r20211210' + if destination is None: - sem_destination = Path("AxonDeepSeg/models/default_SEM_model") - tem_destination = Path("AxonDeepSeg/models/default_TEM_model") - model_seg_pns_bf_destination = Path("AxonDeepSeg/models/model_seg_pns_bf") + sem_destination = Path("AxonDeepSeg/models/model_seg_rat_axon-myelin_sem") + tem_destination = Path("AxonDeepSeg/models/model_seg_mouse_axon-myelin_tem") + bf_destination = Path("AxonDeepSeg/models/model_seg_rat_axon-myelin_bf") else: - destination = convert_path(destination) - sem_destination = destination / "default_SEM_model" - tem_destination = destination / "default_TEM_model" - model_seg_pns_bf_destination = destination / "model_seg_pns_bf" + sem_destination = destination / "model_seg_rat_axon-myelin_sem" + tem_destination = destination / "model_seg_mouse_axon-myelin_tem" + bf_destination = destination / "model_seg_rat_axon-myelin_bf" - url_TEM_model = "https://github.com/axondeepseg/default-TEM-model/archive/refs/tags/r20210615.zip" - url_SEM_model = "https://github.com/axondeepseg/default-SEM-model/archive/refs/tags/r20210615.zip" - url_model_seg_pns_bf = "https://github.com/axondeepseg/model-seg-pns-bf/archive/refs/tags/r20210615.zip" + url_sem_destination = "https://github.com/axondeepseg/default-SEM-model/archive/refs/tags/" + sem_release_version + ".zip" + url_tem_destination = "https://github.com/axondeepseg/default-TEM-model/archive/refs/tags/" + tem_release_version + ".zip" + url_bf_destination = "https://github.com/axondeepseg/default-BF-model/archive/refs/tags/" + bf_release_version + ".zip" files_before = list(Path.cwd().iterdir()) if ( - not download_data(url_TEM_model) and not download_data(url_SEM_model) and not download_data(url_model_seg_pns_bf) + not download_data(url_sem_destination) and not download_data(url_tem_destination) and not download_data(url_bf_destination) ) == 1: print("Data downloaded and unzipped succesfully.") else: @@ -32,29 +35,30 @@ def download_model(destination = None): files_after = list(Path.cwd().iterdir()) # retrieving unknown model folder names - model_folders = list(set(files_after)-set(files_before)) - folder_name_TEM_model = ''.join([str(x) for x in model_folders if 'TEM' in str(x)]) - folder_name_SEM_model = ''.join([str(x) for x in model_folders if 'SEM' in str(x)]) - folder_name_OM_model = ''.join([str(x) for x in model_folders if 'pns-bf' in str(x)]) + folder_name_SEM_model = Path("default-SEM-model-" + sem_release_version) + folder_name_TEM_model = Path("default-TEM-model-" + tem_release_version) + folder_name_BF_model = Path("default-BF-model-" + bf_release_version) if sem_destination.exists(): - print('SEM model folder already existed - deleting old one.') + print('SEM model folder already existed - deleting old one') shutil.rmtree(str(sem_destination)) + if tem_destination.exists(): - print('TEM model folder already existed - deleting old one.') - shutil.rmtree(str(tem_destination)) - if model_seg_pns_bf_destination.exists(): - print('Bright Field Optical Microscopy model folder already existed - deleting old one') - shutil.rmtree(str(model_seg_pns_bf_destination)) + print('TEM model folder already existed - deleting old one') + shutil.rmtree(str(tem_destination)) + + if bf_destination.exists(): + print('BF model folder already existed - deleting old one') + shutil.rmtree(str(bf_destination)) - shutil.move(Path(folder_name_SEM_model).joinpath("default_SEM_model"), str(sem_destination)) - shutil.move(Path(folder_name_TEM_model).joinpath("default_TEM_model"), str(tem_destination)) - shutil.move(Path(folder_name_OM_model).joinpath("model_seg_pns_bf"), str(model_seg_pns_bf_destination)) + shutil.move(folder_name_SEM_model.joinpath("model_seg_rat_axon-myelin_sem"), str(sem_destination)) + shutil.move(folder_name_TEM_model.joinpath("model_seg_mouse_axon-myelin_tem"), str(tem_destination)) + shutil.move(folder_name_BF_model.joinpath("model_seg_rat_axon-myelin_bf"), str(bf_destination)) # remove temporary folders - shutil.rmtree(folder_name_TEM_model) shutil.rmtree(folder_name_SEM_model) - shutil.rmtree(folder_name_OM_model) + shutil.rmtree(folder_name_TEM_model) + shutil.rmtree(folder_name_BF_model) def main(argv=None): download_model() diff --git a/AxonDeepSeg/download_tests.py b/AxonDeepSeg/download_tests.py index 079df0a1..1c562ff2 100644 --- a/AxonDeepSeg/download_tests.py +++ b/AxonDeepSeg/download_tests.py @@ -10,7 +10,7 @@ def download_tests(destination=None): destination = convert_path(destination) test_files_destination = destination / "__test_files__" - url_tests = "https://github.com/axondeepseg/data-testing/archive/refs/tags/r20210906b.zip" + url_tests = "https://github.com/axondeepseg/data-testing/archive/refs/tags/r20220201.zip" files_before = list(Path.cwd().iterdir()) if ( diff --git a/AxonDeepSeg/integrity_test.py b/AxonDeepSeg/integrity_test.py index c2e8f2a2..47c6bf26 100644 --- a/AxonDeepSeg/integrity_test.py +++ b/AxonDeepSeg/integrity_test.py @@ -25,24 +25,14 @@ def integrity_test(): # input parameters path = Path('folder_name') / 'file_name' - model_name = 'default_SEM_model' + model_name = 'model_seg_rat_axon-myelin_sem' path_model = dir_path / 'models' / model_name path_testing = path_model / 'data_test' - path_configfile = path_model / 'config_network.json' image = Path("image.png") - # Read the configuration file - print('Reading test configuration file.') - if not path_model.exists(): - path_model.mkdir(parents=True) - - with open(path_configfile, 'r') as fd: - config_network = json.loads(fd.read()) - - # Launch the axon and myelin segmentation on test image sample provided in the installation print('Computing the segmentation of axon and myelin on test image.') - prediction = axon_segmentation([path_testing], [str(image)], path_model, config_network, prediction_proba_activate=True, verbosity_level=4) + axon_segmentation(path_testing, [str(path_testing / image)], path_model, overlap_value=[48,48], acquired_resolution=0.13) # Read the ground truth mask and the obtained segmentation mask mask = ads.imread(path_testing / 'mask.png') diff --git a/AxonDeepSeg/mapping_results.py b/AxonDeepSeg/mapping_results.py deleted file mode 100644 index 78e59ef6..00000000 --- a/AxonDeepSeg/mapping_results.py +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -import json -from AxonDeepSeg.apply_model import axon_segmentation -import AxonDeepSeg.ads_utils - -# FILE TO UPDATE - -def result_mapping(folder_models, path_datatest): - """ - Create the U-net. - Input : - folder_models : path to the folder containing all the model folders to test. - path_datatest : path to the folder containing the image to segment. - - Output : - None. Save the segmented images in the data_test folder. - """ - model_folders = [f for f in folder_models.iterdir()] - for root in model_folders: - if 'DS_Store' not in root: - subpath_model = folder_models / root - filename = '/config_network.json' - with open(subpath_model / filename, 'r') as fd: - config_network = json.loads(fd.read()) - - axon_segmentation(path_datatest, subpath_model, config_network, segmentations_filenames='segmentation_' + root + '.png') - - return 'segmented' - -def map_model_to_images(folder_model, path_datatests, batch_size=1, gps=0.1, crop_value=25, gpu_per=1.0): - """ - Apply one trained model to all the specified images - """ - - # Load config - with open(folder_model / 'config_network.json', 'r') as fd: - config_network = json.loads(fd.read()) - - path_images = [e for e in path_datatests.iterdir() if (path_datatests / e).is_dir()] - n_images = len(path_images) - path_images_list = list(segment_list(path_images,20)) - - if type(gps) != list: - gps = n_images*[gps] - gps_list = list(segment_list(gps,20)) - - for i,path_images_iter in enumerate(path_images_list): - gps_iter = gps_list[i] - axon_segmentation(path_images_iter, folder_model, config_network, segmentations_filenames='segmentation.png', inference_batch_size=batch_size, write_mode=True, prediction_proba_activate=False, resampled_resolutions=gps_iter, overlap_value= crop_value, gpu_per=gpu_per) - - -def segment_list(l, n): - # For item i in a range that is a length of l, - for i in range(0, len(l), n): - # Create an index range for l of n items: - yield l[i:i+n] - - - - - - diff --git a/AxonDeepSeg/morphometrics/compute_morphometrics.py b/AxonDeepSeg/morphometrics/compute_morphometrics.py index c2dbd79a..9e8fe95d 100644 --- a/AxonDeepSeg/morphometrics/compute_morphometrics.py +++ b/AxonDeepSeg/morphometrics/compute_morphometrics.py @@ -8,7 +8,8 @@ import math import numpy as np from scipy import ndimage as ndi -from skimage import measure, morphology +from skimage import measure +from skimage.segmentation import watershed # Graphs and plots imports from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas @@ -99,7 +100,7 @@ def get_axon_morphometrics(im_axon, path_folder=None, im_myelin=None, pixel_size im_centroid[ind_centroid[0][i], ind_centroid[1][i]] = i + 1 # Watershed segmentation of axonmyelin using distance map - im_axonmyelin_label = morphology.watershed(-distance, im_centroid, mask=im_axonmyelin) + im_axonmyelin_label = watershed(-distance, im_centroid, mask=im_axonmyelin) # Measure properties of each axonmyelin object axonmyelin_objects = measure.regionprops(im_axonmyelin_label) @@ -135,7 +136,13 @@ def get_axon_morphometrics(im_axon, path_folder=None, im_myelin=None, pixel_size 'axon_perimeter': axon_perimeter, 'solidity': solidity, 'eccentricity': eccentricity, - 'orientation': orientation} + 'orientation': orientation, + 'gratio': np.nan, + 'myelin_thickness': np.nan, + 'myelin_area': np.nan, + 'axonmyelin_area': np.nan, + 'axonmyelin_perimeter': np.nan + } # Deal with myelin if im_myelin is not None: @@ -164,11 +171,11 @@ def get_axon_morphometrics(im_axon, path_folder=None, im_myelin=None, pixel_size stats['axonmyelin_perimeter'] = axonmyelin_perimeter except ZeroDivisionError: print(f"ZeroDivisionError caught on invalid object #{idx}.") - stats['gratio'] = float('NaN') - stats['myelin_thickness'] = float('NaN') - stats['myelin_area'] = float('NaN') - stats['axonmyelin_area'] = float('NaN') - stats['axonmyelin_perimeter'] = float('NaN') + stats['gratio'] = np.nan + stats['myelin_thickness'] = np.nan + stats['myelin_area'] = np.nan + stats['axonmyelin_area'] = np.nan + stats['axonmyelin_perimeter'] = np.nan else: print( diff --git a/AxonDeepSeg/network_construction.py b/AxonDeepSeg/network_construction.py deleted file mode 100644 index cd27eb3f..00000000 --- a/AxonDeepSeg/network_construction.py +++ /dev/null @@ -1,156 +0,0 @@ -from keras.layers import ( - Conv2D, - BatchNormalization, - Activation, - Dropout, - Input, - MaxPooling2D, - UpSampling2D, - Concatenate, - ) -from keras.models import Model - -import tensorflow as tf -import AxonDeepSeg.ads_utils - -def conv_relu(x, filters, kernel_size, strides, name, activation='relu', kernel_initializer='glorot_normal', - activate_bn=True, - bn_decay=0.999, keep_prob=1.0): - with tf.name_scope(name): - with tf.name_scope("convolution"): - if activate_bn == True: - - net = Conv2D(filters=filters, kernel_size=kernel_size, strides=strides, padding='same', use_bias=False, - kernel_initializer=kernel_initializer)(x) - net = BatchNormalization(axis=3, momentum=1 - bn_decay)(net) - net = Activation(activation)(net) - - else: - net = Conv2D(filters=filters, kernel_size=kernel_size, strides=strides, activation=activation, - kernel_initializer=kernel_initializer, padding='same')(x) - - net = Dropout(1 - keep_prob)(net) - - return net - - -def downconv(x, filters, name, kernel_size=5, strides=2, activation='relu', kernel_initializer='glorot_normal', - activate_bn=True, bn_decay=0.999): - with tf.name_scope(name): - with tf.name_scope("convolution"): - if activate_bn == True: - - net = Conv2D(filters=filters, kernel_size=kernel_size, strides=strides, padding='same', use_bias=False, - kernel_initializer=kernel_initializer)(x) - net = BatchNormalization(axis=3, momentum=1 - bn_decay)(net) - net = Activation(activation)(net) - - else: - - net = Conv2D(filters=filters, kernel_size=kernel_size, strides=strides, activation=activation, - kernel_initializer=kernel_initializer, padding='same')(x) - - return net - - -# ------------------------ NETWORK STRUCTURE ------------------------ # - - -def uconv_net(training_config, bn_updated_decay=None, verbose=True): - """ - Create the U-net. - Input : - x : TF object to define, ensemble des patchs des images :graph input - config : dict : described in the header. - image_size : int : The image size - - Output : - The U-net. - """ - - # Load the variables - image_size = training_config["trainingset_patchsize"] - n_classes = training_config["n_classes"] - depth = training_config["depth"] - dropout = training_config["dropout"] - number_of_convolutions_per_layer = training_config["convolution_per_layer"] - size_of_convolutions_per_layer = training_config["size_of_convolutions_per_layer"] - features_per_convolution = training_config["features_per_convolution"] - downsampling = training_config["downsampling"] - activate_bn = training_config["batch_norm_activate"] - if bn_updated_decay is None: - bn_decay = training_config["batch_norm_decay_starting_decay"] - else: - bn_decay = bn_updated_decay - - # Input picture shape is [batch_size, height, width, number_channels_in] (number_channels_in = 1 for the input layer) - - data_temp_size = [image_size] - relu_results = [] - - #################################################################### - ######################### CONTRACTION PHASE ######################## - #################################################################### - - X = Input((image_size, image_size, 1)) - net = X - - - for i in range(depth): - - for conv_number in range(number_of_convolutions_per_layer[i]): - - if verbose: - """ - print(('Layer: ', i, ' Conv: ', conv_number, 'Features: ', features_per_convolution[i][conv_number])) - print(('Size:', size_of_convolutions_per_layer[i][conv_number]))""" - - net = conv_relu(net, filters=features_per_convolution[i][conv_number][1], - kernel_size=size_of_convolutions_per_layer[i][conv_number], strides=1, - activation='relu', kernel_initializer='glorot_normal', activate_bn=activate_bn, - bn_decay=bn_decay,keep_prob=dropout, name='cconv-d' + str(i) + '-c' + str(conv_number)) - - relu_results.append(net) # We keep them for the upconvolutions - - if downsampling == 'convolution': - - net = downconv(net, filters=features_per_convolution[i][conv_number][1], kernel_size=5, strides=2, - activation='relu', kernel_initializer='glorot_normal', activate_bn=activate_bn, - bn_decay=bn_decay, name='downconv-d' + str(i)) - - else: - - net = MaxPooling2D((2, 2), padding='valid', strides=2, name='downmp-d' + str(i))(net) - - data_temp_size.append(data_temp_size[-1] // 2) - - #################################################################### - ########################## EXPANSION PHASE ######################### - #################################################################### - - for i in range(depth): - # Upsampling - net = UpSampling2D((2, 2))(net) - - # Convolution - net = conv_relu(net, filters=features_per_convolution[depth - i - 1][-1][1], kernel_size=2, strides=1, - activation='relu', kernel_initializer='glorot_normal', activate_bn=activate_bn, - bn_decay=bn_decay, keep_prob=dropout, name='upconv-d' + str(depth - i - 1)) - - data_temp_size.append(data_temp_size[-1] * 2) - - # concatenation (see U-net article) - net = Concatenate(axis=3)([relu_results[depth - i - 1], net]) - - # Classic convolutions - for conv_number in range(number_of_convolutions_per_layer[depth - i - 1]): - net = conv_relu(net, filters=features_per_convolution[depth - i - 1][conv_number][1], - kernel_size=size_of_convolutions_per_layer[depth - i - 1][conv_number], strides=1, - activation='relu', kernel_initializer='glorot_normal', activate_bn=activate_bn, - bn_decay=bn_decay,keep_prob=dropout, name='econv-d' + str(depth - i - 1) + '-c' + str(conv_number)) - - net = Conv2D(filters=n_classes, kernel_size=1, strides=1, name='finalconv', padding='same', activation="softmax")(net) - - model = Model(inputs=X, outputs=net) - - return model diff --git a/AxonDeepSeg/patch_management_tools.py b/AxonDeepSeg/patch_management_tools.py deleted file mode 100644 index 05e693e2..00000000 --- a/AxonDeepSeg/patch_management_tools.py +++ /dev/null @@ -1,112 +0,0 @@ -# Gathers functions used for patch management, including preprocessing. -import numpy as np -import AxonDeepSeg.ads_utils - - -def im2patches_overlap(img, overlap_value=25, scw=512): - - ''' - Convert an image into patches. - :param img: the image to convert. - :param overlap_value: Int, the number of pixels to use when overlapping the predictions. - :param scw: Int, input size. - :return: the original image, a list of patches, and their positions. - ''' - - # First we crop the image to get the context - cropped = img[overlap_value:-overlap_value, overlap_value:-overlap_value] - - # Then we create patches using the prediction window - spw = scw - 2 * overlap_value # size prediction windows - - qh, rh = divmod(cropped.shape[0], spw) - qw, rw = divmod(cropped.shape[1], spw) - - # Creating positions of prediction windows - L_h = [spw * e for e in range(qh)] - L_w = [spw * e for e in range(qw)] - - # Then if there is a remainder we take the last positions (overlap on the last predictions) - if rh != 0: - L_h.append(cropped.shape[0] - spw) - if rw != 0: - L_w.append(cropped.shape[1] - spw) - - xx, yy = np.meshgrid(L_h, L_w) - P = [np.ravel(xx), np.ravel(yy)] - L_pos = [[P[0][i], P[1][i]] for i in range(len(P[0]))] - - # These positions are also the positions of the context windows in the base image coordinates ! - L_patches = [] - for e in L_pos: - patch = img[e[0]:e[0] + scw, e[1]:e[1] + scw] - L_patches.append(patch) - - return [img, L_patches, L_pos] - - -def patches2im_overlap(L_patches, L_pos, overlap_value=25, scw=512): - - ''' - Stitches patches together to form an image. - :param L_patches: List of segmented patches. - :param L_pos: List of positions of the patches in the image to form. - :param overlap_value: Int, number of pixels to overlap. - :param scw: Int, patch size. - :return: Stitched segmented image. - ''' - - spw = scw - 2 * overlap_value - # L_pred = [e[cropped_value:-cropped_value,cropped_value:-cropped_value] for e in L_patches] - # First : extraction of the predictions - h_l, w_l = np.max(np.stack(L_pos), axis=0) - L_pred = [] - new_img = np.zeros((h_l + scw, w_l + scw)) - - for i, e in enumerate(L_patches): - if L_pos[i][0] == 0: - if L_pos[i][1] == 0: - new_img[0:overlap_value, 0:overlap_value] = e[0:overlap_value, 0:overlap_value] - new_img[overlap_value:scw - overlap_value, 0:overlap_value] = e[overlap_value:-overlap_value, - 0:overlap_value] - new_img[0:overlap_value, overlap_value:scw - overlap_value] = e[0:overlap_value, - overlap_value:-overlap_value] - else: - if L_pos[i][1] == w_l: - new_img[0:overlap_value, -overlap_value:] = e[0:overlap_value, -overlap_value:] - new_img[0:overlap_value, L_pos[i][1] + overlap_value:L_pos[i][1] + scw - overlap_value] = e[ - 0:overlap_value, - overlap_value:-overlap_value] - - if L_pos[i][1] == 0: - if L_pos[i][0] != 0: - new_img[L_pos[i][0] + overlap_value:L_pos[i][0] + scw - overlap_value, 0:overlap_value] = e[ - overlap_value:-overlap_value, - 0:overlap_value] - - if L_pos[i][0] == h_l: - if L_pos[i][1] == w_l: - new_img[-overlap_value:, -overlap_value:] = e[-overlap_value:, -overlap_value:] - new_img[h_l + overlap_value:-overlap_value, -overlap_value:] = e[overlap_value:-overlap_value, - -overlap_value:] - new_img[-overlap_value:, w_l + overlap_value:-overlap_value] = e[-overlap_value:, - overlap_value:-overlap_value] - else: - if L_pos[i][1] == 0: - new_img[-overlap_value:, 0:overlap_value] = e[-overlap_value:, 0:overlap_value] - - new_img[-overlap_value:, L_pos[i][1] + overlap_value:L_pos[i][1] + scw - overlap_value] = e[ - -overlap_value:, - overlap_value:-overlap_value] - if L_pos[i][1] == w_l: - if L_pos[i][0] != h_l: - new_img[L_pos[i][0] + overlap_value:L_pos[i][0] + scw - overlap_value, -overlap_value:] = e[ - overlap_value:-overlap_value, - -overlap_value:] - - L_pred = [e[overlap_value:-overlap_value, overlap_value:-overlap_value] for e in L_patches] - L_pos_corr = [[e[0] + overlap_value, e[1] + overlap_value] for e in L_pos] - for i, e in enumerate(L_pos_corr): - new_img[e[0]:e[0] + spw, e[1]:e[1] + spw] = L_pred[i] - - return new_img diff --git a/AxonDeepSeg/postprocessing.py b/AxonDeepSeg/postprocessing.py index 88e183ed..9908adba 100644 --- a/AxonDeepSeg/postprocessing.py +++ b/AxonDeepSeg/postprocessing.py @@ -80,13 +80,13 @@ def remove_intersection(mask_1, mask_2, priority=1, return_overlap=False): if priority not in [1, 2]: raise Exception("Parameter priority can only be 1 or 2") - array_1 = mask_1.astype(np.bool) - array_2 = mask_2.astype(np.bool) + array_1 = mask_1.astype(bool) + array_2 = mask_2.astype(bool) intersection = (array_1 & array_2).astype(np.uint8) - if priority is 1: + if priority == 1: mask_2 = mask_2 - intersection - if priority is 2: + if priority == 2: mask_1 = mask_1 - intersection if return_overlap is True: diff --git a/AxonDeepSeg/segment.py b/AxonDeepSeg/segment.py index 594d56a7..6b8a2f10 100644 --- a/AxonDeepSeg/segment.py +++ b/AxonDeepSeg/segment.py @@ -1,12 +1,14 @@ + # Segmentation script # ------------------- # This script lets the user segment automatically one or many images based on the segmentation models: SEM, -# TEM or OM. +# TEM or BF. # # Maxime Wabartha - 2017-08-30 # Imports +from os import error import sys from pathlib import Path @@ -24,23 +26,59 @@ from config import axonmyelin_suffix, axon_suffix, myelin_suffix # Global variables -SEM_DEFAULT_MODEL_NAME = "default_SEM_model" -TEM_DEFAULT_MODEL_NAME = "default_TEM_model" -OM_MODEL_NAME = "model_seg_pns_bf" +SEM_DEFAULT_MODEL_NAME = "model_seg_rat_axon-myelin_sem" +TEM_DEFAULT_MODEL_NAME = "model_seg_mouse_axon-myelin_tem" +BF_DEFAULT_MODEL_NAME = "model_seg_rat_axon-myelin_bf" MODELS_PATH = pkg_resources.resource_filename('AxonDeepSeg', 'models') MODELS_PATH = Path(MODELS_PATH) default_SEM_path = MODELS_PATH / SEM_DEFAULT_MODEL_NAME default_TEM_path = MODELS_PATH / TEM_DEFAULT_MODEL_NAME -model_seg_pns_bf_path = MODELS_PATH / OM_MODEL_NAME -default_overlap = 25 +default_BF_path = MODELS_PATH / BF_DEFAULT_MODEL_NAME + +default_overlap = 48 # Definition of the functions -def segment_image(path_testing_image, path_model, - overlap_value, config, resolution_model, - acquired_resolution = None, verbosity_level=0): +def generate_default_parameters(type_acquisition, new_path): + ''' + Generates the parameters used for segmentation for the default model corresponding to the type_model acquisition. + :param type_model: String, the type of model to get the parameters from. + :param new_path: Path to the model to use. + :return: the config dictionary. + ''' + + # If string, convert to Path objects + new_path = convert_path(new_path) + + # Building the path of the requested model if it exists and was supplied, else we load the default model. + if type_acquisition == 'SEM': + if (new_path is not None) and new_path.exists(): + path_model = new_path + else: + path_model = MODELS_PATH / SEM_DEFAULT_MODEL_NAME + elif type_acquisition == 'TEM': + if (new_path is not None) and new_path.exists(): + path_model = new_path + else: + path_model = MODELS_PATH / TEM_DEFAULT_MODEL_NAME + elif type_acquisition == 'BF': + if (new_path is not None) and new_path.exists(): + path_model = new_path + else: + path_model = MODELS_PATH / BF_DEFAULT_MODEL_NAME + else: + raise ValueError + + return path_model + +def segment_image( + path_testing_image, + path_model, + overlap_value, + acquired_resolution = None, + verbosity_level = 0): ''' Segment the image located at the path_testing_image location. @@ -48,8 +86,7 @@ def segment_image(path_testing_image, path_model, :param path_model: where to access the model :param overlap_value: the number of pixels to be used for overlap when doing prediction. Higher value means less border effects but more time to perform the segmentation. - :param config: dict containing the configuration of the network - :param resolution_model: the resolution the model was trained on. + :param acquired_resolution: isotropic pixel resolution of the acquired images. :param verbosity_level: Level of verbosity. The higher, the more information is given about the segmentation process. :return: Nothing. @@ -69,18 +106,12 @@ def segment_image(path_testing_image, path_model, # Get type of model we are using selected_model = path_model.name - - img_name_original = acquisition_name.stem # Performing the segmentation - - axon_segmentation(path_acquisitions_folders=path_acquisition, acquisitions_filenames=[acquisition_name], - path_model_folder=path_model, config_dict=config, ckpt_name='model', - inference_batch_size=1, overlap_value=overlap_value, - resampled_resolutions=resolution_model, verbosity_level=verbosity_level, - acquired_resolution=acquired_resolution, - prediction_proba_activate=False, write_mode=True) + axon_segmentation(path_acquisitions_folders=path_acquisition, acquisitions_filenames=[str(path_acquisition / acquisition_name)], + path_model_folder=path_model, overlap_value=overlap_value, + acquired_resolution=acquired_resolution) if verbosity_level >= 1: print(("Image {0} segmented.".format(path_testing_image))) @@ -92,7 +123,7 @@ def segment_image(path_testing_image, path_model, return None def segment_folders(path_testing_images_folder, path_model, - overlap_value, config, resolution_model, + overlap_value, acquired_resolution = None, verbosity_level=0): ''' @@ -102,8 +133,7 @@ def segment_folders(path_testing_images_folder, path_model, :param path_model: where to access the model. :param overlap_value: the number of pixels to be used for overlap when doing prediction. Higher value means less border effects but more time to perform the segmentation. - :param config: dict containing the configuration of the network - :param resolution_model: the resolution the model was trained on. + :param acquired_resolution: isotropic pixel resolution of the acquired images. :param verbosity_level: Level of verbosity. The higher, the more information is given about the segmentation process. :return: Nothing. @@ -129,16 +159,6 @@ def segment_folders(path_testing_images_folder, path_model, raise e image_size = [height, width] - minimum_resolution = config["trainingset_patchsize"] * resolution_model / min(image_size) - - if acquired_resolution < minimum_resolution: - print("EXCEPTION: The size of one of the images ({0}x{1}) is too small for the provided pixel size ({2}).\n".format(height, width, acquired_resolution), - "The image size must be at least {0}x{0} after resampling to a resolution of {1} to create standard sized patches.\n".format(config["trainingset_patchsize"], resolution_model), - "One of the dimensions of the image has a size of {0} after resampling to that resolution.\n".format(round(acquired_resolution * min(image_size) / resolution_model)), - "Image file location: {0}".format(str(path_testing_images_folder / file_)) - ) - - sys.exit(2) selected_model = path_model.name @@ -148,15 +168,10 @@ def segment_folders(path_testing_images_folder, path_model, img_name_original = file_.stem acquisition_name = file_.name - - axon_segmentation(path_acquisitions_folders=path_testing_images_folder, acquisitions_filenames=[acquisition_name], - path_model_folder=path_model, config_dict=config, ckpt_name='model', - inference_batch_size=1, overlap_value=overlap_value, - acquired_resolution=acquired_resolution, - verbosity_level=verbosity_level, - resampled_resolutions=resolution_model, prediction_proba_activate=False, - write_mode=True) - + + axon_segmentation(path_acquisitions_folders=path_testing_images_folder, acquisitions_filenames=[str(path_testing_images_folder / acquisition_name)], + path_model_folder=path_model, overlap_value=overlap_value, + acquired_resolution=acquired_resolution) if verbosity_level >= 1: tqdm.write("Image {0} segmented.".format(str(path_testing_images_folder / file_))) @@ -164,83 +179,6 @@ def segment_folders(path_testing_images_folder, path_model, return None -def generate_default_parameters(type_acquisition, new_path): - ''' - Generates the parameters used for segmentation for the default model corresponding to the type_model acquisition. - :param type_model: String, the type of model to get the parameters from. - :param new_path: Path to the model to use. - :return: the config dictionary. - ''' - - # If string, convert to Path objects - new_path = convert_path(new_path) - - # Building the path of the requested model if it exists and was supplied, else we load the default model. - if type_acquisition == 'SEM': - if (new_path is not None) and new_path.exists(): - path_model = new_path - else: - path_model = MODELS_PATH / SEM_DEFAULT_MODEL_NAME - elif type_acquisition == 'TEM': - if (new_path is not None) and new_path.exists(): - path_model = new_path - else: - path_model = MODELS_PATH / TEM_DEFAULT_MODEL_NAME - else: - if (new_path is not None) and new_path.exists(): - path_model = new_path - else: - path_model = MODELS_PATH / OM_MODEL_NAME - - path_config_file = path_model / 'config_network.json' - config = generate_config_dict(path_config_file) - - return path_model, config - -def generate_config_dict(path_to_config_file): - ''' - Generates the dictionary version of the configuration file from the path where it is located. - - :param path_to_config: relative path where the file config_network.json is located. - :return: dict containing the configuration of the network, or None if no configuration file was found at the - mentioned path. - ''' - - # If string, convert to Path objects - path_to_config_file = convert_path(path_to_config_file) - - try: - with open(path_to_config_file, 'r') as fd: - config_network = json.loads(fd.read()) - - except: - raise ValueError("No configuration file available at this path.") - - return config_network - -def generate_resolution(type_acquisition, model_input_size): - ''' - Generates the resolution to use related to the trained modeL. - :param type_acquisition: String, "SEM", "TEM" or "OM" - :param model_input_size: String or Int, the size of the input. - :return: Float, the resolution of the model. - ''' - - dict_size = { - "SEM":{ - "512":0.1, - "256":0.2 - }, - "TEM":{ - "512":0.01 - }, - "OM":{ - "512":0.1 - }, - } - - return dict_size[str(type_acquisition)][str(model_input_size)] - # Main loop def main(argv=None): @@ -258,17 +196,14 @@ def main(argv=None): requiredName = ap.add_argument_group('required arguments') # Setting the arguments of the segmentation - requiredName.add_argument('-t', '--type', required=True, choices=['SEM','TEM', 'OM'], help='Type of acquisition to segment. \n'+ + requiredName.add_argument('-t', '--type', required=True, choices=['SEM', 'TEM', 'BF'], help='Type of acquisition to segment. \n'+ 'SEM: scanning electron microscopy samples. \n'+ - 'TEM: transmission electron microscopy samples. \n'+ - 'OM: optical microscopy samples') + 'TEM: transmission electron microscopy samples. \n' + + 'BF: bright-field microscopy samples.') requiredName.add_argument('-i', '--imgpath', required=True, nargs='+', help='Path to the image to segment or path to the folder \n'+ 'where the image(s) to segment is/are located.') - ap.add_argument("-m", "--model", required=False, help='Folder where the model is located. \n'+ - 'The default SEM model path is: \n'+str(default_SEM_path)+'\n'+ - 'The default TEM model path is: \n'+str(default_TEM_path)+'\n'+ - 'The default OM model path is: \n'+str(model_seg_pns_bf_path)+'\n') + ap.add_argument("-m", "--model", required=False, help='Folder where the model is located, if different from the default model.') ap.add_argument('-s', '--sizepixel', required=False, help='Pixel size of the image(s) to segment, in micrometers. \n'+ 'If no pixel size is specified, a pixel_size_in_micrometer.txt \n'+ 'file needs to be added to the image folder path. The pixel size \n'+ @@ -286,14 +221,14 @@ def main(argv=None): 'but also increase the segmentation time. \n'+ 'Default value: '+str(default_overlap)+'\n'+ 'Recommended range of values: [10-100]. \n', - default=25) + default=default_overlap) ap._action_groups.reverse() # Processing the arguments args = vars(ap.parse_args(argv)) type_ = str(args["type"]) verbosity_level = int(args["verbose"]) - overlap_value = int(args["overlap"]) + overlap_value = [int(args["overlap"]), int(args["overlap"])] if args["sizepixel"] is not None: psm = float(args["sizepixel"]) else: @@ -302,8 +237,7 @@ def main(argv=None): new_path = Path(args["model"]) if args["model"] else None # Preparing the arguments to axon_segmentation function - path_model, config = generate_default_parameters(type_, new_path) - resolution_model = generate_resolution(type_, config["trainingset_patchsize"]) + path_model = generate_default_parameters(type_, new_path) # Tuple of valid file extensions validExtensions = ( @@ -340,33 +274,15 @@ def main(argv=None): ) sys.exit(3) - # Check that image size is large enough for given resolution to reach minimum patch size after resizing. - - try: - height, width, _ = ads.imread(str(current_path_target)).shape - except: - try: - height, width = ads.imread(str(current_path_target)).shape - except Exception as e: - raise e - - image_size = [height, width] - minimum_resolution = config["trainingset_patchsize"] * resolution_model / min(image_size) - - if psm < minimum_resolution: - print("EXCEPTION: The size of one of the images ({0}x{1}) is too small for the provided pixel size ({2}).\n".format(height, width, psm), - "The image size must be at least {0}x{0} after resampling to a resolution of {1} to create standard sized patches.\n".format(config["trainingset_patchsize"], resolution_model), - "One of the dimensions of the image has a size of {0} after resampling to that resolution.\n".format(round(psm * min(image_size) / resolution_model)), - "Image file location: {0}".format(current_path_target) - ) - - sys.exit(2) # Performing the segmentation over the image - segment_image(current_path_target, path_model, overlap_value, config, - resolution_model, - acquired_resolution=psm, - verbosity_level=verbosity_level) + segment_image( + path_testing_image=current_path_target, + path_model=path_model, + overlap_value=overlap_value, + acquired_resolution=psm, + verbosity_level=verbosity_level + ) print("Segmentation finished.") @@ -395,10 +311,13 @@ def main(argv=None): sys.exit(3) # Performing the segmentation over all folders in the specified folder containing acquisitions to segment. - segment_folders(current_path_target, path_model, overlap_value, config, - resolution_model, - acquired_resolution=psm, - verbosity_level=verbosity_level) + segment_folders( + path_testing_images_folder=current_path_target, + path_model=path_model, + overlap_value=overlap_value, + acquired_resolution=psm, + verbosity_level=verbosity_level + ) print("Segmentation finished.") diff --git a/AxonDeepSeg/testing/noise_simulation.py b/AxonDeepSeg/testing/noise_simulation.py index 2ceb8a4b..aa280f19 100644 --- a/AxonDeepSeg/testing/noise_simulation.py +++ b/AxonDeepSeg/testing/noise_simulation.py @@ -10,7 +10,6 @@ from skimage.filters import gaussian # AxonDeepSeg imports -from AxonDeepSeg.apply_model import axon_segmentation import AxonDeepSeg.ads_utils def add_additive_gaussian_noise(img,mu=0,sigma=10): diff --git a/AxonDeepSeg/testing/statistics_generation.py b/AxonDeepSeg/testing/statistics_generation.py deleted file mode 100644 index ac16852e..00000000 --- a/AxonDeepSeg/testing/statistics_generation.py +++ /dev/null @@ -1,487 +0,0 @@ -# This files is used to generate statistics on a select sample of images, using the specified model. - -from pathlib import Path -import json -import numpy as np -from tqdm import tqdm -import pickle -from prettytable import PrettyTable -from sklearn.metrics import accuracy_score, log_loss -import time -import pandas as pd - -# AxonDeepSeg imports -import AxonDeepSeg.ads_utils as ads -from AxonDeepSeg.ads_utils import convert_path -from AxonDeepSeg.testing.segmentation_scoring import pw_dice -from AxonDeepSeg.apply_model import axon_segmentation -from AxonDeepSeg.config_tools import rec_update - -def metrics_classic_wrapper(path_model_folder, path_images_folder, resampled_resolution, overlap_value=25, - statistics_filename='model_statistics_validation.json', - create_statistics_file=True, verbosity_level=0): - - """ - Procedure to compute metrics on all the images we want, at the same time. - :param path_model_folder: Path to the (only) model we want to use for statistics generation . - :param path_images_folder: Path to the folder containing all the folders containing the images (each image has its - own folder). - :param resampled_resolution: The size in micrometer of a pixel we resample to. - :param overlap_value: The number of pixels used for overlap. - :param statistics_filename: String, the file name to use when creating a statistics file. - :param create_statistics_file: Boolean. If False, the function just displays the statistics. True by default. - :param verbosity_level: Int. The higher, the more information displayed about the metrics computing process. - :return: Nothing. - """ - - # If string, convert to Path objects - path_model_folder = convert_path(path_model_folder) - path_images_folder = convert_path(path_images_folder) - - # First we load every information independent of the model - # We generate the list of testing folders, each one containing one image - images_folders = filter(lambda p: p.is_dir(), path_images_folder.iterdir()) - path_images_folder = [path_images_folder / x for x in images_folders] - - # We check that the model path we were given exists. - if path_model_folder.is_dir(): - - # Generation of statistics - stats_dict = generate_statistics(path_model_folder, path_images_folder, resampled_resolution, overlap_value, verbosity_level=verbosity_level) - - # We now save the data in a corresponding json file. - save_metrics(stats_dict, path_model_folder, statistics_filename) - - # Finally we print out the results using pprint. - print_metrics(stats_dict) - - -def metrics_single_wrapper(path_model_folder, path_images_folder, resampled_resolution, overlap_value=25, - statistics_filename='model_statistics_validation.json', - create_statistics_file=True, verbosity_level=0): - """ - Procedure to compute the metrics using a model on several images. Computation is made on one image after the other. - :param path_model_folder: Path to the folder where the model is located. - :param path_images_folder: Path to the folders that contain the images to compute the metrics on. - :param resampled_resolution: Float, the resolution to resample to to make the predictions. - :param overlap_value: Int, the number of pixels to use for overlap. - :param statistics_filename: String, the name of the file to use when saving the computed metrics. - :param create_statistics_file: Boolean. If true, creates a statistics file where the computed metrics are stored. - :param verbosity_level: Int. The higher, the more displayed information. - :return: Nothing. - """ - - # If string, convert to Path objects - path_model_folder = convert_path(path_model_folder) - path_images_folder = convert_path(path_images_folder) - - # First we load every information independent of the model - # We generate the list of testing folders, each one containing one image - images_folders = filter(lambda p: p.is_dir(), path_images_folder.iterdir()) - path_images_folder = [path_images_folder / x for x in images_folders] - - # We check that the model path we were given exists. - if path_model_folder.is_dir(): - - # Generation of statistics for each image one after the other - for current_path_images_folder in path_images_folder: - - stats_dict = generate_statistics(path_model_folder, [current_path_images_folder], - resampled_resolution, overlap_value, verbosity_level=verbosity_level) - - # We now save the data in a corresponding json file. - save_metrics(stats_dict, path_model_folder, statistics_filename) - - # Finally we print out the results using pprint. - print_metrics(stats_dict) - - - - - -def print_metrics(metrics_dict, filter_ckpt=None): - """ - Displays the computed metrics entered as input in the terminal. - :param metrics_dict: Dictionary, contains the computed metrics. - :param filter_ckpt: String, name of the checkpoint to use. If None, using all checkpoints. - :return: Nothing. - """ - - dict_ckpt = metrics_dict["data"] - - if filter_ckpt != None: - - # We go through every checkpoint in the list and we only return the checkpoint dictionary which name is the - # one we want to filter. - for ckpt_elem in list(dict_ckpt.values()): - if ckpt_elem['ckpt'] == str(filter_ckpt): - dict_ckpt = [ckpt_elem] - break - - for current_ckpt in list(dict_ckpt.values()): - - print(("Model: " + str(current_ckpt["id_model"]) + \ - ", ckpt: " + str(current_ckpt["ckpt"]) + ", date: " + str(metrics_dict["date"]))) - - for name_image, test_image_stats in list(current_ckpt["testing_stats"].items()): - - t = PrettyTable(["Metric", "Value"]) - t.add_row(["name_image", name_image]) - - for key, value in list(test_image_stats.items()): - t.add_row([key, value]) - print(t) - - - -def generate_statistics(path_model_folder, path_images_folder, resampled_resolution, overlap_value, - verbosity_level=0): - """ - Generates the implemented statistics for all the checkpoints of a given model, for each requested image. - :param path_model_folder: Path to the model to use. - :param path_images_folder: Path to the folders that contain the images to compute the metrics on. - :param resampled_resolution: Float, the resolution to resample to to make the predictions. - :param overlap_value: Int, the number of pixels to use for overlap. - :param verbosity_level: Int. The higher, the more displayed information. - :return: - """ - print(path_images_folder) - # If string, convert to Path objects - path_model_folder = convert_path(path_model_folder) - path_images_folder = convert_path(path_images_folder) - print(path_images_folder) - - model_statistics_dict = {"date":time.strftime("%Y-%m-%d"), - "data":{}} - # First we load the network parameters from the config file - with open(path_model_folder / 'config_network.json', 'r') as fd: - config_network = json.loads(fd.read()) - - n_classes = config_network['n_classes'] - model_name = path_model_folder.parts[-2] # Extraction of the name of the model. - - # We loop over all checkpoint files to compute statistics for each checkpoint. - for checkpoint in path_model_folder.iterdir(): - - if str(checkpoint)[-10:] == '.ckpt.meta': - - result_model = {} - name_checkpoint = str(checkpoint)[:-10] - - result_model.update({'id_model': model_name, - 'ckpt': name_checkpoint, - 'config': config_network}) - - # 1/ We load the saved training statistics, which are independent of the testing images - - try: - f = open(path_model_folder + '/' + name_checkpoint + '.pkl', 'r') - res = pickle.load(f) - acc_stats = res['accuracy'] - loss_stats = res['loss'] - epoch_stats = res['steps'] - - except: - print('No stats file found...') - #f = open(path_model_folder + '/evolution.pkl', 'r') - #res = pickle.load(f) - #epoch_stats = max(res['steps']) - #acc_stats = np.mean(res['accuracy'][-10:]) - #loss_stats = np.mean(res['loss'][-10:]) - - epoch_stats = None - acc_stats = None - loss_stats = None - - result_model.update({ - 'training_stats': { - 'training_epoch': epoch_stats, - 'training_mvg_avg10_acc': acc_stats, - 'training_mvg_avg10_loss': loss_stats - }, - 'testing_stats': {} - }) - - # 2/ Computation of the predictions / outputs of the network for each image at the same time. - - predictions, outputs_network = axon_segmentation(path_images_folder, - ['image.png']*len(path_images_folder), - path_model_folder, - config_network, ckpt_name=name_checkpoint, - overlap_value=overlap_value, - resampled_resolutions=[resampled_resolution]*len(path_images_folder), - prediction_proba_activate=True, - write_mode=False, - gpu_per=1.0, - verbosity_level=verbosity_level - ) - # These two variables are list, as long as the number of images that are tested. - - if verbosity_level>=2: - print('Statistics extraction...') - - # 3/ Computation of the statistics for each image. - for i, image_folder in tqdm(enumerate(path_images_folder)): - - current_prediction = predictions[i] - current_network_output = outputs_network[i] - - # Reading the images and processing them - mask_raw = ads.imread(image_folder / 'mask.png') - mask = labellize(mask_raw) - - # We infer the name of the different files - name_image = image_folder.name - - # Computing metrics and storing them in the json file. - current_proba = output_network_to_proba(current_network_output, n_classes) - testing_stats_dict = compute_metrics(current_prediction, current_proba, mask, n_classes) - result_model['testing_stats'].update({name_image:testing_stats_dict}) - - # We add the metrics for all the checkpoints from this model (on all images) to the data list. - model_statistics_dict["data"].update({name_checkpoint:result_model}) - - return model_statistics_dict - - -def save_metrics(model_statistics_dict, path_model_folder, statistics_filename): - """ - Saves the computed metrics in a json file. - :param model_statistics_dict: Dict, the computed metrics to save. - :param path_model_folder: Path to the folder containing the model to use. - :param statistics_filename: Name of the file where we will store the computed statistics. - :return: - """ - - # If string, convert to Path objects - path_model_folder = convert_path(path_model_folder) - - # If the file already exists we rename the old one with a .old suffix. - path_statistics_file = path_model_folder / statistics_filename - - if path_statistics_file.exists(): - with open(path_statistics_file) as f: - original_stats_dict = json.load(f) - path_statistics_file.unlink() - else: - original_stats_dict = {} - - #original_stats_dict.update(model_statistics_dict) - rec_update(original_stats_dict, model_statistics_dict) - - - with open(path_statistics_file, 'w') as f: - json.dump(original_stats_dict, f, indent=2) - - -def output_network_to_proba(output_network, n_classes): - """ - Softmax function applied to the output of the network. - :param output_network: The pre-activation outputted by the network (function uconv_net), before applying softmax. - :return: Tensor, same shape as output_network, but probabilities instead. - """ - a = np.exp(output_network) - b = np.sum(a, axis=-1) - return np.stack([np.divide(a[:, :, l], b) for l in range(n_classes)], axis=-1) - - -def compute_metrics(prediction, proba, mask, n_classes): - # Generate all the statistics for the current image using the current model, and stores them in a dictionary. - - stats = {} - - # Computation of intermediary metrics which will be used for computing final metrics. - vec_prediction = np.reshape(volumize(prediction, n_classes), (-1, n_classes)) - vec_pred_proba = np.reshape(proba, (-1, n_classes)) - vec_mask = np.reshape(volumize(mask, n_classes), (-1, n_classes)) - gold_standard_axons = volumize(mask, n_classes)[:, :, -1] - prediction_axon = volumize(prediction, n_classes)[:, :, -1] - - # >> Accuracy and XEntropy loss - stats.update({ - 'accuracy': accuracy_score(mask.ravel(), prediction.ravel()), - 'log_loss': log_loss(vec_mask, vec_pred_proba) - }) - # >> Pixel wise dice, both classes - pw_dice_axon = pw_dice(prediction_axon, gold_standard_axons) - stats.update({ - 'pw_dice_axon': pw_dice_axon}) - - if n_classes == 3: - gt_myelin = volumize(mask, n_classes)[:, :, 1] - pred_myelin = volumize(prediction, n_classes)[:, :, 1] - pw_dice_myelin = pw_dice(pred_myelin, gt_myelin) - - stats.update({ - 'pw_dice_myelin': pw_dice_myelin}) - - return stats - - -def labellize(mask_raw, thresh=[0, 0.2, 0.8]): # TODO : check function - max_ = np.max(mask_raw) - n_c = len(thresh) - - mask = np.zeros_like(mask_raw) - for i, e in enumerate(thresh[1:]): - mask[np.where(mask_raw >= e * 255)] = i + 1 - - return mask - -def binarize(mask_raw): - vals = np.unique(mask_raw) - mask = np.zeros((mask_raw.shape[0], mask_raw.shape[1], len(vals))) - for i, e in enumerate(vals): - mask[:, :, i] = mask_raw == e - return mask - -def volumize(mask_labellized, n_class): - ''' - :param mask_labellized: 2-D array with each class being indicated as its corresponding - number. ex : [[0,0,1],[2,2,0],[0,1,2]]. - ''' - mask = np.zeros((mask_labellized.shape[0], mask_labellized.shape[1], n_class)) - - for i in range(n_class): - mask[:, :, i] = mask_labellized == i - - return mask - -# Aggregation of statistics. - - -class metrics(): - """ - We use this class to manage metrics and create easily aggregation. We then are able to save them in csv format. - """ - def __init__(self, statistics_filename='model_statistics_validation.json'): - self.statistics_filename = statistics_filename - self.path_models = set() - self.stats = pd.DataFrame() - self.filtered_stats = pd.DataFrame() - self.aggregated_stats = pd.DataFrame() - self.columns = ['id_model', 'ckpt', 'type_model', 'type_image', 'pw_dice_myelin', - 'pw_dice_axon', 'testing_log_loss', 'testing_accuracy', 'testing_name_image'] - - def add_models(self, path_models): - if type(path_models) != list: - path_models = [path_models] - [self.path_models.add(e) for e in path_models] - - def load_models(self): - for path in self.path_models: - - # If string, convert to Path objects - path = convert_path(path) - - try: - with open(path / self.statistics_filename) as f: - stats_dict = json.loads(f.read())['data'] - except: - raise ValueError('No config file found: statistics json file missing in the model folder.') - - # Now we add a line to the stats dataframe for each model - for ckpt_name, ckpt in list(stats_dict.items()): - - # Getting each part of data - model_name = ckpt['id_model'] - ckpt_name = ckpt['ckpt'] - config = ckpt['config'] - testing_stats_list = ckpt['testing_stats'] - for name_image, testing_stats in list(testing_stats_list.items()): - type_image = name_image.split("_")[0] - pw_dice_myelin = testing_stats['pw_dice_myelin'] - pw_dice_axon = testing_stats['pw_dice_axon'] - testing_log_loss = testing_stats['log_loss'] - testing_accuracy = testing_stats['accuracy'] - - new_line = [[model_name, ckpt_name, config['trainingset'].split('_')[0], - type_image, pw_dice_myelin, pw_dice_axon, - testing_log_loss, testing_accuracy, name_image]] - - # Updating the dataframe with the latest data - self.stats = self.stats.append(pd.DataFrame(columns=self.columns, data=new_line)) - - self.filtered_stats = self.stats.copy() - - def filter_(self, list_acquisitions=None, list_ckpt=None, write_mode=False, name_file=None): - filtered_stats = pd.DataFrame() - - if list_acquisitions != None: - # Processing arguments - if type(list_acquisitions) != list: - list_acquisitions = [list_acquisitions] - - # For each acquisition type - for image_to_take in list_acquisitions: - filtered_stats = filtered_stats.append(self.stats.loc[self.stats['type_image'] == image_to_take]) - if list_ckpt != None: - # Processing arguments - if type(list_ckpt) != list: - list_ckpt = [list_ckpt] - for ckpt in list_ckpt: - filtered_stats = filtered_stats.append(self.stats.loc[self.stats['ckpt'] == ckpt]) - self.filtered_stats = filtered_stats - - if write_mode == True: - if name_file is None: - name_file = 'filtered_' + '_'.join(list_acquisitions) + '_' + time.strftime("%Y-%m-%d") + '.csv' - filtered_stats.T.to_csv(name_file) - - # Outputting the filtered pandas dataframe. - return filtered_stats - - def aggregate(self, list_metrics, write_mode=False, name_file=None): - # Processing arguments - aggregated_stats = pd.DataFrame() - if type(list_metrics) != list: - list_metrics = [list_metrics] - - for metric in list_metrics: - tmp = self.filtered_stats.groupby(['id_model', 'ckpt']).apply(metric) - tmp.columns = [x + '_' + metric.__name__ for x in tmp.columns.tolist()] - aggregated_stats = pd.concat([aggregated_stats, tmp], - axis=1, ignore_index=False) - - if write_mode == True: - if name_file is None: - name_file = 'agg_' + '_'.join([x.__name__ for x in list_metrics]) + '_' + time.strftime( - "%Y-%m-%d") + '.csv' - aggregated_stats.T.to_csv(name_file) - - return aggregated_stats - -# ARGUMENTS - -# m path model -# p path data -# t type of acquisition -# s resampling resolution -# o overlap value - -def main(): - - import argparse - ap = argparse.ArgumentParser() - - ap.add_argument("-m", "--model", required=False, default="../models/defaults/default_SEM_model/") - ap.add_argument("-d", "--data", required=False, default="../../data/baseline_validation/") - ap.add_argument("-t", "--type", required=False, default="single") - ap.add_argument("-r", "--resolution", required=False, default=0.1) - - args = vars(ap.parse_args()) - - path_model = str(args["model"]) - path_data = str(args["data"]) - type_computation = str(args["type"]) - resampling_resolution = float(args["resolution"]) - - - if type_computation == "single": - metrics_single_wrapper(path_model, path_data, resampling_resolution) - - else: - metrics_classic_wrapper(path_model, path_data, resampling_resolution) - -if __name__ == '__main__': - main() diff --git a/AxonDeepSeg/train_network.py b/AxonDeepSeg/train_network.py deleted file mode 100755 index b2d03b43..00000000 --- a/AxonDeepSeg/train_network.py +++ /dev/null @@ -1,386 +0,0 @@ -# -*- coding: utf-8 -*- - -from pathlib import Path -import os - -# AxonDeepSeg imports -from AxonDeepSeg.network_construction import uconv_net -from AxonDeepSeg.data_management.input_data import DataGen -from AxonDeepSeg.ads_utils import convert_path -from AxonDeepSeg.config_tools import generate_config -import AxonDeepSeg.ads_utils - -# Keras imports -import keras -from keras.models import load_model -from keras.callbacks import ModelCheckpoint, TensorBoard -import keras.backend.tensorflow_backend as K - -K.set_session -import tensorflow as tf -from albumentations import ( - Compose, - Flip, - ShiftScaleRotate, - ElasticTransform, - GaussianBlur, - Rotate - ) -import random -import cv2 - - -def train_model( - path_trainingset, - path_model, - config, - path_model_init=None, - save_trainable=True, - gpu=None, - debug_mode=False, - gpu_per=1.0, -): - """ - Main function. Trains a model using the configuration parameters. - :param path_trainingset: Path to access the trainingset. - :param path_model: Path indicating where to save the model. - :param config: Dict, containing the configuration parameters of the network. - :param path_model_init: Path to where the model to use for initialization is stored. - :param save_trainable: Boolean. If True, only saves in the model variables that are trainable (evolve from gradient) - :param gpu: String, name of the gpu to use. Prefer use of CUDA_VISIBLE_DEVICES environment variable. - :param debug_mode: Boolean. If activated, saves more information about the distributions of - most trainable variables, and also outputs more information. - :param gpu_per: Float, between 0 and 1. Percentage of GPU to use. - :return: Nothing. - """ - - # If string, convert to Path objects - path_trainingset = convert_path(path_trainingset) - path_model = convert_path(path_model) - - ################################################################################################################### - ############################################## VARIABLES INITIALIZATION ########################################### - ################################################################################################################### - - # Results and Models - if not path_model.exists(): - path_model.mkdir(parents=True) - - # Translating useful variables from the config file. - learning_rate = config["learning_rate"] - batch_size = config["batch_size"] - epochs = config["epochs"] - image_size = config["trainingset_patchsize"] - thresh_indices = config["thresholds"] - if "checkpoint" in config: - checkpoint = config["checkpoint"] - else: - # For retrocompatibility with old configs - checkpoint = None - if "checkpoint_period" in config: - checkpoint_period = config["checkpoint_period"] - else: - # For retrocompatibility with old configs - checkpoint_period = 5 - - # Training and Validation Path - - path_training_set = path_trainingset / "Train" - path_validation_set = path_trainingset / "Validation" - - # List of Training Ids - no_train_images = int(len(os.listdir(path_training_set)) / 2) - train_ids = [str(i) for i in range(no_train_images)] - - # List of Validation Ids - no_valid_images = int(len(os.listdir(path_validation_set)) / 2) - valid_ids = [str(i) for i in range(no_valid_images)] - - ################################################################################################################### - ############################################# DATA AUGMENTATION ################################################## - ################################################################################################################### - - shifting = (config["da-0-shifting-activate"],) - rescaling = config["da-1-rescaling-activate"] - rotation = config["da-2-random_rotation-activate"] - elastic = config["da-3-elastic-activate"] - flipping = config["da-4-flipping-activate"] - gaussian_blur = False # Gaussian Blur preserved for retrocompatibility with old configs - if "da-5-gaussian_blur-activate" in config: - gaussian_blur = config["da-5-gaussian_blur-activate"] - elif "da-5-noise_addition-activate" in config: - gaussian_blur = config["da-5-noise_addition-activate"] - reflection_border = config[ - "da-6-reflection_border-activate" - ] # Config parameter to determine whether relection or constant(value = 0) is used for border pixel values while performing augmentation operations such as rotation, rescaling and shifting. - - if reflection_border: - border_mode = cv2.BORDER_REFLECT_101 - else: - border_mode = cv2.BORDER_CONSTANT - - p_shift = p_rescale = p_rotate = p_elastic = p_flip = p_blur = 0 - # If the key values of augmentation are set to True then their respective probability are set to 0.5 else to 0. Probalility(p) suggests a certainity of applying data augmentation operations (shift, rotate, blur, elastic, flip) to an image. - - # Probability value of 0.5 is chosen so that the original as well augmented image are taken into account while training the model. - if shifting: - p_shift = 0.5 - if rotation: - p_rotate = 0.5 - if flipping: - p_flip = 0.5 - if gaussian_blur: - p_blur = 0.5 - if elastic: - p_elastic = 0.5 - - #####Data Augmentation parameters##### - - # Elastic transform parameters - alpha_max = 9 - sigma = 3 - alpha = random.choice(list(range(1, alpha_max))) - - # Random rotation parameters - low_bound = 5 - high_bound = 89 - - # Shifting parameters - percentage_max = 0.1 - size_shift = int(percentage_max * image_size) - low_limit = 0 - high_limit = (2 * size_shift - 1) / image_size - - ###################################### - - AUGMENTATIONS_TRAIN = Compose( - [ - # Randomy flips an image either horizontally, vertically or both. - Flip(p=p_flip), - # Randomly rotates an image between low limit and high limit. - ShiftScaleRotate( - shift_limit=(low_limit, high_limit), - scale_limit=(0, 0), - rotate_limit=(0, 0), - border_mode=border_mode, - p=p_shift, - interpolation=cv2.INTER_NEAREST, - ), - # Randomly applies elastic transformation on the image. - ElasticTransform( - alpha=alpha, - sigma=sigma, - p=p_elastic, - alpha_affine=alpha, - interpolation=cv2.INTER_NEAREST, - ), - # Blurs an image using gaussian kernel. - GaussianBlur(p=p_blur), - # Randomly rotates the image between low bound and high bound. - Rotate( - limit=(low_bound, high_bound), - border_mode=border_mode, - p=p_rotate, - interpolation=cv2.INTER_NEAREST, - ), - ] - ) - - AUGMENTATIONS_TEST = Compose([]) - - ################################################################################################################### - - # Loading the Training images and masks in batch - train_generator = DataGen( - train_ids, - path_training_set, - batch_size=batch_size, - image_size=image_size, - thresh_indices=thresh_indices, - augmentations=AUGMENTATIONS_TRAIN, - ) - - # Loading the Validation images and masks in batch - valid_generator = DataGen( - valid_ids, - path_validation_set, - batch_size=batch_size, - image_size=image_size, - thresh_indices=thresh_indices, - augmentations=AUGMENTATIONS_TEST, - ) - - ########################### Initalizing U-Net Model ########### - - model = uconv_net(config, bn_updated_decay=None, verbose=True) - - ########################### Tensorboard for Visualization ########### - tensorboard = TensorBoard(log_dir=str(path_model)) - - ########################## Training Unet Model ########### - - # Adam Optimizer for Unet - adam = keras.optimizers.Adam( - lr=learning_rate, beta_1=0.9, beta_2=0.999, epsilon=1e-08, decay=0.0 - ) - - # Compile the model with Categorical Cross Entropy loss and Adam Optimizer - model.compile( - optimizer=adam, - loss=dice_coef_loss, - metrics=["accuracy", dice_axon, dice_myelin], - ) - - train_steps = len(train_ids) // batch_size - valid_steps = len(valid_ids) // batch_size - - ########################## Use Checkpoints to save best Accuracy and Loss ########### - - # Save the checkpoint in the /models/path_model folder - filepath_acc = str(path_model) + "/best_acc_model.ckpt" - - # Keep only a single checkpoint, the best over test accuracy. - checkpoint_acc = ModelCheckpoint( - filepath_acc, - monitor="val_acc", - verbose=0, - save_best_only=True, - mode="max", - period=checkpoint_period, - ) - - # Save the checkpoint in the /models/path_model folder - filepath_loss = str(path_model) + "/best_loss_model.ckpt" - - # Keep only a single checkpoint, the best over test loss. - checkpoint_loss = ModelCheckpoint( - filepath_loss, - monitor="val_loss", - verbose=0, - save_best_only=True, - mode="min", - period=checkpoint_period, - ) - - ########################## Training ########### - - if checkpoint == "loss": - model.load_weights(filepath_loss) - elif checkpoint == "accuracy": - model.load_weights(filepath_acc) - - model.fit_generator( - train_generator, - validation_data=(valid_generator), - steps_per_epoch=train_steps, - validation_steps=valid_steps, - epochs=epochs, - callbacks=[tensorboard, checkpoint_loss, checkpoint_acc], - ) - - ########################## Save the model after Training ########### - - model.save(str(path_model / "model.hdf5")) - - # Add ops to save and restore all the variables. - saver = tf.train.Saver() - - # Save Model in ckpt format - custom_objects = { - "dice_axon": dice_axon, - "dice_myelin": dice_myelin, - "dice_coef_loss": dice_coef_loss, - } - model = load_model( - str(path_model) + "/model.hdf5", custom_objects=custom_objects - ) - - sess = K.get_session() - # Save the model to be used by TF framework - save_path = saver.save(sess, str(path_model) + "/model.ckpt") - - -# Defining the Loss and Performance Metrics - - -def dice_myelin(y_true, y_pred, smooth=1e-3): - """ - Computes the pixel-wise dice myelin coefficient from the prediction tensor outputted by the network. - :param y_pred: Tensor, the prediction outputted by the network. Shape (N,H,W,C). - :param y_true: Tensor, the gold standard we work with. Shape (N,H,W,C). - :return: dice myelin coefficient for the current batch. - """ - - y_true_f = K.flatten(y_true[..., 1]) - y_pred_f = K.flatten(y_pred[..., 1]) - intersection = K.sum(y_true_f * y_pred_f) - return K.mean( - (2.0 * intersection + smooth) - / (K.sum(y_true_f) + K.sum(y_pred_f) + smooth) - ) - - -def dice_axon(y_true, y_pred, smooth=1e-3): - """ - Computes the pixel-wise dice myelin coefficient from the prediction tensor outputted by the network. - :param y_pred: Tensor, the prediction outputed by the network. Shape (N,H,W,C). - :param y_true: Tensor, the gold standard we work with. Shape (N,H,W,C). - :return: dice axon coefficient for the current batch. - """ - - y_true_f = K.flatten(y_true[..., 2]) - y_pred_f = K.flatten(y_pred[..., 2]) - intersection = K.sum(y_true_f * y_pred_f) - return K.mean( - (2.0 * intersection + smooth) - / (K.sum(y_true_f) + K.sum(y_pred_f) + smooth) - ) - - -def dice_coef(y_true, y_pred, smooth=1e-3): - y_true_f = K.flatten(y_true) - y_pred_f = K.flatten(y_pred) - intersection = K.sum(y_true_f * y_pred_f) - return K.mean( - (2.0 * intersection + smooth) - / (K.sum(y_true_f) + K.sum(y_pred_f) + smooth) - ) - - -def dice_coef_loss(y_true, y_pred): - return 1 - dice_coef(y_true, y_pred) - - -# To Call the training in the terminal - - -def main(): - import argparse - - ap = argparse.ArgumentParser() - ap.add_argument("-p", "--path_training", required=True, help="") - ap.add_argument("-m", "--path_model", required=True, help="") - ap.add_argument( - "-co", - "--config_file", - required=False, - help="", - default="~/.axondeepseg.json", - ) - ap.add_argument("-m_init", "--path_model_init", required=False, help="") - ap.add_argument("-gpu", "--GPU", required=False, help="") - - args = vars(ap.parse_args()) - path_training = Path(args["path_training"]) - path_model = Path(args["path_model"]) - path_model_init = Path(args["path_model_init"]) - config_file = args["config_file"] - gpu = args["GPU"] - - config = generate_config(config_file) - - train_model(path_training, path_model, config, path_model_init, gpu=gpu) - - -if __name__ == "__main__": - main() diff --git a/AxonDeepSeg/trainingforhelios.py b/AxonDeepSeg/trainingforhelios.py deleted file mode 100755 index 7e8fa873..00000000 --- a/AxonDeepSeg/trainingforhelios.py +++ /dev/null @@ -1,50 +0,0 @@ -# -*- coding: utf-8 -*- - -# Script to place in the same folder as train_network.py - -import sys -import os -import json -import AxonDeepSeg.ads_utils - - -def compute_training(configfile, path_trainingset, path_model, path_model_init = None, gpu_per = 1.0): - - os.chdir(sys.path[0]) # Necessary to fix the directory we are working in - with open(path_model / configfile, 'r') as fd: - config_network = json.loads(fd.read()) - - #if not os.path.exists(path_model): # Already created before. But can be useful if we want to create models with date of launch. - # os.makedirs(path_model) - - #with open(path_model + configname, 'w') as f: - # json.dump(config, f, indent=2) - - #with open(path_model + filename, 'r') as fd: - # config_network = json.loads(fd.read()) - - # Training - from AxonDeepSeg.train_network import train_model - train_model(path_trainingset, path_model, config_network, gpu_per=gpu_per) - - -def main(): - import argparse - ap = argparse.ArgumentParser() - ap.add_argument("-co", "--configfile", required=True, help="") - ap.add_argument("-t", "--path_trainingset", required=True, help="") - ap.add_argument("-m", "--path_model", required=True, help="") - ap.add_argument("-i", "--path_model_init", required=False, default = None, help="") - ap.add_argument("-g", "--gpu_per", required=False, default = 1.0, help="") - - args = vars(ap.parse_args()) - configfile = str(args["configfile"]) - path_trainingset = str(args["path_trainingset"]) - path_model = str(args["path_model"]) - path_model_init = str(args["path_model_init"]) - gpu_per = float(args["gpu_per"]) - - compute_training(configfile, path_trainingset, path_model, path_model_init, gpu_per = gpu_per) - -if __name__ == '__main__': - main() \ No newline at end of file diff --git a/AxonDeepSeg/visualization/get_masks.py b/AxonDeepSeg/visualization/get_masks.py index 6ff67ad0..87e32535 100644 --- a/AxonDeepSeg/visualization/get_masks.py +++ b/AxonDeepSeg/visualization/get_masks.py @@ -8,6 +8,7 @@ # AxonDeepSeg modules import import AxonDeepSeg.ads_utils as ads from AxonDeepSeg.ads_utils import convert_path +from config import axonmyelin_suffix, axon_suffix, myelin_suffix def get_masks(path_prediction): # If string, convert to Path objects @@ -30,8 +31,8 @@ def get_masks(path_prediction): filename_part = filename_part.split('.png')[0] # Save masks - filename_axon = filename_part + '_seg-axon.png' - filename_myelin = filename_part + '_seg-myelin.png' + filename_axon = filename_part + str(axon_suffix) + filename_myelin = filename_part + str(myelin_suffix) ads.imwrite(folder_path / filename_axon, axon_prediction.astype(int)) ads.imwrite(folder_path / filename_myelin, myelin_prediction.astype(int)) diff --git a/AxonDeepSeg/visualization/merge_masks.py b/AxonDeepSeg/visualization/merge_masks.py index f4ffd64d..1b02d362 100644 --- a/AxonDeepSeg/visualization/merge_masks.py +++ b/AxonDeepSeg/visualization/merge_masks.py @@ -5,7 +5,7 @@ import AxonDeepSeg.ads_utils as ads from AxonDeepSeg.ads_utils import convert_path -def merge_masks(path_axon, path_myelin): +def merge_masks(path_axon, path_myelin, output_filename): # If string, convert to Path objects path_axon = convert_path(path_axon) @@ -16,8 +16,8 @@ def merge_masks(path_axon, path_myelin): # get main path path_folder = path_axon.parent - + # save the masks - ads.imwrite(path_folder / 'axon_myelin_mask.png', both) + ads.imwrite(path_folder / output_filename, both) return both diff --git a/AxonDeepSeg/visualization/visualize.py b/AxonDeepSeg/visualization/visualize.py deleted file mode 100644 index 36754277..00000000 --- a/AxonDeepSeg/visualization/visualize.py +++ /dev/null @@ -1,265 +0,0 @@ -# coding: utf-8 - -import sys -from pathlib import Path -import pickle - -# Scientific modules imports -import numpy as np -from sklearn.metrics import accuracy_score -from sklearn import preprocessing -from tabulate import tabulate - -# Graphs and plots imports -from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas -from matplotlib.figure import Figure - -# AxonDeepSeg imports -import AxonDeepSeg.ads_utils as ads -from AxonDeepSeg.ads_utils import convert_path -from AxonDeepSeg.testing.segmentation_scoring import score_analysis, dice - - -def visualize_training(path_model, iteration_start_for_viz=0): - """ - :param path_model: path of the folder with the model parameters .ckpt - :param iteration_start_for_viz: first iterations can reach extreme values, - iteration_start_for_viz set a beginning other than epoch 0 - :return: matplotlib.figure.Figure - - The returned figure represents the evolution of the loss and the accuracy - evaluated on the validation set along the learning process. - If the learning began from an initial model, the figure plots first the - accuracy and loss evolution from this initial model and then stacks the - evolution of the model. - """ - - # If string, convert to Path objects - path_model = convert_path(path_model) - - def _create_figure_helper(data_evolution): - fig = Figure() - FigureCanvas(fig) - - # Drawing the evolution curves - ax1 = fig.subplots() - ax2 = ax1.twinx() - ax1.plot( - data_evolution["steps"][iteration_start_for_viz:], - data_evolution["accuracy"][iteration_start_for_viz:], - "-", - label="accuracy", - ) - ax1.set_ylim(ymin=0) - ax2.plot( - data_evolution["steps"][iteration_start_for_viz:], - data_evolution["loss"][iteration_start_for_viz:], - "-r", - label="loss", - ) - - # Annotating the graph - ax1.set_title("Accuracy and loss evolution") - ax1.set_xlabel("Epoch") - ax1.set_ylabel("Accuracy") - ax2.set_ylabel("Loss") - return fig - - evolution = retrieve_training_data(path_model) - fig = _create_figure_helper(evolution) - - return fig - - -def visualize_segmentation(path): - """ - :param path: path of the folder including the data and the results obtained - after by the segmentation process. - :return: list of matplotlib.figure.Figure - if there is a mask (ground truth) in the folder, - scores are calculated: sensitivity, errors and dice - figure(1) segmentation without mrf - figure(2) segmentation with mrf - if there is MyelinSeg.jpg in the folder, myelin and image, myelin and axon - segmentated, myelin and groundtruth are represented - """ - # If string, convert to Path objects - path = convert_path(path) - - figs = [] - - def _create_fig_helper(overlayed_img, fig_title): - """ - Helper function to create a figure - :param overlayed_img: the image to add on top on image_init - :param fig_title: str title of the figure - :return: matplotlib.figure.Figure - """ - fig = Figure() - FigureCanvas(fig) - ax = fig.subplots() - ax.set_title(fig_title) - ax.imshow(image_init, cmap="gray") - ax.imshow(overlayed_img, cmap="hsv", alpha=0.7) - return fig - - path_img = path / "image.png" - mask = False - cur_dir_items = [item.name for item in path] - - if "results.pkl" not in cur_dir_items: - print("results not present") - - file = open(path + "/results.pkl", "r") - res = pickle.load(file) - - prediction_mrf = res["prediction_mrf"] - prediction = res["prediction"] - - image_init = ads.imread(path_img) - predict = np.ma.masked_where(prediction == 0, prediction) - predict_mrf = np.ma.masked_where(prediction_mrf == 0, prediction_mrf) - - title = "Axon Segmentation (with mrf) mask" - fig1 = _create_fig_helper(predict_mrf, title) - figs.append(fig1) - - title = "Axon Segmentation (without mrf) mask" - fig2 = _create_fig_helper(predict, title) - figs.append(fig2) - - if "mask.png" in cur_dir_items: - Mask = True - path_mask = path / "mask.png" - mask = preprocessing.binarize( - ads.imread(path_mask), threshold=125 - ) - - acc = accuracy_score(prediction.reshape(-1, 1), mask.reshape(-1, 1)) - score = score_analysis(image_init, mask, prediction) - Dice = dice(image_init, mask, prediction)["dice"] - Dice_mean = Dice.mean() - acc_mrf = accuracy_score(prediction_mrf.reshape(-1, 1), mask.reshape(-1, 1)) - score_mrf = score_analysis(image_init, mask, prediction_mrf) - Dice_mrf = dice(image_init, mask, prediction_mrf)["dice"] - Dice_mrf_mean = Dice_mrf.mean() - - headers = ["MRF", "accuracy", "sensitivity", "precision", "diffusion", "Dice"] - table = [ - ["False", acc, score[0], score[1], score[2], Dice_mean], - ["True", acc_mrf, score_mrf[0], score_mrf[1], score_mrf[2], Dice_mrf_mean], - ] - - subtitle2 = "\n\n---Scores---\n\n" - scores = tabulate(table, headers) - text = subtitle2 + scores - - subtitle3 = "\n\n---Dice Percentiles---\n\n" - headers = ["MRF", "Dice 10th", "50th", "90th"] - table = [ - [ - "False", - np.percentile(Dice, 10), - np.percentile(Dice, 50), - np.percentile(Dice, 90), - ], - [ - "True", - np.percentile(Dice_mrf, 10), - np.percentile(Dice_mrf, 50), - np.percentile(Dice_mrf, 90), - ], - ] - scores_2 = tabulate(table, headers) - - text = text + subtitle3 + subtitle3 + scores_2 - print(text) - - file = open(path / "Report_results.txt", "w") - file.write(text) - file.close() - - if "MyelinSeg.jpg" in cur_dir_items: - path_myelin = path / "MyelinSeg.jpg" - myelin = preprocessing.binarize( - ads.imread(path_myelin), threshold=125 - ) - myelin = np.ma.masked_where(myelin == 0, myelin) - - title = "Myelin Segmentation" - fig3 = _create_fig_helper(myelin, title) - figs.append(fig3) - - if Mask: - # New base image for plotting - image_init = mask - # Create figure - title = "Myelin - GroundTruth" - fig4 = _create_fig_helper(myelin, title) - figs.append(fig4) - return figs - - -def retrieve_training_data(path_model, path_model_init=None): - """ - :param path_model: path of the folder with the model parameters .ckpt - :param path_model_init: if the model is initialized by another, path of its - folder - :return: dictionary {steps, accuracy, loss} describing the evolution over - epochs of the performance of the model. Stacks the initial model if needed - """ - - # If string, convert to Path objects - path_model = convert_path(path_model) - - file = open( - path_model / "evolution.pkl", "rb" - ) # training variables : loss, accuracy, epoch - evolution = pickle.load(file) - - if path_model_init: - # If string, convert to Path objects - path_model_init = convert_path(path_model_init) - - file_init = open(path_model_init / "evolution.pkl", "rb") - evolution_init = pickle.load(file_init) - last_epoch = evolution_init["steps"][-1] - - evolution_merged = ( - {} - ) # Merging the two plots : learning of the init and learning of the model - for key in ["steps", "accuracy", "loss"]: - evolution_merged[key] = evolution_init[key] + evolution[key] - - evolution = evolution_merged - - return evolution - - -def retrieve_hyperparameters(path_model): - - """ - :param path_model: path of the folder with the model parameters .ckpt - :return: the dict containing the hyperparameters - """ - - # If string, convert to Path objects - path_model = convert_path(path_model) - - file = open( - path_model / "hyperparameters.pkl", "r" - ) # training variables : loss, accuracy, epoch - return pickle.load(file) - - -if __name__ == "__main__": - import argparse - - ap = argparse.ArgumentParser() - ap.add_argument("-m", "--path_model", required=True, help="") - - args = vars(ap.parse_args()) - path_model = args["path_model"] - - fig = visualize_training(path_model) - fig.savefig("./visualize_training_acc_vs_loss_evolution.png") diff --git a/CHANGELOG.md b/CHANGELOG.md index 15bf3246..99c337ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,17 @@ Changelog =============================================================================== +## Version 4.0.0 - 2022-01-14 +[View detailed changelog](https://github.com/neuropoly/axondeepseg/compare/v3.3.0...v4.0.0) + +**FEATURE** + + - Integrate IVADOMED into project [View pull request](https://github.com/neuropoly/axondeepseg/pull/547) + +**INSTALLATION** + + - Added Mac M1 compatibility [View pull request](https://github.com/neuropoly/axondeepseg/pull/547) + ## Version 3.3.0 - 2022-01-14 [View detailed changelog](https://github.com/neuropoly/axondeepseg/compare/v3.2.0...v3.3.0) @@ -71,6 +82,7 @@ Changelog - Add PR template [View pull request](https://github.com/neuropoly/axondeepseg/pull/467) - Remove old config file (set_config) [View pull request](https://github.com/neuropoly/axondeepseg/pull/456) + ## Version 3.2.0 - 2020-10-16 [View detailed changelog](https://github.com/neuropoly/axondeepseg/compare/v3.0...v3.2.0) diff --git a/ads_plugin.py b/ads_plugin.py index c48f625e..1107b0fa 100644 --- a/ads_plugin.py +++ b/ads_plugin.py @@ -20,6 +20,8 @@ import AxonDeepSeg from AxonDeepSeg.apply_model import axon_segmentation from AxonDeepSeg.segment import segment_image + + import AxonDeepSeg.morphometrics.compute_morphometrics as compute_morphs from AxonDeepSeg import postprocessing, params, ads_utils from config import axonmyelin_suffix, axon_suffix, myelin_suffix, index_suffix, axonmyelin_index_suffix @@ -33,7 +35,7 @@ import pandas as pd import imageio -VERSION = "0.2.19" +VERSION = "0.2.20" class ADSsettings: """ @@ -49,7 +51,7 @@ def __init__(self, ads_control): self.ads_control = ads_control # Declare the settings used - self.overlap_value = 25 + self.overlap_value = 48 self.model_resolution = 0.01 # Unused self.use_custom_resolution = False # Unused self.custom_resolution = 0.07 # Unused @@ -383,7 +385,7 @@ def on_apply_model_button(self, event): # Get the image name and directory image_overlay = self.get_visible_image_overlay() - if self.get_visible_image_overlay() is None: + if self.get_visible_image_overlay() == None: return n_loaded_images = self.png_image_name.__len__() @@ -394,7 +396,7 @@ def on_apply_model_button(self, event): image_name = self.png_image_name[i] image_directory = self.image_dir_path[i] - if (image_name is None) or (image_directory is None): + if (image_name == None) or (image_directory == None): self.show_message( "Couldn't find the path to the loaded image. " "Please use the plugin's image loader to import the image you wish to segment. " @@ -426,7 +428,7 @@ def on_apply_model_button(self, event): pixel_size_exists = (image_directory / "pixel_size_in_micrometer.txt").exists() # if it doesn't exist, ask the user to input the pixel size - if pixel_size_exists is False: + if pixel_size_exists == False: with wx.TextEntryDialog( self, "Enter the pixel size in micrometer", value="0.07" ) as text_entry: @@ -440,20 +442,13 @@ def on_apply_model_button(self, event): resolution_file = open((image_directory / "pixel_size_in_micrometer.txt").__str__(), 'r') pixel_size_float = float(resolution_file.read()) - # Load model configs and apply prediction - model_configfile = model_path / "config_network.json" - with open(model_configfile.__str__(), "r") as fd: - config_network = json.loads(fd.read()) - segment_image( - image_path, - model_path, - self.settings.overlap_value, - config_network, - resolution, - acquired_resolution=pixel_size_float * self.settings.zoom_factor, - verbosity_level=3 - ) + path_testing_image=image_path, + path_model=model_path, + overlap_value=[int(self.settings.overlap_value), int(self.settings.overlap_value)], + acquired_resolution=pixel_size_float * self.settings.zoom_factor, + verbosity_level=3 + ) # The axon_segmentation function creates the segmentation masks and stores them as PNG files in the same folder # as the original image file. @@ -475,11 +470,11 @@ def on_save_segmentation_button(self, event): # Find the visible myelin and axon masks axon_mask_overlay = self.get_corrected_axon_overlay() - if axon_mask_overlay is None: + if axon_mask_overlay == None: axon_mask_overlay = self.get_visible_axon_overlay() myelin_mask_overlay = self.get_visible_myelin_overlay() - if (axon_mask_overlay is None) or (myelin_mask_overlay is None): + if (axon_mask_overlay == None) or (myelin_mask_overlay == None): return # Ask the user where to save the segmentation @@ -529,7 +524,7 @@ def on_save_segmentation_button(self, event): axon_array = axon_array * params.intensity['binary'] - image_name = myelin_mask_overlay.name[:-len("_seg-myelin")] + image_name = myelin_mask_overlay.name.split("-myelin")[0] myelin_and_axon_array = (myelin_array // 2 + axon_array).astype(np.uint8) @@ -548,7 +543,7 @@ def on_run_watershed_button(self, event): axon_mask_overlay = self.get_visible_axon_overlay() myelin_mask_overlay = self.get_visible_myelin_overlay() - if (axon_mask_overlay is None) or (myelin_mask_overlay is None): + if (axon_mask_overlay == None) or (myelin_mask_overlay == None): return # Extract the data from the overlays @@ -562,7 +557,7 @@ def on_run_watershed_button(self, event): # If a watershed mask already exists, remove it. for an_overlay in self.overlayList: - if (self.most_recent_watershed_mask_name is not None) and ( + if (self.most_recent_watershed_mask_name != None) and ( an_overlay.name == self.most_recent_watershed_mask_name ): self.overlayList.remove(an_overlay) @@ -596,9 +591,9 @@ def on_fill_axons_button(self, event): myelin_mask_overlay = self.get_visible_myelin_overlay() axon_mask_overlay = self.get_visible_axon_overlay() - if myelin_mask_overlay is None: + if myelin_mask_overlay == None: return - if axon_mask_overlay is None: + if axon_mask_overlay == None: return # Extract the data from the overlays @@ -635,11 +630,11 @@ def on_compute_morphometrics_button(self, event): # Find the visible myelin and axon masks axon_mask_overlay = self.get_corrected_axon_overlay() - if axon_mask_overlay is None: + if axon_mask_overlay == None: axon_mask_overlay = self.get_visible_axon_overlay() myelin_mask_overlay = self.get_visible_myelin_overlay() - if (axon_mask_overlay is None) or (myelin_mask_overlay is None): + if (axon_mask_overlay == None) or (myelin_mask_overlay == None): return # store the data of the masks in variables as numpy arrays. @@ -772,7 +767,7 @@ def get_watershed_segmentation(self, im_axon, im_myelin, return_centroids=False) # Measure properties for each axon object axon_objects = measure.regionprops(im_axon_label) # Deal with myelin mask - if im_myelin is not None: + if im_myelin != None: # sum axon and myelin masks im_axonmyelin = im_axon + im_myelin # Compute distance between each pixel and the background. Note: this distance is calculated from the im_axon, @@ -797,7 +792,7 @@ def get_watershed_segmentation(self, im_axon, im_myelin, return_centroids=False) im_axonmyelin_label = morphology.watershed( -distance, im_centroid, mask=im_axonmyelin ) - if return_centroids is True: + if return_centroids == True: return im_axonmyelin_label, ind_centroid else: return im_axonmyelin_label @@ -826,7 +821,7 @@ def load_png_image_from_path( # Open the 2D image img_png2D = ads_utils.imread(image_path) - if is_mask is True: + if is_mask == True: img_png2D = img_png2D // params.intensity['binary'] # Segmentation masks should be binary # Flip the image on the Y axis so that the morphometrics file shows the right coordinates @@ -849,7 +844,7 @@ def load_png_image_from_path( ] # Display the overlay - if add_to_overlayList is True: + if add_to_overlayList == True: self.overlayList.append(img_overlay) opts = self.displayCtx.getOpts(img_overlay) opts.cmap = colormap @@ -866,7 +861,7 @@ def get_visible_overlays(self): visible_overlay_list = [] for an_overlay in self.overlayList: an_overlay_display = self.displayCtx.getDisplay(an_overlay) - if an_overlay_display.enabled is True: + if an_overlay_display.enabled == True: visible_overlay_list.append(an_overlay) return visible_overlay_list @@ -882,11 +877,11 @@ def get_visible_image_overlay(self): image_overlay = None n_found_overlays = 0 - if visible_overlay_list.__len__() is 0: + if visible_overlay_list.__len__() == 0: self.show_message("No overlays are displayed") return None - if visible_overlay_list.__len__() is 1: + if visible_overlay_list.__len__() == 1: return visible_overlay_list[0] for an_overlay in visible_overlay_list: @@ -897,6 +892,8 @@ def get_visible_image_overlay(self): and (not an_overlay.name.endswith("-Myelin")) and (not an_overlay.name.endswith("-Axon")) and (not an_overlay.name.endswith("-axon")) + and (not an_overlay.name.endswith("-axon")) + and (not an_overlay.name.endswith("-myelin")) ): n_found_overlays = n_found_overlays + 1 image_overlay = an_overlay @@ -904,7 +901,7 @@ def get_visible_image_overlay(self): if n_found_overlays > 1: self.show_message("More than one microscopy image has been found") return None - if n_found_overlays is 0: + if n_found_overlays == 0: self.show_message("No visible microscopy image has been found") return None @@ -920,19 +917,19 @@ def get_visible_axon_overlay(self): axon_overlay = None n_found_overlays = 0 - if visible_overlay_list.__len__() is 0: + if visible_overlay_list.__len__() == 0: self.show_message("No overlays are displayed") return None for an_overlay in visible_overlay_list: - if (an_overlay.name.endswith("-axon")) or (an_overlay.name.endswith("-Axon")): + if (an_overlay.name.endswith("-axon")) or (an_overlay.name.endswith("-Axon")) or (an_overlay.name.endswith("-axon")): n_found_overlays = n_found_overlays + 1 axon_overlay = an_overlay if n_found_overlays > 1: self.show_message("More than one axon mask has been found") return None - if n_found_overlays is 0: + if n_found_overlays == 0: self.show_message("No visible axon mask has been found") return None @@ -948,19 +945,19 @@ def get_corrected_axon_overlay(self): axon_overlay = None n_found_overlays = 0 - if visible_overlay_list.__len__() is 0: + if visible_overlay_list.__len__() == 0: self.show_message("No overlays are displayed") return None for an_overlay in visible_overlay_list: - if (an_overlay.name.endswith("-axon-corr")) or (an_overlay.name.endswith("-Axon-corr")): + if (an_overlay.name.endswith("-axon-corr")) or (an_overlay.name.endswith("-Axon-corr")) or (an_overlay.name.endswith("-axon-corr")): n_found_overlays = n_found_overlays + 1 axon_overlay = an_overlay if n_found_overlays > 1: self.show_message("More than one corrected axon mask has been found") return None - if n_found_overlays is 0: + if n_found_overlays == 0: return None return axon_overlay @@ -975,19 +972,19 @@ def get_visible_myelin_overlay(self): myelin_overlay = None n_found_overlays = 0 - if visible_overlay_list.__len__() is 0: + if visible_overlay_list.__len__() == 0: self.show_message("No overlays are displayed") return None for an_overlay in visible_overlay_list: - if (an_overlay.name.endswith("-myelin")) or (an_overlay.name.endswith("-Myelin")): + if (an_overlay.name.endswith("-myelin")) or (an_overlay.name.endswith("-Myelin")) or (an_overlay.name.endswith("-myelin")): n_found_overlays = n_found_overlays + 1 myelin_overlay = an_overlay if n_found_overlays > 1: self.show_message("More than one myelin mask has been found") return None - if n_found_overlays is 0: + if n_found_overlays == 0: self.show_message("No visible myelin mask has been found") return None @@ -1022,7 +1019,7 @@ def verrify_version(self): # Check if the plugin file exists plugin_file_exists = plugin_file.exists() - if plugin_file_exists is False: + if plugin_file_exists == False: return # Check the version of the plugin @@ -1040,7 +1037,7 @@ def verrify_version(self): if not (lines == version_line): plugin_is_up_to_date = False - if (version_found is False) or (plugin_is_up_to_date is False): + if (version_found == False) or (plugin_is_up_to_date == False): message = ( "A more recent version of the AxonDeepSeg plugin was found in your AxonDeepSeg installation folder. " "You will need to replace the current FSLeyes plugin which the new one. " diff --git a/docs/source/documentation.rst b/docs/source/documentation.rst index b7825537..08686ae5 100644 --- a/docs/source/documentation.rst +++ b/docs/source/documentation.rst @@ -13,7 +13,7 @@ The following sections will help you install all the tools you need to run AxonD Miniconda --------- -Starting with version 3.2.0, AxonDeepSeg is only supported using Python 3.7.x. Although your system may already have a Python environment installed, we strongly recommend that AxonDeepSeg be used with `Miniconda `_, which is a lightweight version of the `Anaconda distribution `_. Miniconda is typically used to create virtual Python environments, which provides a separation of installation dependencies between different Python projects. Although it can be possible to install AxonDeepSeg without Miniconda or virtual environments, we will only provide instructions for this recommended installation setup. +Starting with version 4.4.0, AxonDeepSeg is only supported using Python 3.8.x. Although your system may already have a Python environment installed, we strongly recommend that AxonDeepSeg be used with `Miniconda `_, which is a lightweight version of the `Anaconda distribution `_. Miniconda is typically used to create virtual Python environments, which provides a separation of installation dependencies between different Python projects. Although it can be possible to install AxonDeepSeg without Miniconda or virtual environments, we will only provide instructions for this recommended installation setup. First, verify if you already have an AxonDeepSeg-compatible version of Miniconda or Anaconda properly installed and is in your systems path. @@ -21,7 +21,7 @@ In a new terminal window (macOS or Linux) or Anaconda Prompt (Windows – if it conda search python -If a list of available Python versions are displayed and versions >=3.7.0 are available, you may skip to the next section (git). +If a list of available Python versions are displayed and versions >=3.8.0 are available, you may skip to the next section (git). Linux ~~~~~ @@ -98,7 +98,7 @@ Once your virtual environment is installed and activated, install the AxonDeepSe git pull pip install -e . -.. WARNING :: When re-installing the application, the ``default_SEM_model``, ``default_TEM_model`` and ``model_seg_pns_bf`` folders in ``AxonDeepSeg/models`` will be deleted and re-downloaded. Please do not store valuable data in these folders. +.. WARNING :: When re-installing the application, the model folders in ``AxonDeepSeg/models`` will be deleted and re-downloaded. Please do not store valuable data in these folders. .. raw:: html @@ -121,8 +121,6 @@ This integrity test automatically performs the axon and myelin segmentation of a * * * Integrity test passed. AxonDeepSeg is correctly installed. * * * -.. NOTE :: For some users, the test may fail because Keras is using Theano backend instead of Tensorflow. In that case, you will see the line ``Using Theano backend.`` when launching ``axondeepseg_test``. To fix this issue, add the line ``export KERAS_BACKEND="tensorflow"`` at the end of the ``\envs\/etc/conda/activate.d/keras_activate.sh`` file, then deactivate and reactivate your environment. The test should print ``Using Tensorflow backend.`` and pass. - Comprehensive test ~~~~~~~~~~~~~~~~~~ @@ -134,7 +132,7 @@ To run the entire testing suite (more code coverage), go to your AxonDeepSeg pro If all tests pass, AxonDeepSeg was installed succesfully. -Graphical User Interface (GUI) (optional) +Graphical User Interface (GUI) ----------------------------------------- AxonDeepSeg can be run via a Graphical User Interface (GUI) instead of the Terminal command line. This GUI is a plugin for the software `FSLeyes `_. Beyond the convenience of running AxonDeepSeg with the click of a button, this GUI is also an excellent way to manually correct output segmentations (if need to). @@ -149,6 +147,7 @@ In FSLeyes, do the following: - Click on ``file -> load plugin`` - Select ``ads_plugin.py`` (found in AxonDeepSeg folder) - When asked ``Install permanently`` click on ``yes``. +- Close FSLeyes and re-open it. From now on, you can access the plugin on the FSLeyes interface by selecting ``Settings -> Ortho View -> ADScontrol``. @@ -169,23 +168,6 @@ Known issues 1. The FSLeyes installation doesn't always work on Linux. Refer to the `FSLeyes installation guide `_ if you need. In our testing, most issues came from the installation of the wxPython package. -GPU-compatible installation ---------------------------- -.. NOTE :: This feature is not available if you are using a macOS. - -Linux and Windows 10 -~~~~~~~~~~~~~~~~~~~~ - -By default, AxonDeepSeg installs the CPU version of TensorFlow. To train a model using your GPU, you need to uninstall the TensorFlow from your virtual environment, and install the GPU version of it:: - - conda uninstall tensorflow - conda install -c anaconda tensorflow-gpu==1.13.1 - -This might uninstall keras in the process, so we need to install it again :: - - conda install -c conda-forge keras==2.2.4 - - Existing models =============== @@ -194,7 +176,7 @@ The three models are described below: * A SEM model, that works at a resolution of 0.1 micrometer per pixel. * A TEM model, that works at a resolution of 0.01 micrometer per pixel. -* A OM model, that works at a resolution of 0.1 micrometer per pixel. +* A BF (bright-field) model, that works at a resolution of 0.1 micrometer per pixel. Using AxonDeepSeg ================= @@ -231,17 +213,14 @@ The script to launch is called **axondeepseg**. It takes several arguments: Type of acquisition to segment. SEM: scanning electron microscopy samples. TEM: transmission electron microscopy samples. - OM: bright field optical microscopy samples. + BF: bright field optical microscopy samples. -i IMGPATH Path to the image to segment or path to the folder where the image(s) to segment is/are located. **Optional arguments:** --m MODEL Folder where the model is located. - The default SEM model path is **default_SEM_model**. - The default TEM model path is **default_TEM_model**. - The default OM model path is **model_seg_pns_bf**. +-m MODEL Folder where the model is located, if different from the default model. -s SIZEPIXEL Pixel size of the image(s) to segment, in micrometers. If no pixel size is specified, a **pixel_size_in_micrometer.txt** file needs to be added to the image folder path ( that file should contain a single float number corresponding to the resolution of the image, i.e. the pixel size). The pixel size in that file will be used for the segmentation. @@ -253,7 +232,7 @@ The script to launch is called **axondeepseg**. It takes several arguments: **3**: Also displays the patch number being processed in the current sample. --overlap Overlap value (in pixels) of the patches when doing the segmentation. - Higher values of overlap can improve the segmentation at patch borders, but also increase the segmentation time. Default value: 25. Recommended range of values: [10-100]. + Higher values of overlap can improve the segmentation at patch borders, but also increase the segmentation time. Default value: 48. Recommended range of values: [10-100]. .. NOTE :: You can get the detailed description of all the arguments of the **axondeepseg** command at any time by using the **-h** argument: :: @@ -433,7 +412,7 @@ Circle ^^^^^^ **Usage** :: - axondeepseg -i test_segmentation/test_sem_image/image1_sem/77.png -a circle + axondeepseg_morphometrics -i test_segmentation/test_sem_image/image1_sem/77.png -a circle **Studies using Circle as axon shape:** @@ -444,7 +423,7 @@ Ellipse ^^^^^^^ **Usage** :: - axondeepseg -i test_segmentation/test_sem_image/image1_sem/77.png -a ellipse + axondeepseg_morphometrics -i test_segmentation/test_sem_image/image1_sem/77.png -a ellipse **Studies using Ellipse as axon shape:** @@ -503,89 +482,28 @@ Jupyter notebooks Here is a list of useful Jupyter notebooks available with AxonDeepSeg: -* `getting_started.ipynb `_: +* `00-getting_started.ipynb `_: Notebook that shows how to perform axon and myelin segmentation of a given sample using a Jupyter notebook (i.e. not using the command line tool of AxonDeepSeg). You can also launch this specific notebook without installing and/or cloning the repository by using the `Binder link `_. -* `guide_dataset_building.ipynb `_: - Notebook that shows how to prepare a dataset for training. It automatically divides the dataset samples and corresponding label masks in patches of same size. - -* `training_guideline.ipynb `_: - Notebook that shows how to train a new model on AxonDeepSeg. It also defines the main parameters that are needed in order to build the neural network. - -* `performance_metrics.ipynb `_: +* `01-performance_metrics.ipynb `_: Notebook that computes a large set of segmentation metrics to assess the axon and myelin segmentation quality of a given sample (compared against a ground truth mask). Metrics include sensitivity, specificity, precision, accuracy, Dice, Jaccard, F1 score, Hausdorff distance. -* `morphometrics_extraction.ipynb `_: +* `02-morphometrics_extraction.ipynb `_: Notebook that shows how to extract morphometrics from a sample segmented with AxonDeepSeg. The user can extract and save morphometrics for each axon (diameter, solidity, ellipticity, centroid, ...), estimate aggregate morphometrics of the sample from the axon/myelin segmentation (g-ratio, AVF, MVF, myelin thickness, axon density, ...), and generate overlays of axon/myelin segmentation masks, colocoded for axon diameter. .. NOTE :: - If it is the first time, install the Jupyter notebook package in the terminal:: - - pip install jupyter - - Then, go to the notebooks/ subfolder of AxonDeepSeg and launch a particular notebook as follows:: - - cd notebooks - jupyter notebook name_of_the_notebook.ipynb - + To open a notebook, go to the notebooks/ subfolder of AxonDeepSeg and launch a particular notebook as follows:: + + cd notebooks + jupyter notebook name_of_the_notebook.ipynb .. WARNING :: - The current models available for segmentation are trained for patches of 512x512 pixels. This means that your input image(s) should be at least 512x512 pixels in size **after the resampling to the target pixel size of the model you are using to segment**. + The current models available for segmentation are trained for patches of 256x256 pixels for SEM and 512x512 pixels for TEM and BF. This means that your input image(s) should be at least 256x256 or 512x512 pixels in size **after the resampling to the target pixel size of the model you are using to segment**. For instance, the TEM model currently available has a target resolution of 0.01 micrometers per pixel, which means that the minimum size of the input image (in micrometers) is 5.12x5.12. **Option:** If your image to segment is too small, you can use padding to artificially increase its size (i.e. add empty pixels around the borders). -Guide for manual labelling -========================== - -Manual masks for training your own model ----------------------------------------- - -To be able to train your own model, you will need to manually segment a set of masks. The deep learning model quality will only be as good as your manual masks, so it's important to take care at this step and define your cases. - -Technical properties of the manual masks: - -* They should be 8-bit PNG files with 1 channel (256 grayscale). -* They should be the same height and width as the images. -* They should contain only 3 unique color values : 0 (black) for background, 127 (gray) for myelin and 255 (white) for axons, and no other intermediate values on strutures edges. -* If you are unfamiliar with those properties, don't worry, the detailed procedures provided in the section below will allow you to follow these guidelines. - -Qualitative properties of the manual masks: - -* Make sure that every structure (background, myelin or axons) contains only the color of that specific structure (e.g., no black pixels (background) in the axons or the myelin, no white pixels (axons) in the background or myelin, etc.) -* For normal samples without myelin splitting away from the axons, make sure that there is no black pixels (background) on the edges between myelin and axons. - -To create a manual mask for training, you can try one of the following: - -* Try segmenting your images with AxonDeepSeg's default models and make manual corrections of the segmentation masks in FSLeyes or GIMP software. -* Create a new manual mask using GIMP software. - -These options and detailed procedures are described in the section below "Manual correction of segmentation masks". - -Here are examples of an image, a good manual mask and a bad manual mask. - -.. figure:: https://raw.githubusercontent.com/axondeepseg/doc-figures/main/introduction/image_example.png - :width: 750px - :align: center - :alt: Image example - - Image example - -.. figure:: https://raw.githubusercontent.com/axondeepseg/doc-figures/main/introduction/good_mask_example.png - :width: 750px - :align: center - :alt: Good manual mask example - - Good manual mask example - -.. figure:: https://raw.githubusercontent.com/axondeepseg/doc-figures/main/introduction/bad_mask_example.png - :width: 750px - :align: center - :alt: Bad manual mask example - - Bad manual mask example - Manual correction of segmentation masks --------------------------------------- diff --git a/environment.yml b/environment.yml index 1e681ae7..b69a2434 100644 --- a/environment.yml +++ b/environment.yml @@ -2,17 +2,16 @@ name: ads_venv channels: - conda-forge dependencies: - - python=3.7 - - fsleyes=0.33.1 + - python=3.8 + - fsleyes - h5py=2.10.0 - numpy - scipy - - scikit-learn=0.19.2 - - scikit-image=0.14.2 + - scikit-learn=1.0.1 + - scikit-image=0.18.3 - tabulate - pandas - matplotlib=3.3.4 - - tensorflow=1.13.1 - mpld3 - tqdm - requests @@ -23,12 +22,14 @@ dependencies: - prettytable - raven - jupyter - - Keras - - Keras-Applications - - Keras-Preprocessing - - albumentations=0.3.0 - openpyxl - pip - pip: - opencv-contrib-python - opencv-python-headless + - --find-links https://download.pytorch.org/whl/torch_stable.html + - torch==1.8.0+cpu; sys_platform != "darwin" + - torchvision==0.9.0+cpu; sys_platform != "darwin" + - torch==1.8.0;sys_platform == "darwin" + - torchvision==0.9.0; sys_platform == "darwin" + - git+https://github.com/ivadomed/ivadomed.git diff --git a/notebooks/00-getting_started.ipynb b/notebooks/00-getting_started.ipynb index 805303a2..b3fa87fd 100644 --- a/notebooks/00-getting_started.ipynb +++ b/notebooks/00-getting_started.ipynb @@ -53,6 +53,7 @@ "metadata": {}, "outputs": [], "source": [ + "from pathlib import Path\n", "import json\n", "import os\n", "import matplotlib.pyplot as plt\n", @@ -89,7 +90,8 @@ "metadata": {}, "outputs": [], "source": [ - "path_testing = os.path.join('..','AxonDeepSeg','models','default_SEM_model','data_test')" + "ads_path = Path(os.path.abspath('')).resolve().parent\n", + "path_testing = Path(os.path.join(ads_path,'AxonDeepSeg','models','model_seg_rat_axon-myelin_sem','data_test'))" ] }, { @@ -98,7 +100,7 @@ "source": [ "**1.2. Select the trained model you want to use for the segmentation.**\n", "\n", - "Here, we specify the deep learning model we want to use in order to segment our sample. We currently propose 3 models: one for scanning electron microscopy (SEM) samples, a second for transmission electron microscopy (TEM) samples, and a third for bright field optical microscopy (OM) samples. The current versions of the models are **'default_SEM_model'**, **'default_TEM_model'**, and **'OM_model' (model_seg_pns_bf)**, respectively. In this case, our test sample is a SEM spinal cord sample of the rat, so we select the SEM model available." + "Here, we specify the deep learning model we want to use in order to segment our sample. We currently propose 3 models: one for scanning electron microscopy (SEM) samples, a second for transmission electron microscopy (TEM) samples, and a third for bright field optical microscopy (BF) samples. The current versions of the models are **'model_seg_rat_axon-myelin_sem'**, **'model_seg_mouse_axon-myelin_tem\"'**, and **'model_seg_rat_axon-myelin_bf'**, respectively. In this case, our test sample is a SEM spinal cord sample of the rat, so we select the SEM model available." ] }, { @@ -107,7 +109,7 @@ "metadata": {}, "outputs": [], "source": [ - "model_name = 'default_SEM_model'" + "model_name = 'model_seg_rat_axon-myelin_sem'" ] }, { @@ -125,31 +127,7 @@ "metadata": {}, "outputs": [], "source": [ - "path_model = os.path.join('..','AxonDeepSeg','models',model_name)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**1.4. Read the parameters of the selected model.**\n", - "\n", - "In *AxonDeepSeg*, each trained model has a corresponding *json* file named **'config_network.json'**. This file documents all the parameters of the model, such as the number of convolutional layers, the number of features per convolutional layer, the dropout and the number of classes. The json file is a good way to keep track of the similarities and differences between the trained models available." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "path_configfile = os.path.join(path_model,'config_network.json')\n", - "\n", - "if not os.path.exists(path_model):\n", - " os.makedirs(path_model)\n", - "\n", - "with open(path_configfile, 'r') as fd:\n", - " config_network = json.loads(fd.read())" + "path_model = Path(os.path.join(ads_path,'AxonDeepSeg','models',model_name))" ] }, { @@ -165,7 +143,7 @@ "source": [ "**2.1. Import the function that performs the segmentation from AxonDeepSeg.**\n", "\n", - "The function *axon_segmentation* in the **'apply_model.py'** script computes the axon/myelin segmentation of a sample." + "The function *segment_image* in the **'segment.py'** script computes the axon/myelin segmentation of a sample." ] }, { @@ -174,7 +152,7 @@ "metadata": {}, "outputs": [], "source": [ - "from AxonDeepSeg.apply_model import axon_segmentation" + "from AxonDeepSeg.segment import segment_image" ] }, { @@ -183,7 +161,7 @@ "source": [ "**2.2. Launch the segmentation of the image.**\n", "\n", - "Here, we launch the segmentation. Here, we specify the following inputs in the *axon_segmentation* function: (i) the path of the image, (ii) the name of the image, (iii) the path of the model, (iv) the configuration json file of the model (v) the target resolution (resampled_resolutions) and (vi) the verbosity level. The target resolution of the current version of the models are 0.1 for the **'default_SEM_model'** and **'OM_model' (model_seg_pns_bf)**, and 0.01 for the **'default_TEM_model'**. In this case, our test sample is a SEM spinal cord sample of the rat, so we set resampled_resolutions to 0.1. Note that more parameters are available (see description of the *axon_segmentation* function in the code repository).\n", + "Here, we launch the segmentation. Here, we specify the following inputs in the *segment_image* function: (i) the path of the image, (ii) the path of the model, (iii) the overlap value between patches and (iv) the resolution of the image (pixel size in micrometers).\n", "\n", "The output here will be the predicted image, which consists of a 3-label mask (background=0, myelin=127, axon=255). By default, the output prediction will be saved in the same directory as the input image, and named **'image_seg-axonmyelin.png'**." ] @@ -194,7 +172,12 @@ "metadata": {}, "outputs": [], "source": [ - "prediction = axon_segmentation([path_testing], [\"image.png\"], path_model, config_network, resampled_resolutions=0.1, verbosity_level=3)" + "segment_image(\n", + " path_testing_image=path_testing / 'image.png',\n", + " path_model=path_model,\n", + " overlap_value=[48, 48],\n", + " acquired_resolution=0.13\n", + ")" ] }, { @@ -365,7 +348,7 @@ "metadata": { "anaconda-cloud": {}, "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -379,7 +362,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.10" + "version": "3.8.12" } }, "nbformat": 4, diff --git a/notebooks/01-guide_dataset_building.ipynb b/notebooks/01-guide_dataset_building.ipynb deleted file mode 100644 index e1ece925..00000000 --- a/notebooks/01-guide_dataset_building.ipynb +++ /dev/null @@ -1,363 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Dataset creation notebook\n", - "\n", - "This notebook shows how to build a dataset for the training of a new model in AxonDeepSeg. It covers the following steps:\n", - "\n", - "* How to structure the raw data.\n", - "* How to define the parameters of the patch extraction and divide the raw labelled dataset into patches.\n", - "* How to generate the training dataset of patches by combining all raw data patches.\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### STEP 0: IMPORTS." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from AxonDeepSeg.data_management.dataset_building import split_data, raw_img_to_patches, patched_to_dataset\n", - "from AxonDeepSeg.ads_utils import download_data\n", - "import os, shutil\n", - "from pathlib import Path" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### STEP 1: GENERATE THE DATASET.\n", - "\n", - "### Suggested procedure for training/validation split of the dataset:\n", - "\n", - "* **Example use case:** we have 6 labelled samples in our dataset. To respect the split convention (between 10-30% of samples kept for validation), we can keep 5 samples for the training and the remaining one for the validation. \n", - "\n", - "---\n", - "##### The folder structure *before* the training/validation split:\n", - "\n", - "* ***folder_of_your_raw_data***\n", - "\n", - " * **sample1**\n", - " * *image.png*\n", - " * *mask.png*\n", - " * *pixel_size_in_micrometer.txt*\n", - " * **sample2**\n", - " * *image.png*\n", - " * *mask.png*\n", - " * *pixel_size_in_micrometer.txt*\n", - " \n", - " ...\n", - " \n", - " * **sample6**\n", - " * *image.png*\n", - " * *mask.png*\n", - " * *pixel_size_in_micrometer.txt*\n", - " \n", - "---\n", - "#### The folder structure *after* the training/validation split:\n", - "\n", - "* ***folder_of_your_raw_data***\n", - "\n", - " * **Train**\n", - " * **sample1**\n", - " * *image.png*\n", - " * *mask.png*\n", - " * *pixel_size_in_micrometer.txt*\n", - " * **sample2**\n", - " * *image.png*\n", - " * *mask.png*\n", - " * *pixel_size_in_micrometer.txt*\n", - " \n", - " ...\n", - " \n", - " * **sample5**\n", - " * *image.png*\n", - " * *mask.png*\n", - " * *pixel_size_in_micrometer.txt*\n", - " \n", - " * **Validation**\n", - " * **sample6**\n", - " * *image.png*\n", - " * *mask.png*\n", - " * *pixel_size_in_micrometer.txt*\n", - "--- " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "For this notebook, we'll download a sample dataset hosted on OSF.io that's in the correct raw data folder structure , and use our AxonDeepSeg tools to split the dataset. In the next sections, this notebook will resample the images into patches and group them together in the correct directory structure needed to run the [training guideline notebook](training_guideline.ipynb)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# This cell downloads the raw data\n", - "\n", - "# Sample SEM dataset: https://osf.io/vrdpe/?action=download\n", - "# Sample TEM dataset: https://osf.io/uewd9/?action=download\n", - "url_example_data = \"https://osf.io/vrdpe/?action=download\"\n", - "\n", - "# Sample SEM zipped file: SEM_dataset\n", - "# Sample TEM zipped file: TEM_dataset\n", - "downloaded_data = Path(\"./SEM_dataset\")\n", - "\n", - "if not download_data(url_example_data)==0:\n", - " print('ERROR: Data was not succesfully downloaded and unzipped - please check your link and filename and try again.')\n", - "else:\n", - " print('Data downloaded and unzipped succesfully.')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Define the directory names for the dataset building folders\n", - "\n", - "data_split_path = Path(\"./split\")\n", - "data_patched_path = Path(\"./patched\")\n", - "data_training_path = Path(\"./training\")\n", - "\n", - "# If dataset building folders already exist, remove them.\n", - "if data_split_path.exists():\n", - " shutil.rmtree(data_split_path)\n", - "if data_patched_path.exists():\n", - " shutil.rmtree(data_patched_path)\n", - "if data_training_path.exists():\n", - " shutil.rmtree(data_training_path)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Set seed (changing value will change how the dataset is split)\n", - "seed = 2019\n", - "\n", - "# Set dataset split fraction [Train, Validation]\n", - "split = [0.8, 0.2]\n", - "\n", - "# Split data into training and validation datasets \n", - "split_data(downloaded_data, data_split_path, seed=seed, split=split)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### 1.1. Define the parameters of the patch extraction.\n", - "\n", - "* **path_raw_data**: Path of the folder that contains the raw data. Each labelled sample of the dataset should be in a different subfolder. For each sample (and subfolder), the expected files are the following:\n", - " * *\"image.png\"*: The microscopy sample image (uint8 format).\n", - " * *\"mask.png\"*: The microscopy sample image (uint8 format).\n", - " * *\"pixel_size_in_micrometer.txt\"*: A one-line text file with the value of the pixel size of the sample. For instance, if the pixel size of the sample is 0.02um, the value in the text file should be **\"0.02\"**.\n", - " \n", - "* **path_patched_data**: Path of the folder that will contain the raw data divided into patches. Each sample (i.e. subfolder) of the raw dataset will be divided into patches and saved in this folder. For instance, if a sample of the original data is divided into 10 patches, the corresponding folder in the **path_patched_dataset** will contain 10 image and mask patches, named **image_0.png** to **image_9.png** and **mask_0.png** to **mask_9.png**, respectively. \n", - "\n", - "* **patch_size**: The size of the patches in pixels. For instance, a patch size of **128** means that each generated patch will be 128x128 pixels.\n", - "\n", - "* **general_pixel_size**: The pixel size (i.e. resolution) of the generated patches in micrometers. The pixel size will be the same for all generated patches. If the selected pixel size is different from the native pixel sizes of the samples, downsampling or upsampling will be performed. Note that the pixel size should be chosen by taking into account the modality of the dataset and the patch size. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Define the paths for the training samples\n", - "path_raw_data_train = data_split_path / 'Train'\n", - "path_patched_data_train = data_patched_path / 'Train'\n", - "\n", - "# Define the paths for the validation samples\n", - "path_raw_data_validation = data_split_path / 'Validation'\n", - "path_patched_data_validation = data_patched_path / 'Validation'\n", - "\n", - "patch_size = 512\n", - "general_pixel_size = 0.1" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### 1.2. Divide the training/validation samples into patches.\n", - "\n", - "In the **path_patched_data** folder defined above, the original samples are going to be split into patches of same size. For instance, the sample 1 of the training set of the example use case above will be split into *n* patches and its corresponding subfolder in the **path_patched_data** folder will have the following structure:\n", - "\n", - "---\n", - "* ***folder_of_your_patched_data***\n", - "\n", - " * **Train**\n", - " * **sample1**\n", - " * *image_0.png*\n", - " * *mask_0.png*\n", - " * *image_1.png* \n", - " * *mask_1.png*\n", - " * *image_2.png*\n", - " * *mask_2.png*\n", - " \n", - " ...\n", - " \n", - " * *image_n.png* \n", - " * *mask_n.png* \n", - "---\n", - "\n", - "\n", - "* Run the *raw_img_to_patches* function on both *Train* and *Validation* subfolders to split the data into patches. Note the input param. **thresh_indices** is a list of the threshold values to use in order to generate the classes of the training masks. The default value is [0, 0.2, 0.8], meaning that the mask labels (background=0, myelin=0.5, axon=1) will be split into our 3 classes." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Split the *Train* dataset into patches\n", - "raw_img_to_patches(path_raw_data_train, path_patched_data_train, thresh_indices = [0, 0.2, 0.8], patch_size=patch_size, resampling_resolution=general_pixel_size)\n", - "\n", - "# Split the *Validation* dataset into patches\n", - "raw_img_to_patches(path_raw_data_validation, path_patched_data_validation, thresh_indices = [0, 0.2, 0.8], patch_size=patch_size, resampling_resolution=general_pixel_size)\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### 1.3. Regroup all the divided patches in the same training/validation folder.\n", - "\n", - "Finally, to build the dataset folder that is going to be used for the training, all patches obtained from the different samples are regrouped into the same folder and renamed. The final training and validation folders will have the following structure (*m* is the total number of training patches and *p* is the total number of validation patches):\n", - "\n", - "---\n", - "* ***folder_of_your_final_patched_data***\n", - "\n", - " * **Train**\n", - " * *image_0.png*\n", - " * *mask_0.png*\n", - " * *image_1.png* \n", - " * *mask_1.png*\n", - " * *image_2.png*\n", - " * *mask_2.png*\n", - " \n", - " ...\n", - " \n", - " * *image_m.png* \n", - " * *mask_m.png* \n", - " \n", - " * **Validation**\n", - " * *image_0.png*\n", - " * *mask_0.png*\n", - " * *image_1.png* \n", - " * *mask_1.png*\n", - " * *image_2.png*\n", - " * *mask_2.png*\n", - " \n", - " ...\n", - " \n", - " * *image_p.png* \n", - " * *mask_p.png* \n", - " \n", - "---\n", - "\n", - "Note that we define a random seed in the input of the *patched_to_dataset* function in order to reproduce the exact same images each time we run the function. This is done to enable the generation of the same training and validation sets (for reproducibility). Also note that the **type_** input argument of the function can be set to **\"unique\"** or **\"mixed\"** to specify if the generated dataset comes from the same modality, or contains more than one modality." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Path of the final training dataset\n", - "path_final_dataset_train = data_training_path / 'Train'\n", - "\n", - "# Path of the final validation dataset\n", - "path_final_dataset_validation = data_training_path / 'Validation'" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Regroup all training patches\n", - "patched_to_dataset(path_patched_data_train, path_final_dataset_train, type_='unique', random_seed=2017)\n", - "\n", - "# Regroup all validation patches\n", - "patched_to_dataset(path_patched_data_validation, path_final_dataset_validation, type_='unique', random_seed=2017)\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Remove intermediate dataset building folders\n", - "if downloaded_data.exists():\n", - " shutil.rmtree(downloaded_data)\n", - "if data_split_path.exists():\n", - " shutil.rmtree(data_split_path)\n", - "if data_patched_path.exists():\n", - " shutil.rmtree(data_patched_path)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Next step\n", - "\n", - "Now that you've resampled, patched, and organized your data correctly, the next step is to run the [training guideline notebook](training_guideline.ipynb)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.10" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/notebooks/03-performance_metrics.ipynb b/notebooks/01-performance_metrics.ipynb similarity index 91% rename from notebooks/03-performance_metrics.ipynb rename to notebooks/01-performance_metrics.ipynb index 978a89e5..77203e55 100644 --- a/notebooks/03-performance_metrics.ipynb +++ b/notebooks/01-performance_metrics.ipynb @@ -20,6 +20,7 @@ "metadata": {}, "outputs": [], "source": [ + "from pathlib import Path\n", "import os\n", "import json\n", "from shutil import copy\n", @@ -51,7 +52,8 @@ "metadata": {}, "outputs": [], "source": [ - "path_img = '../AxonDeepSeg/models/default_SEM_model/data_test/image.png'" + "ads_path = Path(os.path.abspath('')).resolve().parent\n", + "path_img = Path(os.path.join(ads_path,'AxonDeepSeg','models','model_seg_rat_axon-myelin_sem','data_test')) / 'image.png'" ] }, { @@ -63,11 +65,8 @@ "# Set paths\n", "img = ads.imread(path_img)\n", "path_folder, file_name = os.path.split(path_img)\n", - "model_name = 'default_SEM_model'\n", - "path_model = os.path.join('..','AxonDeepSeg','models',model_name)\n", - "path_configfile = os.path.join(path_model,'config_network.json')\n", - "with open(path_configfile, 'r') as fd:\n", - " config_network = json.loads(fd.read())\n", + "model_name = 'model_seg_rat_axon-myelin_sem'\n", + "path_model = Path(os.path.join(ads_path,'AxonDeepSeg','models',model_name))\n", "\n", "# Groundtruth image\n", "mask = ads.imread(os.path.join(path_folder,'mask.png'))\n", @@ -218,26 +217,12 @@ "# A way to remove the y labels\n", "ax.set_yticklabels([]);\n" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { "anaconda-cloud": {}, "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -251,7 +236,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.10" + "version": "3.8.10" } }, "nbformat": 4, diff --git a/notebooks/04-morphometrics_extraction.ipynb b/notebooks/02-morphometrics_extraction.ipynb similarity index 93% rename from notebooks/04-morphometrics_extraction.ipynb rename to notebooks/02-morphometrics_extraction.ipynb index d2f74e35..5cd9f763 100644 --- a/notebooks/04-morphometrics_extraction.ipynb +++ b/notebooks/02-morphometrics_extraction.ipynb @@ -22,13 +22,12 @@ "metadata": {}, "outputs": [], "source": [ + "from pathlib import Path\n", "import numpy as np\n", "import math\n", "import os\n", "import matplotlib.pyplot as plt\n", "\n", - - "from AxonDeepSeg.morphometrics.compute_morphometrics import (\n", " get_axon_morphometrics,\n", " save_axon_morphometrics,\n", @@ -38,7 +37,6 @@ " write_aggregate_morphometrics\n", " )\n", " \n", - "import AxonDeepSeg.ads_utils as ads\n", "from config import axonmyelin_suffix\n", "\n", @@ -59,8 +57,9 @@ "outputs": [], "source": [ "# Change the image and segmentation paths here for your sample:\n", - "path_img = '../AxonDeepSeg/models/default_SEM_model/data_test/image.png'\n", - "path_pred = '../AxonDeepSeg/models/default_SEM_model/data_test/image' + str(axonmyelin_suffix)" + "ads_path = Path(os.path.abspath('')).resolve().parent\n", + "path_img = Path(os.path.join(ads_path,'AxonDeepSeg','models','model_seg_rat_axon-myelin_sem','data_test')) / 'image.png'\n", + "path_pred = Path(os.path.join(ads_path,'AxonDeepSeg','models','model_seg_rat_axon-myelin_sem','data_test')) / os.path.join('image' + str(axonmyelin_suffix))" ] }, { @@ -150,9 +149,7 @@ "metadata": {}, "outputs": [], "source": [ - "# Plot histogram of axon diameter distribution \n", - "plt.figure(figsize=(8,6))\n", "n, bins, patches = plt.hist(axon_diam_list,30, facecolor='g', alpha=0.7)\n", "plt.xlabel('Axon diameter in um',fontsize=10)\n", @@ -209,9 +206,7 @@ "metadata": {}, "outputs": [], "source": [ - - "fig = draw_axon_diameter(img,path_pred,pred_axon,pred_myelin, axon_shape=shape)\n", - + "fig = draw_axon_diameter(img,path_pred,pred_axon,pred_myelin, axon_shape=axon_shape)\n", "from IPython.core.display import display\n", "fig.canvas.draw()\n", "display(fig)" @@ -231,7 +226,7 @@ "outputs": [], "source": [ "# Compute aggregate metrics\n", - "aggregate_metrics = get_aggregate_morphometrics(pred_axon,pred_myelin,path_folder, shape=shape)\n", + "aggregate_metrics = get_aggregate_morphometrics(pred_axon,pred_myelin,path_folder, axon_shape=axon_shape)\n", "print(aggregate_metrics)" ] }, @@ -339,12 +334,19 @@ "max_diam = np.max(axon_diam_list)\n", "print(max_diam)" ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { "anaconda-cloud": {}, "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -358,9 +360,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.10" + "version": "3.8.10" } }, "nbformat": 4, "nbformat_minor": 2 -} \ No newline at end of file +} diff --git a/notebooks/02-training_guideline.ipynb b/notebooks/02-training_guideline.ipynb deleted file mode 100644 index 688d0d45..00000000 --- a/notebooks/02-training_guideline.ipynb +++ /dev/null @@ -1,406 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Train a new model\n", - "\n", - "In this notebook we will learn how to train a new model for axon & myelin segmentation. It covers the following scenario:\n", - "\n", - "* Train a model from scratch by defining the parameters of the network\n", - "* Make inference using the trained model\n", - "\n", - "**Important:** If you have access to a GPU card, we strongly recommend you use it. By default, AxonDeepSeg will only use the CPU. To install the GPU compatible version of AxonDeepSeg, refer to the documentation: https://axondeepseg.readthedocs.io/en/latest/documentation.html#gpu-compatible-installation" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import os\n", - "import json\n", - "import time\n", - "from pathlib import Path\n", - "import requests\n", - "from requests.adapters import HTTPAdapter\n", - "from requests.packages.urllib3.util import Retry\n", - "\n", - "# Scientific package imports\n", - "import imageio\n", - "import numpy as np\n", - "import tensorflow as tf\n", - "from skimage import io\n", - "import matplotlib.pyplot as plt\n", - "\n", - "# Utils import\n", - "from shutil import copy\n", - "import zipfile\n", - "from tqdm import tqdm\n", - "import cgi\n", - "import tempfile\n", - "\n", - "# AxonDeepSeg imports\n", - "try:\n", - " from AxonDeepSeg.ads_utils import download_data\n", - "except ModuleNotFoundError:\n", - " # Change cwd to project main folder \n", - " os.chdir(\"..\")\n", - " try :\n", - " from AxonDeepSeg.ads_utils import download_data\n", - " except:\n", - " raise\n", - "except:\n", - " raise\n", - "# If no exceptions were raised import all folders \n", - "from AxonDeepSeg.config_tools import validate_config\n", - "from AxonDeepSeg.train_network import train_model\n", - "from AxonDeepSeg.apply_model import axon_segmentation\n", - "import AxonDeepSeg.ads_utils as ads \n", - "from config import axonmyelin_suffix\n", - "\n", - "# reset the tensorflow graph for new training\n", - "tf.reset_default_graph()\n", - "\n", - "%matplotlib inline" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 0. Download example data" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Before running this notebook, please make sure you have split, patched, and organized your data in the correct way by running the [guide_dataset_building.ipynb](guide_dataset_building.ipynb) tutorial." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 1. Train a new model\n", - "#### 1.1. Define the name and path of the training set\n", - "\n", - "Here we assume that the training data folder has already been created by following the guidelines detailed in [guide_dataset_building.ipynb](https://github.com/neuropoly/axondeepseg/blob/master/notebooks/guide_dataset_building.ipynb).\n", - "\n", - "The expected structure of the training data folder is the following:\n", - "\n", - "~~~\n", - "data\n", - " └── Train\n", - " └── image_0.png\n", - " └── mask_0.png\n", - " └── image_1.png\n", - " └── mask_1.png\n", - " ...\n", - " └── Validation\n", - " └── image_0.png\n", - " └── mask_0.png\n", - " └── image_1.png\n", - " └── mask_1.png\n", - " ...\n", - "~~~" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "path_training = Path(\"./training\") # folder containing training data" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### 1.2. Define the U-Net architecture and hyper-parameters\n", - "\n", - "Here we defined the network and training parameters (i.e. hyperparameters). We use a lighter network than the ones used in the original [AxonDeepSeg article](https://www.nature.com/articles/s41598-018-22181-4), because they require large GPU memory (>12GB). The network below runs on an NVIDIA TitanX in ~2h. Note that the architecture below might not produce satisfactory results on your data so you likely need to play around with the architecture and hyperparameters.\n", - "\n", - "**Important:** The pixel size is not defined at the training step. During inference however, the parameter `-t {SEM,TEM,OM}` sets the resampling resolution to 0.1µm or 0.01µm depending on the model (i.e., implying the pixel size of the training data should be around 0.1µm for SEM and OM, and 0.01µm for TEM). This is definitely a limitation of the current version of AxonDeepSeg, which we are planning to solve at some point (for more info, see [Issue #152](https://github.com/neuropoly/axondeepseg/issues/152)). \n", - "\n", - "**Note about data augmentation:**\n", - "For each type of data augmentation, the order needs to be specified if you decide to apply more than one transformation sequentially. For instance, setting `da-0-shifting-activate` to `True` means that the shifting is the first transformation that will be applied to the sample(s) during training. The default ranges of transformations are:\n", - "- **Shifing**: Random horizontal and vertical shifting between 0 and 10% of the patch size, sampled from a uniform distribution.\n", - "- **Rotation**: Random rotation, angle between 5 and 89 degrees, sampled from a uniform distribution.\n", - "- **Rescaling**: Random rescaling of a randomly sampled factor between 1/1.2 and 1.2.\n", - "- **Flipping**: Random fipping: vertical fipping or horizontal fipping.\n", - "- **Elastic deformation**: Random elastic deformation with uniformly sampled deformation coefficient α=[1–8] and fixed standard deviation σ=4.\n", - "\n", - "You can find more information about the range of transformations applied to the patches for each data augmentation technique in the file [data_augmentation.py](https://github.com/neuropoly/axondeepseg/blob/master/AxonDeepSeg/data_management/data_augmentation.py)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Example of network configuration for TEM data (small network trainable on a Titan X GPU card)\n", - "config = {\n", - " \n", - "# General parameters: \n", - " \"n_classes\": 3, # Number of classes. For this application, the number of classes should be set to **3** (i.e. axon pixel, myelin pixel, or background pixel).\n", - " \"thresholds\": [0, 0.2, 0.8], # Thresholds for the 3-class classification problem. Do not modify. \n", - " \"trainingset_patchsize\": 512, # Patch size of the training set in pixels (note that the patches have the same size in both dimensions). \n", - " \"trainingset\": \"TEM\", # Name of the training set.\n", - " \"batch_size\": 8, # Batch size, i.e. the number of training patches used in one iteration of the training. Note that a larger batch size will take more memory.\n", - " \"epochs\":600,\n", - " \"checkpoint_period\": 5, # Number of epoch after which the model checkpoint is saved.\n", - " \"checkpoint\": None, # Checkpoint to use to resume training. Option: \"loss\", \"accuracy\" or None.\n", - "\n", - "# Network architecture parameters: \n", - " \"depth\": 4, # Depth of the network (i.e. number of blocks of the U-net).\n", - " \"convolution_per_layer\": [2, 2, 2, 2], # Number of convolution layers used at each block.\n", - " \"size_of_convolutions_per_layer\": [[5, 5], [3, 3], [3, 3], [3, 3]], # Kernel size of each convolution layer of the network.\n", - " \"features_per_convolution\": [[[1, 16], [16, 16]], [[16, 32], [32, 32]], [[32, 64], [64, 64]], [[64, 128], [128, 128]]], # Number of features of each convolution layer.\n", - " \"downsampling\": \"convolution\", # Type of downsampling to use in the downsampling layers of the network. Option \"maxpooling\" for standard max pooling layer or option \"convolution\" for learned convolutional downsampling.\n", - " \"dropout\": 0.75, # Dropout to use for the training. Note: In TensorFlow, the keep probability is used instead. For instance, setting this param. to 0.75 means that 75% of the neurons of the network will be kept (i.e. dropout of 25%).\n", - " \n", - "# Learning rate parameters: \n", - " \"learning_rate\": 0.01, # Learning rate to use in the training. \n", - " \"learning_rate_decay_activate\": True, # Set to \"True\" to use a decay on the learning rate. \n", - " \"learning_rate_decay_period\": 24000, # Period of the learning rate decay, expressed in number of images (samples) seen.\n", - " \"learning_rate_decay_type\": \"polynomial\", # Type of decay to use. An exponential decay will be used by default unless this param. is set to \"polynomial\" (to use a polynomial decay).\n", - " \"learning_rate_decay_rate\": 0.99, # Rate of the decay to use for the exponential decay. This only applies when the user does not set the decay type to \"polynomial\".\n", - " \n", - "# Batch normalization parameters: \n", - " \"batch_norm_activate\": True, # Set to \"True\" to use batch normalization during the training.\n", - " \"batch_norm_decay_decay_activate\": True, # Set to \"True\" to activate an exponential decay for the batch normalization step of the training. \n", - " \"batch_norm_decay_starting_decay\": 0.7, # The starting decay value for the batch normalization. \n", - " \"batch_norm_decay_ending_decay\": 0.9, # The ending decay value for the batch normalization.\n", - " \"batch_norm_decay_decay_period\": 16000, # Period of the batch normalization decay, expressed in number of images (samples) seen.\n", - " \n", - "# Weighted cost parameters: \n", - " \"weighted_cost-activate\": True, # Set to \"True\" to use weights based on the class in the cost function for the training.\n", - " \"weighted_cost-balanced_activate\": True, # Set to \"True\" to use weights in the cost function to correct class imbalance. \n", - " \"weighted_cost-balanced_weights\": [1.1, 1, 1.3], # Values of the weights for the class imbalance. Typically, larger weights are assigned to classes with less pixels to add more penalty in the cost function when there is a misclassification. Order of the classes in the weights list: background, myelin, axon.\n", - " \"weighted_cost-boundaries_sigma\": 2, # Set to \"True\" to add weights to the boundaries (e.g. penalize more when misclassification happens in the axon-myelin interface).\n", - " \"weighted_cost-boundaries_activate\": False, # Value to control the distribution of the boundary weights (if activated). \n", - " \n", - "# Data augmentation parameters:\n", - " \"da-type\": \"all\", # Type of data augmentation procedure. Option \"all\" applies all selected data augmentation transformations sequentially, while option \"random\" only applies one of the selected transformations (randomly) to the sample(s). List of available data augmentation transformations: 'random_rotation', 'noise_addition', 'elastic', 'shifting', 'rescaling' and 'flipping'. \n", - " \"da-0-shifting-activate\": True, \n", - " \"da-1-rescaling-activate\": False,\n", - " \"da-2-random_rotation-activate\": False, \n", - " \"da-3-elastic-activate\": True, \n", - " \"da-4-flipping-activate\": True, \n", - " \"da-6-reflection_border-activate\": False\n", - "}\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### 1.3. Define training path and save configuration parameters\n", - "\n", - "Here we define the path where the new model will be saved. It is useful to add date+time in path definition in case multiple training are launched (to avoid conflicts).\n", - "\n", - "The network configuration parameters defined at 1.2. are saved into a .json file in the model folder. This .json file keeps tract of the network and model parameters in a structured way." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Define path to where the trained model will be saved\n", - "dir_name = Path(config[\"trainingset\"] + '_' + time.strftime(\"%Y-%m-%d\") + '_' + time.strftime(\"%H-%M-%S\"))\n", - "path_model = \"../models\" / dir_name\n", - "\n", - "print(\"This training session's model will be saved in the folder: \" + str(path_model.resolve().absolute()))\n", - "\n", - "# Create directory\n", - "if not os.path.exists(path_model):\n", - " os.makedirs(path_model)\n", - "\n", - "# Define file name of network configuration\n", - "file_config = 'config_network.json'\n", - "\n", - "# Load/Write config file (depending if it already exists or not)\n", - "fname_config = os.path.join(path_model, file_config)\n", - "if os.path.exists(fname_config):\n", - " with open(fname_config, 'r') as fd:\n", - " config_network = json.loads(fd.read())\n", - "else:\n", - " with open(fname_config, 'w') as f:\n", - " json.dump(config, f, indent=2)\n", - " with open(fname_config, 'r') as fd:\n", - " config_network = json.loads(fd.read())" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### 1.4. Launch the training procedure\n", - "\n", - "The training can be launched by calling the *'train_model'* function. After each epoch, the function will display the loss and accuracy of the model. The model checkpoints will be saved according to the \"checkpoint_period\" parameter in \"config\"." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# reset the tensorflow graph for new testing\n", - "tf.reset_default_graph()\n", - "\n", - "train_model(str(path_training), str(path_model), config)\n", - "# Note: For multi-OS compatibility of this notebook, paths were defined as Path objects from the pathlib module.\n", - "# They need to be converted into strings prior to be given as arguments to train_model(), as some old-style string\n", - "# concatenation (e.g. \"+\") are still used in it.\n", - "# In your own application, simply defining paths with correct syntax for your OS as strings instead of Path\n", - "# objects would be sufficient." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### 1.5. Monitor the training with Tensorboard\n", - "\n", - "[TensorBoard](https://www.tensorflow.org/guide/summaries_and_tensorboard) can be used to monitor the training procedure (loss and accuracy graphs, gradients, activations, identify bugs, etc.). To run TensorBoard, activate ADS virtual environment and run:\n", - "```\n", - "tensorboard --logdir PATH_MODEL --port 6006\n", - "```\n", - "where `PATH_MODEL` corresponds to this notebook's `path_model` variable (folder where model is being trained), and `port` is the port number where the TensorBoard local web server will be sent to (e.g., port 6006). Once the command is run, open a web browser with the address:\n", - "```\n", - "http://localhost:6006/\n", - "```" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### 1.6. Resume training from checkpoint\n", - "\n", - "To resume training from a checkpoint, change the \"checkpoint\" parameter in \"config\" from None to \"loss\" or \"accuracy\"." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "path_model = \"../models/Path_of_the_model\" # Path of the model where the checkpoint is saved\n", - "\n", - "train_model(str(path_training), str(path_model), config)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 2. Test the trained model\n", - "#### 2.1. Set the path of the test image to be segmented with the trained model" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Modify the lines below to use your image\n", - "path_img = Path(\"../AxonDeepSeg/models/default_TEM_model/data_test\")\n", - "file_img = \"image.png\"" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### 2.2. Launch the image segmentation\n", - "\n", - "The target resolution of the current version of the models are 0.1 for the **'default_SEM_model'** and **'OM_model' (model_seg_pns_bf)**, and 0.01 for the **'default_TEM_model'**. In this case, our test sample is a TEM brain sample of the mouse, so we set resampled_resolutions to 0.01.\n", - "\n", - "For your own trained model, use a resampled_resolutions corresponding to the general_pixel_size that was set in the 01-guide_dataset_building notebook in section 1.1. \"Define the parameters of the patch extraction\" when you created the dataset." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# In case you want to test the segmentation with a pre-trained default model, uncomment the line below.\n", - "#path_model = Path(\"../AxonDeepSeg/models/default_TEM_model\")\n", - "\n", - "# reset the tensorflow graph for new testing\n", - "tf.reset_default_graph()\n", - "prediction = axon_segmentation(path_img, file_img, path_model, config_network, resampled_resolutions=0.01, verbosity_level=3)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### 2.3. Display the resulted segmentation" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "file_img_seg = Path(file_img).stem + str(axonmyelin_suffix) # axon+myelin segmentation\n", - "\n", - "img_seg = ads.imread(path_img / file_img_seg)\n", - "img = ads.imread(path_img / file_img)\n", - "# Note: The arguments of the two function calls above use the pathlib syntax for path concatenation.\n", - "\n", - "fig, axes = plt.subplots(1,2, figsize=(13,10))\n", - "ax1, ax2 = axes[0], axes[1]\n", - "ax1.set_title('Original image')\n", - "ax1.imshow(img, cmap='gray')\n", - "ax2.set_title('Prediction with the trained model')\n", - "ax2.imshow(img_seg,cmap='gray')\n", - "plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.10" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 00000000..a65c393b --- /dev/null +++ b/pytest.ini @@ -0,0 +1,5 @@ +[pytest] +markers = + unit + integration + exceptionhandling \ No newline at end of file diff --git a/setup.py b/setup.py index 6af81e25..efbbe359 100644 --- a/setup.py +++ b/setup.py @@ -26,7 +26,7 @@ def run(self): setup( name='AxonDeepSeg', - python_requires='>=3.7, <3.8', + python_requires='>=3.8, <3.9', version=AxonDeepSeg.__version__, description='Python tool for automatic axon and myelin segmentation', long_description=long_description, @@ -37,7 +37,7 @@ def run(self): classifiers=[ 'Development Status :: 3 - Alpha', 'License :: OSI Approved :: MIT License', - 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.8', ], keywords='', @@ -46,6 +46,7 @@ def run(self): package_data={ "AxonDeepSeg": ['models/default_SEM_model/*', 'models/default_TEM_model/*', + 'models/default_BF_model/*' 'data_test/*'], }, include_package_data=True, diff --git a/test/data_management/test_dataset_building.py b/test/data_management/test_dataset_building.py deleted file mode 100644 index 6e15470d..00000000 --- a/test/data_management/test_dataset_building.py +++ /dev/null @@ -1,235 +0,0 @@ -# coding: utf-8 - -import imageio -from pathlib import Path -import shutil -import pytest -from tqdm import tqdm -import numpy as np -from AxonDeepSeg.data_management.dataset_building import raw_img_to_patches, patched_to_dataset, split_data -from AxonDeepSeg.visualization.get_masks import get_image_unique_vals_properties -from AxonDeepSeg.ads_utils import download_data - -class TestCore(object): - def setup(self): - # Get the directory where this current file is saved - self.fullPath = Path(__file__).resolve().parent - # Move up to the test directory, "test/" - self.testPath = self.fullPath.parent - - _create_new_test_folder = ( - lambda s, t: self.testPath - / '__test_files__' - / s - / t - ) - - self.rawPath = _create_new_test_folder('__test_patch_files__', 'raw') - self.patchPath = _create_new_test_folder('__test_patch_files__', 'patched') - self.datasetPath = _create_new_test_folder('__test_patch_files__', 'dataset') - self.mixedPatchPath = _create_new_test_folder('__test_patch_files__', 'mixedPatched') - self.mixedDatasetPath = _create_new_test_folder('__test_patch_files__', 'mixedDataset') - - self.rawPath16b = _create_new_test_folder('__test_16b_file__', 'raw') - self.patchPath16b = _create_new_test_folder('__test_16b_file__', 'patched') - - self.downloaded_data = Path("./SEM_dataset") - self.data_split_path = Path("./SEM_split") - - @classmethod - def teardown_class(cls): - # Get the directory where this current file is saved - fullPath = Path(__file__).resolve().parent - # Move up to the test directory, "test/" - testPath = fullPath.parent - - _create_new_test_folder = ( - lambda s, t: testPath - / '__test_files__' - / s - / t - ) - - patchPath = _create_new_test_folder('__test_patch_files__', 'patched') - datasetPath = _create_new_test_folder('__test_patch_files__', 'dataset') - mixedPatchPath = _create_new_test_folder('__test_patch_files__', 'mixedPatched') - mixedDatasetPath = _create_new_test_folder('__test_patch_files__', 'mixedDataset') - - patchPath16b = _create_new_test_folder('__test_16b_file__', 'patched') - - downloaded_data = Path("./SEM_dataset") - data_split_path = Path("./SEM_split") - - if patchPath.is_dir(): - shutil.rmtree(patchPath) - - if datasetPath.is_dir(): - shutil.rmtree(datasetPath) - - if mixedPatchPath.is_dir(): - shutil.rmtree(mixedPatchPath) - - if mixedDatasetPath.is_dir(): - shutil.rmtree(mixedDatasetPath) - - if patchPath16b.is_dir(): - shutil.rmtree(patchPath16b) - - if downloaded_data.is_dir(): - shutil.rmtree(downloaded_data) - - if data_split_path.is_dir(): - shutil.rmtree(data_split_path) - - # --------------raw_img_to_patches tests-------------- # - @pytest.mark.unit - def test_raw_img_to_patches_creates_expected_folders_and_files(self): - if self.patchPath.is_dir(): - shutil.rmtree(self.patchPath) - - raw_img_to_patches(str(self.rawPath), str(self.patchPath)) - - assert self.patchPath.is_dir() - - # These demo image and mask are split into 6 patches each - path_to_data1 = self.patchPath / 'data1' - assert(path_to_data1.is_dir()) - assert len([item for item in path_to_data1.iterdir()]) == 12 - - # These demo image and mask are split into 12 patches each - path_to_data2 = self.patchPath / 'data2' - assert(path_to_data2.is_dir()) - assert len([item for item in path_to_data2.iterdir()]) == 24 - - @pytest.mark.unit - def test_raw_img_to_patches_doesnt_cutoff_16bit_files(self): - if self.patchPath16b.is_dir(): - shutil.rmtree(self.patchPath16b) - - raw_img_to_patches(str(self.rawPath16b), str(self.patchPath16b), patch_size=512, resampling_resolution=0.005) - - img_folder_names = [im.name for im in self.patchPath16b.iterdir()] - for img_folder in tqdm(img_folder_names): - path_img_folder = self.patchPath16b / img_folder - - if path_img_folder.is_dir(): - # We go through every file in the image folder - data_names = [d.name for d in path_img_folder.iterdir()] - for data in data_names: - # Skip the mask files - if 'mask' not in data: - print(data) - img = imageio.imread(path_img_folder / data) - img_bins = np.bincount(np.ndarray.flatten(img)) - - # Assert that not more than 50% of the pixels are the minimum value - assert img_bins[0]/sum(img_bins) < 0.5 - - # Assert that not more than 50% of the pixels are the maximum value - assert img_bins[-1]/sum(img_bins) < 0.5 - - @pytest.mark.unit - def test_raw_img_to_patches_creates_masks_with_expected_number_of_unique_values(self): - if self.patchPath.is_dir(): - shutil.rmtree(self.patchPath) - - raw_img_to_patches(str(self.rawPath), str(self.patchPath)) - - - img_folder_names = [im.name for im in self.patchPath.iterdir()] - for img_folder in tqdm(img_folder_names): - path_img_folder = self.patchPath / img_folder - if path_img_folder.is_dir(): - # We go through every file in the image folder - data_names = [d.name for d in path_img_folder.iterdir()] - for data in data_names: - - if 'mask' in data: - mask = imageio.imread(path_img_folder / data) - - image_properties = get_image_unique_vals_properties(mask) - - assert image_properties['num_uniques'] == 3 - assert np.array_equal(image_properties['unique_values'], [0, 128, 255]) - - # --------------patched_to_dataset tests-------------- # - @pytest.mark.unit - def test_patched_to_dataset_creates_expected_folders_and_files(self): - if self.datasetPath.is_dir(): - shutil.rmtree(self.datasetPath) - - patched_to_dataset(str(self.patchPath), str(self.datasetPath), 'unique') - - assert self.datasetPath.is_dir() - - # Dataset folder merges all the patch folders generated - assert len([item for item in self.datasetPath.iterdir()]) == 12+24 - - @pytest.mark.unit - def test_patched_to_dataset_fake_mixed_dataset_creates_expected_dir(self): - # TEM images are too large to be included in repo (6+ megs), so simply - # create fake duplicate dataset with SEM images. - if self.mixedDatasetPath.is_dir(): - shutil.rmtree(self.mixedDatasetPath) - - raw_img_to_patches(str(self.rawPath), str(self.mixedPatchPath / 'SEM')) - - raw_img_to_patches(str(self.rawPath), str(self.mixedPatchPath / 'TEM')) - - patched_to_dataset(str(self.mixedPatchPath), str(self.mixedDatasetPath), 'mixed') - - assert self.mixedDatasetPath.is_dir() - - # Dataset folder merges all the patch folders generated - assert len([item for item in self.mixedDatasetPath.iterdir()]) == (12+24)*2 - - @pytest.mark.unit - def test_split_data_outputs_expected_number_of_folders(self): - url_example_data = "https://osf.io/vrdpe/?action=download" # URL of example data hosted on OSF - file_data = "SEM_dataset.zip" - - if not download_data(url_example_data)==0: - print('ERROR: Data was not succesfully downloaded and unzipped - please check your link and filename and try again.') - else: - print('Data downloaded and unzipped succesfully.') - - split_data(self.downloaded_data, self.data_split_path, seed=2019, split = [0.8, 0.2]) - - train_dir = self.data_split_path / "Train" - valid_dir = self.data_split_path / "Validation" - - # get sorted list of train/validation directories - train_subdirs=sorted([x for x in train_dir.iterdir() if x.is_dir()]) - valid_subdirs=sorted([x for x in valid_dir.iterdir() if x.is_dir()]) - - assert len(train_subdirs)==7 - assert len(valid_subdirs)==2 - - @pytest.mark.unit - def test_split_data_throws_error_for_existing_folder(self): - url_example_data = "https://osf.io/vrdpe/?action=download" # URL of example data hosted on OSF - file_data = "SEM_dataset.zip" - - if not download_data(url_example_data)==0: - print('ERROR: Data was not succesfully downloaded and unzipped - please check your link and filename and try again.') - else: - print('Data downloaded and unzipped succesfully.') - - assert self.data_split_path.is_dir() - with pytest.raises(IOError): - split_data(self.downloaded_data, self.data_split_path, seed=2019, split = [0.8, 0.2]) - - @pytest.mark.unit - def test_split_data_works_with_override(self): - url_example_data = "https://osf.io/vrdpe/?action=download" # URL of example data hosted on OSF - file_data = "SEM_dataset.zip" - - if not download_data(url_example_data)==0: - print('ERROR: Data was not succesfully downloaded and unzipped - please check your link and filename and try again.') - else: - print('Data downloaded and unzipped succesfully.') - - assert self.data_split_path.is_dir() - split_data(self.downloaded_data, self.data_split_path, seed=2019, split = [0.8, 0.2], override=True) - - assert self.data_split_path.is_dir() diff --git a/test/data_management/test_patch_extraction.py b/test/data_management/test_patch_extraction.py deleted file mode 100644 index 18ed5b54..00000000 --- a/test/data_management/test_patch_extraction.py +++ /dev/null @@ -1,63 +0,0 @@ -# coding: utf-8 - -import pytest -import numpy as np -from imageio import imread -from AxonDeepSeg.data_management.patch_extraction import extract_patch - - -class TestCore(object): - def setup(self): - # Remember: the stop value in "arrange" is not included - x = np.arange(0, 16, dtype='uint8') - y = np.arange(0, 16, dtype='uint8') - xv, yv = np.meshgrid(x, y) - self.testImage = xv+yv - - self.mask = np.ones((16, 16, 3), dtype=int) - - def teardown(self): - pass - - # --------------extract_patch tests-------------- # - @pytest.mark.unit - def test_extract_patch_script_returns_expected_patches(self): - to_extract = [self.testImage, self.mask] - patch_size = 4 - - output = extract_patch(to_extract, patch_size) - - expectedTopLeftPatch = self.testImage[0:4, 0:4] - expectedBottomRightPatch = self.testImage[12:16, 12:16] - - assert np.array_equal(output[0][0], expectedTopLeftPatch) - assert np.array_equal(output[-1][0], expectedBottomRightPatch) - - # Current implementation of patch extration in ADS contains some - # overlap which is not specified/controllable, so other cases are - # difficult to test. When a controllable overlap is implemented, add - # more tests accordingly. - - @pytest.mark.unit - def test_extract_patch_script_errors_for_patch_size_smaller_than_3(self): - to_extract = [self.testImage, self.mask] - patch_size = 2 - - with pytest.raises(ValueError): - extract_patch(to_extract, patch_size) - - @pytest.mark.unit - def test_extract_patch_script_errors_for_patch_size_eq_to_image_dim(self): - to_extract = [self.testImage, self.mask] - patch_size = min(self.testImage.shape) - - with pytest.raises(ValueError): - extract_patch(to_extract, patch_size) - - @pytest.mark.unit - def test_extract_patch_script_errors_for_incorrect_first_arg_format(self): - to_extract = self.testImage - patch_size = 4 - - with pytest.raises(ValueError): - extract_patch(to_extract, patch_size) diff --git a/test/morphometrics/test_compute_morphometrics.py b/test/morphometrics/test_compute_morphometrics.py index 1855a330..6026a53f 100644 --- a/test/morphometrics/test_compute_morphometrics.py +++ b/test/morphometrics/test_compute_morphometrics.py @@ -151,7 +151,12 @@ def test_get_axon_morphometrics_returns_expected_keys(self): 'axonmyelin_perimeter', 'solidity', 'eccentricity', - 'orientation' + 'orientation', + 'gratio', + 'myelin_thickness', + 'myelin_area', + 'axonmyelin_area', + 'axonmyelin_perimeter' } stats_array = get_axon_morphometrics(self.pred_axon, str(self.test_folder_path)) @@ -189,6 +194,7 @@ def test_get_axon_morphometrics_with_invalid_gratio_with_axon_as_ellipse(self): im_myelin=self.bad_pred_myelin, axon_shape=self.axon_shape ) + assert np.isnan(stats_array[0]['gratio']) @pytest.mark.unit @@ -633,7 +639,7 @@ def test_save_axon_morphometrics_throws_error_if_folder_doesnt_exist(self): @pytest.mark.unit def test_load_axon_morphometrics_returns_identical_var_as_was_saved(self): - original_stats_array = get_axon_morphometrics(self.pred_axon, str(self.test_folder_path)) + original_stats_array = get_axon_morphometrics(self.pred_axon, str(self.test_folder_path), im_myelin=self.pred_myelin) save_axon_morphometrics(str(self.tmpDir), original_stats_array) @@ -641,7 +647,11 @@ def test_load_axon_morphometrics_returns_identical_var_as_was_saved(self): # 'axonlist.npy' will be in directory. loaded_stats_array = load_axon_morphometrics(str(self.tmpDir)) - assert np.array_equal(loaded_stats_array, original_stats_array) + # Because of the occasional presence in NaNs, which can't be compared well in our lists of dicts, + # loop through each row and skip the axons that are not well behaving. + for row_original, row_loaded in zip(original_stats_array, loaded_stats_array): + if any(math.isnan(val) for val in row_original.values()) == False and any(math.isnan(val) for val in row_loaded.values()) == False: + assert np.array_equal(row_loaded, row_original) @pytest.mark.unit def test_load_axon_morphometrics_throws_error_if_folder_doesnt_exist(self): @@ -652,6 +662,25 @@ def test_load_axon_morphometrics_throws_error_if_folder_doesnt_exist(self): with pytest.raises(IOError): load_axon_morphometrics(str(nonExistingFolder)) + # --------------check consistency with reference morphometrics-------------- # + @pytest.mark.unit + def test_morphometrics_consistency(self): + path_morphometrics_reference = Path( + self.testPath / + '__test_files__' / + '__test_demo_files__' / + '__morphometrics__' + ) + + reference_stats_array = load_axon_morphometrics(str(path_morphometrics_reference)) + new_stats_array = get_axon_morphometrics(self.pred_axon, str(self.test_folder_path), im_myelin=self.pred_myelin) + + for row_ref, row_new in zip(reference_stats_array, new_stats_array): + row_ref_vals = np.array(list(row_ref.values())) + row_new_vals = np.array(list(row_new.values())) + assert np.allclose(row_ref_vals, row_new_vals, rtol=0, atol=1e-11, equal_nan=True) + + # --------------draw_axon_diameter tests-------------- # @pytest.mark.unit def test_draw_axon_diameter_creates_file_in_expected_location(self): diff --git a/test/test_ads_utils.py b/test/test_ads_utils.py index 54523a3e..5e8a9a89 100644 --- a/test/test_ads_utils.py +++ b/test/test_ads_utils.py @@ -85,7 +85,7 @@ def test_extract_data_returns_expected_arrays(self): @pytest.mark.unit def test_get_existing_models_list_returns_known_models(self): - known_models = ['default_TEM_model', 'default_SEM_model'] + known_models = ['model_seg_rat_axon-myelin_sem', 'model_seg_mouse_axon-myelin_tem', 'model_seg_rat_axon-myelin_bf'] for model in known_models: assert model in get_existing_models_list() diff --git a/test/test_config_tools.py b/test/test_config_tools.py deleted file mode 100644 index 819f7990..00000000 --- a/test/test_config_tools.py +++ /dev/null @@ -1,220 +0,0 @@ -# coding: utf-8 - -import json -from pathlib import Path -import shutil - -import pytest - -from AxonDeepSeg.config_tools import validate_config, generate_config, grid_config, generate_name_config - - -class TestCore(object): - def setup(self): - - self.config = { - - # General parameters: - "n_classes": 3, - "thresholds": [0, 0.2, 0.8], - "trainingset_patchsize": 256, - "trainingset": "SEM_3c_256", - "batch_size": 8, - - # Network architecture parameters: - "depth": 2, - "convolution_per_layer": [2, 2], - "size_of_convolutions_per_layer": [[3, 3], [3, 3]], - "features_per_convolution": [ - [[1, 5], [5, 5]], - [[5, 10], [10, 10]] - ], - "downsampling": "maxpooling", - "dropout": 0.75, - - # Learning rate parameters: - "learning_rate": 0.001, - "learning_rate_decay_activate": True, - "learning_rate_decay_period": 24000, - "learning_rate_decay_type": "polynomial", - "learning_rate_decay_rate": 0.99, - - # Batch normalization parameters: - "batch_norm_activate": True, - "batch_norm_decay_decay_activate": True, - "batch_norm_decay_starting_decay": 0.7, - "batch_norm_decay_ending_decay": 0.9, - "batch_norm_decay_decay_period": 16000, - - # Weighted cost parameters: - "weighted_cost-activate": True, - "weighted_cost-balanced_activate": True, - "weighted_cost-balanced_weights": [1.1, 1, 1.3], - "weighted_cost-boundaries_sigma": 2, - "weighted_cost-boundaries_activate": False, - - # Data augmentation parameters: - "da-type": "all", - "da-2-random_rotation-activate": False, - "da-5-noise_addition-activate": False, - "da-3-elastic-activate": True, - "da-0-shifting-activate": True, - "da-4-flipping-activate": True, - "da-1-rescaling-activate": False - } - - # Create temp folder - # Get the directory where this current file is saved - self.fullPath = Path(__file__).resolve().parent - self.tmpPath = self.fullPath / '__tmp__' - if not self.tmpPath.exists(): - self.tmpPath.mkdir() - - @classmethod - def teardown_class(cls): - fullPath = Path(__file__).resolve().parent - tmpPath = fullPath / '__tmp__' - if tmpPath.exists(): - shutil.rmtree(tmpPath) - - # --------------validate_config tests-------------- # - @pytest.mark.unit - def test_validate_config_for_demo_config(self): - assert validate_config(self.config) - - @pytest.mark.unit - def test_validate_config_for_invalid_config(self): - - invalidConfig = { - "1nval1d_k3y": 0 - } - - assert not validate_config(invalidConfig) - - # --------------generate_config tests-------------- # - @pytest.mark.unit - def test_generate_config_creates_valid_config(self): - generatedConfig = generate_config() - - assert validate_config(generatedConfig) - - @pytest.mark.unit - def test_generate_config_with_config_path(self): - # Create temp config file - configPath = self.tmpPath / 'config_network.json' - - if configPath.exists() : - configPath.unlink() - - with open(configPath, 'w') as f: - json.dump(self.config, f, indent=2) - - generatedConfig = generate_config(config_path=str(configPath)) - assert generatedConfig != generate_config() - assert validate_config(generatedConfig) - - if configPath.exists(): - configPath.unlink() - - @pytest.mark.unit - def test_generate_config_with_config_path_and_invalid_config(self): - invalidConfig = { - "1nval1d_k3y": 0 - } - - configPath = self.tmpPath / 'config_network.json' - - if configPath.exists(): - configPath.unlink() - with open(configPath, 'w') as f: - json.dump(invalidConfig, f, indent=2) - else: # There is no config file for the moment - with open(configPath, 'w') as f: - json.dump(invalidConfig, f, indent=2) - - with pytest.raises(ValueError): - generatedConfig = generate_config(config_path=str(configPath)) - - if configPath.exists(): - configPath.unlink() - - # --------------grid_config tests-------------- # - @pytest.mark.unit - def test_grid_config_feature_augmentation_x(self): - # Sample L_struct and dict_params values - L_struct = [{ - 'structure': [[5, 5, 5], [3, 3, 3], [3, 3, 3], [3, 3, 3]], - 'features_augmentation': 'x2', - 'first_num_features': 16 - }] - dict_params = { - 'trainingset': ['SEM_3c_512'], - 'trainingset_patchsize': 512, - 'learning_rate_decay_period': 24000 - } - - config_list = grid_config( - L_struct, - dict_params, - base_config=self.config - ) - - for key in list(config_list.keys()): - assert validate_config(config_list[key]) - - @pytest.mark.unit - def test_grid_config_feature_augmentation_p(self): - # Sample L_struct and dict_params values - L_struct = [{ - 'structure': [[5, 5, 5], [3, 3, 3], [3, 3, 3], [3, 3, 3]], - 'features_augmentation': 'p2', - 'first_num_features': 16 - }] - dict_params = { - 'trainingset': ['SEM_3c_512'], - 'trainingset_patchsize': 512, - 'learning_rate_decay_period': 24000 - } - - config_list = grid_config( - L_struct, - dict_params, - base_config=self.config - ) - - for key in list(config_list.keys()): - assert validate_config(config_list[key]) - - @pytest.mark.unit - def test_grid_config_feature_augmentation_invalid(self): - # Sample L_struct and dict_params values - - # Note: 'features_augmentation':'d2'-> d is not a valid augmentation - # flag. - L_struct = [{ - 'structure': [[5, 5, 5], [3, 3, 3], [3, 3, 3], [3, 3, 3]], - 'features_augmentation': 'd2', - 'first_num_features': 16 - }] - dict_params = { - 'trainingset': ['SEM_3c_512'], - 'trainingset_patchsize': 512, - 'learning_rate_decay_period': 24000 - } - - with pytest.raises(ValueError): - config_list = grid_config( - L_struct, - dict_params, - base_config=self.config - ) - - # --------------generate_name_config tests-------------- # - @pytest.mark.unit - def test_generate_name_config_convolution_downsampling_first_letters(self): - tmpConfig = self.config - tmpConfig['downsampling'] = 'convolution' - - configName = generate_name_config(tmpConfig) - - assert configName[:3] == 'cv_' diff --git a/test/test_download_models.py b/test/test_download_models.py index 78674a2f..242c88ce 100644 --- a/test/test_download_models.py +++ b/test/test_download_models.py @@ -25,18 +25,15 @@ def setup(self): self.sem_model_path = ( self.tmpPath / - 'default_SEM_model' + 'model_seg_rat_axon-myelin_sem' ) - print(self.sem_model_path) - self.tem_model_path = ( self.tmpPath / - 'default_TEM_model' + 'model_seg_mouse_axon-myelin_tem' ) - self.bf_model_path = ( self.tmpPath / - 'model_seg_pns_bf' + 'model_seg_rat_axon-myelin_bf' ) def teardown(self): @@ -58,7 +55,6 @@ def test_download_models_works(self): assert not self.sem_model_path.exists() assert not self.tem_model_path.exists() assert not self.bf_model_path.exists() - print(self.sem_model_path.absolute()) download_model(self.tmpPath) diff --git a/test/test_segment.py b/test/test_segment.py index a3dfe0e4..6cb49832 100644 --- a/test/test_segment.py +++ b/test/test_segment.py @@ -5,9 +5,7 @@ import pytest from AxonDeepSeg.segment import ( - generate_config_dict, - generate_resolution, - generate_default_parameters, + generate_default_parameters, segment_folders, segment_image ) @@ -25,7 +23,7 @@ def setup(self): self.projectPath / 'AxonDeepSeg' / 'models' / - 'default_SEM_model' + 'model_seg_rat_axon-myelin_sem' ) self.imageFolderPath = ( @@ -65,48 +63,27 @@ def teardown_class(cls): 'image' + str(axon_suffix), 'image' + str(myelin_suffix), 'image' + str(axonmyelin_suffix), - 'image2' + str(axon_suffix), - 'image2' + str(myelin_suffix), - 'image2' + str(axonmyelin_suffix) + 'image.nii.gz', + 'image_2' + str(axon_suffix), + 'image_2' + str(myelin_suffix), + 'image_2' + str(axonmyelin_suffix), + 'image_2.nii.gz' ] for fileName in outputFiles: + if (imageFolderPath / fileName).exists(): (imageFolderPath / fileName).unlink() if (imageFolderPathWithPixelSize / fileName).exists(): (imageFolderPathWithPixelSize / fileName).unlink() - # --------------generate_config_dict tests-------------- # - @pytest.mark.unit - def test_generate_config_dict_outputs_dict(self): - - config = generate_config_dict(str(self.modelPath / 'config_network.json')) - - assert type(config) is dict - - @pytest.mark.unit - def test_generate_config_dict_throws_exception_for_nonexisting_file(self): - - with pytest.raises(ValueError): - config = generate_config_dict(str(self.modelPath / 'n0n_3xist1ng_f1l3.json')) - - # --------------generate_resolution tests-------------- # - @pytest.mark.unit - def test_generate_resolution_returns_expected_known_project_cases(self): - - assert generate_resolution('SEM', 512) == 0.1 - assert generate_resolution('SEM', 256) == 0.2 - assert generate_resolution('TEM', 512) == 0.01 - # --------------segment_folders tests-------------- # @pytest.mark.integration def test_segment_folders_creates_expected_files(self): + path_model = generate_default_parameters('SEM', str(self.modelPath)) - path_model, config = generate_default_parameters('SEM', str(self.modelPath)) - - overlap_value = 25 - resolution_model = generate_resolution('SEM', 512) + overlap_value = [48,48] outputFiles = [ 'image' + str(axon_suffix), @@ -114,15 +91,10 @@ def test_segment_folders_creates_expected_files(self): 'image' + str(axonmyelin_suffix) ] - for fileName in outputFiles: - assert not (self.imageFolderPath / fileName).exists() - segment_folders( path_testing_images_folder=str(self.imageFolderPath), path_model=str(path_model), overlap_value=overlap_value, - config=config, - resolution_model=resolution_model, acquired_resolution=0.37, verbosity_level=2 ) @@ -133,10 +105,9 @@ def test_segment_folders_creates_expected_files(self): @pytest.mark.integration def test_segment_folders_runs_with_relative_path(self): - path_model, config = generate_default_parameters('SEM', str(self.modelPath)) + path_model = generate_default_parameters('SEM', str(self.modelPath)) - overlap_value = 25 - resolution_model = generate_resolution('SEM', 512) + overlap_value = [48,48] outputFiles = [ 'image' + str(axon_suffix), @@ -144,12 +115,11 @@ def test_segment_folders_runs_with_relative_path(self): 'image' + str(axonmyelin_suffix) ] + segment_folders( path_testing_images_folder=str(self.relativeImageFolderPath), path_model=str(path_model), overlap_value=overlap_value, - config=config, - resolution_model=resolution_model, acquired_resolution=0.37, verbosity_level=2 ) @@ -160,10 +130,9 @@ def test_segment_image_creates_runs_successfully(self): # Since segment_folders should have already run, the output files # should already exist, which this test tests for. - path_model, config = generate_default_parameters('SEM', str(self.modelPath)) + path_model = generate_default_parameters('SEM', str(self.modelPath)) - overlap_value = 25 - resolution_model = generate_resolution('SEM', 512) + overlap_value = [48,48] outputFiles = [ 'image' + str(axon_suffix), @@ -171,17 +140,11 @@ def test_segment_image_creates_runs_successfully(self): 'image' + str(axonmyelin_suffix) ] - for fileName in outputFiles: - assert (self.imageFolderPath / fileName).exists() - segment_image( path_testing_image=str(self.imagePath), path_model=str(path_model), overlap_value=overlap_value, - config=config, - resolution_model=resolution_model, acquired_resolution=0.37, - verbosity_level=2 ) for fileName in outputFiles: @@ -200,7 +163,7 @@ def test_main_cli_runs_succesfully_with_valid_inputs(self): def test_main_cli_runs_succesfully_with_valid_inputs_with_overlap_value(self): with pytest.raises(SystemExit) as pytest_wrapped_e: - AxonDeepSeg.segment.main(["-t", "SEM", "-i", str(self.imagePath), "-v", "2", "-s", "0.37", '--overlap', '25']) + AxonDeepSeg.segment.main(["-t", "SEM", "-i", str(self.imagePath), "-v", "2", "-s", "0.37", '--overlap', '48']) assert (pytest_wrapped_e.type == SystemExit) and (pytest_wrapped_e.value.code == 0) @@ -212,20 +175,6 @@ def test_main_cli_runs_succesfully_with_valid_inputs_with_pixel_size_file(self): assert (pytest_wrapped_e.type == SystemExit) and (pytest_wrapped_e.value.code == 0) - @pytest.mark.exceptionhandling - def test_main_cli_handles_exception_for_too_small_resolution_due_to_min_resampled_patch_size(self): - - image_size = [436, 344] # of self.imagePath - default_SEM_resolution = 0.1 - default_SEM_patch_size = 512 - - minimum_resolution = default_SEM_patch_size * default_SEM_resolution / min(image_size) - - with pytest.raises(SystemExit) as pytest_wrapped_e: - AxonDeepSeg.segment.main(["-t", "SEM", "-i", str(self.imagePath), "-v", "2", "-s", str(round(0.99*minimum_resolution,3))]) - - assert (pytest_wrapped_e.type == SystemExit) and (pytest_wrapped_e.value.code == 2) - @pytest.mark.exceptionhandling def test_main_cli_handles_exception_missing_resolution_size(self): @@ -253,20 +202,6 @@ def test_main_cli_runs_succesfully_with_valid_inputs_for_folder_input_with_pixel assert (pytest_wrapped_e.type == SystemExit) and (pytest_wrapped_e.value.code == 0) - @pytest.mark.exceptionhandling - def test_main_cli_handles_exception_for_too_small_resolution_due_to_min_resampled_patch_size_for_folder_input(self): - - image_size = [436, 344] # of self.imagePath - default_SEM_resolution = 0.1 - default_SEM_patch_size = 512 - - minimum_resolution = default_SEM_patch_size * default_SEM_resolution / min(image_size) - - with pytest.raises(SystemExit) as pytest_wrapped_e: - AxonDeepSeg.segment.main(["-t", "SEM", "-i", str(self.imageFolderPath), "-v", "2", "-s", str(round(0.99*minimum_resolution,3))]) - - assert (pytest_wrapped_e.type == SystemExit) and (pytest_wrapped_e.value.code == 2) - @pytest.mark.exceptionhandling def test_main_cli_handles_exception_missing_resolution_size_for_folder_input(self): diff --git a/test/test_train_network.py b/test/test_train_network.py deleted file mode 100644 index 8a5ec21c..00000000 --- a/test/test_train_network.py +++ /dev/null @@ -1,150 +0,0 @@ -# coding: utf-8 - -import json -from pathlib import Path -import shutil -import os - -import keras.backend.tensorflow_backend as K - - -import pytest - -from AxonDeepSeg.train_network import train_model - - -class TestCore(object): - def setup(self): - # reset the tensorflow graph for new training - - K.clear_session() - # Get the directory where this current file is saved - self.fullPath = Path(__file__).resolve().parent - - self.modelPath = ( - self.fullPath / - '__test_files__' / - '__test_training_files__' / - 'Model' - ) - - self.configPath = ( - self.fullPath / - '__test_files__' / - '__test_training_files__' / - 'Model' / - 'config_network.json' - ) - - self.trainingPath = ( - self.fullPath / - '__test_files__' / - '__test_training_files__' - ) - - self.modelPath.mkdir(parents=True, exist_ok=True) - - self.config = { - # General parameters: - "n_classes": 3, - "thresholds": [0, 0.2, 0.8], - "trainingset_patchsize": 256, - "trainingset": "SEM_3c_256", - "batch_size": 2, - "epochs":2, - "save_epoch_freq": 1, - "checkpoint": None, - "checkpoint_period": 5, - - # Network architecture parameters: - "depth": 2, - "convolution_per_layer": [2, 2], - "size_of_convolutions_per_layer": [[3, 3], [3, 3]], - "features_per_convolution": [ - [[1, 5], [5, 5]], - [[5, 10], [10, 10]] - ], - "downsampling": "maxpooling", - "dropout": 0.75, - - # Learning rate parameters: - "learning_rate": 0.001, - "learning_rate_decay_activate": True, - "learning_rate_decay_period": 4, - "learning_rate_decay_type": "polynomial", - "learning_rate_decay_rate": 0.99, - - # Batch normalization parameters: - "batch_norm_activate": True, - "batch_norm_decay_decay_activate": True, - "batch_norm_decay_starting_decay": 0.7, - "batch_norm_decay_ending_decay": 0.9, - "batch_norm_decay_decay_period": 16000, - - # Weighted cost parameters: - "weighted_cost-activate": True, - "weighted_cost-balanced_activate": True, - "weighted_cost-balanced_weights": [1.1, 1, 1.3], - "weighted_cost-boundaries_sigma": 2, - "weighted_cost-boundaries_activate": False, - - # Data augmentation parameters: - "da-type": "all", - "da-2-random_rotation-activate": False, - "da-3-elastic-activate": True, - "da-0-shifting-activate": True, - "da-4-flipping-activate": True, - "da-1-rescaling-activate": False, - "da-6-reflection_border-activate": True, - } - - if not self.configPath.exists(): - with open(self.configPath, 'w') as f: - json.dump(self.config, f, indent=2) - - with open(self.configPath, 'r') as fd: - self.config_network = json.loads(fd.read()) - - @classmethod - def teardown_class(cls): - fullPath = Path(__file__).resolve().parent - - modelPath = ( - fullPath / - '__test_files__' / - '__test_training_files__' / - 'Model' - ) - - if modelPath.is_dir(): - try: - shutil.rmtree(modelPath) - except OSError: - print("Could not clean up {} - you may want to remove it manually.".format(modelPath)) - - # --------------train_model tests-------------- # - @pytest.mark.integration - def test_train_model_runs_successfully_for_simplified_case(self): - # Note: This test is simply a mock test to ensure that the pipeline - # runs successfully, and is not a test of the quality of the model - # itself. - - train_model( - str(self.trainingPath), - str(self.modelPath), - self.config_network, - debug_mode=True - ) - - expectedFiles = [ - "checkpoint", - "config_network.json", - "model.ckpt.data-00000-of-00001", - "model.ckpt.index", - "model.ckpt.meta" - ] - - existingFiles = [f.name for f in self.modelPath.iterdir()] - - for fileName in expectedFiles: - assert fileName in existingFiles diff --git a/test/testing/test_statistics_generation.py b/test/testing/test_statistics_generation.py deleted file mode 100644 index 2c0a7166..00000000 --- a/test/testing/test_statistics_generation.py +++ /dev/null @@ -1,122 +0,0 @@ -# coding: utf-8 - -import json -from pathlib import Path -import tensorflow as tf -import pandas as pd - -import pytest - -from AxonDeepSeg.testing.statistics_generation import metrics_single_wrapper, metrics, metrics_classic_wrapper - - -class TestCore(object): - def setup(self): - # Get the directory where this current file is saved - self.fullPath = Path(__file__).resolve().parent - # Move up to the test directory, "test/" - self.testPath = self.fullPath.parent - self.projectPath = self.testPath.parent - - self.modelPath = ( - self.projectPath / - 'AxonDeepSeg' / - 'models' / - 'default_SEM_model' - ) - - self.imagesPath = ( - self.testPath / - '__test_files__' / - '__test_training_files__' / - 'Testing' - ) - - self.statsFilename = 'model_statistics_validation.json' - - @classmethod - def teardown_class(cls): - # Get the directory where this current file is saved - fullPath = Path(__file__).resolve().parent - # Move up to the test directory, "test/" - testPath = fullPath.parent - projectPath = testPath.parent - - modelPath = ( - projectPath / - 'AxonDeepSeg' / - 'models' / - 'default_SEM_model' - ) - - statsFilename = 'model_statistics_validation.json' - - if (modelPath / statsFilename).exists(): - (modelPath / statsFilename).unlink() - - # --------------metrics_single_wrapper tests-------------- # - @pytest.mark.integration - def test_metrics_single_wrapper_runs_successfully_and_outfile_exists(self): - # reset the tensorflow graph for new training - tf.reset_default_graph() - - path_model_folder = self.modelPath - path_images_folder = self.imagesPath - resampled_resolution = 0.1 - metrics_single_wrapper( - path_model_folder, - path_images_folder, - resampled_resolution, - overlap_value=25, - statistics_filename=self.statsFilename, - create_statistics_file=True, - verbosity_level=2 - ) - - assert (self.modelPath / self.statsFilename).exists() - (self.modelPath / self.statsFilename).unlink() - - # --------------metrics_classic_wrapper tests-------------- # - @pytest.mark.integration - def test_metrics_classic_wrapper_runs_successfully_and_outfile_exists(self): - # reset the tensorflow graph for new training - tf.reset_default_graph() - - path_model_folder = self.modelPath - path_images_folder = self.imagesPath - resampled_resolution = 0.1 - metrics_classic_wrapper( - str(path_model_folder), - str(path_images_folder), - resampled_resolution, - overlap_value=25, - statistics_filename=self.statsFilename, - create_statistics_file=True, - verbosity_level=2) - - assert (self.modelPath /self.statsFilename).exists() - - # --------------metrics class tests-------------- # - # Though conceptually these could be classified as unit tests, they - # depend on the outputs of the previous integrity tests, so we count these - # as part of the same integrity test workflow - @pytest.mark.integration - def test_metrics_class_loads_stats_table(self): - met = metrics(statistics_filename='model_statistics_validation.json') - - assert type(met.filtered_stats) is pd.core.frame.DataFrame - assert met.filtered_stats.empty - - met.add_models(self.modelPath) - met.load_models() - - assert not met.filtered_stats.empty - - @pytest.mark.integration - def test_metrics_class_throws_exception_for_missing_stats_file(self): - met = metrics(statistics_filename='n0n-3x1st1ng.json') - - met.add_models(self.modelPath) - - with pytest.raises(ValueError): - met.load_models() diff --git a/test/visualization/test_merge_masks.py b/test/visualization/test_merge_masks.py index 4ce8d7a4..39ca9089 100644 --- a/test/visualization/test_merge_masks.py +++ b/test/visualization/test_merge_masks.py @@ -7,7 +7,7 @@ import pytest from AxonDeepSeg.visualization.merge_masks import merge_masks -from config import axon_suffix, myelin_suffix +from config import axon_suffix, myelin_suffix, axonmyelin_suffix class TestCore(object): @@ -16,7 +16,7 @@ def setup(self): self.fullPath = Path(__file__).resolve().parent # Move up to the test directory, "test/" self.testPath = self.fullPath.parent - + self.output_filename = Path('image' + str(axonmyelin_suffix)) self.path_folder = ( self.testPath / '__test_files__' / @@ -24,8 +24,8 @@ def setup(self): ) def teardown(self): - if (self.path_folder / 'axon_myelin_mask.png').is_file(): - (self.path_folder / 'axon_myelin_mask.png').unlink() + if (self.path_folder / self.output_filename ).is_file(): + (self.path_folder / self.output_filename ).unlink() # --------------merge_masks tests-------------- # @pytest.mark.unit @@ -35,12 +35,12 @@ def test_merge_masks_outputs_expected_volume_and_writes_files(self): path_myelin = self.path_folder / ('image' + str(myelin_suffix)) - expectedFilePath = self.path_folder / 'axon_myelin_mask.png' + expectedFilePath = self.path_folder / self.output_filename if expectedFilePath.is_file(): expectedFilePath.unlink() - both = merge_masks(str(path_axon), str(path_myelin)) + both = merge_masks(str(path_axon), str(path_myelin), str(self.output_filename)) assert expectedFilePath.is_file() assert np.array_equal(both, imageio.imread(expectedFilePath)) diff --git a/test/visualization/test_visualize.py b/test/visualization/test_visualize.py deleted file mode 100644 index d33d05aa..00000000 --- a/test/visualization/test_visualize.py +++ /dev/null @@ -1,31 +0,0 @@ -# coding: utf-8 - -from pathlib import Path - -import pytest - -from AxonDeepSeg.visualization.visualize import visualize_training - - -class TestCore(object): - def setup(self): - # Get the directory where this current file is saved - self.fullPath = Path(__file__).resolve().parent - # Move up to the test directory, "test/" - self.testPath = self.fullPath.parent - - self.pathModel = ( - self.testPath / - '__test_files__' / - '__test_model__' / - 'Model' - ) - - def teardown(self): - pass - - # --------------visualize_training tests-------------- # - @pytest.mark.unit - def test_visualize_training_runs_successfully(self): - - assert visualize_training(str(self.pathModel))