Skip to content

Commit 1b94abb

Browse files
authored
Merge pull request #2 from ramonaoptics/fix_workflows
Fix workflows and add basic tests.
2 parents 3b4636a + 1e82f5c commit 1b94abb

11 files changed

+355
-31
lines changed

.github/workflows/pypi.yaml

+4-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ jobs:
3030
run: python -m pip install --upgrade pip build
3131

3232
- name: Build Distributions
33-
run: python -m build --sdist --wheel --outdir dist/
33+
# While I would love to build a wheel, I just don't know how
34+
# with a c-dependency
35+
# run: python -m build --sdist --wheel --outdir dist/
36+
run: python -m build --sdist --outdir dist/
3437

3538
- name: Publish to PyPI
3639
uses: pypa/gh-action-pypi-publish@release/v1

.github/workflows/tests.yml

+20-7
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,19 @@ on:
88

99
jobs:
1010
build:
11-
name: Tests on Python ${{ matrix.python-version }}
11+
name: Tests on (${{ matrix.python-version }}, ${{ matrix.os }})
1212
runs-on: ${{ matrix.os }}
1313
strategy:
14-
fail-fast: false
1514
matrix:
1615
os: ["ubuntu-latest", "macos-latest", "windows-latest"]
17-
python-version: ["3.10", "3.11", "3.12"]
16+
python-version: ["3.10"]
17+
include:
18+
- os: "ubuntu-latest"
19+
installer-url: "https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-Linux-x86_64.sh"
20+
- os: "macos-latest"
21+
installer-url: "https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-MacOSX-x86_64.sh"
22+
- os: "windows-latest"
23+
installer-url: "https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-Windows-x86_64.exe"
1824
max-parallel: 8
1925
fail-fast: false
2026

@@ -33,28 +39,35 @@ jobs:
3339
- uses: conda-incubator/setup-miniconda@v3
3440
with:
3541
auto-update-conda: true
42+
installer-url: ${{ matrix.installer-url }}
3643
python-version: ${{ matrix.python-version }}
3744
- name: Conda info
3845
shell: bash -el {0}
39-
run: conda info
46+
run: |
47+
conda config --remove channels defaults
48+
conda config --add channels mark.harfouche
49+
conda info
4050
- name: Conda list
4151
shell: pwsh
4252
run: |
4353
conda list
44-
conda config --add channels mark.harfouche
4554
4655
- name: Install dependencies
56+
shell: bash -el {0}
4757
run: |
48-
mamba install numpy openjph setuptools pytest --yes
58+
conda install compilers pybind11 numpy openjph setuptools pytest --yes
4959
5060
- name: Install package
61+
shell: bash -el {0}
5162
run: |
5263
export OJPH_GIT_DESCRIBE=${{ steps.ghd.outputs.describe }}
53-
python -m pip install . -vvv
64+
python -m pip install -e . -vvv
5465
5566
- name: Run tests
67+
shell: bash -el {0}
5668
run: |
5769
export OJPH_GIT_DESCRIBE=${{ steps.ghd.outputs.describe }}
5870
python --version
5971
pip check
6072
python -c "import ojph; print(ojph.__version__)"
73+
pytest

ojph/__init__.py

+5
Original file line numberDiff line numberDiff line change
@@ -1 +1,6 @@
11
from ._version import __version__
2+
3+
from ._imwrite import imwrite
4+
from ._imread import imread
5+
6+
__all__ = ["imwrite", "imread"]

ojph/_imread.py

+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import os
2+
import numpy as np
3+
import ctypes
4+
from .ojph_bindings import J2CInfile, Codestream
5+
from warnings import warn
6+
7+
# Copy the imageio.v3.imread signature
8+
def imread(uri, *, index=None, plugin=None, extension=None, format_hint=None, **kwargs):
9+
if index is not None:
10+
warn(f"index {index} is ignored", stacklevel=2)
11+
if plugin is not None:
12+
warn(f"plugin {plugin} is ignored", stacklevel=2)
13+
if extension is not None:
14+
warn(f"extension {extension} is ignored", stacklevel=2)
15+
if format_hint is not None:
16+
warn(f"format_hint {format_hint} is ignored", stacklevel=2)
17+
18+
return OJPHImageFile(uri).read_image()
19+
20+
21+
class OJPHImageFile:
22+
def __init__(self, filename):
23+
self._codestream = None
24+
self._ojph_file = None
25+
self._filename = filename
26+
27+
self._ojph_file = J2CInfile()
28+
self._ojph_file.open(str(filename))
29+
self._codestream = Codestream()
30+
self._codestream.read_headers(self._ojph_file)
31+
32+
33+
siz = self._codestream.access_siz()
34+
extents = siz.get_image_extent()
35+
self._shape = extents.y, extents.x
36+
self._is_planar = self._codestream.is_planar()
37+
38+
bit_depth = siz.get_bit_depth(0)
39+
is_signed = siz.is_signed(0)
40+
if bit_depth == 8 and not is_signed:
41+
self._dtype = np.uint8
42+
elif bit_depth == 8 and is_signed:
43+
self._dtype = np.int8
44+
elif bit_depth == 16 and not is_signed:
45+
self._dtype = np.uint16
46+
elif bit_depth == 16 and is_signed:
47+
self._dtype = np.int16
48+
elif bit_depth == 32 and not is_signed:
49+
self._dtype = np.uint32
50+
elif bit_depth == 32 and is_signed:
51+
self._dtype = np.int32
52+
else:
53+
raise ValueError("Unsupported bit depth")
54+
55+
def _open_file(self):
56+
self._ojph_file = J2CInfile()
57+
self._ojph_file.open(self._filename)
58+
self._codestream = Codestream()
59+
self._codestream.read_headers(self._ojph_file)
60+
61+
def read_image(self, *, level=0):
62+
if self._codestream is None:
63+
self._open_file()
64+
65+
self._codestream.restrict_input_resolution(level, level)
66+
siz = self._codestream.access_siz()
67+
68+
height = siz.get_recon_height(0)
69+
width = siz.get_recon_width(0)
70+
self._codestream.create()
71+
72+
image = np.zeros(
73+
(height, width),
74+
dtype=self._dtype
75+
)
76+
77+
for h in range(height):
78+
line = self._codestream.pull(0)
79+
# Convert the address to a ctypes pointer to int32
80+
i32_ptr = ctypes.cast(line.i32_address, ctypes.POINTER(ctypes.c_uint32))
81+
82+
# Calculate the total number of bytes (size of the array in elements * size of int32)
83+
line_array = np.ctypeslib.as_array(
84+
ctypes.cast(i32_ptr, ctypes.POINTER(ctypes.c_uint32)),
85+
shape=(line.size,)
86+
)
87+
image[h] = line_array
88+
89+
self._close_codestream_and_file()
90+
return image
91+
92+
def _close_codestream_and_file(self):
93+
if self._codestream is not None:
94+
self._codestream.close()
95+
# The codestream will close the infile automatically
96+
elif self._ojph_file is not None:
97+
self._ojph_file.close()
98+
self._codestream = None
99+
self._ojph_file = None
100+
101+
def __del__(self):
102+
self._close_codestream_and_file()

ojph/_imwrite.py

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import numpy as np
2+
import ctypes
3+
4+
from .ojph_bindings import Codestream, J2COutfile, Point
5+
6+
7+
def imwrite(filename, image):
8+
# In the future we might be able to pass in a channel_order parameter
9+
if image.ndim == 2:
10+
channel_order = 'HW'
11+
else:
12+
channel_order = 'HWC'
13+
14+
channel_order = channel_order.upper()
15+
16+
if len(channel_order) != image.ndim:
17+
raise ValueError(
18+
f"The channel order ({channel_order}) must be consistent "
19+
f"with the image dimensions ({image.ndim})."
20+
)
21+
22+
ojph_file = J2COutfile()
23+
ojph_file.open(str(filename))
24+
codestream = Codestream()
25+
26+
siz = codestream.access_siz()
27+
width = image.shape[channel_order.index('W')]
28+
height = image.shape[channel_order.index('H')]
29+
30+
# What does planar mean? it doesn't seem to mean components...
31+
# is_planar = 'C' in channel_order
32+
siz.set_image_extent(Point(width, height))
33+
if 'C' in channel_order:
34+
num_components = channel_order.index('C')
35+
else:
36+
num_components = 1
37+
38+
bit_depth = image.dtype.itemsize * 8
39+
# Is there a better way to detect signed dtypes???
40+
is_signed = image.dtype.kind != 'u'
41+
siz.set_num_components(num_components);
42+
for i in range(num_components):
43+
# is it necessary to do this in a loop?
44+
siz.set_component(
45+
i,
46+
Point(1, 1), # component downsampling
47+
bit_depth,
48+
is_signed,
49+
)
50+
cod = codestream.access_cod()
51+
# cod.set_progression_oder
52+
# code.set_color_Transform
53+
cod.set_reversible(True)
54+
# codestream.set_profile()
55+
56+
# set tile_size
57+
# set tile offset
58+
# planar true is likely for things like
59+
# YUV420 where the Y, U, and V planes are stored
60+
# sparately
61+
codestream.set_planar(False)
62+
63+
codestream.write_headers(ojph_file, None, 0)
64+
c = 0
65+
line = codestream.exchange(None, c)
66+
for i in range(height):
67+
for c in range(num_components):
68+
i32_ptr = ctypes.cast(line.i32_address, ctypes.POINTER(ctypes.c_uint32))
69+
# Calculate the total number of bytes (size of the array in elements * size of int32)
70+
line_array = np.ctypeslib.as_array(
71+
ctypes.cast(i32_ptr, ctypes.POINTER(ctypes.c_uint32)),
72+
shape=(line.size,)
73+
)
74+
if image.ndim == 2:
75+
line_array[...] = image[i, :]
76+
else:
77+
line_array[...] = image[i, :, c]
78+
c_before = c
79+
line = codestream.exchange(line, c)
80+
81+
codestream.flush()
82+
codestream.close()

0 commit comments

Comments
 (0)