diff --git a/.gitignore b/.gitignore index 2303f13..59be580 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,8 @@ # Ignore -# Cache +# Caches __pycache__/ +.pytest_cache/ # Virtual environments venv*/ diff --git a/dicomparser/DICOMParser.py b/dicomparser/DICOMParser.py index 3d2544c..09ec3f5 100755 --- a/dicomparser/DICOMParser.py +++ b/dicomparser/DICOMParser.py @@ -134,18 +134,55 @@ def extract_common_metadata(self): @staticmethod def get_bscan_images_from_pixel_array(pixel_arr): - bscan_count = pixel_arr.shape[0] - bscan_images = {} + """ + Converts a 3D numpy array to a dictionary of PIL Image objects. + + Parameters: + ----------- + pixel_arr : numpy.ndarray + 3D array with shape (n_scans, height, width) + First dimension represents individual B-scan slices + + Returns: + -------- + dict + Dictionary with keys "bscan{i+1}" and values as PIL Image objects + B-scans are numbered starting from 1 in the output keys + """ + bscan_count = pixel_arr.shape[0] # Get number of B-scans + bscan_images = {} # Initialize empty dictionary + + # Process each B-scan slice for i in range(bscan_count): + # Convert 2D array slice to PIL Image bscan_image = Image.fromarray(pixel_arr[i, :, :]) + + # Store in dictionary with 1-based indexing bscan_images[f"bscan{i+1}"] = bscan_image return bscan_images @staticmethod def save_bscan_images(meta, output_pth): + """ + Saves B-scan images from the metadata dictionary to the specified output path. + + Parameters: + ----------- + meta : dict + Dictionary containing 'SOP Instance' ID and 'bscan_images' dictionary + 'bscan_images' should contain PIL Image objects + + output_pth : str + Base directory where images will be saved + """ + # Create folder path using SOP Instance ID sop_path = os.path.join(output_pth, f"{meta['SOP Instance']}") - if not os.path.exists(sop_path): os.makedirs(sop_path) # make pdf (png) folder + + # Create directory if it doesn't exist + if not os.path.exists(sop_path): os.makedirs(sop_path) + + # Save each B-scan image as PNG for bscan in meta['bscan_images'].keys(): meta['bscan_images'][bscan].save(os.path.join(sop_path, f"{bscan}.png")) diff --git a/pyproject.toml b/pyproject.toml index 6ad3724..2010b24 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,12 +6,15 @@ authors = [ {name = "bbearce",email = "bbearce@gmail.com"} ] readme = "README.md" -requires-python = ">=3.12" +requires-python = ">=3.10" dependencies = [ "hvf-extraction-script (>=0.0.4,<0.0.5)", "matplotlib (>=3.10.1,<4.0.0)", "pymupdf (>=1.25.4,<2.0.0)", - "oct-converter (>=0.6.0,<0.6.3)" + "oct-converter (>=0.6.0,<0.6.3)", + "pytest (>=8.3.5, <8.3.6)", + "pytest-cov (>=6.1.1,<6.1.2)", + "tox (>=4.25.0,<4.26.0)" ] diff --git a/tests/dicomparser/base_class/test_get_bscan_images_from_pixel_array.py b/tests/dicomparser/base_class/test_get_bscan_images_from_pixel_array.py new file mode 100644 index 0000000..1a8db9e --- /dev/null +++ b/tests/dicomparser/base_class/test_get_bscan_images_from_pixel_array.py @@ -0,0 +1,41 @@ +import pytest +import numpy as np +from PIL import Image +from dicomparser.DICOMParser import DICOMParser + +@pytest.mark.filterwarnings("ignore") +@pytest.mark.parametrize("pixel_arr, expected_keys, expected_shapes", [ + # Test case 1: Single B-scan, 2x2 image + (np.array([[[1, 2], + [3, 4]]], dtype=np.uint8), + ["bscan1"], + [(2, 2)]), + + # Test case 2: Two B-scans, 3x3 images + (np.array([[[1, 2, 3], + [4, 5, 6], + [7, 8, 9]], + [[9, 8, 7], + [6, 5, 4], + [3, 2, 1]]], dtype=np.uint8), + ["bscan1", "bscan2"], + [(3, 3), (3, 3)]), + + # Test case 3: Empty input (0 B-scans) + (np.empty((0, 2, 2), dtype=np.uint8), + [], + []), +]) +def test_get_bscan_images_from_pixel_array(pixel_arr, expected_keys, expected_shapes): + """ + Test the conversion of a 3D numpy array to a dictionary of PIL Images. + """ + dicom_path = '/persist/QTIM/Active/23-0284/dashboard/Data/topcon_samples_from_steve_03_27_2025/OCT/100100_10142024_151925_OP_R_001.dcm' + dicom_parser = DICOMParser(dicom_path=dicom_path) + # Act + result = dicom_parser.get_bscan_images_from_pixel_array(pixel_arr) + + # Assert + assert list(result.keys()) == expected_keys + assert all(isinstance(result[k], Image.Image) for k in expected_keys) + assert [result[k].size for k in expected_keys] == [(w, h) for h, w in expected_shapes] diff --git a/tests/dicomparser/base_class/test_save_bscan_images.py b/tests/dicomparser/base_class/test_save_bscan_images.py new file mode 100644 index 0000000..42f587c --- /dev/null +++ b/tests/dicomparser/base_class/test_save_bscan_images.py @@ -0,0 +1,50 @@ +import pytest +import numpy as np +import os +from PIL import Image +from dicomparser.DICOMParser import DICOMParser + +@pytest.fixture +def dicom_parser(): + dicom_path = '/persist/QTIM/Active/23-0284/dashboard/Data/topcon_samples_from_steve_03_27_2025/OCT/100100_10142024_151925_OP_R_001.dcm' + return DICOMParser(dicom_path=dicom_path) + +@pytest.mark.parametrize("bscan_count, img_shape", [ + (1, (10, 10)), + (3, (5, 5)), + (19, (8, 8)) +]) +def test_save_bscan_images(dicom_parser, tmp_path, bscan_count, img_shape): + """ + Test that B-scan images are correctly saved to disk and match the original image content. + """ + # Create dummy pixel data and images + bscan_images = {} + original_arrays = [] + + for i in range(bscan_count): + arr = np.random.randint(0, 255, img_shape, dtype=np.uint8) + original_arrays.append(arr) + bscan_images[f"bscan{i+1}"] = Image.fromarray(arr) + + meta = { + "SOP Instance": "TEST123", + "bscan_images": bscan_images + } + + # Use temporary directory for output + dicom_parser.save_bscan_images(meta, tmp_path) + + # Check that all images were saved correctly + saved_dir = os.path.join(tmp_path, "TEST123") + assert os.path.isdir(saved_dir) + + for i in range(bscan_count): + fname = f"bscan{i+1}.png" + fpath = os.path.join(saved_dir, fname) + assert os.path.exists(fpath) + + # Open saved image and compare pixel values + saved_img = Image.open(fpath).convert("L") # Ensure grayscale + saved_arr = np.array(saved_img) + np.testing.assert_array_equal(saved_arr, original_arrays[i]) diff --git a/tests/example/test_example.py b/tests/example/test_example.py new file mode 100644 index 0000000..356b9d9 --- /dev/null +++ b/tests/example/test_example.py @@ -0,0 +1,19 @@ +import pytest + + +@pytest.mark.parametrize("num1, num2, expected", [ + (2, 3, 6), + (5, 4, 20), + (10, 0, 0), + (-2, 3, -6), + (2.5, 2, 5.0), +]) +def test_multiplication(num1, num2, expected): + """ + Test the multiplication of two numbers. + """ + # Act + result = num1 * num2 + + # Assert + assert result == expected \ No newline at end of file