Skip to content

Commit

Permalink
Add tests (#37)
Browse files Browse the repository at this point in the history
* Add test workflow

* Add test directory

* Alpha test for Container

* Bump version to 1.1.0

* Add Container test

* Update docstring on container.py

* Update docstring on block.py

* Add default precision to Block.compute_xx()

* Update Block.compute_pca

* Prevents ZeroDivisionError on Block

* Add Block test

* Update the return value of ImageObject.is_valid()
  • Loading branch information
rahmatnazali authored Feb 10, 2023
1 parent 37cc1b6 commit ba112ff
Show file tree
Hide file tree
Showing 8 changed files with 122 additions and 14 deletions.
40 changes: 40 additions & 0 deletions .github/workflows/python-app.yml
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand Down
23 changes: 18 additions & 5 deletions src/pimage/block.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import List

import numpy

from sklearn.decomposition import PCA
Expand All @@ -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
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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))
Expand Down
6 changes: 3 additions & 3 deletions src/pimage/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
10 changes: 5 additions & 5 deletions src/pimage/image_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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):
"""
Expand Down
Empty file added src/tests/__init__.py
Empty file.
28 changes: 28 additions & 0 deletions src/tests/test_block.py
Original file line number Diff line number Diff line change
@@ -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]
27 changes: 27 additions & 0 deletions src/tests/test_container.py
Original file line number Diff line number Diff line change
@@ -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])
]

0 comments on commit ba112ff

Please sign in to comment.