diff --git a/.pre-commit-config.yaml b/.github/workflows/pre-commit-config.yml similarity index 100% rename from .pre-commit-config.yaml rename to .github/workflows/pre-commit-config.yml diff --git a/.github/workflows/style.yml b/.github/workflows/style.yml new file mode 100644 index 0000000000..d8e1edd689 --- /dev/null +++ b/.github/workflows/style.yml @@ -0,0 +1,43 @@ +name: Style check + +on: + push: + branches: + - main + - master + + pull_request: + branches: + - main + - master + +jobs: + flake8_py3: + permissions: write-all + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - uses: actions/setup-python@v5 + with: + python-version: "3.10" + + - name: Install flake8 and plugins + run: | + pip install flake8 flake8-docstrings flake8-annotations + + - name: Configure Flake8 + run: | + echo "[flake8]" > .flake8 + echo "extend-ignore = E402" >> .flake8 + echo "exclude = .github,autoop/tests" >> .flake8 + # exclude A101, A102, D100 and everything that starts with D2 and D4 + echo "ignore = ANN101,ANN102,D100,D2,D4,ANN002,ANN003" >> .flake8 + + - name: Run flake8 + uses: suo/flake8-github-action@releases/v1 + with: + checkName: "flake8_py3" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/main.py b/main.py index e6e9af30ea..e69de29bb2 100644 --- a/main.py +++ b/main.py @@ -1,8 +0,0 @@ -# This is a sample Python script. - -def hello_world(): - return "Hello, World!" - - -if __name__ == '__main__': - hello_world() diff --git a/__init__.py b/project_name/data/temp.py similarity index 100% rename from __init__.py rename to project_name/data/temp.py diff --git a/project_name/__init__.py b/project_name/features/temp.py similarity index 100% rename from project_name/__init__.py rename to project_name/features/temp.py diff --git a/project_name/models/Preprocessing_class.py b/project_name/models/Preprocessing_class.py new file mode 100644 index 0000000000..81d542222e --- /dev/null +++ b/project_name/models/Preprocessing_class.py @@ -0,0 +1,151 @@ +import numpy as np +import matplotlib.pyplot as plt +from typing import Tuple, Union, List + + +class Preprocessing: + def __init__(self, tile_size: Tuple[int, int] = (256, 256)) -> None: + """ + Initialize the Preprocessing class with a tile size. + """ + self.tile_size = tile_size + self.last_padding_info: dict[int, dict] = {} + + def is_8_bit(self, np_array: np.ndarray) -> bool: + """ + Check if a numpy array is of type uint8. + """ + return np_array.dtype == np.uint8 + + def normalize(self, np_array: np.ndarray) -> np.ndarray: + """ + Normalize an array to the [0, 1] range as float32. + """ + if self.is_8_bit(np_array): + return np_array.astype(np.float32) / 255.0 + if np.issubdtype(np_array.dtype, np.floating): + return np_array.astype(np.float32) + return (np_array / np.iinfo(np_array.dtype).max).astype(np.float32) + + def tile_with_padding( + self, + np_arrays: Union[np.ndarray, List[np.ndarray]], + pad_mode: str = 'constant' + ) -> np.ndarray: + """ + Tile one or more images with padding to fit the specified tile size. + """ + if not isinstance(np_arrays, (list, tuple)): + np_arrays = [np_arrays] + + all_tiles = [] + for idx, np_array in enumerate(np_arrays): + if not isinstance(np_array, np.ndarray): + raise TypeError("Input must be numpy array") + if not self.is_8_bit(np_array): + raise ValueError("Input must be uint8 array") + + original_shape = np_array.shape + normalized = self.normalize(np_array.copy()) + + tile_h, tile_w = self.tile_size + h, w = original_shape[:2] + + pad_h = (tile_h - (h % tile_h)) % tile_h + pad_w = (tile_w - (w % tile_w)) % tile_w + + self.last_padding_info[idx] = { + 'original_shape': original_shape, + 'pad_h': pad_h, + 'pad_w': pad_w, + 'is_grayscale': len(original_shape) == 2 + } + + if len(original_shape) == 3: + pad_width = ((0, pad_h), (0, pad_w), (0, 0)) + else: + pad_width = ((0, pad_h), (0, pad_w)) + + padded = np.pad(normalized, pad_width, mode=pad_mode) + padded_h, padded_w = padded.shape[:2] + + tiles = [] + for i in range(0, padded_h, tile_h): + for j in range(0, padded_w, tile_w): + tile = padded[i:i + tile_h, j:j + tile_w] + if tile.shape[:2] != (tile_h, tile_w): + tile = np.pad( + tile, + ((0, tile_h - tile.shape[0]), + (0, tile_w - tile.shape[1])), + mode=pad_mode + ) + tiles.append(tile) + all_tiles.extend(tiles) + + return np.array(all_tiles) + + def reconstruct_image( + self, + tiles: np.ndarray, + original_idx: int = 0 + ) -> np.ndarray: + """ + Reconstruct the original image from tiles. + """ + info = self.last_padding_info.get(original_idx) + if not info: + raise ValueError("No padding info found for this index") + + tile_h, tile_w = self.tile_size + h, w = info['original_shape'][:2] + pad_h = info['pad_h'] + pad_w = info['pad_w'] + + rows = (h + pad_h) // tile_h + cols = (w + pad_w) // tile_w + + if info['is_grayscale']: + recon_shape = (h + pad_h, w + pad_w) + else: + recon_shape = (h + pad_h, w + pad_w, info['original_shape'][2]) + + reconstructed = np.zeros(recon_shape, dtype=np.float32) + + for i in range(rows): + for j in range(cols): + idx = i * cols + j + reconstructed[ + i * tile_h:(i + 1) * tile_h, + j * tile_w:(j + 1) * tile_w + ] = tiles[idx] + + final_image = np.clip(reconstructed[:h, :w], 0, 1) + if info['is_grayscale']: + final_image = np.squeeze(final_image) + + return (final_image * 255).astype(np.uint8) + + def depth_to_rgb( + self, + depth_map: np.ndarray, + cmap: str = 'plasma' + ) -> np.ndarray: + """ + Convert a 2D depth map to a 3-channel RGB image using a colormap. + """ + if not isinstance(depth_map, np.ndarray) or depth_map.ndim != 2: + raise ValueError("Input must be a 2D numpy array " + "(grayscale depth map)") + + min_val = np.min(depth_map) + max_val = np.max(depth_map) + if max_val - min_val == 0: + norm_depth = np.zeros_like(depth_map, dtype=np.float32) + else: + norm_depth = (depth_map - min_val) / (max_val - min_val) + + colormap = plt.get_cmap(cmap) + colored = colormap(norm_depth) # Returns RGBA + rgb = (colored[:, :, :3] * 255).astype(np.uint8) + return rgb diff --git a/project_name/models/__init__.py b/project_name/models/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/data/__init__.py b/tests/data/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/project_name/data/__init__.py b/tests/data/temp.py similarity index 100% rename from project_name/data/__init__.py rename to tests/data/temp.py diff --git a/tests/features/__init__.py b/tests/features/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/project_name/features/__init__.py b/tests/features/temp.py similarity index 100% rename from project_name/features/__init__.py rename to tests/features/temp.py diff --git a/tests/models/__init__.py b/tests/models/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/models/unittest_preprocessing.py b/tests/models/unittest_preprocessing.py new file mode 100644 index 0000000000..b346403e88 --- /dev/null +++ b/tests/models/unittest_preprocessing.py @@ -0,0 +1,103 @@ +import unittest +import numpy as np +from project_name.models.Preprocessing_class import Preprocessing + + +class TestPreprocessing(unittest.TestCase): + def setUp(self): + self.tile_size = (256, 256) + self.preprocessor = Preprocessing(tile_size=self.tile_size) + self.rgb_image = np.random.randint( + 0, 256, (510, 510, 3), dtype=np.uint8 + ) + self.gray_image = np.random.randint( + 0, 256, (510, 510), dtype=np.uint8 + ) + + def test_is_8_bit(self): + self.assertTrue(self.preprocessor.is_8_bit(self.rgb_image)) + self.assertFalse( + self.preprocessor.is_8_bit( + self.rgb_image.astype(np.float32) + ) + ) + + def test_normalize_uint8(self): + norm = self.preprocessor.normalize(self.rgb_image) + self.assertTrue(np.all(norm >= 0.0) and np.all(norm <= 1.0)) + self.assertEqual(norm.dtype, np.float32) + + def test_normalize_float(self): + float_array = self.rgb_image.astype(np.float32) / 255.0 + norm = self.preprocessor.normalize(float_array) + self.assertTrue(np.allclose(float_array, norm)) + self.assertEqual(norm.dtype, np.float32) + + def test_tile_with_padding_rgb(self): + tiles = self.preprocessor.tile_with_padding(self.rgb_image) + expected_num_tiles = ( + (510 + (256 - 510 % 256)) // 256 + ) ** 2 # 2x2 = 4 + self.assertEqual(tiles.shape[0], expected_num_tiles) + self.assertEqual(tiles.shape[1:], (256, 256, 3)) + + def test_tile_with_padding_grayscale(self): + tiles = self.preprocessor.tile_with_padding(self.gray_image) + expected_num_tiles = ( + (510 + (256 - 510 % 256)) // 256 + ) ** 2 # 2x2 = 4 + self.assertEqual(tiles.shape[0], expected_num_tiles) + self.assertEqual(tiles.shape[1:], (256, 256)) + + def test_reconstruction_rgb(self): + tiles = self.preprocessor.tile_with_padding(self.rgb_image) + recon = self.preprocessor.reconstruct_image(tiles) + self.assertEqual(recon.shape, self.rgb_image.shape) + self.assertTrue(np.allclose(recon, self.rgb_image, atol=2)) + + def test_reconstruction_grayscale(self): + tiles = self.preprocessor.tile_with_padding(self.gray_image) + recon = self.preprocessor.reconstruct_image(tiles) + self.assertEqual(recon.shape, self.gray_image.shape) + self.assertTrue(np.allclose(recon, self.gray_image, atol=2)) + + def test_invalid_input_type(self): + with self.assertRaises(TypeError): + self.preprocessor.tile_with_padding("not an array") + + def test_invalid_input_dtype(self): + with self.assertRaises(ValueError): + self.preprocessor.tile_with_padding( + np.ones((100, 100), dtype=np.float32) + ) + + def test_missing_padding_info(self): + with self.assertRaises(ValueError): + self.preprocessor.reconstruct_image( + np.zeros((4, 256, 256, 3)) + ) + + def test_depth_to_rgb(self): + # Create a mock depth map with float32 values from 0 to 10 + depth_map = np.random.uniform( + low=0.0, high=10.0, size=(480, 640) + ).astype(np.float32) + rgb_depth = self.preprocessor.depth_to_rgb( + depth_map, cmap='plasma' + ) + + self.assertEqual(rgb_depth.shape, (480, 640, 3)) + self.assertEqual(rgb_depth.dtype, np.uint8) + self.assertTrue( + np.all(rgb_depth >= 0) and np.all(rgb_depth <= 255) + ) + self.assertGreater(np.std(rgb_depth), 0) + + with self.assertRaises(ValueError): + self.preprocessor.depth_to_rgb( + np.ones((10, 10, 3)) # Not 2D + ) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_main.py b/tests/test_main.py deleted file mode 100644 index 1a0f0a6355..0000000000 --- a/tests/test_main.py +++ /dev/null @@ -1,11 +0,0 @@ -import unittest -from main import hello_world - - -class MainTest(unittest.TestCase): - def test_hello(self): - self.assertEqual(hello_world(), "Hello, World!") - - -if __name__ == '__main__': - unittest.main()