diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0300dec --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +**/.venv diff --git a/case1-stardist/.gitignore b/case1-stardist/.gitignore new file mode 100644 index 0000000..a802a57 --- /dev/null +++ b/case1-stardist/.gitignore @@ -0,0 +1,2 @@ +stardist* +chatty-frog/ diff --git a/case1-stardist/QuPath_Export_Labels.groovy b/case1-stardist/QuPath_Export_Labels.groovy new file mode 100644 index 0000000..165a9ce --- /dev/null +++ b/case1-stardist/QuPath_Export_Labels.groovy @@ -0,0 +1,25 @@ +/** + * QuPath script to export labeled images corresponding to nucleus annotations to train StarDist. + * Written for QuPath v0.6.0. + */ + +// Create a labeled image that assigns integer labels to all unclassified annotations. +// Note that the 'parent' annotation created before running StarDist should have a classification +// to make sure that it is not included here. +def imageData = getCurrentImageData() +def labelServer = new LabeledImageServer.Builder(imageData) + .useInstanceLabels() + .useFilter(a -> a.isAnnotation() && !a.classification) + .build() + + +// Specify the folder where to save the results +def outputDir = buildPathInProject("export") +mkdirs(outputDir) + +// Write the image +// For very large images, .ome.tif or .ome.zarr may be needed +def name = getCurrentImageNameWithoutExtension() +def path = buildFilePath(outputDir, name + ".tif") +writeImage(labelServer, path) +println "Image written successfully to " + path diff --git a/case1-stardist/QuPath_Run_StarDist.groovy b/case1-stardist/QuPath_Run_StarDist.groovy new file mode 100644 index 0000000..32ef8c3 --- /dev/null +++ b/case1-stardist/QuPath_Run_StarDist.groovy @@ -0,0 +1,39 @@ +/** + * Groovy script to apply Stardist using a model from bioimage.io in QuPath v0.6. + * + * It requires installing the StarDist extension in QuPath. + * For more information, see https://qupath.readthedocs.io/en/0.6/docs/deep/stardist.html + */ + + +import static qupath.lib.gui.scripting.QPEx.* +import qupath.ext.stardist.* + +// Specify the path to the model +var pathModel = buildPathInProject('chatty-frog') + +// Customize how StarDist will operate +var stardist = StarDistBioimageIo.builder(pathModel) + .createAnnotations() // Create annotations (which are editable); remove to create detections + .threshold(0.4) // Specify probability threshold (lower to detect more objects) + .build() + +// Get the current image +var imageData = getCurrentImageData() + +// If the image is small, and we have no objects, create a rectangle around everything +if (getAllObjectsWithoutRoot().isEmpty() && Math.max(imageData.server.width, imageData.server.height) <= 2048) { + println "Creating a full image annotation" + createFullImageAnnotation(true) + // Assign a classification so we can distinguish our region from detected nuclei + getSelectedObject().classification = "Region*" +} + +// Run detection for the selected objects +var pathObjects = getSelectedObjects().findAll {it.isAnnotation()} +if (pathObjects.isEmpty()) { + println "At least one annotation needs to be selected!" +} +stardist.detectObjects(imageData, pathObjects) +stardist.close() +println 'Done!' diff --git a/case1-stardist/README.md b/case1-stardist/README.md index 082ba81..3b30811 100644 --- a/case1-stardist/README.md +++ b/case1-stardist/README.md @@ -3,23 +3,28 @@ In this use-case, we apply the [Stardist H&E](https://bioimage.io/#/?tags=stardist&id=10.5281%2Fzenodo.6338614) model from the BioImage Model Zoo which was pretrained on [MoNuSeg](https://monuseg.grand-challenge.org/Data/) and [TNBC](https://zenodo.org/record/1175282#.X6mwG9so-CN) datasets. This model is applied to the [Lizard dataset](https://www.kaggle.com/datasets/aadimator/lizard-dataset) in [deepImageJ](https://deepimagej.github.io), in [QuPath](https://qupath.github.io), in [ZeroCostDL4Mic](https://github.com/HenriquesLab/ZeroCostDL4Mic/wiki) and with a Python notebook. -## Apply stardist model in QuPath and correct segmentation +## Apply StarDist model in QuPath and correct segmentation -We apply the pretrained model in QuPath, that has advanced image annotation capabilities. This allows correction of the stardist segmentation, for example for a more correct analysis of the result, or for training of a better model. +We apply the pretrained model in QuPath, that has advanced image annotation capabilities. +This allows correction of the StarDist segmentation, for example for a more correct analysis of the result, or for training of a better model. Step-by-step: -- Download the stardist model with `download_stardist_model.py` -- Run `prepare_data_for_qupath.py` to select the data for QuPath +- Download the StarDist H&E model from: https://bioimage.io/#/artifacts/chatty-frog + Be sure to extract the downloaded zip, and also extract the TF_SavedModel folder inside the extracted folder. +- Download the images from https://www.kaggle.com/datasets/aadimator/lizard-dataset + You can download the entire dataset (~800MB) or just a few images. - Prepare QuPath for running StarDist: - - install the StarDist extension: https://qupath.readthedocs.io/en/stable/docs/advanced/stardist.html#getting-the-stardist-extension - - install the tensorflow extension: https://qupath.readthedocs.io/en/stable/docs/advanced/stardist.html#use-tensorflow -- Apply stardist to the lizard images with the `apply_stardist_qupath.groovy` script using the [QuPath scripting functionality](https://qupath.readthedocs.io/en/stable/docs/scripting/workflows_to_scripts.html#running-a-script-for-a-single-image). - - To run it adapt the path to the model in the script here: https://github.com/bioimage-io/use-cases/blob/main/case2/apply_stardist_qupath.groovy#L27 -- Correct the predictions using the qupath annotation functionality (check out [these tweets](https://twitter.com/petebankhead/status/1295965136646176768) for a short overview of this functionality) -- Export the label image using the `export_labels_qupath.groovy` script. - - Important: Remove the rectangular annotation that the stardist plugin creates around the whole image before exporting the labels, otherwise the export script will not work correctly. + - install the StarDist extension: https://qupath.readthedocs.io/en/0.6/docs/intro/extensions.html#extensions + - install TensorFlow (and possibly CUDA): https://qupath.readthedocs.io/en/0.6/docs/deep/djl.html +- Create a project by dragging an empty folder onto QuPath: https://qupath.readthedocs.io/en/0.6/docs/tutorials/projects.html +- Add the lizard images to the project by drag & drop: https://qupath.readthedocs.io/en/0.6/docs/tutorials/projects.html#add-images +- Apply StarDist to the lizard images with the `QuPath_Run_StarDist.groovy` script using [QuPath's scripting functionality](https://qupath.readthedocs.io/en/0.6/docs/scripting/workflows_to_scripts.html#running-a-script-for-multiple-images). + - Either copy the `chatty-frog` folder to be inside the QuPath project, or update the model path defined in the script: https://github.com/bioimage-io/use-cases/blob/main/case1-stardist/QuPath_Run_StarDist.groovy +- Correct the predictions using QuPath's annotation tools (check out [these tweets](https://twitter.com/petebankhead/status/1295965136646176768) for a short overview) +- Export the label image using the `QuPath_Export_Labels.groovy` script. -See a short video demonstrating the label correction in qu-path: + +See a short video demonstrating the label correction in QuPath: https://user-images.githubusercontent.com/4263537/160414686-10ae46ae-5903-4a67-a35b-1f043b68711d.mp4 @@ -30,7 +35,7 @@ And images of application in QuPath: ### Dependencies -- qupath 0.3.2 +- QuPath 0.6.0 ## Apply the model in deepImageJ @@ -40,7 +45,7 @@ And images of application in QuPath: - Select the model "StarDist H&E Nuclei Segmentation" from the drop down menu and click on Install. - Adjust the Probability Threshold to a 0,40. You can try different thresholds depending on the number of objects that you want to detect. - Apply the model by clicking `Run`. - - This will result in the stardist predictions. You can change the lookup table for a better visualization of the segmented nuclei. + - This will result in the StarDist predictions. You can change the lookup table for a better visualization of the segmented nuclei. See a screenshot of the deepImageJ StarDist interface: drawing @@ -54,7 +59,7 @@ The example was run using: - DeepImageJ 3.1.0 -## Apply the model in stardist python +## Apply the model in StarDist python - `run_stardist_python.py` diff --git a/case1-stardist/README.md.old b/case1-stardist/README.md.old deleted file mode 100644 index eae84c4..0000000 --- a/case1-stardist/README.md.old +++ /dev/null @@ -1,42 +0,0 @@ -# Use-case 2: Stardist H&E nucleus segmentation - -In this use-casel, we use the stardist H&E model pretrained on [MoNuSeg](https://monuseg.grand-challenge.org/Data/) and [TNBC](https://zenodo.org/record/1175282#.X6mwG9so-CN): https://bioimage.io/#/?tags=stardist&id=10.5281%2Fzenodo.6338614. -We apply it to the [Lizard dataset](https://warwick.ac.uk/fac/cross_fac/tia/data/lizard/), see the pretrained model applied to one of the lizard images below, colored instance masks are stardist predictions, red instance outlines mark the expected nuclei. - -drawing - -Note that the images are upscaled by a factor of 2 to match the resolution of images that the stardist model was trained on. - -## Apply stardist model in QuPath and correct segmentation - -Apply the pretrained H&E model to some images from Lizard and correct the predictions: -- Download the stardist model with `download_stardist_model.py` -- Run `prepare_data_for_qupath.py` to select the data for QuPath -- Prepare QuPath for running StarDist: - - install the StarDist extension: https://qupath.readthedocs.io/en/stable/docs/advanced/stardist.html#getting-the-stardist-extension - - install the tensorflow extension: https://qupath.readthedocs.io/en/stable/docs/advanced/stardist.html#use-tensorflow -- Apply stardist to the lizard images with the `apply_stardist_qupath.groovy` script using the [QuPath scripting functionality](https://qupath.readthedocs.io/en/stable/docs/scripting/workflows_to_scripts.html#running-a-script-for-a-single-image). - - To run it adapt the path to the model in the script here: https://github.com/bioimage-io/use-cases/blob/main/case2/apply_stardist_qupath.groovy#L27 -- Correct the predictions using the qupath annotation functionality (check out [these tweets](https://twitter.com/petebankhead/status/1295965136646176768) for a short overview of this functionality) -- Export the label image using the `export_labels_qupath.groovy` script. - - Important: Remove the rectangular annotation that the stardist plugin creates around the whole image before exporting the labels, otherwise the export script will not work correctly. - -See a short video demonstrating the label correction in qu-path: - -https://user-images.githubusercontent.com/4263537/160414686-10ae46ae-5903-4a67-a35b-1f043b68711d.mp4 - - -## Retrain with corrected segmentation in zero-cost - -TODO: -- use stardist modelzoo library in zero-cost notebook - -## Apply retrained model in deepimagej - -TODO: -- figure out how exactly to run stardist post-processing in deepIJ - -## Evaluate retrained model in python - -TODO: -- quantitatively compare performance of the retrained model with the pretrained model on the test images using the available ground-truth labels diff --git a/case1-stardist/apply_stardist_qupath.groovy b/case1-stardist/apply_stardist_qupath.groovy deleted file mode 100644 index 2a48f07..0000000 --- a/case1-stardist/apply_stardist_qupath.groovy +++ /dev/null @@ -1,105 +0,0 @@ -/** - * Groovy script to apply Stardist using a model from bioimage.io. - * For more information about StarDist + QuPath in general, see https://qupath.readthedocs.io/en/stable/docs/advanced/stardist.html - * - * Note that this is written for compatibility with QuPath v0.3.2... and is therefore rather awkward. - * It requires separately installing two extensions: - * - https://github.com/qupath/qupath-extension-stardist - * - https://github.com/qupath/qupath-extension-tensorflow - * It also only supports percentile normalization, with values that should be specified in the script - * (since the YAML is not parsed automatically). - * - * Future QuPath versions will reduce the complexity a bit by including more useful built-in methods. - * - * @author Pete Bankhead - */ - - -import static qupath.lib.gui.scripting.QPEx.* -import org.bytedeco.opencv.opencv_core.Mat -import qupath.lib.gui.dialogs.Dialogs -import qupath.ext.stardist.StarDist2D -import qupath.opencv.tools.OpenCVTools - -setPixelSizeMicrons(0.5, 0.5) - -// specify the path to the model -var pathModel = '/home/pape/Work/bioimageio/use-cases/case2/he-model-pretrained/bioimageio' -// Define nput normalization percentiles etc -double minPercentile = 1.0 // Input normalization min percentile -double maxPercentile = 99.8 // Input normalization max percentile -boolean jointChannelNormalize = true // Optionally normalize channels together (rather than independently) -double predictionThreshold = 0.5 // Prediction threshold -double pixelSizeMicrons = 0.25 // Specify if input resolution should be set (rather than using full resolution) -boolean createAnnotations = true // Create cells as annotations, which can then be edited (default is to create detection objects) - -// Customize how StarDist will operate -var stardistBuilder = StarDist2D.builder(pathModel) - .threshold(predictionThreshold) -if (pixelSizeMicrons && Double.isFinite(pixelSizeMicrons)) - stardistBuilder.pixelSize(pixelSizeMicrons) - -if (createAnnotations) - stardistBuilder.createAnnotations() - -if (jointChannelNormalize) - stardistBuilder.preprocess( - new NormalizePercentileJointOp(minPercentile, maxPercentile) - ) -else - stardistBuilder.normalizePercentiles(minPercentile, maxPercentile) - -// If necessary, customize tile size -//stardistBuilder.tileSize(1024, 1024) - -var stardist = stardistBuilder.build() - - -// Run detection for the selected objects -var imageData = getCurrentImageData() -var pathObjects = getSelectedObjects() -if (pathObjects.isEmpty()) { - // If there are no annotations at all, and the image isn't too big, create an annotation for the full image - if (getAnnotationObjects().isEmpty() && imageData.getServer().getWidth() <= 2048 && imageData.getServer().getHeight() <= 2048) { - println "No objects are selected - I'll select the whole image then" - createSelectAllObject(true) - } else { - Dialogs.showErrorMessage("StarDist", "Please select one or more parent objects!") - return - } -} -stardist.detectObjects(imageData, pathObjects) -println 'Done!' - - - -/** - * For QuPath v0.3.2, we need a custom ImageOp to perform joint channel normalization - */ -class NormalizePercentileJointOp implements qupath.opencv.ops.ImageOp { - - private double[] percentiles - - NormalizePercentileJointOp(double percentileMin, double percentileMax) { - this.percentiles = [percentileMin, percentileMax] as double[] - if (percentileMin == percentileMax) - throw new IllegalArgumentException("Percentile min and max values cannot be identical!") - } - - @Override - public Mat apply(Mat mat) { - // Need to reshape for percentiles to be correct in QuPath v0.3.2 - var matTemp = mat.reshape(1, mat.rows()*mat.cols()) - var range = OpenCVTools.percentiles(matTemp, percentiles) - double scale - if (range[1] == range[0]) { - println("Normalization percentiles give the same value ({}), scale will be Infinity", range[0]) - scale = Double.POSITIVE_INFINITY - } else - scale = 1.0/(range[1] - range[0]) - double offset = -range[0] - mat.convertTo(mat, mat.type(), scale, offset*scale) - return mat; - } - -} diff --git a/case1-stardist/bkp/evaluate_predictions.py b/case1-stardist/bkp/evaluate_predictions.py deleted file mode 100644 index 2586ae1..0000000 --- a/case1-stardist/bkp/evaluate_predictions.py +++ /dev/null @@ -1,93 +0,0 @@ -import argparse -import json -import os -from glob import glob - -import imageio -import numpy as np -import scipy.io as sio -from stardist.matching import matching -from tqdm import tqdm - - -def evaluate_prediction(pred_path, label_path): - seg = imageio.imread(pred_path) - gt = sio.loadmat(label_path)["inst_map"] - assert seg.shape == gt.shape, f"{seg.shape}, {gt.shape}" - score = matching(gt, seg) - return score.mean_matched_score - - -def compare(pred_folders, pred_scores, image_folder): - import napari - - name1, name2 = os.path.basename(pred_folders[0].rstrip("/")), os.path.basename(pred_folders[1].rstrip("/")) - paths1, paths2 = glob(os.path.join(pred_folders[0], "*.tif")), glob(os.path.join(pred_folders[1], "*.tif")) - paths1.sort() - paths2.sort() - - diffs = [] - for ii, (p1, p2) in enumerate(zip(paths1, paths2)): - s1, s2 = pred_scores[name1][ii], pred_scores[name2][ii] - diff = np.abs(s1 - s2) - diffs.append(diff) - - max_diff_dis = np.argsort(diffs)[::-1] - for diff_id in max_diff_dis: - p1, p2 = paths1[diff_id], paths2[diff_id] - diff = s1 - s2 - im_name = os.path.basename(p1).replace(".tif", ".png") - im_path = os.path.join(image_folder, im_name) - assert os.path.exists(im_path), im_path - - im = imageio.imread(im_path) - seg1 = imageio.imread(p1) - seg2 = imageio.imread(p2) - - msg = f"{name1} is better than {name2}" if s1 > s2 else f"{name2} is better than {name1}" - title = f"Im={im_name}, diff={np.abs(diff)}, {msg}" - v = napari.Viewer() - v.add_image(im) - v.add_labels(seg1, name=name1) - v.add_labels(seg2, name=name2) - v.title = title - napari.run() - - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument("--prediction_folder", "-p", required=True, nargs="+") - parser.add_argument("--label_folder", "-l", required=True) - parser.add_argument("-c", "--compare", default=0, type=int) - args = parser.parse_args() - - pred_scores = {} - for pred_folder in args.prediction_folder: - method_name = os.path.basename(pred_folder.rstrip("/")) - predictions = glob(os.path.join(pred_folder, "*.tif")) - predictions.sort() - scores = [] - for pred in tqdm(predictions): - name = os.path.basename(pred) - lab = os.path.join(args.label_folder, name.replace(".tif", ".mat")) - assert os.path.exists(lab), lab - scores.append(evaluate_prediction(pred, lab)) - - save_path = os.path.join(pred_folder, "scores.json") - with open(save_path, "w") as f: - json.dump([float(score) for score in scores], f) - - score = np.mean(scores) - pred_scores[method_name] = scores - print("Evaluation for", method_name) - print("Mean IoU-50:", score) - - if bool(args.compare): - assert len(pred_scores) == 2, "Can only compare two predictions" - # TODO don't hard-code - image_folder = "/home/pape/Work/data/lizard/train_images" - compare(args.prediction_folder, pred_scores, image_folder) - - -if __name__ == "__main__": - main() diff --git a/case1-stardist/bkp/export_labels_qupath.groovy b/case1-stardist/bkp/export_labels_qupath.groovy deleted file mode 100644 index b4424bb..0000000 --- a/case1-stardist/bkp/export_labels_qupath.groovy +++ /dev/null @@ -1,17 +0,0 @@ -def imageData = getCurrentImageData() - -def labelServer = new LabeledImageServer.Builder(imageData) - .backgroundLabel(0, 0) // Specify background label (usually 0 or 255) - .useInstanceLabels() - .useAnnotations() - .multichannelOutput(false) - .build() - - -// specify the folder where to save the results -def outputDir = '/home/pape/Work/bioimageio/use-cases/case2/images_for_qupath/labels' -mkdirs(outputDir) -def name = GeneralTools.getNameWithoutExtension(imageData.getServer().getMetadata().getName()) -def path = buildFilePath(outputDir, name + ".tif") - -writeImage(labelServer, path) \ No newline at end of file diff --git a/case1-stardist/bkp/finetuning/.gitignore b/case1-stardist/bkp/finetuning/.gitignore deleted file mode 100644 index fa8ff75..0000000 --- a/case1-stardist/bkp/finetuning/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -models/ -predictions/ -training_data/ diff --git a/case1-stardist/bkp/finetuning/README.md b/case1-stardist/bkp/finetuning/README.md deleted file mode 100644 index 68708a4..0000000 --- a/case1-stardist/bkp/finetuning/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# Scores - -baseline: 0.7866029100142583 - -| Name | from-scratch | finetuned | -| --------- | ------------- | --------- | -| gt-full | 0.7812 | - | -| qupath-v3 | - | 0.7577 | -| gt-v3 | 0.7310 | 0.7497 | -| zcost | - | 0.7303 | diff --git a/case1-stardist/bkp/finetuning/evaluate_finetuned.py b/case1-stardist/bkp/finetuning/evaluate_finetuned.py deleted file mode 100644 index 0e3bd06..0000000 --- a/case1-stardist/bkp/finetuning/evaluate_finetuned.py +++ /dev/null @@ -1,152 +0,0 @@ -import argparse -import os -import json -from glob import glob -from concurrent import futures -from pathlib import Path - -# don't parallelize internally -n_threads = 1 -os.environ["OMP_NUM_THREADS"] = str(n_threads) -os.environ["OPENBLAS_NUM_THREADS"] = str(n_threads) -os.environ["MKL_NUM_THREADS"] = str(n_threads) -os.environ["VECLIB_NUM_THREADS"] = str(n_threads) -os.environ["NUMEXPR_NUM_THREADS"] = str(n_threads) - -import imageio -import numpy as np -import scipy.io as sio -from csbdeep.utils import normalize -try: - from stardist.models import StarDist2D -except ImportError: - StarDist2D = None - print("Could not import stardist!!!!") -from stardist.matching import matching -from tqdm import tqdm - - -PRETRAINED_SCORE = 0.7866029100142583 - - -def apply_model(model, image_path, save_path, scale): - input_ = imageio.imread(image_path) - input_ = normalize(input_.astype("float32"), 1.0, 99.8) - nuclei, _ = model.predict_instances(input_, scale=scale) - assert nuclei.shape == input_.shape[:-1] - imageio.imsave(save_path, nuclei) - - -def prediction(model_name, scale): - input_folder = "/g/kreshuk/data/lizard/train_images" - images = glob(os.path.join(input_folder, "*png")) - print("Applying stardist model to", len(images), "images") - - output_folder = f"./predictions/{os.path.basename(model_name)}" - os.makedirs(output_folder, exist_ok=True) - model_root = Path(f"./{model_name}") - - def _predict(im): - name = os.path.basename(im).replace(".png", ".tif") - save_path = os.path.join(output_folder, name) - if os.path.exists(save_path): - return - model_folder = Path(model_root) - model = StarDist2D(None, model_folder.name, model_folder.parent) - apply_model(model, im, save_path, scale) - - n_threads = 12 - with futures.ThreadPoolExecutor(n_threads) as tp: - list(tqdm(tp.map(_predict, images), total=len(images), desc="Run prediction")) - return output_folder - - -def evaluate_prediction(pred_path, label_path): - seg = imageio.imread(pred_path) - gt = sio.loadmat(label_path)["inst_map"] - assert seg.shape == gt.shape, f"{seg.shape}, {gt.shape}" - score = matching(gt, seg) - return score.mean_matched_score - - -def evaluation(pred_folder): - label_folder = "/g/kreshuk/data/lizard/train_labels/Labels" - scores = [] - predictions = glob(os.path.join(pred_folder, "*.tif")) - predictions.sort() - - def _eval(pred): - name = os.path.basename(pred) - label_path = os.path.join(label_folder, name.replace(".tif", ".mat")) - score = evaluate_prediction(pred, label_path) - return score - - n_threads = 8 - with futures.ThreadPoolExecutor(n_threads) as tp: - scores = list(tqdm(tp.map(_eval, predictions), total=len(predictions), desc="Run evaluation")) - print("Fine-tuned score:", np.mean(scores)) - print("Baseline:", PRETRAINED_SCORE) - return scores - - -def compare(pred_folder, new_scores): - import napari - - image_folder = "/home/pape/Work/data/lizard/train_images" - baseline_folder = "/home/pape/Work/data/lizard/predictions/train/he-model-pretrained" - pred_folders = [baseline_folder, pred_folder] - - name1, name2 = os.path.basename(pred_folders[0].rstrip("/")), os.path.basename(pred_folders[1].rstrip("/")) - paths1, paths2 = glob(os.path.join(pred_folders[0], "*.tif")), glob(os.path.join(pred_folders[1], "*.tif")) - paths1.sort() - paths2.sort() - - baseline_scores = "/home/pape/Work/data/lizard/predictions/train/he-model-pretrained/scores.json" - with open(baseline_scores, "r") as f: - baseline_scores = json.load(f) - pred_scores = { - name1: baseline_scores, name2: new_scores - } - - diffs = [] - for ii, (p1, p2) in enumerate(zip(paths1, paths2)): - s1, s2 = pred_scores[name1][ii], pred_scores[name2][ii] - diff = np.abs(s1 - s2) - diffs.append(diff) - - max_diff_dis = np.argsort(diffs)[::-1] - for diff_id in max_diff_dis: - p1, p2 = paths1[diff_id], paths2[diff_id] - diff = s1 - s2 - im_name = os.path.basename(p1).replace(".tif", ".png") - im_path = os.path.join(image_folder, im_name) - assert os.path.exists(im_path), im_path - - im = imageio.imread(im_path) - seg1 = imageio.imread(p1) - seg2 = imageio.imread(p2) - - msg = f"{name1} is better than {name2}" if s1 > s2 else f"{name2} is better than {name1}" - title = f"Im={im_name}, diff={np.abs(diff)}, {msg}" - v = napari.Viewer() - v.add_image(im) - v.add_labels(seg1, name=name1) - v.add_labels(seg2, name=name2) - v.title = title - napari.run() - - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument("-m", "--model", required=True) - parser.add_argument("-c", "--compare", type=int, default=0) - parser.add_argument("-s", "--scale", default=1, type=int) - args = parser.parse_args() - pred_folder = prediction(args.model, args.scale) - scores = evaluation(pred_folder) - if bool(args.compare): - compare(pred_folder, scores) - - -if __name__ == "__main__": - main() diff --git a/case1-stardist/bkp/finetuning/finetune.py b/case1-stardist/bkp/finetuning/finetune.py deleted file mode 100644 index 25e524a..0000000 --- a/case1-stardist/bkp/finetuning/finetune.py +++ /dev/null @@ -1,89 +0,0 @@ -import argparse -import os -from glob import glob -from pathlib import Path -from shutil import copytree, rmtree - -import imageio -import numpy as np -from stardist.models import StarDist2D -from csbdeep.utils import normalize - - -def random_fliprot(img, mask): - assert img.ndim >= mask.ndim - axes = tuple(range(mask.ndim)) - perm = tuple(np.random.permutation(axes)) - img = img.transpose(perm + tuple(range(mask.ndim, img.ndim))) - mask = mask.transpose(perm) - for ax in axes: - if np.random.rand() > 0.5: - img = np.flip(img, axis=ax) - mask = np.flip(mask, axis=ax) - return img, mask - - -def random_intensity_change(img): - img = img*np.random.uniform(0.6, 2) + np.random.uniform(-0.2, 0.2) - return img - - -def augmenter(x, y): - """Augmentation of a single input/label image pair. - x is an input image - y is the corresponding ground-truth label image - """ - x, y = random_fliprot(x, y) - x = random_intensity_change(x) - # add some gaussian noise - sig = 0.02*np.random.uniform(0, 1) - x = x + sig*np.random.normal(0, 1, x.shape) - return x, y - - -def finetune(): - parser = argparse.ArgumentParser() - parser.add_argument("-n", "--name", required=True) - parser.add_argument("-r", "--root", required=True) - parser.add_argument("--n_epochs", default=5, type=int) - args = parser.parse_args() - - im_paths = glob(os.path.join(args.root, "images/*.tif")) - im_paths.sort() - label_paths = glob(os.path.join(args.root, "labels/*.tif")) - label_paths.sort() - - val_names = ["consep_5", "consep_11", "crag_60"] - val_ids = tuple(ii for ii, p in enumerate(im_paths) if any(nn in p for nn in val_names)) - train_ids = tuple(ii for ii, p in enumerate(im_paths) if not any(nn in p for nn in val_names)) - print("Train ids:", train_ids) - print("Val ids:", val_ids) - assert len(val_ids) == len(val_names) - assert len(train_ids) == len(im_paths) - len(val_names) - - x = list(map(imageio.imread, im_paths)) - y = list(map(imageio.imread, label_paths)) - - axis_norm = (0, 1) - x = [normalize(xx, 1, 99.8, axis=axis_norm) for xx in x] - - x_train, y_train = [x[tid] for tid in train_ids], [y[tid] for tid in train_ids] - x_val, y_val = [x[vid] for vid in val_ids], [y[vid] for vid in val_ids] - - model_folder = f"./models/finetuned-{args.name}" - if os.path.exists(model_folder): - rmtree(model_folder) - copytree("../he-model-pretrained", model_folder) - model_folder = Path(model_folder) - model = StarDist2D(None, model_folder.name, model_folder.parent) - model.config.train_patch_size = [384, 384] - - print("Start training ....") - model.train(x_train, y_train, validation_data=(x_val, y_val), augmenter=augmenter, epochs=args.n_epochs) - - print("Optimize thresholds ...") - model.optimize_thresholds(x_val, y_val) - - -if __name__ == "__main__": - finetune() diff --git a/case1-stardist/bkp/finetuning/from_scratch.py b/case1-stardist/bkp/finetuning/from_scratch.py deleted file mode 100644 index f0c7acc..0000000 --- a/case1-stardist/bkp/finetuning/from_scratch.py +++ /dev/null @@ -1,93 +0,0 @@ -import argparse -import os -from glob import glob -from pathlib import Path - -import imageio -import numpy as np -from stardist.models import StarDist2D, Config2D -from csbdeep.utils import normalize - - -def random_fliprot(img, mask): - assert img.ndim >= mask.ndim - axes = tuple(range(mask.ndim)) - perm = tuple(np.random.permutation(axes)) - img = img.transpose(perm + tuple(range(mask.ndim, img.ndim))) - mask = mask.transpose(perm) - for ax in axes: - if np.random.rand() > 0.5: - img = np.flip(img, axis=ax) - mask = np.flip(mask, axis=ax) - return img, mask - - -def random_intensity_change(img): - img = img*np.random.uniform(0.6, 2) + np.random.uniform(-0.2, 0.2) - return img - - -def augmenter(x, y): - """Augmentation of a single input/label image pair. - x is an input image - y is the corresponding ground-truth label image - """ - x, y = random_fliprot(x, y) - x = random_intensity_change(x) - # add some gaussian noise - sig = 0.02*np.random.uniform(0, 1) - x = x + sig*np.random.normal(0, 1, x.shape) - return x, y - - -def from_scratch(): - parser = argparse.ArgumentParser() - parser.add_argument("-n", "--name", required=True) - parser.add_argument("-r", "--root", required=True) - parser.add_argument("--n_epochs", default=200, type=int) - parser.add_argument("--use_gpu", "-g", default=0, type=int) - args = parser.parse_args() - - im_paths = glob(os.path.join(args.root, "images/*.tif")) - im_paths.sort() - label_paths = glob(os.path.join(args.root, "labels/*.tif")) - label_paths.sort() - - val_names = ["consep_5", "consep_11", "crag_60"] - val_ids = tuple(ii for ii, p in enumerate(im_paths) if any(nn in p for nn in val_names)) - train_ids = tuple(ii for ii, p in enumerate(im_paths) if not any(nn in p for nn in val_names)) - print("Train ids:", train_ids) - print("Val ids:", val_ids) - assert len(val_ids) == len(val_names) - assert len(train_ids) == len(im_paths) - len(val_names) - - x = list(map(imageio.imread, im_paths)) - y = list(map(imageio.imread, label_paths)) - - axis_norm = (0, 1) - x = [normalize(xx, 1, 99.8, axis=axis_norm) for xx in x] - - x_train, y_train = [x[tid] for tid in train_ids], [y[tid] for tid in train_ids] - x_val, y_val = [x[vid] for vid in val_ids], [y[vid] for vid in val_ids] - - conf = Config2D( - n_rays=32, - grid=(2, 2), - use_gpu=bool(args.use_gpu), - n_channel_in=3 - ) - - model_folder = f"./models/scratch-{args.name}" - model_folder = Path(model_folder) - model = StarDist2D(conf, model_folder.name, model_folder.parent) - model.config.train_patch_size = [384, 384] - - print("Start training ....") - model.train(x_train, y_train, validation_data=(x_val, y_val), augmenter=augmenter, epochs=args.n_epochs) - - print("Optimize thresholds ...") - model.optimize_thresholds(x_val, y_val) - - -if __name__ == "__main__": - from_scratch() diff --git a/case1-stardist/bkp/finetuning/make_gt_train_set.py b/case1-stardist/bkp/finetuning/make_gt_train_set.py deleted file mode 100644 index f14c00f..0000000 --- a/case1-stardist/bkp/finetuning/make_gt_train_set.py +++ /dev/null @@ -1,75 +0,0 @@ -import argparse -import os -from glob import glob - -import imageio -import scipy.io as sio - - -# create gt training set equivalent to the current corrected qupath set - - -def make_gt_train_set(): - image_folder = "/g/kreshuk/data/lizard/train_images" - label_folder = "/g/kreshuk/data/lizard/train_labels/Labels" - - refs = glob("./training_data/qupath/v*") - refs.sort() - ref_folder = refs[-1] - version = os.path.basename(ref_folder) - ref_folder = os.path.join(ref_folder, "images") - - im_out = f"./training_data/gt/{version}/images" - label_out = f"./training_data/gt/{version}/labels" - os.makedirs(im_out, exist_ok=True) - os.makedirs(label_out, exist_ok=True) - - ref = glob(os.path.join(ref_folder, "*.tif")) - for re in ref: - name = os.path.splitext(os.path.basename(re))[0] - im_path = os.path.join(image_folder, f"{name}.png") - im = imageio.imread(im_path) - - label_path = os.path.join(label_folder, f"{name}.mat") - labels = sio.loadmat(label_path)["inst_map"] - - assert labels.shape == im.shape[:-1], f"{labels.shape}, {im.shape}" - imageio.imwrite(os.path.join(im_out, f"{name}.tif"), im) - imageio.imwrite(os.path.join(label_out, f"{name}.tif"), labels) - - -def make_full_gt(): - image_folder = "/g/kreshuk/data/lizard/train_images" - label_folder = "/g/kreshuk/data/lizard/train_labels/Labels" - - im_out = "./training_data/gt/full/images" - label_out = "./training_data/gt/full/labels" - os.makedirs(im_out, exist_ok=True) - os.makedirs(label_out, exist_ok=True) - - images = glob(os.path.join(image_folder, "*.png")) - for im in images: - name = os.path.splitext(os.path.basename(im))[0] - im = imageio.imread(im) - - label_path = os.path.join(label_folder, f"{name}.mat") - labels = sio.loadmat(label_path)["inst_map"] - - assert labels.shape == im.shape[:-1], f"{labels.shape}, {im.shape}" - imageio.imwrite(os.path.join(im_out, f"{name}.tif"), im) - imageio.imwrite(os.path.join(label_out, f"{name}.tif"), labels) - - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument("-f", "--full", type=int, default=0) - args = parser.parse_args() - - if bool(args.full): - make_full_gt() - else: - make_gt_train_set() - - -if __name__ == "__main__": - main() diff --git a/case1-stardist/bkp/finetuning/make_qupath_train_set.py b/case1-stardist/bkp/finetuning/make_qupath_train_set.py deleted file mode 100644 index b0ceaeb..0000000 --- a/case1-stardist/bkp/finetuning/make_qupath_train_set.py +++ /dev/null @@ -1,44 +0,0 @@ -import argparse -import os -from glob import glob -import imageio - - -def make_qupath_train_set(version): - - input_images = glob("../images_for_qupath/*.png") - input_labels = glob("../images_for_qupath/labels/*.tif") - assert len(input_images) == len(input_labels) - input_images.sort() - input_labels.sort() - - # TODO implement getting the latest version numberw - if version is None: - assert False - - im_out = f"./training_data/qupath/{version}/images" - label_out = f"./training_data/qupath/{version}/labels" - os.makedirs(im_out, exist_ok=True) - os.makedirs(label_out, exist_ok=True) - - for im, label in zip(input_images, input_labels): - name = os.path.basename(im).replace(".png", ".tif") - im = imageio.imread(im) - imp = os.path.join(im_out, name) - imageio.imwrite(imp, im) - - name = os.path.basename(label) - label = imageio.imread(label) - labelp = os.path.join(label_out, name) - imageio.imwrite(labelp, label) - - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument("-v", "--version") - args = parser.parse_args() - make_qupath_train_set(args.version) - - -if __name__ == "__main__": - main() diff --git a/case1-stardist/bkp/prepare_data_for_qupath.py b/case1-stardist/bkp/prepare_data_for_qupath.py deleted file mode 100644 index 8e2e07c..0000000 --- a/case1-stardist/bkp/prepare_data_for_qupath.py +++ /dev/null @@ -1,33 +0,0 @@ -import argparse -import os -import imageio -from skimage.transform import rescale - - -def prepare_for_qupath(data_folder, output_folder, image_names, rescale_image): - os.makedirs(output_folder, exist_ok=True) - scale_factors = (1, 2, 2) - for name in image_names: - path = os.path.join(data_folder, name) - assert os.path.exists(path), path - im = imageio.imread(path) - if rescale_image: - im = rescale(im, scale_factors, preserve_range=True).astype(im.dtype) - out_path = os.path.join(output_folder, name) - imageio.imsave(out_path, im) - - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument("-i", "--input_folder", required=True) - parser.add_argument("-r", "--rescale", default=0, type=int) - args = parser.parse_args() - # select the images for corrections in qupath - image_names = ["consep_1.png", "consep_2.png", "consep_4.png", "consep_5.png"] - prepare_for_qupath( - args.input_folder, "./images_for_qupath", image_names, rescale_image=bool(args.rescale) - ) - - -if __name__ == "__main__": - main() diff --git a/case1-stardist/bkp/run_stardist_batch.py b/case1-stardist/bkp/run_stardist_batch.py deleted file mode 100644 index 1f0821c..0000000 --- a/case1-stardist/bkp/run_stardist_batch.py +++ /dev/null @@ -1,58 +0,0 @@ -import argparse -import os -from glob import glob -from concurrent import futures -from pathlib import Path - -# don't parallelize internally -n_threads = 1 -os.environ["OMP_NUM_THREADS"] = str(n_threads) -os.environ["OPENBLAS_NUM_THREADS"] = str(n_threads) -os.environ["MKL_NUM_THREADS"] = str(n_threads) -os.environ["VECLIB_NUM_THREADS"] = str(n_threads) -os.environ["NUMEXPR_NUM_THREADS"] = str(n_threads) - -import imageio -from csbdeep.utils import normalize -from stardist.models import StarDist2D -from tqdm import tqdm - - -def apply_model(model, image_path, save_path, scale): - if os.path.exists(save_path): - return - input_ = imageio.imread(image_path) - input_ = normalize(input_.astype("float32"), 1.0, 99.8) - nuclei, _ = model.predict_instances(input_, scale=scale) - assert nuclei.shape == input_.shape[:-1] - imageio.imsave(save_path, nuclei) - - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument("--input_folder", "-i", required=True) - parser.add_argument("--output_root", "-o", required=True) - parser.add_argument("--model_folder", "-m", required=True) - parser.add_argument("--n_threads", "-n", default=16, type=int) - parser.add_argument("--scale", "-s", default=1, type=int) - args = parser.parse_args() - - images = glob(os.path.join(args.input_folder, "*png")) - print("Applying stardist model to", len(images), "images") - - output_folder = os.path.join(args.output_root, os.path.basename(args.model_folder)) - os.makedirs(output_folder, exist_ok=True) - - def _predict(im): - model_folder = Path(args.model_folder) - model = StarDist2D(None, model_folder.name, model_folder.parent) - name = os.path.basename(im).replace(".png", ".tif") - save_path = os.path.join(output_folder, name) - apply_model(model, im, save_path, scale=args.scale) - - with futures.ThreadPoolExecutor(args.n_threads) as tp: - list(tqdm(tp.map(_predict, images), total=len(images))) - - -if __name__ == "__main__": - main() diff --git a/case1-stardist/download_stardist_model.py b/case1-stardist/download_stardist_model.py deleted file mode 100644 index e45a837..0000000 --- a/case1-stardist/download_stardist_model.py +++ /dev/null @@ -1,27 +0,0 @@ -import os -import zipfile -import stardist - - -def download_stardist(model_name, doi): - out_folder = f"he-model-{model_name}" - bioimageio_folder = os.path.join(out_folder, "bioimageio") - if os.path.exists(out_folder): - assert os.path.exists(os.path.join(bioimageio_folder, "rdf.yaml")) - print("The", model_name, "H&E model has been downloaded already.") - return - stardist.import_bioimageio(doi, out_folder) - # extract the zipped tensorflow weights for running it in qupath - tf_path = os.path.join(bioimageio_folder, "TF_SavedModel.zip") - assert os.path.exists(tf_path) - with zipfile.ZipFile(tf_path) as f: - f.extractall(bioimageio_folder) - - -def main(): - doi = "10.5281/zenodo.6338614", - download_stardist("pretrained", doi) - - -if __name__ == "__main__": - main() diff --git a/case1-stardist/images/consep_1.png b/case1-stardist/images/consep_1.png new file mode 100644 index 0000000..e764039 Binary files /dev/null and b/case1-stardist/images/consep_1.png differ diff --git a/case1-stardist/images/stardist-qupath1.png b/case1-stardist/images/stardist-qupath1.png index 2225a12..b9904fc 100644 Binary files a/case1-stardist/images/stardist-qupath1.png and b/case1-stardist/images/stardist-qupath1.png differ diff --git a/case1-stardist/images/stardist-qupath2.png b/case1-stardist/images/stardist-qupath2.png index 87f3b50..125f032 100644 Binary files a/case1-stardist/images/stardist-qupath2.png and b/case1-stardist/images/stardist-qupath2.png differ diff --git a/case1-stardist/prepare_data_for_qupath.py b/case1-stardist/prepare_data_for_qupath.py deleted file mode 100644 index 8e2e07c..0000000 --- a/case1-stardist/prepare_data_for_qupath.py +++ /dev/null @@ -1,33 +0,0 @@ -import argparse -import os -import imageio -from skimage.transform import rescale - - -def prepare_for_qupath(data_folder, output_folder, image_names, rescale_image): - os.makedirs(output_folder, exist_ok=True) - scale_factors = (1, 2, 2) - for name in image_names: - path = os.path.join(data_folder, name) - assert os.path.exists(path), path - im = imageio.imread(path) - if rescale_image: - im = rescale(im, scale_factors, preserve_range=True).astype(im.dtype) - out_path = os.path.join(output_folder, name) - imageio.imsave(out_path, im) - - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument("-i", "--input_folder", required=True) - parser.add_argument("-r", "--rescale", default=0, type=int) - args = parser.parse_args() - # select the images for corrections in qupath - image_names = ["consep_1.png", "consep_2.png", "consep_4.png", "consep_5.png"] - prepare_for_qupath( - args.input_folder, "./images_for_qupath", image_names, rescale_image=bool(args.rescale) - ) - - -if __name__ == "__main__": - main() diff --git a/case1-stardist/run_stardist_python.py b/case1-stardist/run_stardist_python.py index 807a8ee..500d75e 100644 --- a/case1-stardist/run_stardist_python.py +++ b/case1-stardist/run_stardist_python.py @@ -24,7 +24,7 @@ def run_stardist(image, scale=2): def main(): - input_path = "/home/pape/Work/data/lizard/train_images/consep_13.png" + input_path = "consep_13.png" image = imageio.imread(input_path) nuclei = run_stardist(image)