ubo-morph 1.0.0__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.
- ubo_morph-1.0.0/.gitignore +10 -0
- ubo_morph-1.0.0/PKG-INFO +281 -0
- ubo_morph-1.0.0/README.md +258 -0
- ubo_morph-1.0.0/pyproject.toml +66 -0
- ubo_morph-1.0.0/src/ubo_morph/__init__.py +21 -0
- ubo_morph-1.0.0/src/ubo_morph/cli.py +769 -0
- ubo_morph-1.0.0/src/ubo_morph/landmarks/__init__.py +111 -0
- ubo_morph-1.0.0/src/ubo_morph/landmarks/dlib.py +70 -0
- ubo_morph-1.0.0/src/ubo_morph/landmarks/mediapipe.py +153 -0
- ubo_morph-1.0.0/src/ubo_morph/morphing/__init__.py +12 -0
- ubo_morph-1.0.0/src/ubo_morph/morphing/alignment.py +106 -0
- ubo_morph-1.0.0/src/ubo_morph/morphing/backend.py +189 -0
- ubo_morph-1.0.0/src/ubo_morph/morphing/core.py +628 -0
- ubo_morph-1.0.0/src/ubo_morph/morphing/cpu/__init__.py +5 -0
- ubo_morph-1.0.0/src/ubo_morph/morphing/cpu/backend.py +232 -0
- ubo_morph-1.0.0/src/ubo_morph/morphing/cupy/__init__.py +19 -0
- ubo_morph-1.0.0/src/ubo_morph/morphing/cupy/backend.py +551 -0
- ubo_morph-1.0.0/src/ubo_morph/morphing/cupy/kernels.cu +876 -0
- ubo_morph-1.0.0/src/ubo_morph/morphing/cupy/kernels.py +33 -0
- ubo_morph-1.0.0/src/ubo_morph/morphing/points.py +114 -0
- ubo_morph-1.0.0/src/ubo_morph/py.typed +0 -0
- ubo_morph-1.0.0/src/ubo_morph/utils.py +24 -0
- ubo_morph-1.0.0/src/ubo_morph/visualization.py +121 -0
ubo_morph-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: ubo-morph
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Author-email: ndido98 <ndido98@gmail.com>
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
12
|
+
Requires-Python: >=3.10
|
|
13
|
+
Requires-Dist: numpy<2.5.0,>=2.0.0
|
|
14
|
+
Requires-Dist: opencv-contrib-python>=5.0.0.93
|
|
15
|
+
Requires-Dist: tqdm>=4.68.4
|
|
16
|
+
Provides-Extra: cupy
|
|
17
|
+
Requires-Dist: cupy-cuda12x[ctk]>=13.0; extra == 'cupy'
|
|
18
|
+
Provides-Extra: dlib
|
|
19
|
+
Requires-Dist: dlib>=20.0.1; extra == 'dlib'
|
|
20
|
+
Provides-Extra: mediapipe
|
|
21
|
+
Requires-Dist: mediapipe>=0.10.35; extra == 'mediapipe'
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# UBO Morphing Algorithm
|
|
25
|
+
|
|
26
|
+
This package contains the reference implementation in Python of the UBO landmark-based morphing algorithm
|
|
27
|
+
described in the paper "Decoupling texture blending and shape warping in face mopring" by M. Ferrara et al.
|
|
28
|
+
|
|
29
|
+
## Quick start
|
|
30
|
+
|
|
31
|
+
UBO Morph supports Python 3.10 and newer.
|
|
32
|
+
|
|
33
|
+
The examples below use MediaPipe and its `face_landmarker.task` model. Put the
|
|
34
|
+
model and two face images in the working directory:
|
|
35
|
+
|
|
36
|
+
```console
|
|
37
|
+
pip install "ubo-morph[mediapipe]"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Morph from the command line
|
|
41
|
+
|
|
42
|
+
Run one morph with a shape-warping factor and a texture-blending factor of
|
|
43
|
+
`0.5` (the defaults):
|
|
44
|
+
|
|
45
|
+
```console
|
|
46
|
+
ubo-morph first.jpg second.jpg --extractor mediapipe --model face_landmarker.task --output-dir output
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The resulting PNG is written under `output/` with an `M_...png` filename.
|
|
50
|
+
Use `--factor` to produce several linked shape and texture blends in one run:
|
|
51
|
+
|
|
52
|
+
```console
|
|
53
|
+
ubo-morph first.jpg second.jpg --extractor mediapipe --model face_landmarker.task --output-dir output --factor 0.25 0.50 0.75
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Inspect the landmark mesh and pipeline images for one factor combination with:
|
|
57
|
+
|
|
58
|
+
```console
|
|
59
|
+
ubo-morph first.jpg second.jpg --extractor mediapipe --model face_landmarker.task --output-dir output --intermediate-results
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
This creates a factor-qualified directory containing `morphed.png`, its
|
|
63
|
+
`morphed_annotated.png` landmark-mesh visualization, and the available
|
|
64
|
+
intermediate images.
|
|
65
|
+
|
|
66
|
+
### Morph from Python
|
|
67
|
+
|
|
68
|
+
OpenCV reads and writes BGR images, matching the package API. This example
|
|
69
|
+
writes the default midpoint morph to `output.png`:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
import cv2
|
|
73
|
+
|
|
74
|
+
from ubo_morph import MediaPipeLandmarkExtractor, morph_images
|
|
75
|
+
|
|
76
|
+
image1 = cv2.imread("first.jpg", cv2.IMREAD_COLOR)
|
|
77
|
+
image2 = cv2.imread("second.jpg", cv2.IMREAD_COLOR)
|
|
78
|
+
if image1 is None or image2 is None:
|
|
79
|
+
raise FileNotFoundError("Could not read first.jpg or second.jpg")
|
|
80
|
+
|
|
81
|
+
with MediaPipeLandmarkExtractor("face_landmarker.task") as extractor:
|
|
82
|
+
result = morph_images(image1, image2, extractor)
|
|
83
|
+
|
|
84
|
+
if not cv2.imwrite("output.png", result):
|
|
85
|
+
raise OSError("Could not write output.png")
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Set `warping_factor` and `blending_factor` independently when the facial shape
|
|
89
|
+
and texture should progress at different rates:
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
with MediaPipeLandmarkExtractor("face_landmarker.task") as extractor:
|
|
93
|
+
result = morph_images(
|
|
94
|
+
image1,
|
|
95
|
+
image2,
|
|
96
|
+
extractor,
|
|
97
|
+
warping_factor=0.25,
|
|
98
|
+
blending_factor=0.75,
|
|
99
|
+
)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Python API
|
|
103
|
+
|
|
104
|
+
The high-level entry points and both landmark extractors are available directly
|
|
105
|
+
from `ubo_morph`. Choose one extractor and provide its compatible model file:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from ubo_morph import (
|
|
109
|
+
DlibLandmarkExtractor,
|
|
110
|
+
MediaPipeLandmarkExtractor,
|
|
111
|
+
morph_images,
|
|
112
|
+
morph_with_landmarks,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# MediaPipe returns its face-landmarker mesh.
|
|
116
|
+
with MediaPipeLandmarkExtractor("face_landmarker.task") as extractor:
|
|
117
|
+
result = morph_images(image1, image2, extractor)
|
|
118
|
+
|
|
119
|
+
# Dlib is an alternative extractor that requires a 68-point shape predictor.
|
|
120
|
+
with DlibLandmarkExtractor("shape_predictor_68_face_landmarks.dat") as extractor:
|
|
121
|
+
result = morph_images(image1, image2, extractor)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Once an extractor is selected, pass it to `morph_images` with any pipeline
|
|
125
|
+
options:
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
with MediaPipeLandmarkExtractor("face_landmarker.task") as extractor:
|
|
129
|
+
# Control the number of points added to each image edge, or disable them.
|
|
130
|
+
result = morph_images(image1, image2, extractor, points_per_border=7)
|
|
131
|
+
result = morph_images(image1, image2, extractor, points_per_border=0)
|
|
132
|
+
|
|
133
|
+
# Cap the shortest detector-input side at 640 px, then morph full-size inputs.
|
|
134
|
+
result = morph_images(
|
|
135
|
+
image1,
|
|
136
|
+
image2,
|
|
137
|
+
extractor,
|
|
138
|
+
landmark_extraction_short_side=640,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Select one of the exact backend names: "cpu" or "cupy".
|
|
142
|
+
result = morph_images(image1, image2, extractor, backend="cupy")
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`cpu` is the default. Backend selection is explicit: unavailable accelerators
|
|
146
|
+
raise an error and never fall back to CPU.
|
|
147
|
+
|
|
148
|
+
## Backend interface
|
|
149
|
+
|
|
150
|
+
The backend contract and lazy selector are available from `ubo_morph.morphing`:
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
from ubo_morph.morphing import Backend, BackendName, get_backend
|
|
154
|
+
|
|
155
|
+
cpu = get_backend("cpu")
|
|
156
|
+
assert cpu.name == "cpu"
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Concrete classes are exposed only by their backend subpackages:
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
from ubo_morph.morphing.cpu import CPUBackend
|
|
163
|
+
|
|
164
|
+
backend = CPUBackend()
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Install CuPy support together with at least one landmark extractor before
|
|
168
|
+
selecting `backend="cupy"`.
|
|
169
|
+
|
|
170
|
+
## Extractors and backends
|
|
171
|
+
|
|
172
|
+
Concrete dlib and MediaPipe extractors are available from the top-level package.
|
|
173
|
+
Both require a compatible model file supplied by the caller and select the
|
|
174
|
+
largest detected face when multiple faces are returned.
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
from ubo_morph import DlibLandmarkExtractor, MediaPipeLandmarkExtractor
|
|
178
|
+
|
|
179
|
+
with DlibLandmarkExtractor("shape_predictor_68_face_landmarks.dat") as extractor:
|
|
180
|
+
dlib_result = morph_images(image1, image2, extractor)
|
|
181
|
+
|
|
182
|
+
with MediaPipeLandmarkExtractor("face_landmarker.task") as extractor:
|
|
183
|
+
mediapipe_result = morph_images(image1, image2, extractor)
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Install dlib instead of MediaPipe with:
|
|
187
|
+
|
|
188
|
+
```console
|
|
189
|
+
pip install "ubo-morph[dlib]"
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## CLI reference
|
|
193
|
+
|
|
194
|
+
Morph one pair directly with either landmark backend:
|
|
195
|
+
|
|
196
|
+
```console
|
|
197
|
+
ubo-morph first.jpg second.jpg --extractor mediapipe --model face_landmarker.task
|
|
198
|
+
ubo-morph first.jpg second.jpg --extractor dlib --model shape_predictor_68_face_landmarks.dat
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Pass multiple linked factors with `--factor`; each value is used for both
|
|
202
|
+
warping and blending. Separate factor lists produce their Cartesian product:
|
|
203
|
+
|
|
204
|
+
```console
|
|
205
|
+
ubo-morph first.jpg second.jpg --extractor mediapipe --model face_landmarker.task --factor 0.25 0.50 0.75
|
|
206
|
+
ubo-morph first.jpg second.jpg --extractor mediapipe --model face_landmarker.task --warping-factor 0.25 0.50 --blending-factor 0.50 0.75
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Batch input is passed as a positional CSV path:
|
|
210
|
+
|
|
211
|
+
```console
|
|
212
|
+
ubo-morph pairs.csv --extractor mediapipe --model face_landmarker.task
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Headerless CSV files accept two image columns and an optional third output
|
|
216
|
+
filename column. Headered CSV files may use `factor`, or both `warping_factor`
|
|
217
|
+
and `blending_factor`; an optional `output`, `output_filename`, or `filename`
|
|
218
|
+
column controls the destination name. Relative image paths are resolved from the
|
|
219
|
+
CSV directory. CSV factor columns cannot be combined with CLI factor arguments.
|
|
220
|
+
During each CLI run, landmarks are cached in memory by resolved image path, so
|
|
221
|
+
images reused across pairs are extracted only once. The cache is discarded when
|
|
222
|
+
the command exits.
|
|
223
|
+
|
|
224
|
+
By default, a failing pair stops the command. Pass `--skip-failing-pairs` to
|
|
225
|
+
report the affected file or files and failure reason, then continue with the
|
|
226
|
+
remaining pairs.
|
|
227
|
+
|
|
228
|
+
Use `--intermediate-results` to create a factor-qualified `M_...png/` directory
|
|
229
|
+
containing `morphed.png` and every image-valued intermediate field from
|
|
230
|
+
`MorphResult`. Every saved image, including `morphed.png`, has an accompanying
|
|
231
|
+
`_annotated.png` version containing indexed facial landmarks, unindexed border
|
|
232
|
+
points, and the Delaunay triangulation used by the morph. The intermediate
|
|
233
|
+
images include the aligned images before color equalization, the image actually
|
|
234
|
+
changed by equalization when it runs, the warped images, and the blended image
|
|
235
|
+
when background substitution follows. `MorphResult` also exposes the original
|
|
236
|
+
and aligned landmarks for both inputs. Use `--points-per-border COUNT` to change
|
|
237
|
+
the default of five; zero disables border points, as does the
|
|
238
|
+
`--no-border-points` convenience flag. Run `ubo-morph --help` for all alignment,
|
|
239
|
+
retouching, background, and extractor-specific settings.
|
|
240
|
+
|
|
241
|
+
For faster landmark detection on large inputs, set
|
|
242
|
+
`--landmark-extraction-short-side PIXELS`. Images whose shortest side exceeds
|
|
243
|
+
that limit are resized so it equals the limit, with the other side scaled
|
|
244
|
+
proportionally. Only the extractor input is resized; detected coordinates are
|
|
245
|
+
mapped back to the original image size before full-resolution morphing. The
|
|
246
|
+
default value of zero disables this resizing.
|
|
247
|
+
|
|
248
|
+
## Module layout
|
|
249
|
+
|
|
250
|
+
```text
|
|
251
|
+
ubo_morph/
|
|
252
|
+
morphing/
|
|
253
|
+
backend.py # typed backend contract and lazy selector
|
|
254
|
+
core.py # shared geometry, retouching, triangulation, and flow
|
|
255
|
+
points.py # shared point and mask operations
|
|
256
|
+
cpu/backend.py # NumPy/OpenCV primitive implementation
|
|
257
|
+
cupy/backend.py # optional CuPy primitive implementation
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## Validation
|
|
261
|
+
|
|
262
|
+
```console
|
|
263
|
+
uv run pytest -v
|
|
264
|
+
uv run ruff check .
|
|
265
|
+
uv run ty check src tests
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
## Contributing
|
|
269
|
+
|
|
270
|
+
Contributions are welcome. Create a focused branch, add or update tests for
|
|
271
|
+
behavior changes, and run the validation commands above before opening a pull
|
|
272
|
+
request. Keep changes scoped to the issue being addressed and include a clear
|
|
273
|
+
description of the behavior change in the pull request. Commit messages must
|
|
274
|
+
follow the Conventional Commits specification, for example `feat: add GPU
|
|
275
|
+
batching` or `fix(cli): report invalid input`. To validate messages before each
|
|
276
|
+
commit, install the optional hook:
|
|
277
|
+
|
|
278
|
+
```console
|
|
279
|
+
uv sync --group dev
|
|
280
|
+
uv run pre-commit install --hook-type commit-msg
|
|
281
|
+
```
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
# UBO Morphing Algorithm
|
|
2
|
+
|
|
3
|
+
This package contains the reference implementation in Python of the UBO landmark-based morphing algorithm
|
|
4
|
+
described in the paper "Decoupling texture blending and shape warping in face mopring" by M. Ferrara et al.
|
|
5
|
+
|
|
6
|
+
## Quick start
|
|
7
|
+
|
|
8
|
+
UBO Morph supports Python 3.10 and newer.
|
|
9
|
+
|
|
10
|
+
The examples below use MediaPipe and its `face_landmarker.task` model. Put the
|
|
11
|
+
model and two face images in the working directory:
|
|
12
|
+
|
|
13
|
+
```console
|
|
14
|
+
pip install "ubo-morph[mediapipe]"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### Morph from the command line
|
|
18
|
+
|
|
19
|
+
Run one morph with a shape-warping factor and a texture-blending factor of
|
|
20
|
+
`0.5` (the defaults):
|
|
21
|
+
|
|
22
|
+
```console
|
|
23
|
+
ubo-morph first.jpg second.jpg --extractor mediapipe --model face_landmarker.task --output-dir output
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The resulting PNG is written under `output/` with an `M_...png` filename.
|
|
27
|
+
Use `--factor` to produce several linked shape and texture blends in one run:
|
|
28
|
+
|
|
29
|
+
```console
|
|
30
|
+
ubo-morph first.jpg second.jpg --extractor mediapipe --model face_landmarker.task --output-dir output --factor 0.25 0.50 0.75
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Inspect the landmark mesh and pipeline images for one factor combination with:
|
|
34
|
+
|
|
35
|
+
```console
|
|
36
|
+
ubo-morph first.jpg second.jpg --extractor mediapipe --model face_landmarker.task --output-dir output --intermediate-results
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
This creates a factor-qualified directory containing `morphed.png`, its
|
|
40
|
+
`morphed_annotated.png` landmark-mesh visualization, and the available
|
|
41
|
+
intermediate images.
|
|
42
|
+
|
|
43
|
+
### Morph from Python
|
|
44
|
+
|
|
45
|
+
OpenCV reads and writes BGR images, matching the package API. This example
|
|
46
|
+
writes the default midpoint morph to `output.png`:
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
import cv2
|
|
50
|
+
|
|
51
|
+
from ubo_morph import MediaPipeLandmarkExtractor, morph_images
|
|
52
|
+
|
|
53
|
+
image1 = cv2.imread("first.jpg", cv2.IMREAD_COLOR)
|
|
54
|
+
image2 = cv2.imread("second.jpg", cv2.IMREAD_COLOR)
|
|
55
|
+
if image1 is None or image2 is None:
|
|
56
|
+
raise FileNotFoundError("Could not read first.jpg or second.jpg")
|
|
57
|
+
|
|
58
|
+
with MediaPipeLandmarkExtractor("face_landmarker.task") as extractor:
|
|
59
|
+
result = morph_images(image1, image2, extractor)
|
|
60
|
+
|
|
61
|
+
if not cv2.imwrite("output.png", result):
|
|
62
|
+
raise OSError("Could not write output.png")
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Set `warping_factor` and `blending_factor` independently when the facial shape
|
|
66
|
+
and texture should progress at different rates:
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
with MediaPipeLandmarkExtractor("face_landmarker.task") as extractor:
|
|
70
|
+
result = morph_images(
|
|
71
|
+
image1,
|
|
72
|
+
image2,
|
|
73
|
+
extractor,
|
|
74
|
+
warping_factor=0.25,
|
|
75
|
+
blending_factor=0.75,
|
|
76
|
+
)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Python API
|
|
80
|
+
|
|
81
|
+
The high-level entry points and both landmark extractors are available directly
|
|
82
|
+
from `ubo_morph`. Choose one extractor and provide its compatible model file:
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from ubo_morph import (
|
|
86
|
+
DlibLandmarkExtractor,
|
|
87
|
+
MediaPipeLandmarkExtractor,
|
|
88
|
+
morph_images,
|
|
89
|
+
morph_with_landmarks,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# MediaPipe returns its face-landmarker mesh.
|
|
93
|
+
with MediaPipeLandmarkExtractor("face_landmarker.task") as extractor:
|
|
94
|
+
result = morph_images(image1, image2, extractor)
|
|
95
|
+
|
|
96
|
+
# Dlib is an alternative extractor that requires a 68-point shape predictor.
|
|
97
|
+
with DlibLandmarkExtractor("shape_predictor_68_face_landmarks.dat") as extractor:
|
|
98
|
+
result = morph_images(image1, image2, extractor)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Once an extractor is selected, pass it to `morph_images` with any pipeline
|
|
102
|
+
options:
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
with MediaPipeLandmarkExtractor("face_landmarker.task") as extractor:
|
|
106
|
+
# Control the number of points added to each image edge, or disable them.
|
|
107
|
+
result = morph_images(image1, image2, extractor, points_per_border=7)
|
|
108
|
+
result = morph_images(image1, image2, extractor, points_per_border=0)
|
|
109
|
+
|
|
110
|
+
# Cap the shortest detector-input side at 640 px, then morph full-size inputs.
|
|
111
|
+
result = morph_images(
|
|
112
|
+
image1,
|
|
113
|
+
image2,
|
|
114
|
+
extractor,
|
|
115
|
+
landmark_extraction_short_side=640,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# Select one of the exact backend names: "cpu" or "cupy".
|
|
119
|
+
result = morph_images(image1, image2, extractor, backend="cupy")
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`cpu` is the default. Backend selection is explicit: unavailable accelerators
|
|
123
|
+
raise an error and never fall back to CPU.
|
|
124
|
+
|
|
125
|
+
## Backend interface
|
|
126
|
+
|
|
127
|
+
The backend contract and lazy selector are available from `ubo_morph.morphing`:
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from ubo_morph.morphing import Backend, BackendName, get_backend
|
|
131
|
+
|
|
132
|
+
cpu = get_backend("cpu")
|
|
133
|
+
assert cpu.name == "cpu"
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Concrete classes are exposed only by their backend subpackages:
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
from ubo_morph.morphing.cpu import CPUBackend
|
|
140
|
+
|
|
141
|
+
backend = CPUBackend()
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Install CuPy support together with at least one landmark extractor before
|
|
145
|
+
selecting `backend="cupy"`.
|
|
146
|
+
|
|
147
|
+
## Extractors and backends
|
|
148
|
+
|
|
149
|
+
Concrete dlib and MediaPipe extractors are available from the top-level package.
|
|
150
|
+
Both require a compatible model file supplied by the caller and select the
|
|
151
|
+
largest detected face when multiple faces are returned.
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
from ubo_morph import DlibLandmarkExtractor, MediaPipeLandmarkExtractor
|
|
155
|
+
|
|
156
|
+
with DlibLandmarkExtractor("shape_predictor_68_face_landmarks.dat") as extractor:
|
|
157
|
+
dlib_result = morph_images(image1, image2, extractor)
|
|
158
|
+
|
|
159
|
+
with MediaPipeLandmarkExtractor("face_landmarker.task") as extractor:
|
|
160
|
+
mediapipe_result = morph_images(image1, image2, extractor)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Install dlib instead of MediaPipe with:
|
|
164
|
+
|
|
165
|
+
```console
|
|
166
|
+
pip install "ubo-morph[dlib]"
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## CLI reference
|
|
170
|
+
|
|
171
|
+
Morph one pair directly with either landmark backend:
|
|
172
|
+
|
|
173
|
+
```console
|
|
174
|
+
ubo-morph first.jpg second.jpg --extractor mediapipe --model face_landmarker.task
|
|
175
|
+
ubo-morph first.jpg second.jpg --extractor dlib --model shape_predictor_68_face_landmarks.dat
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Pass multiple linked factors with `--factor`; each value is used for both
|
|
179
|
+
warping and blending. Separate factor lists produce their Cartesian product:
|
|
180
|
+
|
|
181
|
+
```console
|
|
182
|
+
ubo-morph first.jpg second.jpg --extractor mediapipe --model face_landmarker.task --factor 0.25 0.50 0.75
|
|
183
|
+
ubo-morph first.jpg second.jpg --extractor mediapipe --model face_landmarker.task --warping-factor 0.25 0.50 --blending-factor 0.50 0.75
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Batch input is passed as a positional CSV path:
|
|
187
|
+
|
|
188
|
+
```console
|
|
189
|
+
ubo-morph pairs.csv --extractor mediapipe --model face_landmarker.task
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Headerless CSV files accept two image columns and an optional third output
|
|
193
|
+
filename column. Headered CSV files may use `factor`, or both `warping_factor`
|
|
194
|
+
and `blending_factor`; an optional `output`, `output_filename`, or `filename`
|
|
195
|
+
column controls the destination name. Relative image paths are resolved from the
|
|
196
|
+
CSV directory. CSV factor columns cannot be combined with CLI factor arguments.
|
|
197
|
+
During each CLI run, landmarks are cached in memory by resolved image path, so
|
|
198
|
+
images reused across pairs are extracted only once. The cache is discarded when
|
|
199
|
+
the command exits.
|
|
200
|
+
|
|
201
|
+
By default, a failing pair stops the command. Pass `--skip-failing-pairs` to
|
|
202
|
+
report the affected file or files and failure reason, then continue with the
|
|
203
|
+
remaining pairs.
|
|
204
|
+
|
|
205
|
+
Use `--intermediate-results` to create a factor-qualified `M_...png/` directory
|
|
206
|
+
containing `morphed.png` and every image-valued intermediate field from
|
|
207
|
+
`MorphResult`. Every saved image, including `morphed.png`, has an accompanying
|
|
208
|
+
`_annotated.png` version containing indexed facial landmarks, unindexed border
|
|
209
|
+
points, and the Delaunay triangulation used by the morph. The intermediate
|
|
210
|
+
images include the aligned images before color equalization, the image actually
|
|
211
|
+
changed by equalization when it runs, the warped images, and the blended image
|
|
212
|
+
when background substitution follows. `MorphResult` also exposes the original
|
|
213
|
+
and aligned landmarks for both inputs. Use `--points-per-border COUNT` to change
|
|
214
|
+
the default of five; zero disables border points, as does the
|
|
215
|
+
`--no-border-points` convenience flag. Run `ubo-morph --help` for all alignment,
|
|
216
|
+
retouching, background, and extractor-specific settings.
|
|
217
|
+
|
|
218
|
+
For faster landmark detection on large inputs, set
|
|
219
|
+
`--landmark-extraction-short-side PIXELS`. Images whose shortest side exceeds
|
|
220
|
+
that limit are resized so it equals the limit, with the other side scaled
|
|
221
|
+
proportionally. Only the extractor input is resized; detected coordinates are
|
|
222
|
+
mapped back to the original image size before full-resolution morphing. The
|
|
223
|
+
default value of zero disables this resizing.
|
|
224
|
+
|
|
225
|
+
## Module layout
|
|
226
|
+
|
|
227
|
+
```text
|
|
228
|
+
ubo_morph/
|
|
229
|
+
morphing/
|
|
230
|
+
backend.py # typed backend contract and lazy selector
|
|
231
|
+
core.py # shared geometry, retouching, triangulation, and flow
|
|
232
|
+
points.py # shared point and mask operations
|
|
233
|
+
cpu/backend.py # NumPy/OpenCV primitive implementation
|
|
234
|
+
cupy/backend.py # optional CuPy primitive implementation
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
## Validation
|
|
238
|
+
|
|
239
|
+
```console
|
|
240
|
+
uv run pytest -v
|
|
241
|
+
uv run ruff check .
|
|
242
|
+
uv run ty check src tests
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## Contributing
|
|
246
|
+
|
|
247
|
+
Contributions are welcome. Create a focused branch, add or update tests for
|
|
248
|
+
behavior changes, and run the validation commands above before opening a pull
|
|
249
|
+
request. Keep changes scoped to the issue being addressed and include a clear
|
|
250
|
+
description of the behavior change in the pull request. Commit messages must
|
|
251
|
+
follow the Conventional Commits specification, for example `feat: add GPU
|
|
252
|
+
batching` or `fix(cli): report invalid input`. To validate messages before each
|
|
253
|
+
commit, install the optional hook:
|
|
254
|
+
|
|
255
|
+
```console
|
|
256
|
+
uv sync --group dev
|
|
257
|
+
uv run pre-commit install --hook-type commit-msg
|
|
258
|
+
```
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "ubo-morph"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "Add your description here"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "ndido98", email = "ndido98@gmail.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Programming Language :: Python :: 3",
|
|
12
|
+
"Programming Language :: Python :: 3.10",
|
|
13
|
+
"Programming Language :: Python :: 3.11",
|
|
14
|
+
"Programming Language :: Python :: 3.12",
|
|
15
|
+
"Programming Language :: Python :: 3.13",
|
|
16
|
+
"Programming Language :: Python :: 3.14",
|
|
17
|
+
]
|
|
18
|
+
dependencies = [
|
|
19
|
+
"numpy>=2.0.0,<2.5.0",
|
|
20
|
+
"opencv-contrib-python>=5.0.0.93",
|
|
21
|
+
"tqdm>=4.68.4",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.scripts]
|
|
25
|
+
ubo-morph = "ubo_morph.cli:main"
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
dlib = [
|
|
29
|
+
"dlib>=20.0.1",
|
|
30
|
+
]
|
|
31
|
+
mediapipe = [
|
|
32
|
+
"mediapipe>=0.10.35",
|
|
33
|
+
]
|
|
34
|
+
cupy = [
|
|
35
|
+
"cupy-cuda12x[ctk]>=13.0",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[build-system]
|
|
39
|
+
requires = ["hatchling==1.32.0"]
|
|
40
|
+
build-backend = "hatchling.build"
|
|
41
|
+
|
|
42
|
+
[tool.hatch.build.targets.sdist]
|
|
43
|
+
only-include = ["src", "README.md", "pyproject.toml"]
|
|
44
|
+
|
|
45
|
+
[tool.hatch.build.targets.wheel]
|
|
46
|
+
packages = ["src/ubo_morph"]
|
|
47
|
+
|
|
48
|
+
[tool.semantic_release]
|
|
49
|
+
branch = "main"
|
|
50
|
+
version_toml = ["pyproject.toml:project.version"]
|
|
51
|
+
|
|
52
|
+
[tool.commitizen]
|
|
53
|
+
name = "cz_conventional_commits"
|
|
54
|
+
|
|
55
|
+
[tool.ty.analysis]
|
|
56
|
+
# CuPy is an optional accelerator and is intentionally absent from base CI.
|
|
57
|
+
allowed-unresolved-imports = ["cupy", "cupyx.**"]
|
|
58
|
+
|
|
59
|
+
[dependency-groups]
|
|
60
|
+
dev = [
|
|
61
|
+
"commitizen>=4.13.0",
|
|
62
|
+
"pre-commit>=4.0.0",
|
|
63
|
+
"pytest>=8.0",
|
|
64
|
+
"ruff>=0.15.21",
|
|
65
|
+
"ty>=0.0.59",
|
|
66
|
+
]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from ubo_morph.landmarks import (
|
|
2
|
+
DlibLandmarkExtractor,
|
|
3
|
+
LandmarkExtractor,
|
|
4
|
+
Landmarks,
|
|
5
|
+
MediaPipeLandmarkExtractor,
|
|
6
|
+
)
|
|
7
|
+
from ubo_morph.morphing import (
|
|
8
|
+
MorphResult,
|
|
9
|
+
morph_images,
|
|
10
|
+
morph_with_landmarks,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"DlibLandmarkExtractor",
|
|
15
|
+
"LandmarkExtractor",
|
|
16
|
+
"Landmarks",
|
|
17
|
+
"MediaPipeLandmarkExtractor",
|
|
18
|
+
"MorphResult",
|
|
19
|
+
"morph_images",
|
|
20
|
+
"morph_with_landmarks",
|
|
21
|
+
]
|