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
def test_linecount_finished(self, capsys): progress_bar(100) captured = capsys.readouterr() assert self.count_lines(captured.out) == 2
def progress_update(result, progress): utilities.progress_bar(progress, message = "Processing images")
def test_linecount(self, capsys): progress_bar(50) progress_bar(75.4) captured = capsys.readouterr() assert self.count_lines(captured.out) == 1
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()