focalpy 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- focalpy-0.0.1/.gitattributes +2 -0
- focalpy-0.0.1/.github/workflows/wheels.yml +69 -0
- focalpy-0.0.1/.gitignore +8 -0
- focalpy-0.0.1/CMakeLists.txt +48 -0
- focalpy-0.0.1/LICENSE +21 -0
- focalpy-0.0.1/PKG-INFO +95 -0
- focalpy-0.0.1/README.md +66 -0
- focalpy-0.0.1/include/focal/border.hpp +52 -0
- focalpy-0.0.1/include/focal/buffer.hpp +45 -0
- focalpy-0.0.1/include/focal/check.hpp +44 -0
- focalpy-0.0.1/include/focal/convert.hpp +54 -0
- focalpy-0.0.1/include/focal/io.hpp +31 -0
- focalpy-0.0.1/include/focal/ops/blur.hpp +20 -0
- focalpy-0.0.1/include/focal/ops/point.hpp +110 -0
- focalpy-0.0.1/include/focal/sample.hpp +28 -0
- focalpy-0.0.1/include/focal/srgb.hpp +21 -0
- focalpy-0.0.1/include/focal/version.hpp +8 -0
- focalpy-0.0.1/include/focal/view.hpp +133 -0
- focalpy-0.0.1/pyproject.toml +63 -0
- focalpy-0.0.1/python/bindings.cpp +238 -0
- focalpy-0.0.1/python/focal/__init__.py +35 -0
- focalpy-0.0.1/src/convert.cpp +45 -0
- focalpy-0.0.1/src/io.cpp +143 -0
- focalpy-0.0.1/src/ops/blur.cpp +66 -0
- focalpy-0.0.1/src/ops/point.cpp +89 -0
- focalpy-0.0.1/src/sample.cpp +37 -0
- focalpy-0.0.1/src/srgb.cpp +26 -0
- focalpy-0.0.1/src/version.cpp +7 -0
- focalpy-0.0.1/tests/python/test_blur.py +64 -0
- focalpy-0.0.1/tests/python/test_border.py +60 -0
- focalpy-0.0.1/tests/python/test_io.py +116 -0
- focalpy-0.0.1/tests/python/test_point.py +108 -0
- focalpy-0.0.1/tests/python/test_sample.py +40 -0
- focalpy-0.0.1/tests/python/test_srgb.py +32 -0
- focalpy-0.0.1/third_party/stb/stb_image.h +7988 -0
- focalpy-0.0.1/third_party/stb/stb_image_write.h +1724 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
name: Wheels
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
tags: ["v*"]
|
|
7
|
+
pull_request:
|
|
8
|
+
workflow_dispatch:
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
build_wheels:
|
|
12
|
+
name: wheels on ${{ matrix.os }}
|
|
13
|
+
runs-on: ${{ matrix.os }}
|
|
14
|
+
strategy:
|
|
15
|
+
fail-fast: false
|
|
16
|
+
matrix:
|
|
17
|
+
# macos-14 is arm64 and cross-compiles the x86_64 wheel too (see
|
|
18
|
+
# [tool.cibuildwheel.macos] archs). GitHub retired the Intel macos-13
|
|
19
|
+
# runners, so requesting one queues forever.
|
|
20
|
+
os: [ubuntu-latest, ubuntu-24.04-arm, macos-14, windows-latest]
|
|
21
|
+
steps:
|
|
22
|
+
- uses: actions/checkout@v4
|
|
23
|
+
|
|
24
|
+
- uses: actions/setup-python@v5
|
|
25
|
+
with:
|
|
26
|
+
python-version: "3.12"
|
|
27
|
+
|
|
28
|
+
# Pin this to an exact version once you have a build you trust.
|
|
29
|
+
- run: python -m pip install "cibuildwheel>=2.20"
|
|
30
|
+
|
|
31
|
+
# Config lives in [tool.cibuildwheel] in pyproject.toml. Because the
|
|
32
|
+
# extension is abi3, this builds cp312 only and the result covers every
|
|
33
|
+
# later Python.
|
|
34
|
+
- run: python -m cibuildwheel --output-dir wheelhouse
|
|
35
|
+
|
|
36
|
+
- uses: actions/upload-artifact@v4
|
|
37
|
+
with:
|
|
38
|
+
name: wheels-${{ matrix.os }}
|
|
39
|
+
path: wheelhouse/*.whl
|
|
40
|
+
|
|
41
|
+
make_sdist:
|
|
42
|
+
name: sdist
|
|
43
|
+
runs-on: ubuntu-latest
|
|
44
|
+
steps:
|
|
45
|
+
- uses: actions/checkout@v4
|
|
46
|
+
- run: pipx run build --sdist
|
|
47
|
+
- uses: actions/upload-artifact@v4
|
|
48
|
+
with:
|
|
49
|
+
name: sdist
|
|
50
|
+
path: dist/*.tar.gz
|
|
51
|
+
|
|
52
|
+
publish:
|
|
53
|
+
name: publish to PyPI
|
|
54
|
+
needs: [build_wheels, make_sdist]
|
|
55
|
+
runs-on: ubuntu-latest
|
|
56
|
+
# Only on a version tag: git tag v0.0.2 && git push --tags
|
|
57
|
+
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
|
|
58
|
+
environment: pypi
|
|
59
|
+
permissions:
|
|
60
|
+
# Trusted publishing: PyPI verifies this workflow via OIDC, so there is
|
|
61
|
+
# no API token to store. Register the repo at
|
|
62
|
+
# https://pypi.org/manage/account/publishing/ first.
|
|
63
|
+
id-token: write
|
|
64
|
+
steps:
|
|
65
|
+
- uses: actions/download-artifact@v4
|
|
66
|
+
with:
|
|
67
|
+
path: dist
|
|
68
|
+
merge-multiple: true
|
|
69
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
focalpy-0.0.1/.gitignore
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
cmake_minimum_required(VERSION 3.26)
|
|
2
|
+
project(focal LANGUAGES CXX VERSION 0.0.1)
|
|
3
|
+
|
|
4
|
+
set(CMAKE_CXX_STANDARD 20)
|
|
5
|
+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|
6
|
+
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
|
7
|
+
set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # for clangd / IDE tooling
|
|
8
|
+
|
|
9
|
+
if(NOT CMAKE_BUILD_TYPE)
|
|
10
|
+
set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
|
|
11
|
+
endif()
|
|
12
|
+
|
|
13
|
+
# The C++ library. Globbed with CONFIGURE_DEPENDS so new files under src/
|
|
14
|
+
# get picked up without editing this file.
|
|
15
|
+
file(GLOB_RECURSE FOCAL_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp")
|
|
16
|
+
add_library(focal_core STATIC ${FOCAL_SOURCES})
|
|
17
|
+
target_include_directories(focal_core PUBLIC
|
|
18
|
+
"${CMAKE_CURRENT_SOURCE_DIR}/include"
|
|
19
|
+
"${CMAKE_CURRENT_SOURCE_DIR}/third_party")
|
|
20
|
+
target_compile_options(focal_core PRIVATE
|
|
21
|
+
$<$<CXX_COMPILER_ID:AppleClang,Clang,GNU>:-Wall -Wextra>)
|
|
22
|
+
|
|
23
|
+
if(MSVC)
|
|
24
|
+
# windows.h (pulled in by stb under STBI_WINDOWS_UTF8) defines min/max as
|
|
25
|
+
# macros, which breaks std::min and std::max at every call site. stb also
|
|
26
|
+
# uses fopen/sprintf, which MSVC flags as deprecated.
|
|
27
|
+
target_compile_definitions(focal_core PRIVATE NOMINMAX _CRT_SECURE_NO_WARNINGS)
|
|
28
|
+
endif()
|
|
29
|
+
|
|
30
|
+
# The Python extension module.
|
|
31
|
+
# Development.SABIModule is what nanobind links against for STABLE_ABI below;
|
|
32
|
+
# without it the STABLE_ABI flag is silently ignored and you get one wheel per
|
|
33
|
+
# Python version instead of one per platform.
|
|
34
|
+
find_package(Python 3.12 REQUIRED
|
|
35
|
+
COMPONENTS Interpreter Development.Module Development.SABIModule)
|
|
36
|
+
|
|
37
|
+
execute_process(
|
|
38
|
+
COMMAND "${Python_EXECUTABLE}" -m nanobind --cmake_dir
|
|
39
|
+
OUTPUT_VARIABLE NANOBIND_CMAKE_DIR OUTPUT_STRIP_TRAILING_WHITESPACE
|
|
40
|
+
COMMAND_ERROR_IS_FATAL ANY)
|
|
41
|
+
list(APPEND CMAKE_PREFIX_PATH "${NANOBIND_CMAKE_DIR}")
|
|
42
|
+
find_package(nanobind CONFIG REQUIRED)
|
|
43
|
+
|
|
44
|
+
file(GLOB_RECURSE FOCAL_BINDINGS CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/python/*.cpp")
|
|
45
|
+
nanobind_add_module(_focal STABLE_ABI NB_STATIC ${FOCAL_BINDINGS})
|
|
46
|
+
target_link_libraries(_focal PRIVATE focal_core)
|
|
47
|
+
|
|
48
|
+
install(TARGETS _focal LIBRARY DESTINATION focal)
|
focalpy-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Edison Sun
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
focalpy-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: focalpy
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Image processing in C++ with a zero-copy NumPy interface
|
|
5
|
+
Keywords: image-processing,imaging,computer-vision,numpy,cpp
|
|
6
|
+
Author-Email: Edison Sun <edisonsun31@gmail.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: Programming Language :: C++
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Topic :: Multimedia :: Graphics
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Image Processing
|
|
18
|
+
Project-URL: Homepage, https://github.com/edisons3608/focal
|
|
19
|
+
Project-URL: Repository, https://github.com/edisons3608/focal
|
|
20
|
+
Project-URL: Issues, https://github.com/edisons3608/focal/issues
|
|
21
|
+
Requires-Python: >=3.12
|
|
22
|
+
Requires-Dist: numpy>=1.23
|
|
23
|
+
Provides-Extra: test
|
|
24
|
+
Requires-Dist: pytest; extra == "test"
|
|
25
|
+
Requires-Dist: scipy; extra == "test"
|
|
26
|
+
Requires-Dist: scikit-image; extra == "test"
|
|
27
|
+
Requires-Dist: pillow; extra == "test"
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# focal
|
|
31
|
+
|
|
32
|
+
Image processing in C++20 with a zero-copy NumPy interface.
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
import focal
|
|
36
|
+
|
|
37
|
+
img = focal.imread("photo.jpg") # linear float32, (h, w, c)
|
|
38
|
+
img = focal.exposure(img, 0.5) # half a stop brighter
|
|
39
|
+
img = focal.gaussian_blur(img, 2.0)
|
|
40
|
+
focal.imwrite("out.png", img) # encodes back to sRGB
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Design
|
|
44
|
+
|
|
45
|
+
**Everything is a view.** The core type is a non-owning `{pointer, dtype, shape,
|
|
46
|
+
strides}` struct, so a crop, a single channel, and a NumPy array handed over
|
|
47
|
+
from Python are all the same type, and none of them copy. Strides are in bytes,
|
|
48
|
+
matching NumPy exactly, so wrapping an incoming array is copying six integers.
|
|
49
|
+
|
|
50
|
+
**Nothing converts silently.** Array arguments are declared `.noconvert()`, so
|
|
51
|
+
passing a float64 or non-contiguous array raises `TypeError` instead of quietly
|
|
52
|
+
costing a full-size copy. Converting is the caller's decision.
|
|
53
|
+
|
|
54
|
+
**Filtering happens in linear light.** `imread` decodes sRGB to linear float32
|
|
55
|
+
and `imwrite` encodes back. Blurring or resizing gamma-encoded values darkens
|
|
56
|
+
the result — on a synthetic test chart, a σ=3 blur done in gamma space came out
|
|
57
|
+
29% darker than the same blur done correctly, with individual pixels off by
|
|
58
|
+
74/255.
|
|
59
|
+
|
|
60
|
+
## What's here
|
|
61
|
+
|
|
62
|
+
| | |
|
|
63
|
+
|---|---|
|
|
64
|
+
| I/O | `imread`, `imwrite` — PNG, JPEG, BMP, TGA via stb |
|
|
65
|
+
| Point ops | `gamma`, `exposure`, `brightness_contrast`, `invert`, `clip`, `blend`, `apply_lut` |
|
|
66
|
+
| Filtering | `gaussian_blur` (separable) |
|
|
67
|
+
| Sampling | `sample` — nearest and bilinear, border-aware |
|
|
68
|
+
| Color | `srgb_to_linear`, `linear_to_srgb` |
|
|
69
|
+
|
|
70
|
+
Border modes (`clamp`, `reflect`, `reflect101`, `wrap`, `constant`) are shared
|
|
71
|
+
by every spatial op and match `scipy.ndimage`'s semantics.
|
|
72
|
+
|
|
73
|
+
## Install
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
pip install focalpy
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Building from source needs a C++20 compiler and CMake:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
git clone https://github.com/edisons3608/focal
|
|
83
|
+
cd focal
|
|
84
|
+
pip install -e . --no-build-isolation
|
|
85
|
+
pytest
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Status
|
|
89
|
+
|
|
90
|
+
Early. The API will change. Tests compare against `scipy.ndimage` and `PIL`
|
|
91
|
+
where an oracle exists.
|
|
92
|
+
|
|
93
|
+
## License
|
|
94
|
+
|
|
95
|
+
MIT. Bundles [stb](https://github.com/nothings/stb) (public domain).
|
focalpy-0.0.1/README.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# focal
|
|
2
|
+
|
|
3
|
+
Image processing in C++20 with a zero-copy NumPy interface.
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
import focal
|
|
7
|
+
|
|
8
|
+
img = focal.imread("photo.jpg") # linear float32, (h, w, c)
|
|
9
|
+
img = focal.exposure(img, 0.5) # half a stop brighter
|
|
10
|
+
img = focal.gaussian_blur(img, 2.0)
|
|
11
|
+
focal.imwrite("out.png", img) # encodes back to sRGB
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Design
|
|
15
|
+
|
|
16
|
+
**Everything is a view.** The core type is a non-owning `{pointer, dtype, shape,
|
|
17
|
+
strides}` struct, so a crop, a single channel, and a NumPy array handed over
|
|
18
|
+
from Python are all the same type, and none of them copy. Strides are in bytes,
|
|
19
|
+
matching NumPy exactly, so wrapping an incoming array is copying six integers.
|
|
20
|
+
|
|
21
|
+
**Nothing converts silently.** Array arguments are declared `.noconvert()`, so
|
|
22
|
+
passing a float64 or non-contiguous array raises `TypeError` instead of quietly
|
|
23
|
+
costing a full-size copy. Converting is the caller's decision.
|
|
24
|
+
|
|
25
|
+
**Filtering happens in linear light.** `imread` decodes sRGB to linear float32
|
|
26
|
+
and `imwrite` encodes back. Blurring or resizing gamma-encoded values darkens
|
|
27
|
+
the result — on a synthetic test chart, a σ=3 blur done in gamma space came out
|
|
28
|
+
29% darker than the same blur done correctly, with individual pixels off by
|
|
29
|
+
74/255.
|
|
30
|
+
|
|
31
|
+
## What's here
|
|
32
|
+
|
|
33
|
+
| | |
|
|
34
|
+
|---|---|
|
|
35
|
+
| I/O | `imread`, `imwrite` — PNG, JPEG, BMP, TGA via stb |
|
|
36
|
+
| Point ops | `gamma`, `exposure`, `brightness_contrast`, `invert`, `clip`, `blend`, `apply_lut` |
|
|
37
|
+
| Filtering | `gaussian_blur` (separable) |
|
|
38
|
+
| Sampling | `sample` — nearest and bilinear, border-aware |
|
|
39
|
+
| Color | `srgb_to_linear`, `linear_to_srgb` |
|
|
40
|
+
|
|
41
|
+
Border modes (`clamp`, `reflect`, `reflect101`, `wrap`, `constant`) are shared
|
|
42
|
+
by every spatial op and match `scipy.ndimage`'s semantics.
|
|
43
|
+
|
|
44
|
+
## Install
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install focalpy
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Building from source needs a C++20 compiler and CMake:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
git clone https://github.com/edisons3608/focal
|
|
54
|
+
cd focal
|
|
55
|
+
pip install -e . --no-build-isolation
|
|
56
|
+
pytest
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Status
|
|
60
|
+
|
|
61
|
+
Early. The API will change. Tests compare against `scipy.ndimage` and `PIL`
|
|
62
|
+
where an oracle exists.
|
|
63
|
+
|
|
64
|
+
## License
|
|
65
|
+
|
|
66
|
+
MIT. Bundles [stb](https://github.com/nothings/stb) (public domain).
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <cstdint>
|
|
4
|
+
|
|
5
|
+
namespace focal {
|
|
6
|
+
|
|
7
|
+
// What happens when an op reads outside the image. Every spatial op routes
|
|
8
|
+
// its out-of-range reads through map_index() so they all agree about edges.
|
|
9
|
+
//
|
|
10
|
+
// For a 4-wide row `a b c d`, reading left of it gives:
|
|
11
|
+
// Clamp a a a a | a b c d (scipy: "nearest")
|
|
12
|
+
// Reflect d c b a | a b c d (scipy: "reflect") edge sample doubled
|
|
13
|
+
// Reflect101 d c b | a b c d (scipy: "mirror") edge sample not repeated
|
|
14
|
+
// Wrap a b c d | a b c d (scipy: "grid-wrap")
|
|
15
|
+
// Constant -- out of range, caller substitutes a fill value
|
|
16
|
+
enum class Border { Clamp, Reflect, Reflect101, Wrap, Constant };
|
|
17
|
+
|
|
18
|
+
// Sentinel returned for Border::Constant when the index is outside the image.
|
|
19
|
+
inline constexpr int64_t kOutOfBounds = -1;
|
|
20
|
+
|
|
21
|
+
// Fold an arbitrary index into [0, n), or kOutOfBounds under Border::Constant.
|
|
22
|
+
inline int64_t map_index(int64_t i, int64_t n, Border b) {
|
|
23
|
+
if (n <= 0) return kOutOfBounds;
|
|
24
|
+
if (i >= 0 && i < n) return i;
|
|
25
|
+
if (n == 1) return b == Border::Constant ? kOutOfBounds : 0;
|
|
26
|
+
|
|
27
|
+
switch (b) {
|
|
28
|
+
case Border::Clamp:
|
|
29
|
+
return i < 0 ? 0 : n - 1;
|
|
30
|
+
|
|
31
|
+
case Border::Reflect: { // period 2n, edge sample repeated
|
|
32
|
+
const int64_t period = 2 * n;
|
|
33
|
+
int64_t k = ((i % period) + period) % period;
|
|
34
|
+
return k < n ? k : period - 1 - k;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
case Border::Reflect101: { // period 2n-2, edge sample not repeated
|
|
38
|
+
const int64_t period = 2 * n - 2;
|
|
39
|
+
int64_t k = ((i % period) + period) % period;
|
|
40
|
+
return k < n ? k : period - k;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
case Border::Wrap:
|
|
44
|
+
return ((i % n) + n) % n;
|
|
45
|
+
|
|
46
|
+
case Border::Constant:
|
|
47
|
+
return kOutOfBounds;
|
|
48
|
+
}
|
|
49
|
+
return kOutOfBounds;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
} // namespace focal
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <cstddef>
|
|
4
|
+
#include <vector>
|
|
5
|
+
|
|
6
|
+
#include "focal/view.hpp"
|
|
7
|
+
|
|
8
|
+
namespace focal {
|
|
9
|
+
|
|
10
|
+
// Owns pixels. Ops never take a Buffer -- they take a View, so the same code
|
|
11
|
+
// works on memory we allocated and on memory NumPy allocated.
|
|
12
|
+
class Buffer {
|
|
13
|
+
public:
|
|
14
|
+
Buffer() = default;
|
|
15
|
+
|
|
16
|
+
Buffer(int64_t height, int64_t width, int64_t channels, Dtype dt)
|
|
17
|
+
: dtype_(dt),
|
|
18
|
+
height_(height),
|
|
19
|
+
width_(width),
|
|
20
|
+
channels_(channels),
|
|
21
|
+
storage_(static_cast<size_t>(height * width * channels *
|
|
22
|
+
dtype_size(dt))) {}
|
|
23
|
+
|
|
24
|
+
View view() {
|
|
25
|
+
return make_view(storage_.data(), dtype_, height_, width_, channels_);
|
|
26
|
+
}
|
|
27
|
+
ConstView view() const {
|
|
28
|
+
return make_view(storage_.data(), dtype_, height_, width_, channels_);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
Dtype dtype() const { return dtype_; }
|
|
32
|
+
int64_t height() const { return height_; }
|
|
33
|
+
int64_t width() const { return width_; }
|
|
34
|
+
int64_t channels() const { return channels_; }
|
|
35
|
+
size_t size_bytes() const { return storage_.size(); }
|
|
36
|
+
|
|
37
|
+
private:
|
|
38
|
+
Dtype dtype_ = Dtype::F32;
|
|
39
|
+
int64_t height_ = 0;
|
|
40
|
+
int64_t width_ = 0;
|
|
41
|
+
int64_t channels_ = 0;
|
|
42
|
+
std::vector<std::byte> storage_;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
} // namespace focal
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <stdexcept>
|
|
4
|
+
#include <string>
|
|
5
|
+
|
|
6
|
+
#include "focal/view.hpp"
|
|
7
|
+
|
|
8
|
+
namespace focal {
|
|
9
|
+
|
|
10
|
+
// Shared preconditions. Ops call these once at the top, never per pixel.
|
|
11
|
+
|
|
12
|
+
inline void check_f32(ConstView v, const char *who) {
|
|
13
|
+
if (v.dtype != Dtype::F32)
|
|
14
|
+
throw std::invalid_argument(std::string(who) +
|
|
15
|
+
": expected an F32 view; convert at the edges "
|
|
16
|
+
"of the pipeline, not inside ops");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
inline void check_same_shape(ConstView a, ConstView b, const char *who) {
|
|
20
|
+
if (a.height != b.height || a.width != b.width || a.channels != b.channels)
|
|
21
|
+
throw std::invalid_argument(std::string(who) + ": shape mismatch (" +
|
|
22
|
+
std::to_string(a.height) + "x" +
|
|
23
|
+
std::to_string(a.width) + "x" +
|
|
24
|
+
std::to_string(a.channels) + " vs " +
|
|
25
|
+
std::to_string(b.height) + "x" +
|
|
26
|
+
std::to_string(b.width) + "x" +
|
|
27
|
+
std::to_string(b.channels) + ")");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
inline void check_channels(ConstView v, int64_t want, const char *who) {
|
|
31
|
+
if (v.channels != want)
|
|
32
|
+
throw std::invalid_argument(std::string(who) + ": expected " +
|
|
33
|
+
std::to_string(want) + " channels, got " +
|
|
34
|
+
std::to_string(v.channels));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// The last channel of a 2- or 4-channel image is alpha: a linear coverage
|
|
38
|
+
// fraction, not a colour. Tone curves and transfer functions must skip it.
|
|
39
|
+
// io.cpp applies the same rule; keep them agreeing by using this.
|
|
40
|
+
inline bool is_alpha_channel(int64_t channels, int64_t c) {
|
|
41
|
+
return (channels == 2 || channels == 4) && c == channels - 1;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
} // namespace focal
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <algorithm>
|
|
4
|
+
#include <cstdint>
|
|
5
|
+
|
|
6
|
+
#include "focal/buffer.hpp"
|
|
7
|
+
#include "focal/view.hpp"
|
|
8
|
+
|
|
9
|
+
namespace focal {
|
|
10
|
+
|
|
11
|
+
// Normalization convention, decided once here so no op re-decides it:
|
|
12
|
+
// u8 255 <-> 1.0f
|
|
13
|
+
// u16 65535 <-> 1.0f
|
|
14
|
+
// f32 passthrough, unclamped
|
|
15
|
+
// Values are clamped to [0, 1] on the way down into an integer type.
|
|
16
|
+
|
|
17
|
+
inline float unit_from_u8(uint8_t v) { return v * (1.0f / 255.0f); }
|
|
18
|
+
inline float unit_from_u16(uint16_t v) { return v * (1.0f / 65535.0f); }
|
|
19
|
+
|
|
20
|
+
inline uint8_t u8_from_unit(float f) {
|
|
21
|
+
return static_cast<uint8_t>(std::clamp(f, 0.0f, 1.0f) * 255.0f + 0.5f);
|
|
22
|
+
}
|
|
23
|
+
inline uint16_t u16_from_unit(float f) {
|
|
24
|
+
return static_cast<uint16_t>(std::clamp(f, 0.0f, 1.0f) * 65535.0f + 0.5f);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Read one sample as a unit float, whatever the view's dtype. Used at the
|
|
28
|
+
// edges of the library; ops themselves run on f32 views and skip this.
|
|
29
|
+
inline float load_unit(ConstView v, int64_t y, int64_t x, int64_t c = 0) {
|
|
30
|
+
switch (v.dtype) {
|
|
31
|
+
case Dtype::U8: return unit_from_u8(v.at<uint8_t>(y, x, c));
|
|
32
|
+
case Dtype::U16: return unit_from_u16(v.at<uint16_t>(y, x, c));
|
|
33
|
+
case Dtype::F32: return v.at<float>(y, x, c);
|
|
34
|
+
}
|
|
35
|
+
return 0.0f;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
inline void store_unit(View v, int64_t y, int64_t x, int64_t c, float f) {
|
|
39
|
+
switch (v.dtype) {
|
|
40
|
+
case Dtype::U8: v.at<uint8_t>(y, x, c) = u8_from_unit(f); break;
|
|
41
|
+
case Dtype::U16: v.at<uint16_t>(y, x, c) = u16_from_unit(f); break;
|
|
42
|
+
case Dtype::F32: v.at<float>(y, x, c) = f; break;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Whole-image dtype conversion.
|
|
47
|
+
Buffer convert(ConstView src, Dtype dst);
|
|
48
|
+
|
|
49
|
+
// Decode integer sRGB to linear f32, and encode back. These are the two calls
|
|
50
|
+
// that belong at the very edges of a pipeline.
|
|
51
|
+
Buffer srgb_to_linear_f32(ConstView src);
|
|
52
|
+
Buffer linear_f32_to_srgb(ConstView src, Dtype dst);
|
|
53
|
+
|
|
54
|
+
} // namespace focal
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <string>
|
|
4
|
+
|
|
5
|
+
#include "focal/buffer.hpp"
|
|
6
|
+
#include "focal/view.hpp"
|
|
7
|
+
|
|
8
|
+
namespace focal {
|
|
9
|
+
|
|
10
|
+
// Image file I/O, backed by stb_image / stb_image_write (vendored in
|
|
11
|
+
// third_party/). Supports 8-bit PNG, JPEG, BMP, TGA and reads GIF/PSD/PIC.
|
|
12
|
+
// Not for 16-bit PNG, EXR, TIFF or camera raw -- swap in libpng /
|
|
13
|
+
// libjpeg-turbo / tinyexr / libraw when those matter.
|
|
14
|
+
//
|
|
15
|
+
// Files hold sRGB-encoded bytes. load() decodes them to LINEAR f32 so that
|
|
16
|
+
// everything downstream -- blurring, resizing, compositing -- is correct;
|
|
17
|
+
// save() encodes back. Use load_raw()/save_raw() only when you deliberately
|
|
18
|
+
// want the stored values, e.g. inspecting a file's actual bytes.
|
|
19
|
+
|
|
20
|
+
// Decode to linear f32, channels as stored in the file (1, 2, 3 or 4).
|
|
21
|
+
// Pass want_channels to force a channel count (1 = grey, 3 = RGB, 4 = RGBA).
|
|
22
|
+
Buffer load(const std::string &path, int want_channels = 0);
|
|
23
|
+
|
|
24
|
+
// Decode to u8 exactly as stored, with no transfer-function conversion.
|
|
25
|
+
Buffer load_raw(const std::string &path, int want_channels = 0);
|
|
26
|
+
|
|
27
|
+
// Encode linear f32 (or pass u8 through) and write. Format comes from the
|
|
28
|
+
// file extension: .png .jpg .jpeg .bmp .tga
|
|
29
|
+
void save(const std::string &path, ConstView img, int jpeg_quality = 95);
|
|
30
|
+
|
|
31
|
+
} // namespace focal
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <vector>
|
|
4
|
+
|
|
5
|
+
#include "focal/border.hpp"
|
|
6
|
+
#include "focal/view.hpp"
|
|
7
|
+
|
|
8
|
+
namespace focal {
|
|
9
|
+
|
|
10
|
+
// Normalized 1D Gaussian weights, radius = int(truncate * sigma + 0.5).
|
|
11
|
+
// Matches scipy.ndimage's kernel exactly, which is what the tests compare to.
|
|
12
|
+
std::vector<float> gaussian_kernel1d(float sigma, float truncate = 4.0f);
|
|
13
|
+
|
|
14
|
+
// Separable Gaussian blur. src and dst must be F32, same shape, and must not
|
|
15
|
+
// alias. Separable means two 1D passes (O(2r) per pixel) instead of one 2D
|
|
16
|
+
// convolution (O(r^2)) -- the whole reason to special-case Gaussians.
|
|
17
|
+
void gaussian_blur(ConstView src, View dst, float sigma,
|
|
18
|
+
Border border = Border::Reflect101, float cval = 0.0f);
|
|
19
|
+
|
|
20
|
+
} // namespace focal
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <cstdint>
|
|
4
|
+
|
|
5
|
+
#include "focal/check.hpp"
|
|
6
|
+
#include "focal/view.hpp"
|
|
7
|
+
|
|
8
|
+
namespace focal {
|
|
9
|
+
|
|
10
|
+
// Point operations: every output sample depends only on the sample at the same
|
|
11
|
+
// position. No neighbourhood, no border handling, and safe to run in place
|
|
12
|
+
// (pass the same memory as src and dst).
|
|
13
|
+
//
|
|
14
|
+
// WHICH COLOUR SPACE
|
|
15
|
+
//
|
|
16
|
+
// Filtering must happen in linear light because blurring MIXES pixels, and
|
|
17
|
+
// mixing light is physics. Tone adjustment is different -- it remaps a single
|
|
18
|
+
// value, and the result people expect comes from remapping DISPLAY-REFERRED
|
|
19
|
+
// (sRGB-encoded) values. So:
|
|
20
|
+
//
|
|
21
|
+
// exposure() linear. It is a light-amount change: multiply.
|
|
22
|
+
// gamma() display. Definitionally an encoding change.
|
|
23
|
+
// brightness_contrast() display. Matches what an editor's sliders do.
|
|
24
|
+
// apply_lut() display, normally -- it is a curve on tone.
|
|
25
|
+
// invert(), blend() either; blend of colours wants linear.
|
|
26
|
+
//
|
|
27
|
+
// These functions do not convert for you. Hand them a view already in the
|
|
28
|
+
// space you mean.
|
|
29
|
+
|
|
30
|
+
// The workhorse. Everything else in this header is three lines on top of it.
|
|
31
|
+
// fn is called per sample; alpha is passed through untouched when skip_alpha.
|
|
32
|
+
template <typename F>
|
|
33
|
+
void map(ConstView src, View dst, F fn, bool skip_alpha = true) {
|
|
34
|
+
check_f32(src, "map");
|
|
35
|
+
check_f32(dst, "map");
|
|
36
|
+
check_same_shape(src, dst, "map");
|
|
37
|
+
|
|
38
|
+
for (int64_t y = 0; y < src.height; ++y) {
|
|
39
|
+
for (int64_t x = 0; x < src.width; ++x) {
|
|
40
|
+
for (int64_t c = 0; c < src.channels; ++c) {
|
|
41
|
+
const float v = src.at<float>(y, x, c);
|
|
42
|
+
dst.at<float>(y, x, c) =
|
|
43
|
+
(skip_alpha && is_alpha_channel(src.channels, c)) ? v : fn(v);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
template <typename F>
|
|
51
|
+
void map2(ConstView src1, ConstView src2, View dst, F fn, bool skip_alpha = true) {
|
|
52
|
+
check_f32(src1, "map2");
|
|
53
|
+
check_f32(src2, "map2");
|
|
54
|
+
check_f32(dst, "map2");
|
|
55
|
+
check_same_shape(src1, src2, "map2");
|
|
56
|
+
check_same_shape(src1, dst, "map2");
|
|
57
|
+
|
|
58
|
+
for (int64_t y = 0; y < src1.height; ++y) {
|
|
59
|
+
for (int64_t x = 0; x < src1.width; ++x) {
|
|
60
|
+
for (int64_t c = 0; c < src1.channels; ++c) {
|
|
61
|
+
const float v = src1.at<float>(y, x, c);
|
|
62
|
+
const float w = src2.at<float>(y, x, c);
|
|
63
|
+
dst.at<float>(y, x, c) =
|
|
64
|
+
(skip_alpha && is_alpha_channel(src1.channels, c)) ? v : fn(v, w);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// v -> v^g. Values below zero are left alone (pow of a negative is NaN).
|
|
71
|
+
void gamma(ConstView src, View dst, float g);
|
|
72
|
+
|
|
73
|
+
// v -> v * 2^stops. Linear-light only; on encoded values it is meaningless.
|
|
74
|
+
void exposure(ConstView src, View dst, float stops);
|
|
75
|
+
|
|
76
|
+
// v -> (v - pivot) * contrast + pivot + brightness
|
|
77
|
+
//
|
|
78
|
+
// The pivot is why contrast does not also brighten: it is the value held
|
|
79
|
+
// fixed while everything else spreads away from it. 0.5 for display-referred
|
|
80
|
+
// values, 0.18 (middle grey) if you insist on working linear.
|
|
81
|
+
void brightness_contrast(ConstView src, View dst, float brightness,
|
|
82
|
+
float contrast, float pivot = 0.5f);
|
|
83
|
+
|
|
84
|
+
// v -> 1 - v.
|
|
85
|
+
void invert(ConstView src, View dst);
|
|
86
|
+
|
|
87
|
+
// v -> clip(v, lo, hi).
|
|
88
|
+
void clip(ConstView src, View dst, float lo = 0.0f, float hi = 1.0f);
|
|
89
|
+
|
|
90
|
+
// dst = a + (b - a) * t, elementwise. a and b must have the same shape.
|
|
91
|
+
void blend(ConstView a, ConstView b, View dst, float t);
|
|
92
|
+
|
|
93
|
+
// Apply a tone curve held as a lookup table.
|
|
94
|
+
//
|
|
95
|
+
// DOMAIN: the n entries span [0, 1] inclusive, so lut[0] is the output for
|
|
96
|
+
// input 0.0 and lut[n-1] the output for 1.0. Inputs between entries are
|
|
97
|
+
// linearly interpolated; inputs outside [0, 1] are clamped to the ends.
|
|
98
|
+
//
|
|
99
|
+
// This is also the fast path for any expensive curve -- bake gamma or an sRGB
|
|
100
|
+
// transfer into 1024 entries once instead of calling pow() per pixel.
|
|
101
|
+
void apply_lut(ConstView src, View dst, const float *lut, int n);
|
|
102
|
+
|
|
103
|
+
// Fill `lut` (n entries, same domain as above) by sampling fn over [0, 1].
|
|
104
|
+
template <typename F>
|
|
105
|
+
void bake_lut(float *lut, int n, F fn) {
|
|
106
|
+
for (int i = 0; i < n; ++i)
|
|
107
|
+
lut[i] = fn(static_cast<float>(i) / static_cast<float>(n - 1));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
} // namespace focal
|