From 48242e18059acdf06a5be105ac24cc4512d562b4 Mon Sep 17 00:00:00 2001 From: Elaine Ho Date: Thu, 1 Dec 2022 11:57:28 +0000 Subject: [PATCH 1/6] create Quoll Image + tiles from np array --- src/quoll/io/reader.py | 61 +++++++++++++++++++++++++++++------------- src/quoll/io/tiles.py | 46 +++++++++++++++++-------------- 2 files changed, 68 insertions(+), 39 deletions(-) diff --git a/src/quoll/io/reader.py b/src/quoll/io/reader.py index 6372c83..051f3ea 100644 --- a/src/quoll/io/reader.py +++ b/src/quoll/io/reader.py @@ -12,15 +12,16 @@ # either express or implied. See the License for the specific # language governing permissions and limitations under the License. -import mrcfile import os -import skimage -import tifffile +from typing import Optional + import matplotlib.pyplot as plt +import mrcfile +import numpy as np +import tifffile from quoll.io.mdoc_parser import Mdoc -from typing import Optional class Image: """ @@ -29,23 +30,33 @@ class Image: def __init__( self, - filename: str, + filename: Optional[str] = None, + img_data: Optional[np.ndarray] = None, pixel_size: Optional[float] = None, unit: Optional[str] = None, ): - """ - Initialises an image object + """Class holding Image data + metadata Args: - filename (str): Path to the image - pixel_size (Optional[float], optional): Size of pixel in physical units. Defaults to None. - unit (Optional[str], optional): Unit of pixel size. Defaults to None. + filename (Optional[str], optional): Filename for the image, + in tiff or mrc. Defaults to None. + img_data (Optional[np.ndarray], optional): Array holding image + data. Defaults to None. + pixel_size (Optional[float], optional): Pixel size. + Defaults to None. + unit (Optional[str], optional): Physical units. Defaults to None. """ self.filename = filename self.pixel_size = pixel_size self.unit = unit - self.get_image() + + if img_data is not None: + self.img_data = img_data + self.img_dims = self.img_data.shape + self.img_bitdepth = self.img_data.dtype + else: + self.get_image() # extra attributes for holding tiles self.tiles = {} @@ -55,15 +66,23 @@ def get_image(self): """ Import an image """ - if os.path.splitext(self.filename)[1] != ".mrc": - self.img_data = tifffile.imread(self.filename) - - elif os.path.splitext(self.filename)[1] == ".mrc": - with mrcfile.open(self.filename) as mrc: - self.img_data = mrc.data + if self.filename is not None: + if os.path.splitext(self.filename)[1] != ".mrc": + self.img_data = tifffile.imread(self.filename) + + elif os.path.splitext(self.filename)[1] == ".mrc": + with mrcfile.open(self.filename) as mrc: + self.img_data = mrc.data + + else: + raise IOError( + "Image cannot be imported. Quoll uses skimage and mrcfile" + ) else: - raise IOError("Image cannot be imported. Quoll uses skimage and mrcfile") + raise IOError( + "No filename or image data has been specified." + ) self.img_dims = self.img_data.shape self.img_bitdepth = self.img_data.dtype @@ -99,7 +118,11 @@ def __init__( mdoc_file: Optional[str] = None, tilt_angle_file: Optional[str] = None, ): - super().__init__(filename, pixel_size, unit) + super().__init__( + filename=filename, + pixel_size=pixel_size, + unit=unit + ) self.img_type = "TiltSeries" self.mdoc_file = mdoc_file self.tilt_angle_file = tilt_angle_file diff --git a/src/quoll/io/tiles.py b/src/quoll/io/tiles.py index aba9a6a..29e3ad6 100644 --- a/src/quoll/io/tiles.py +++ b/src/quoll/io/tiles.py @@ -76,7 +76,7 @@ def extract_tiles(Image, tile_size_x, tile_size_y, pad=True): return tiles, (nTilesX, nTilesY) -def create_patches(Image, tile_size, tiles_output, pad=True): +def create_patches(Image, tile_size, tiles_output=None, pad=True): """Split an image into square patches. All patches will be square if pad=True. Otherwise, patches at the edges do not fit neatly into the square patches will be returned (these are not square). Patches are saved as .tif's in tiles_output. @@ -93,25 +93,31 @@ def create_patches(Image, tile_size, tiles_output, pad=True): tiles, (nTilesX, nTilesY) = extract_tiles(Image, tile_size, tile_size, pad) resolution = (1/Image.pixel_size, 1/Image.pixel_size) - if os.path.isdir(tiles_output) is False: - os.mkdir(tiles_output) - if len(os.listdir(tiles_output)) != 0: - for f in list(os.listdir(tiles_output)): - try: - os.remove(os.path.join(tiles_output, f)) - except: - print(f"Could not remove {f}") - for i, tile in enumerate(tiles): - tile_fn = os.path.join(tiles_output, f"{i:03}.tif") - tifffile.imwrite( - tile_fn, - tile.astype('uint8'), - imagej=True, - resolution=resolution, - metadata={'unit': Image.unit} - ) - - Image.tiles[tile_fn] = tile.astype('uint8') + if tiles_output is not None: + if os.path.isdir(tiles_output) is False: + os.mkdir(tiles_output) + if len(os.listdir(tiles_output)) != 0: + for f in list(os.listdir(tiles_output)): + try: + os.remove(os.path.join(tiles_output, f)) + except: + print(f"Could not remove {f}") + for i, tile in enumerate(tiles): + tile_fn = os.path.join(tiles_output, f"{i:03}.tif") + tifffile.imwrite( + tile_fn, + tile.astype('uint8'), + imagej=True, + resolution=resolution, + metadata={'unit': Image.unit} + ) + + Image.tiles[i] = tile.astype('uint8') + + else: + for i, tile in enumerate(tiles): + Image.tiles[i] = tile.astype('uint8') + Image.tile_arrangement = (nTilesX, nTilesY) From 6874dd5ab1ed59fa088dc61b0a6076f445dadd73 Mon Sep 17 00:00:00 2001 From: Elaine Ho Date: Thu, 1 Dec 2022 11:57:43 +0000 Subject: [PATCH 2/6] calculate frc res from np array --- src/quoll/frc/oneimg.py | 47 ++++++++++++++++++++++++++++------------- 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/src/quoll/frc/oneimg.py b/src/quoll/frc/oneimg.py index 00cc18c..d164fbd 100644 --- a/src/quoll/frc/oneimg.py +++ b/src/quoll/frc/oneimg.py @@ -29,8 +29,8 @@ from miplib.data.containers.fourier_correlation_data import \ FourierCorrelationDataCollection from miplib.data.containers.image import Image as miplibImage -from miplib.data.io import read as miplibread from miplib.processing import windowing + from quoll.io import reader, tiles @@ -44,13 +44,13 @@ def miplib_oneimg_FRC_calibrated( """ Calculate single image FRC, adapted from miplib Args: - image (miplib image object): x-values in the FRC curve + image (miplib.data.containers.image.Image): Image to be evaluated args (argparse Namespace): parameters for FRC calculation. see *miplib.ui.frc_options* for details. default = None - calibration_func (callable): function that applies a correction factor to the - frequencies of the FRC curve to match the 1 img - FRC to the 2 img FRC + calibration_func (callable): function that applies a correction factor + to the frequencies of the FRC curve to + match the 1 img FRC to the 2 img FRC Returns: FourierCorrelationData object: special dict which contains the results. @@ -91,7 +91,11 @@ def miplib_oneimg_FRC_calibrated( frc_data[0].correlation["frequency"] = calibration_func(freqs) # Analyze results - analyzer = frc_analysis.FourierCorrelationAnalysis(frc_data, image1.spacing[0], args) + analyzer = frc_analysis.FourierCorrelationAnalysis( + frc_data, + image1.spacing[0], + args + ) result = analyzer.execute(z_correction=z_correction)[0] @@ -127,7 +131,10 @@ def calc_frc_res(Image: reader.Image): FourierCorrelationData object: special dict which contains the results. """ if Image.img_dims[0] == Image.img_dims[1]: - miplibImg = miplibread.get_image(Image.filename) + miplibImg = miplibImage( + Image.img_data, + (Image.pixel_size, Image.pixel_size) + ) args = opts.get_frc_script_options([None]) result = miplib_oneimg_FRC_calibrated( miplibImg, @@ -142,33 +149,43 @@ def calc_frc_res(Image: reader.Image): def calc_local_frc( Image: reader.Image, tile_size: int, - tiles_dir: str, + tiles_dir: Optional[str] = None, ): """ Calculates local FRC on a quoll Image - Image is split into square tiles and FRC resolution is calculated for all tiles + Image is split into square tiles and FRC resolution is calculated for all + tiles Args: Image (reader.Image): Quoll.io.reader.Image instance tile_size (int): length of one side of the square tile in pixels - tiles_dir (str): path to directory holding tiles + tiles_dir (str): path to directory holding tiles, none by default. + Tiles only saved if tiles_dir is not none. Returns: pandas DataFrame: df containing the resolutions in physical units of all tiles """ # Create patches - tiles.create_patches(Image, tile_size, tiles_dir) + tiles.create_patches( + Image=Image, + tile_size=tile_size, + tiles_output=tiles_dir + ) # Calculate local FRC on the patches resolutions = {"Resolution": {}} - for tile in os.listdir(tiles_dir): + for i in list(Image.tiles.keys()): try: - Img = reader.Image(os.path.join(tiles_dir, tile)) + Img = reader.Image( + img_data=Image.tiles[i], + pixel_size=Image.pixel_size, + unit=Image.unit, + ) result = calc_frc_res(Img) - resolutions["Resolution"][tile] = result.resolution["resolution"] + resolutions["Resolution"][i] = result.resolution["resolution"] except: - resolutions["Resolution"][tile] = np.nan + resolutions["Resolution"][i] = np.nan return pd.DataFrame.from_dict(resolutions) From de5405c576a9aeb8af56049736b9cfe75d265001 Mon Sep 17 00:00:00 2001 From: Elaine Ho Date: Thu, 1 Dec 2022 11:59:19 +0000 Subject: [PATCH 3/6] updated tests --- tests/test_frc_oneimg.py | 2 +- tests/test_reader.py | 16 ++++++++++++++++ tests/test_tiles.py | 4 ++-- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/tests/test_frc_oneimg.py b/tests/test_frc_oneimg.py index a7d8d96..56fd2c1 100644 --- a/tests/test_frc_oneimg.py +++ b/tests/test_frc_oneimg.py @@ -49,7 +49,7 @@ def test_calc_frc_res(self): """ Tests that the one-image FRC can be calculated from quoll.io.reader.Image """ - Img = reader.Image("./data/042.tif") + Img = reader.Image("./data/042.tif", pixel_size=3.3724) result = oneimg.calc_frc_res(Img) self.assertAlmostEqual(result.resolution["resolution"], 32.1159278) self.assertIsNotNone(result) diff --git a/tests/test_reader.py b/tests/test_reader.py index a401d10..b2ec7d2 100644 --- a/tests/test_reader.py +++ b/tests/test_reader.py @@ -15,6 +15,7 @@ # Utility imports import unittest +import numpy as np from quoll.io import reader @@ -74,6 +75,21 @@ def test_3D_mrcreader(self): self.assertEqual(Img.unit, "Angstrom") self.assertEqual(Img.img_dims, (41, 720, 512)) + def test_reader_nparray_input(self): + """ + Tests that Image can be created from np arrays + """ + Img = reader.Image( + img_data=np.random.rand(5,10,10), + pixel_size=1.0, + unit="nm" + ) + + self.assertIsNone(Img.filename) + self.assertEqual(Img.img_dims, (5,10,10)) + self.assertEqual(Img.pixel_size, 1.0) + self.assertEqual(Img.unit, "nm") + def test_TS_importer_mrc_mdoc(self): """ Tests that tilt series can be imported correctly with mdoc diff --git a/tests/test_tiles.py b/tests/test_tiles.py index dcb5c4f..ae94a3f 100644 --- a/tests/test_tiles.py +++ b/tests/test_tiles.py @@ -52,7 +52,7 @@ def test_square_tiles_saving(self): Img, tile_size=128, tiles_output=tiles_dir, - pad=True + pad=True, ) self.assertEqual(len(os.listdir(tiles_dir)), 4) @@ -83,7 +83,7 @@ def test_reassemble_tiles(self): Img, tile_size=128, tiles_output=tiles_dir, - pad=True + pad=True, ) reassembled = tiles.reassemble_tiles( From a4e1513742a175963c1eeb4229651a8d8405a29e Mon Sep 17 00:00:00 2001 From: Elaine Ho Date: Thu, 1 Dec 2022 16:00:32 +0000 Subject: [PATCH 4/6] remove redundant test --- tests/test_frc_oneimg.py | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/tests/test_frc_oneimg.py b/tests/test_frc_oneimg.py index 56fd2c1..4a9dac9 100644 --- a/tests/test_frc_oneimg.py +++ b/tests/test_frc_oneimg.py @@ -19,7 +19,8 @@ import numpy as np import miplib.ui.cli.miplib_entry_point_options as opts -from miplib.data.io import read as miplibread +# from miplib.data.io import read as miplibread +from miplib.data.containers.image import Image as miplibImage from quoll.frc import oneimg from quoll.io import reader @@ -30,21 +31,6 @@ class OneImgFRCTest(unittest.TestCase): Test image = ChargeSuppression/SerialFIB-79, Tile 42 """ - def test_miplib_basic(self): - """ - Tests that the adapted miplib one image FRC works with basic case - """ - miplibImg = miplibread.get_image("./data/042.tif") - args = opts.get_frc_script_options([None]) - - result = oneimg.miplib_oneimg_FRC_calibrated( - miplibImg, - args, - oneimg.calibration_func - ) - - self.assertIsNotNone(result) - def test_calc_frc_res(self): """ Tests that the one-image FRC can be calculated from quoll.io.reader.Image From 4d2a259730e18eeac11855c3e9ea97c4d4a04b76 Mon Sep 17 00:00:00 2001 From: Elaine Ho Date: Thu, 1 Dec 2022 16:46:26 +0000 Subject: [PATCH 5/6] including changes from main (forgot to pull) --- src/quoll/frc/oneimg.py | 3 +++ tests/test_frc_oneimg.py | 23 +++-------------------- 2 files changed, 6 insertions(+), 20 deletions(-) diff --git a/src/quoll/frc/oneimg.py b/src/quoll/frc/oneimg.py index cf8402e..6d6bc5a 100644 --- a/src/quoll/frc/oneimg.py +++ b/src/quoll/frc/oneimg.py @@ -90,6 +90,9 @@ def miplib_oneimg_FRC_calibrated( # Apply correction if calibration_func is not None: frc_data[0].correlation["frequency"] = calibration_func(freqs) + + else: + frc_data[0].correlation["frequency"] = freqs # Analyze results analyzer = frc_analysis.FourierCorrelationAnalysis( diff --git a/tests/test_frc_oneimg.py b/tests/test_frc_oneimg.py index ce259f4..9552c42 100644 --- a/tests/test_frc_oneimg.py +++ b/tests/test_frc_oneimg.py @@ -93,27 +93,10 @@ def test_set_calibration_func(self): In this test, calibration function is set to return the exact same value, so the result should be equal to uncalibrated. """ - Img = reader.Image("./data/042.tif") - result_calibrated = oneimg.calc_frc_res( - Image=Img, - calibration_func=lambda x: x # just return original value - ) - result_uncalibrated = oneimg.calc_frc_res( - Image=Img, - calibration_func=None + Img = reader.Image( + filename="./data/042.tif", + pixel_size=3.3724 ) - self.assertAlmostEqual( - result_calibrated.resolution["resolution"], - result_uncalibrated.resolution["resolution"]) - self.assertIsNotNone(result_calibrated.resolution["resolution"]) - self.assertIsNotNone(result_uncalibrated.resolution["resolution"]) - def test_set_calibration_func(self): - """ - Tests that calibration function can be set to a custom function - In this test, calibration function is set to return the exact same - value, so the result should be equal to uncalibrated. - """ - Img = reader.Image("./data/042.tif") result_calibrated = oneimg.calc_frc_res( Image=Img, calibration_func=lambda x: x # just return original value From 02b5d5fcb14d1dab90333b473a9e1981dc65e95b Mon Sep 17 00:00:00 2001 From: Elaine Ho Date: Tue, 20 Dec 2022 16:42:52 +0000 Subject: [PATCH 6/6] update deps to use minimiplib --- pyproject.toml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6ae22ed..36f65ff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,17 +6,15 @@ build-backend = "setuptools.build_meta" name = "quoll" version = "0.0.2" dependencies = [ - "jpype1==0.7.5", - "pims==0.4.1", "matplotlib", - "miplib", + "miplib @ git+https://github.com/elainehoml/miplib.git@frc_deps_only", "mrcfile", "notebook", "numpy", "pandas", "scikit-image", "scipy", - "tifffile>2021", + "tifffile", ] authors = [ {name="Elaine Ho", email="elaine.ho@rfi.ac.uk"}