Skip to content

Commit d836632

Browse files
Implement py bindings and py wheel
1 parent a290242 commit d836632

File tree

13 files changed

+282
-20
lines changed

13 files changed

+282
-20
lines changed

.github/workflows/ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,9 @@ jobs:
196196
- name: Run tests
197197
run: |
198198
docker run --rm ${{ secrets.DOCKER_USERNAME }}/libgaussianblur:linux ./GaussianBlurTests
199+
- name: Run python bindings tests
200+
run: |
201+
docker run --rm ${{ secrets.DOCKER_USERNAME }}/libgaussianblur:linux sh -c "pip3 install \$(realpath gaussianblur.whl) && pytest python/tests/*.py"
199202
200203
coverage:
201204
name: Generate Coverage Report

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@ external/linux
1111
external/android
1212
external/ios
1313
.docker/
14-
coverage/
14+
coverage/
15+
**/__pycache__

CMakeLists.txt

Lines changed: 66 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,16 @@ option(WITH_EXAMPLES "Include examples" OFF)
1414
option(WITH_TESTS "Include tests" OFF)
1515
# Coverage
1616
option(WITH_COVERAGE "Include coverage" OFF)
17+
# Bindings
18+
option(WITH_BINDINGS "Build Python bindings" OFF)
1719

1820
if(WITH_COVERAGE AND WITH_EXAMPLES)
1921
message(FATAL_ERROR "Coverage can affect the performance of the examples. Pick one or the other.")
2022
endif()
2123

24+
if(WITH_COVERAGE AND WITH_BINDINGS)
25+
message(FATAL_ERROR "Coverage can affect the performance of the bindings. Pick one or the other.")
26+
endif()
2227

2328
# If compiling to WebAssembly
2429
if(WASM)
@@ -56,10 +61,12 @@ endif()
5661
# Append additional flags for WebAssembly
5762
if(WASM)
5863
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -msimd128")
64+
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -msimd128")
5965
endif()
6066

6167
if(ENABLE_MULTITHREADING)
6268
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DENABLE_MULTITHREADING -pthread")
69+
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -pthread")
6370
endif()
6471

6572
# Add Framework Accelerate for macOS (SIMD)
@@ -118,38 +125,44 @@ set_target_properties(GaussianBlurLib PROPERTIES OUTPUT_NAME "Gaussianblur")
118125
if(WITH_EXAMPLES)
119126
message(STATUS "Building examples")
120127
if(WASM)
121-
add_executable(GaussianBlur "${CMAKE_SOURCE_DIR}/examples/wasm/main.cpp")
128+
add_executable(GaussianBlurExample "${CMAKE_SOURCE_DIR}/examples/wasm/main.cpp")
122129
set(WASM_COMMON_LINK_OPTIONS
123130
"SHELL: --closure 1"
124-
"-sALLOW_MEMORY_GROWTH=1"
125-
"-sMAXIMUM_MEMORY=4GB"
126-
"-sINITIAL_MEMORY=314572800"
127131
"-O3"
128132
"--bind"
129-
"-sWASM_BIGINT"
130133
"-sEXPORTED_FUNCTIONS=['_malloc']"
131134
"-msimd128"
132135
)
133136

134137
# Append specific options based on SINGLE or ENABLE_MULTITHREADING
135138
if(ENABLE_MULTITHREADING)
136139
list(APPEND WASM_COMMON_LINK_OPTIONS
137-
"-pthread"
140+
"-pthread" # Enable multithreading
141+
"-sMALLOC=mimalloc" # Use mimalloc
142+
"-sINITIAL_MEMORY=4095mb" # Use maximum memory and avoid memory growth
138143
"-sPTHREAD_POOL_SIZE=navigator.hardwareConcurrency"
139144
)
145+
else()
146+
list(APPEND WASM_COMMON_LINK_OPTIONS
147+
"-sINITIAL_MEMORY=300mb"
148+
"-sMAXIMUM_MEMORY=4GB"
149+
"-sALLOW_MEMORY_GROWTH=1"
150+
)
140151
endif()
141152

142153
# Apply the options to the target
143-
target_link_options(GaussianBlur PRIVATE ${WASM_COMMON_LINK_OPTIONS})
154+
target_link_options(GaussianBlurExample PRIVATE ${WASM_COMMON_LINK_OPTIONS})
144155

145156
# Install the WASM file
157+
set_target_properties(GaussianBlurExample PROPERTIES OUTPUT_NAME "GaussianBlur")
146158
install(FILES "${CMAKE_BINARY_DIR}/GaussianBlur.wasm" DESTINATION bin)
147159
else()
148-
add_executable(GaussianBlur "${CMAKE_SOURCE_DIR}/examples/desktop/main.cpp")
160+
add_executable(GaussianBlurExample "${CMAKE_SOURCE_DIR}/examples/desktop/main.cpp")
161+
set_target_properties(GaussianBlurExample PROPERTIES OUTPUT_NAME "GaussianBlur")
149162
endif()
150163

151-
target_link_libraries(GaussianBlur GaussianBlurLib)
152-
install(TARGETS GaussianBlur
164+
target_link_libraries(GaussianBlurExample GaussianBlurLib)
165+
install(TARGETS GaussianBlurExample
153166
BUNDLE DESTINATION Applications
154167
RUNTIME DESTINATION bin
155168
)
@@ -184,4 +197,47 @@ if(WITH_TESTS)
184197
target_link_libraries(GaussianBlurTests GaussianBlurLib gtest_main)
185198
add_test(NAME GaussianBlurTests COMMAND GaussianBlurTests)
186199
install(TARGETS GaussianBlurTests DESTINATION bin)
200+
endif()
201+
202+
203+
if(WITH_BINDINGS)
204+
if(WASM OR ANDROID OR IOS)
205+
message(FATAL_ERROR "Python bindings are not supported for this platform")
206+
endif()
207+
208+
message(STATUS "Building Python bindings")
209+
find_package(pybind11 REQUIRED)
210+
211+
212+
set(BINDING_SOURCES ${CMAKE_SOURCE_DIR}/python/bindings/gaussianblur_py.cpp)
213+
214+
pybind11_add_module(gaussianblur MODULE ${BINDING_SOURCES})
215+
216+
target_link_libraries(gaussianblur PRIVATE GaussianBlurLib)
217+
218+
219+
# Set the output directory for the shared library to:
220+
# <repo_root>/python/bindings/gaussianblur/
221+
set_target_properties(gaussianblur PROPERTIES
222+
LIBRARY_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/python/gaussianblur
223+
)
224+
225+
# Optionally, add an install rule for the bindings.
226+
install(TARGETS gaussianblur
227+
LIBRARY DESTINATION python/gaussianblur
228+
)
229+
230+
# Build the wheel (under-the-hood just copies the shared libraries in a whl package)
231+
add_custom_target(build_py_wheel ALL
232+
DEPENDS gaussianblur
233+
COMMAND ${CMAKE_COMMAND} -E remove_directory ${CMAKE_BINARY_DIR}/py_wheel
234+
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_BINARY_DIR}/py_wheel/gaussianblur
235+
COMMAND ${CMAKE_COMMAND} -E copy $<TARGET_FILE:gaussianblur> ${CMAKE_BINARY_DIR}/py_wheel/gaussianblur/
236+
COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_SOURCE_DIR}/python/gaussianblur/__init__.py ${CMAKE_BINARY_DIR}/py_wheel/gaussianblur/
237+
COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_SOURCE_DIR}/python/setup.py ${CMAKE_BINARY_DIR}/py_wheel/
238+
COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_SOURCE_DIR}/python/pyproject.toml ${CMAKE_BINARY_DIR}/py_wheel/
239+
COMMAND python3 -m build ${CMAKE_BINARY_DIR}/py_wheel
240+
COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_BINARY_DIR}/py_wheel/dist/*.whl ${CMAKE_SOURCE_DIR}/python/
241+
COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_BINARY_DIR}/py_wheel/dist/*.whl ${PrefixPath}/python/
242+
)
187243
endif()

Dockerfile

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
11
FROM debian:bookworm-slim AS base
22

3+
34
# Install dependencies
45
RUN apt-get -qq update; \
56
apt-get install -qqy --no-install-recommends gnupg2 wget ca-certificates \
6-
apt-transport-https curl unzip make cmake xz-utils cppcheck
7+
apt-transport-https curl unzip make cmake xz-utils cppcheck
8+
9+
# Install python3.11 and pybind11 for bindings.
10+
ENV PIP_BREAK_SYSTEM_PACKAGES=1 PIP_ROOT_USER_ACTION=ignore
11+
RUN apt-get install -qqy --no-install-recommends curl python3.11 python3-venv python3-dev && \
12+
ln -sf /usr/bin/python3.11 /usr/bin/python3 && \
13+
curl -sS https://bootstrap.pypa.io/get-pip.py | python3.11 && \
14+
pip install pybind11 pytest numpy build
715

816
# Install LLVM
917
RUN echo "deb https://apt.llvm.org/bookworm llvm-toolchain-bookworm-16 main" \
@@ -31,6 +39,8 @@ FROM builder-env AS linux
3139
# Build gaussian_blur
3240
RUN bootstrap/bootstrap.sh linux && rm -rf build
3341
RUN ln -s /app/external/linux/x86_64/bin/GaussianBlurTests /app/GaussianBlurTests
42+
RUN find /app/external/linux/x86_64/python/ -name "gaussianblur-*.whl" -exec ln -s {} /app/gaussianblur.whl \;
43+
RUN mkdir /app/python && ln -s /app/.deps/gaussian_blur/python/tests /app/python/tests
3444

3545
FROM builder-env AS coverage
3646
RUN mkdir -p /app/.deps/gaussian_blur/build
@@ -73,8 +83,8 @@ RUN bootstrap/bootstrap.sh android && rm -rf build
7383

7484

7585
FROM builder-env AS wasm
76-
# Download emsdk 3.1.66
77-
RUN wget -q https://github.com/emscripten-core/emsdk/archive/refs/tags/3.1.74.zip -O /opt/emsdk.zip
86+
# Download emsdk 4.0.5
87+
RUN wget -q https://github.com/emscripten-core/emsdk/archive/refs/tags/4.0.5.zip -O /opt/emsdk.zip
7888
# Extract emsdk and create in symlink in /root (aka $HOME)
7989
ENV EMSDK=/opt/emsdk
8090
RUN unzip /opt/emsdk.zip -d /opt/ && mv /opt/emsdk-* /opt/emsdk && rm /opt/emsdk.zip && ln -s /opt/emsdk /root/emsdk

README.md

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -158,12 +158,12 @@ This will generate the necessary files in the `external/ios` folder.
158158

159159
#### Manual Build
160160

161-
##### Desktop (Linux, macOS)
161+
##### Desktop (Linux, macOS) with python bindings and examples
162162

163163
```sh
164164
mkdir build
165165
cd build
166-
cmake -DENABLE_MULTITHREADING=ON -DWITH_EXAMPLES=ON -DWITH_TESTS=ON -DWITH_COVERAGE=OFF ..
166+
cmake -DENABLE_MULTITHREADING=ON -DWITH_EXAMPLES=ON -DWITH_BINDINGS=ON -DWITH_TESTS=ON -DWITH_COVERAGE=OFF ..
167167
cmake --build .
168168
```
169169

@@ -196,9 +196,21 @@ make
196196
```
197197

198198

199-
## Usage
199+
## Usage in Python
200200

201-
When building with `WITH_EXAMPLES=ON`, you can try the library. These examples are intended to provide a simple idea of how the library is used.
201+
If you build the library with `WITH_BINDINGS=ON`, a Python wheel will be generated. You will find the wheel under `external/PLATFORM/ABI/python/` along with the shared library.
202+
203+
A basic example has been provided about how to use the module.
204+
First, install the wheel with pip, and then, replace with your input/output image paths:
205+
206+
```sh
207+
pip3 install gaussianblur*.whl
208+
python3 examples/python/main.py
209+
```
210+
211+
## Usage in C++
212+
213+
When building with `WITH_EXAMPLES=ON`, you can try the library. These examples are intended to provide a simple idea of how the library is used.
202214

203215
### Command Line (desktop)
204216

@@ -261,6 +273,7 @@ LibGaussianBlur implements a robust Continuous Integration and Continuous Deploy
261273
- Tests must pass successfully before generating the coverage report and build the Android and WASM libraries.
262274

263275
## Roadmap
276+
- Python wheel packages
264277
- Doxygen
265278
- Flutter plugin with native bindings
266279

bootstrap/bootstrap.gaussianblur.sh

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
PREFIX="gaussian_blur"
77
TEMP_EXTERNAL_BUILD_DIR=.deps
8-
GAUSSIANBLUR_VERSION="1.0.4"
8+
GAUSSIANBLUR_VERSION="1.0.5"
99
GAUSSIANBLUR_FOLDER="v${GAUSSIANBLUR_VERSION}"
1010
GAUSSIANBLUR_BUILD_DIR="${PREFIX}-${GAUSSIANBLUR_FOLDER}"
1111

@@ -56,6 +56,7 @@ compile_gaussian_blur() {
5656
-DANDROID_PLATFORM=23 \
5757
-DANDROID_ABI=$ABI \
5858
-DBUILD_SHARED_LIBS=ON \
59+
-DWITH_BINDINGS=OFF \
5960
-DWITH_TESTS=OFF \
6061
-DWITH_COVERAGE=OFF \
6162
-DANDROID_STL="c++_shared" ..
@@ -75,6 +76,9 @@ compile_gaussian_blur() {
7576
-DCMAKE_INSTALL_PREFIX="$TEMP_PREFIX_DIR" \
7677
-DCMAKE_INSTALL_RPATH=$FINAL_PREFIX_DIR/lib \
7778
-DBUILD_SHARED_LIBS=ON \
79+
-DPYBIND11_FINDPYTHON=ON \
80+
-Dpybind11_DIR=$(python3 -m pybind11 --cmakedir) \
81+
-DWITH_BINDINGS=ON \
7882
-DWITH_TESTS=ON \
7983
-DWITH_COVERAGE=OFF \
8084
..
@@ -91,12 +95,19 @@ compile_gaussian_blur() {
9195
mv ${TEMP_PREFIX_DIR}/include/gaussianblur ${FINAL_PREFIX_DIR}/include/
9296
mv ${TEMP_PREFIX_DIR}/lib/*.so* ${FINAL_PREFIX_DIR}/lib/
9397
mv ${TEMP_PREFIX_DIR}/bin/* ${FINAL_PREFIX_DIR}/bin/
98+
99+
# Copy Python bindings
100+
if [ -d ${FINAL_PREFIX_DIR}/python/gaussianblur ] ; then rm -rf ${FINAL_PREFIX_DIR}/python/gaussianblur ; fi
101+
mkdir -p ${FINAL_PREFIX_DIR}/python/gaussianblur
102+
cp -R ../python/gaussianblur ${FINAL_PREFIX_DIR}/python/
103+
cp ../python/gaussianblur*.whl ${FINAL_PREFIX_DIR}/python/
94104
elif [ "$PLATFORM" = "wasm" ] ; then
95105
cmake -DENABLE_MULTITHREADING=ON \
96106
-DCMAKE_INSTALL_PREFIX="$TEMP_PREFIX_DIR" \
97107
-DCMAKE_INSTALL_RPATH=$FINAL_PREFIX_DIR/lib \
98108
-DBUILD_SHARED_LIBS=OFF \
99109
-DWASM=ON \
110+
-DWITH_BINDINGS=OFF \
100111
-DWITH_EXAMPLES=ON \
101112
-DWITH_TESTS=OFF \
102113
-DWITH_COVERAGE=OFF \
@@ -161,6 +172,7 @@ compile_gaussian_blur() {
161172
-DCMAKE_TOOLCHAIN_FILE="$IOS_TOOLCHAIN" \
162173
-DENABLE_MULTITHREADING=ON \
163174
-DWITH_EXAMPLES=OFF \
175+
-DWITH_BINDINGS=OFF \
164176
-DWITH_TESTS=OFF \
165177
-DWITH_COVERAGE=OFF \
166178
-DCMAKE_XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER="$BUNDLE_ID" \
@@ -192,14 +204,17 @@ compile_gaussian_blur() {
192204
&& ln -s "Versions/Current/Resources" "Resources" \
193205
&& ln -s "Versions/Current/Gaussianblur" "Gaussianblur")
194206
elif [ "$PLATFORM" = "macos" ] && [ "$ABI" = "x86_64" ] ; then
195-
# MacOS arm64
207+
# MacOS x86_64
196208
cmake -DENABLE_MULTITHREADING=ON \
197209
-DWITH_EXAMPLES=ON \
198210
-DCMAKE_INSTALL_PREFIX=${TEMP_PREFIX_DIR} \
199211
-DCMAKE_INSTALL_RPATH=$FINAL_PREFIX_DIR/lib \
200212
-DBUILD_SHARED_LIBS=ON \
213+
-DPYBIND11_FINDPYTHON=ON \
214+
-Dpybind11_DIR=$(python3 -m pybind11 --cmakedir) \
201215
-DWITH_TESTS=ON \
202216
-DWITH_COVERAGE=OFF \
217+
-DWITH_BINDINGS=ON \
203218
-DCMAKE_OSX_ARCHITECTURES="$ABI" \
204219
..
205220
printf "${GREEN}Compiling with ${N_CPU_CORES} cores. This might still take some time\n"
@@ -214,6 +229,12 @@ compile_gaussian_blur() {
214229
mv ${TEMP_PREFIX_DIR}/include/gaussianblur ${FINAL_PREFIX_DIR}/include/
215230
mv ${TEMP_PREFIX_DIR}/lib/*.dylib ${FINAL_PREFIX_DIR}/lib/
216231
mv ${TEMP_PREFIX_DIR}/bin/* ${FINAL_PREFIX_DIR}/bin/
232+
233+
# Copy Python bindings
234+
if [ -d ${FINAL_PREFIX_DIR}/python/gaussianblur ] ; then rm -rf ${FINAL_PREFIX_DIR}/python/gaussianblur ; fi
235+
mkdir -p ${FINAL_PREFIX_DIR}/python/gaussianblur
236+
cp -R ../python/gaussianblur ${FINAL_PREFIX_DIR}/python/
237+
cp ../python/gaussianblur*.whl ${FINAL_PREFIX_DIR}/python/
217238
else
218239
# MacOS arm64
219240
cmake -DENABLE_MULTITHREADING=ON \
@@ -223,6 +244,9 @@ compile_gaussian_blur() {
223244
-DBUILD_SHARED_LIBS=ON \
224245
-DWITH_TESTS=ON \
225246
-DWITH_COVERAGE=OFF \
247+
-DPYBIND11_FINDPYTHON=ON \
248+
-Dpybind11_DIR=$(python3 -m pybind11 --cmakedir) \
249+
-DWITH_BINDINGS=ON \
226250
-DCMAKE_OSX_ARCHITECTURES=arm64 \
227251
..
228252
printf "${GREEN}Compiling with ${N_CPU_CORES} cores. This might still take some time\n"
@@ -237,6 +261,12 @@ compile_gaussian_blur() {
237261
mv ${TEMP_PREFIX_DIR}/include/gaussianblur ${FINAL_PREFIX_DIR}/include/
238262
mv ${TEMP_PREFIX_DIR}/lib/*.dylib ${FINAL_PREFIX_DIR}/lib/
239263
mv ${TEMP_PREFIX_DIR}/bin/* ${FINAL_PREFIX_DIR}/bin/
264+
265+
# Copy Python bindings
266+
if [ -d ${FINAL_PREFIX_DIR}/python/gaussianblur ] ; then rm -rf ${FINAL_PREFIX_DIR}/python/gaussianblur ; fi
267+
mkdir -p ${FINAL_PREFIX_DIR}/python/gaussianblur
268+
cp -R ../python/gaussianblur ${FINAL_PREFIX_DIR}/python/
269+
cp ../python/gaussianblur*.whl ${FINAL_PREFIX_DIR}/python/
240270
fi
241271
}
242272

examples/python/main.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from PIL import Image as PILImage
2+
import numpy as np
3+
import gaussianblur
4+
5+
# This is a simple example of how to use the gaussianblur module in Python.
6+
# The example loads an image, applies a gaussian blur to it, and saves the output image.
7+
def main():
8+
input_file = "input.png"
9+
output_file = "output.png"
10+
# Load image
11+
pil_img = PILImage.open(input_file)
12+
13+
# Set the geometry of the image and copy data buffer in the gaussianblur.Image object
14+
img = gaussianblur.Image()
15+
img.geom.rows = pil_img.size[1]
16+
img.geom.cols = pil_img.size[0]
17+
img.geom.channels = len(pil_img.getbands())
18+
img.data = np.array(pil_img).flatten().tolist()
19+
20+
# sigma
21+
sigma = 7.5
22+
# apply_to_alpha_channel: If True, the alpha channel will be blurred as well (if present)
23+
apply_to_alpha_channel = True
24+
# Apply gaussian blur
25+
gaussianblur.gaussianblur(img, sigma, apply_to_alpha_channel)
26+
print("Gaussian blur applied.")
27+
28+
# Save the output image
29+
out_np_img = np.array(img.data, dtype=np.uint8).reshape(img.geom.rows, img.geom.cols, img.geom.channels)
30+
PILImage.fromarray(out_np_img, pil_img.mode).save(output_file)
31+
print("Output written to", output_file)
32+
33+
if __name__ == "__main__":
34+
main()

0 commit comments

Comments
 (0)