diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml new file mode 100644 index 0000000..37d2263 --- /dev/null +++ b/.github/workflows/python-app.yml @@ -0,0 +1,40 @@ +# This workflow will install Python dependencies, run tests and lint with a single version of Python +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python + +name: Python application + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +permissions: + contents: read + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up Python 3.10 + uses: actions/setup-python@v3 + with: + python-version: "3.10" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install flake8 pytest + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + - name: Lint with flake8 + run: | + # stop the build if there are Python syntax errors or undefined names + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + - name: Test with pytest + run: | + cd src + pytest diff --git a/pyproject.toml b/pyproject.toml index 53666ea..d6d9640 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta" [project] name = "pimage" -version = "1.0.9" +version = "1.1.0" keywords = ["image", "copy-move", "attack", "detection"] authors = [ { name="Rahmat Nazali Salimi", email="rahmatnazali95@gmail.com" }, diff --git a/src/pimage/block.py b/src/pimage/block.py index bcb699c..2c36be8 100644 --- a/src/pimage/block.py +++ b/src/pimage/block.py @@ -1,3 +1,5 @@ +from typing import List + import numpy from sklearn.decomposition import PCA @@ -13,8 +15,8 @@ def __init__(self, grayscale_image_block, rgb_image_block, x_coordinate, y_coord Initializing the input image :param grayscale_image_block: grayscale image block :param rgb_image_block: rgb image block - :param x_coordinate: x coordinate (upper-left) - :param y_coordinate: y coordinate (upper-left) + :param x_coordinate: x coordinate (from upper-left) + :param y_coordinate: y coordinate (from upper-left) :return: None """ self.image_grayscale = grayscale_image_block # block of grayscale image @@ -42,13 +44,14 @@ def compute_block(self): ] return block_data_list - def compute_pca(self, precision): + def compute_pca(self, n_components: int = 1, precision: int = 6) -> List[float]: """ Compute Principal Component Analysis from the image block + :param n_components: the number of resulting PCA component :param precision: characteristic features precision :return: Principal Component from the image block """ - pca_module = PCA(n_components=1) + pca_module = PCA(n_components=n_components) if self.is_image_rgb: image_array = numpy.array(self.image_rgb) red_feature = image_array[:, :, 0] @@ -79,7 +82,7 @@ def compute_pca(self, precision): precise_result = [round(element, precision) for element in list(principal_components.flatten())] return precise_result - def compute_characteristic_features(self, precision): + def compute_characteristic_features(self, precision=4) -> List: """ Compute 7 characteristic features from every image blocks :param precision: feature characteristic precision @@ -149,6 +152,16 @@ def compute_characteristic_features(self, precision): else: c7_part2 += self.image_grayscale_pixels[x_coordinate, y_coordinate] + # Prevents ZeroDivisionError with unusual black/white image (usually when testing) + if c4_part1 + c4_part2 == 0: + c4_part2 = 1 + if c5_part1 + c5_part2 == 0: + c5_part2 = 1 + if c6_part1 + c6_part2 == 0: + c6_part2 = 1 + if c7_part1 + c7_part2 == 0: + c7_part2 = 1 + characteristic_feature_list.append(float(c4_part1) / float(c4_part1 + c4_part2)) characteristic_feature_list.append(float(c5_part1) / float(c5_part1 + c5_part2)) characteristic_feature_list.append(float(c6_part1) / float(c6_part1 + c6_part2)) diff --git a/src/pimage/container.py b/src/pimage/container.py index f72d4a2..d048eff 100644 --- a/src/pimage/container.py +++ b/src/pimage/container.py @@ -18,13 +18,13 @@ def get_length(self): """ return self.container.__len__() - def append_block(self, newData): + def append_block(self, block): """ Insert a data block to the container - :param newData: data to be inserted into the block + :param block: data to be inserted into the container :return: None """ - self.container.append(newData) + self.container.append(block) return def sort_by_features(self): diff --git a/src/pimage/image_object.py b/src/pimage/image_object.py index 0e3c452..280ec05 100644 --- a/src/pimage/image_object.py +++ b/src/pimage/image_object.py @@ -147,11 +147,11 @@ def analyze(self): for i in tqdm(range(feature_container_length - 1), disable=not self.verbose): j = i + 1 - result = self.is_valid(i, j) - if result[0]: + is_valid, offset = self.is_valid(i, j) + if is_valid: self.add_dictionary(self.features_container.container[i][0], self.features_container.container[j][0], - result[1]) + offset) z += 1 def is_valid(self, first_block, second_block): @@ -194,8 +194,8 @@ def is_valid(self, first_block, second_block): # compute the pair's magnitude magnitude = numpy.sqrt(math.pow(offset[0], 2) + math.pow(offset[1], 2)) if magnitude >= self.Nd: - return 1, offset - return 0, + return True, offset + return False, None def add_dictionary(self, first_coordinate, second_coordinate, pair_offset): """ diff --git a/src/tests/__init__.py b/src/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/tests/test_block.py b/src/tests/test_block.py new file mode 100644 index 0000000..bd812f5 --- /dev/null +++ b/src/tests/test_block.py @@ -0,0 +1,28 @@ +from pimage.block import Block +from PIL import Image + +rgb_image = Image.new(mode="RGB", size=(64, 64)) +grayscale_image = Image.new(mode="L", size=(64, 64)) + + +def test_block_initiate(): + block = Block(grayscale_image, rgb_image, 10, 20, 32) + assert block.block_dimension == 32 + assert block.coordinate == (10, 20) + + +def test_block_compute_pca(): + block = Block(grayscale_image, rgb_image, 10, 20, 32) + result = block.compute_pca() + assert isinstance(result, list) + assert len(result) == 64 + assert result[0] == 1.0 + assert result[1] == 0.0 + + +def test_block_compute_characteristic_feature(): + block = Block(grayscale_image, rgb_image, 10, 20, 32) + result = block.compute_characteristic_features() + assert isinstance(result, list) + assert len(result) == 7 + assert result == [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] diff --git a/src/tests/test_container.py b/src/tests/test_container.py new file mode 100644 index 0000000..75f0d72 --- /dev/null +++ b/src/tests/test_container.py @@ -0,0 +1,27 @@ +from pimage.container import Container + + +def test_container_initiate(): + container = Container() + assert container.get_length() == 0 + + +def test_container_add(): + container = Container() + container.append_block(((0, 1), [0, 1, 2], [3, 4, 5])) + assert container.get_length() == 1 + + +def test_container_sort_feature(): + container = Container() + container.append_block(((0, 3), [1, 2, 3], [4, 5, 7])) + container.append_block(((0, 2), [1, 2, 3], [4, 5, 6])) + container.append_block(((0, 1), [0, 1, 2], [3, 4, 5])) + + container.sort_by_features() + assert container.get_length() == 3 + assert container.container == [ + ((0, 1), [0, 1, 2], [3, 4, 5]), + ((0, 2), [1, 2, 3], [4, 5, 6]), + ((0, 3), [1, 2, 3], [4, 5, 7]) + ]