Example #1
0
    def test_message(self, capsys):
        message = "Test message"
        progress_bar(100, message=message)
        captured = capsys.readouterr()

        assert captured.out[slice(-len(message) - 1, -1, 1)] == message
Example #2
0
    def test_linecount_finished(self, capsys):
        progress_bar(100)
        captured = capsys.readouterr()

        assert self.count_lines(captured.out) == 2
Example #3
0
 def progress_update(result, progress):
     utilities.progress_bar(progress, message = "Processing images")
Example #4
0
    def test_linecount(self, capsys):
        progress_bar(50)
        progress_bar(75.4)
        captured = capsys.readouterr()

        assert self.count_lines(captured.out) == 1
Example #5
0
def main():
    parser = argparse_init(
        description=
        "An image analysis tool for measuring microorganism colony growth",
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
        usage="%(prog)s '/image/file/path/' [OPTIONS]")

    # Retrieve and parse arguments
    args = parser.parse_args()
    BASE_PATH = args.path
    ANIMATION = args.animation
    IMAGE_ALIGN_STRATEGY = AlignStrategy[args.image_align]
    IMAGE_ALIGN_TOLERANCE = args.image_align_tolerance
    IMAGE_FORMATS = args.image_formats
    PLOTS = not args.no_plots
    PLATE_LABELS = {
        plate_id: label
        for plate_id, label in enumerate(args.plate_labels, start=1)
    }
    PLATE_LATTICE = tuple(args.plate_lattice)
    PLATE_SIZE = int(
        imaging.mm_to_pixels(args.plate_size,
                             dots_per_inch=args.dots_per_inch))
    PLATE_EDGE_CUT = int(round(PLATE_SIZE * (args.plate_edge_cut / 100)))
    SILENT = args.silent
    USE_CACHED = args.use_cached_data
    VERBOSE = args.verbose
    POOL_MAX = 1
    if not args.single_process:
        POOL_MAX = cpu_count() - 1 if cpu_count() > 1 else 1

    if not SILENT:
        print("Starting ColonyScanalyser analysis")
    if VERBOSE and POOL_MAX > 1:
        print(
            f"Multiprocessing enabled, utilising {POOL_MAX} of {cpu_count()} processors"
        )

    # Resolve working directory
    if BASE_PATH is None:
        raise ValueError("A path to a working directory must be supplied")
    else:
        BASE_PATH = Path(args.path).resolve()
    if not BASE_PATH.exists():
        raise EnvironmentError(
            f"The supplied folder path could not be found: {BASE_PATH}")
    if not SILENT:
        print(f"Working directory: {BASE_PATH}")

    # Check if processed image data is already stored and can be loaded
    plates = None
    if USE_CACHED:
        if not SILENT:
            print("Attempting to load cached data")
        plates = file_access.load_file(BASE_PATH.joinpath(
            config.DATA_DIR, config.CACHED_DATA_FILE_NAME),
                                       file_access.CompressionMethod.LZMA,
                                       pickle=True)
        # Check that segmented image data has been loaded for all plates
        # Also that data is not from an older format (< v0.4.0)
        if (VERBOSE and plates is not None and plates.count
                == PlateCollection.coordinate_to_index(PLATE_LATTICE)
                and isinstance(plates.items[0], Plate)):
            print("Successfully loaded cached data")
            image_files = None
        else:
            print("Unable to load cached data, starting image processing")
            plates = None

    if not USE_CACHED or plates is None:
        # Find images in working directory. Raises IOError if images not loaded correctly
        image_files = ImageFileCollection.from_path(BASE_PATH,
                                                    IMAGE_FORMATS,
                                                    cache_images=False)
        if not SILENT:
            print(f"{image_files.count} images found")

        # Verify image alignment
        if IMAGE_ALIGN_STRATEGY != AlignStrategy.none:
            if not SILENT:
                print(
                    f"Verifying image alignment with '{IMAGE_ALIGN_STRATEGY.name}' strategy. This process will take some time"
                )

            # Initialise the model and determine which images need alignment
            align_model, image_files_align = calculate_transformation_strategy(
                image_files.items,
                IMAGE_ALIGN_STRATEGY,
                tolerance=IMAGE_ALIGN_TOLERANCE)

            # Apply image alignment according to selected strategy
            if len(image_files_align) > 0:
                if not SILENT:
                    print(
                        f"{len(image_files_align)} of {image_files.count} images require alignment"
                    )

                with Pool(processes=POOL_MAX) as pool:
                    results = list()
                    job = pool.imap_unordered(func=partial(
                        apply_align_transform, align_model=align_model),
                                              iterable=image_files_align,
                                              chunksize=2)
                    # Store results and update progress bar
                    for i, result in enumerate(job, start=1):
                        results.append(result)
                        if not SILENT:
                            utilities.progress_bar(
                                (i / len(image_files_align)) * 100,
                                message="Correcting image alignment")

                    image_files.update(results)

        # Process images to Timepoint data objects
        plate_images_mask = None
        plate_timepoints = defaultdict(list)

        if not SILENT:
            print("Preprocessing images to locate plates")

        # Load the first image to get plate coordinates and mask
        with image_files.items[0] as image_file:
            # Only find centers using first image. Assume plates do not move
            if plates is None:
                if VERBOSE:
                    print(
                        f"Locating plate centres in image: {image_file.file_path}"
                    )

                # Create new Plate instances to store the information
                plates = PlateCollection.from_image(
                    shape=PLATE_LATTICE,
                    image=image_file.image_gray,
                    diameter=PLATE_SIZE,
                    search_radius=PLATE_SIZE // 20,
                    edge_cut=PLATE_EDGE_CUT,
                    labels=PLATE_LABELS)

                if not plates.count > 0:
                    if not SILENT:
                        print(
                            f"Unable to locate plates in image: {image_file.file_path}"
                        )
                        print(f"Processing unable to continue")
                    sys.exit()

                if VERBOSE:
                    for plate in plates.items:
                        print(f"Plate {plate.id} center: {plate.center}")

            # Use the first plate image as a noise mask
            plate_noise_masks = plates.slice_plate_image(image_file.image_gray)

        if not SILENT:
            print("Processing colony data from all images")

        # Process images to Timepoints
        with Pool(processes=POOL_MAX) as pool:
            results = list()
            job = pool.imap(func=partial(image_file_to_timepoints,
                                         plates=plates,
                                         plate_noise_masks=plate_noise_masks),
                            iterable=image_files.items,
                            chunksize=2)
            # Store results and update progress bar
            for i, result in enumerate(job, start=1):
                results.append(result)
                if not SILENT:
                    utilities.progress_bar((i / image_files.count) * 100,
                                           message="Processing images")
            plate_timepoints = utilities.dicts_merge(list(results))

        if not SILENT:
            print("Calculating colony properties")

        # Calculate deviation in timestamps (i.e. likelihood of missing data)
        timestamp_diff_std = diff(
            image_files.timestamps_elapsed_seconds[1:]).std()
        timestamp_diff_std += config.COLONY_TIMESTAMP_DIFF_MAX

        # Group and consolidate Timepoints into Colony instances
        plates = plates_colonies_from_timepoints(plates, plate_timepoints,
                                                 config.COLONY_DISTANCE_MAX,
                                                 timestamp_diff_std, POOL_MAX)

        if not any([plate.count for plate in plates.items]):
            if not SILENT:
                print("Unable to locate any colonies in the images provided")
                print(f"ColonyScanalyser analysis completed for: {BASE_PATH}")
            sys.exit()
        elif not SILENT:
            for plate in plates.items:
                print(f"{plate.count} colonies identified on plate {plate.id}")

    # Store pickled data to allow quick re-use
    save_path = file_access.create_subdirectory(BASE_PATH, config.DATA_DIR)
    save_path = save_path.joinpath(config.CACHED_DATA_FILE_NAME)
    save_status = file_access.save_file(save_path, plates,
                                        file_access.CompressionMethod.LZMA)
    if not SILENT:
        if save_status:
            print(f"Cached data saved to {save_path}")
        else:
            print(
                f"An error occurred and cached data could not be written to disk at {save_path}"
            )

    # Store colony data in CSV format
    if not SILENT:
        print("Saving data to CSV")

    save_path = BASE_PATH.joinpath(config.DATA_DIR)
    for plate in plates.items:
        # Save data for all colonies on one plate
        plate.colonies_to_csv(save_path)
        # Save data for each colony on a plate
        plate.colonies_timepoints_to_csv(save_path)

    # Save summarised data for all plates
    plates.plates_to_csv(save_path)

    # Only generate plots when working with original images
    # Can't guarantee that the original images and full list of time points
    # will be available when using cached data
    if image_files is not None:
        if PLOTS or ANIMATION:
            save_path = file_access.create_subdirectory(
                BASE_PATH, config.PLOTS_DIR)
        if PLOTS:
            if not SILENT:
                print("Saving plots")
            # Summary plots for all plates
            plots.plot_growth_curve(plates.items, save_path)
            plots.plot_appearance_frequency(
                plates.items,
                save_path,
                timestamps=image_files.timestamps_elapsed)
            plots.plot_appearance_frequency(
                plates.items,
                save_path,
                timestamps=image_files.timestamps_elapsed,
                bar=True)
            plots.plot_doubling_map(plates.items, save_path)
            plots.plot_colony_map(image_files.items[-1].image, plates.items,
                                  save_path)

            for plate in plates.items:
                if VERBOSE:
                    print(f"Saving plots for plate #{plate.id}")
                save_path_plate = file_access.create_subdirectory(
                    save_path,
                    file_access.file_safe_name(
                        [f"plate{plate.id}", plate.name]))
                # Plot colony growth curves, ID map and time of appearance for each plate
                plots.plot_growth_curve([plate], save_path_plate)
                plots.plot_appearance_frequency(
                    [plate],
                    save_path_plate,
                    timestamps=image_files.timestamps_elapsed)
                plots.plot_appearance_frequency(
                    [plate],
                    save_path_plate,
                    timestamps=image_files.timestamps_elapsed,
                    bar=True)

        if ANIMATION:
            # Plot individual plate images as an animation
            if not SILENT:
                print(
                    "Saving plate image animations. This may take several minutes"
                )

            # Original size images
            plots.plot_plate_images_animation(plates,
                                              image_files,
                                              save_path,
                                              fps=8,
                                              pool_max=POOL_MAX,
                                              image_size_maximum=(800, 800))
            # Smaller images
            plots.plot_plate_images_animation(
                plates,
                image_files,
                save_path,
                fps=8,
                pool_max=POOL_MAX,
                image_size=(250, 250),
                image_name="plate_image_animation_small")

    else:
        if not SILENT:
            print(
                "Unable to generate plots or animations from cached data. Run analysis on original images to generate plot images"
            )

    if not SILENT:
        print(f"ColonyScanalyser analysis completed for: {BASE_PATH}")

    sys.exit()