Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged from main #16

Merged
merged 11 commits into from
Dec 13, 2023
Merged
48 changes: 48 additions & 0 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python

name: build

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

jobs:
build:

runs-on: ubuntu-latest
env:
MODULE_NAME: osculari
strategy:
fail-fast: false
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11"]

steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install -r requirements-dev.txt
pip install -e .
- 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: |
pytest --cov=./ --cov-report=xml
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v3
with:
file: coverage.xml
token: ${{ secrets.CODECOV_TOKEN }}
8 changes: 8 additions & 0 deletions docs/source/osculari.datasets.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,11 @@ osculari.datasets.gratings
:members:
:undoc-members:
:show-inheritance:

osculari.datasets.imutils
---------------------------------

.. automodule:: osculari.datasets.imutils
:members:
:undoc-members:
:show-inheritance:
2 changes: 2 additions & 0 deletions osculari/datasets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@

from .geometrical_shapes import *
from .gratings import *
from . import imutils
from . import dataset_utils
20 changes: 0 additions & 20 deletions osculari/datasets/dataset_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,26 +104,6 @@ def random_colour(channels: Optional[int] = 3) -> Sequence:
return colour


def michelson_contrast(img: npt.NDArray, contrast: float) -> npt.NDArray:
"""
Adjust the contrast of an image using the Michelson contrast formula.

Parameters:
img (npt.NDArray): Input image as a NumPy array.
contrast (float): Contrast adjustment factor. Should be in the range [0, 1].

Returns:
npt.NDArray: Image with adjusted contrast.
"""
# Ensure that the contrast value is within the valid range
assert 0 <= contrast <= 1

# Apply Michelson contrast formula to adjust image contrast
adjusted_img = ((1 - contrast) / 2.0 + np.multiply(img, contrast)).astype(img.dtype)

return adjusted_img


def _uniform_img(img_size: Tuple[int, int], value: Union[Sequence, int],
channels: Optional[int] = 3) -> npt.NDArray:
"""
Expand Down
81 changes: 81 additions & 0 deletions osculari/datasets/imutils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"""
Image manipulation functions.
"""

import numpy as np
import numpy.typing as npt


def _img_max_val(image):
max_val = np.maximum(np.max(image), 1)
for bits in [8, 16, 32, 64]:
if image.dtype == 'uint%d' % bits:
max_val = (2 ** bits) - 1
break
return max_val


def _im2double(image):
return np.float32(image) / _img_max_val(image)


def _double2im(image, org_img):
return (image * _img_max_val(org_img)).astype(org_img.dtype)


def _process_img(fun, in_image, *args, **kwargs):
image = _im2double(in_image.copy())
image = fun(image, *args, **kwargs)
return _double2im(fun(image, *args, **kwargs), in_image)


def michelson_contrast(img: npt.NDArray, contrast: float) -> npt.NDArray:
"""
Adjust the contrast of an image using the Michelson contrast formula.

Parameters:
img (npt.NDArray): Input image as a NumPy array.
contrast (float): Contrast adjustment factor. Should be in the range [0, 1].

Returns:
npt.NDArray: Image with adjusted contrast.
"""
# Ensure that the contrast value is within the valid range
assert 0 <= contrast <= 1

# Check if contrast is already at maximum
if contrast == 1:
return img

# Apply the contrast adjustment
return _process_img(_adjust_contrast, img, contrast)


def _adjust_contrast(image, amount):
return (1 - amount) / 2.0 + np.multiply(image, amount)


def gamma_correction(img: npt.NDArray, gamma: float) -> npt.NDArray:
"""
Adjust the gamma of an image.

Parameters:
img (npt.NDArray): Input image as a NumPy array.
gamma (float): Gamma adjustment factor.

Returns:
npt.NDArray: Image with adjusted gamma.
"""
# Ensure that the gamma value is not zero
assert gamma != 0

# Check if gamma is already at default (gamma=1)
if gamma == 1:
return img

# Apply the gamma adjustment
return _process_img(_adjust_gamma, img, gamma)


def _adjust_gamma(image, amount):
return image ** amount
13 changes: 11 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ classifiers = [
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Topic :: Scientific/Engineering :: Artificial Intelligence",
Expand Down Expand Up @@ -54,4 +53,14 @@ build-backend = "setuptools.build_meta"
[tool.setuptools.dynamic]
dependencies = {file = ["requirements.txt"]}
version = {attr = "osculari.__version__"}
readme = {file = ["README.md"], content-type = "text/markdown"}
readme = {file = ["README.md"], content-type = "text/markdown"}

[tool.setuptools.dynamic.optional-dependencies]
dev = {file = "requirements-dev.txt"}

[tool.pytest.ini_options]
addopts = [
"--import-mode=importlib",
"--color=yes"
]
testpaths = ["tests"]
3 changes: 3 additions & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pytest
pytest-cov
flake8
39 changes: 39 additions & 0 deletions tests/datasets/imutils_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""
Unit tests for imutils_test.py
"""

import numpy as np
import pytest

from osculari.datasets import imutils


@pytest.fixture
def sample_image():
"""Create a sample grayscale image for testing"""
return np.array([[50, 100, 150],
[75, 125, 175],
[100, 150, 200]], dtype='uint8')


def test_michelson_contrast_valid_input(sample_image):
contrast_factor = 0.5
result = imutils.michelson_contrast(sample_image, contrast_factor)

# Ensure that the output has the same shape as the input
assert result.shape == sample_image.shape

# Ensure that the output is a NumPy array
assert isinstance(result, np.ndarray)

# Ensure that the contrast is applied correctly
expected_result = np.array([[108, 120, 133],
[114, 126, 139],
[120, 133, 145]], dtype='uint8')
np.testing.assert_almost_equal(result, expected_result)


def test_michelson_contrast_invalid_contrast():
with pytest.raises(AssertionError):
contrast_factor = 1.5 # Invalid contrast value
imutils.michelson_contrast(np.array([[1, 2], [3, 4]]), contrast_factor)