depth2normal 1.0.0__py3-none-any.whl
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.
- depth2normal/__init__.py +6 -0
- depth2normal/__main__.py +6 -0
- depth2normal/cli.py +52 -0
- depth2normal/converter.py +124 -0
- depth2normal/py.typed +0 -0
- depth2normal-1.0.0.dist-info/METADATA +157 -0
- depth2normal-1.0.0.dist-info/RECORD +10 -0
- depth2normal-1.0.0.dist-info/WHEEL +4 -0
- depth2normal-1.0.0.dist-info/entry_points.txt +2 -0
- depth2normal-1.0.0.dist-info/licenses/LICENSE +21 -0
depth2normal/__init__.py
ADDED
depth2normal/__main__.py
ADDED
depth2normal/cli.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Command-line interface for depth2normal."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from depth2normal.converter import METHODS, convert
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@click.command()
|
|
11
|
+
@click.argument("input_path", type=click.Path(exists=True, dir_okay=False))
|
|
12
|
+
@click.option(
|
|
13
|
+
"-o",
|
|
14
|
+
"--output",
|
|
15
|
+
default="normal_map.png",
|
|
16
|
+
show_default=True,
|
|
17
|
+
help="Output path for the generated normal map.",
|
|
18
|
+
)
|
|
19
|
+
@click.option(
|
|
20
|
+
"-s",
|
|
21
|
+
"--strength",
|
|
22
|
+
default=1.0,
|
|
23
|
+
show_default=True,
|
|
24
|
+
type=float,
|
|
25
|
+
help="Gradient multiplier controlling normal intensity.",
|
|
26
|
+
)
|
|
27
|
+
@click.option(
|
|
28
|
+
"-m",
|
|
29
|
+
"--method",
|
|
30
|
+
default="gaussian",
|
|
31
|
+
show_default=True,
|
|
32
|
+
type=click.Choice(METHODS, case_sensitive=False),
|
|
33
|
+
help="Gradient algorithm.",
|
|
34
|
+
)
|
|
35
|
+
@click.option(
|
|
36
|
+
"--sigma",
|
|
37
|
+
default=1.0,
|
|
38
|
+
show_default=True,
|
|
39
|
+
type=float,
|
|
40
|
+
help="Gaussian kernel sigma (only for --method gaussian).",
|
|
41
|
+
)
|
|
42
|
+
@click.version_option(package_name="depth2normal")
|
|
43
|
+
def cli(
|
|
44
|
+
input_path: str,
|
|
45
|
+
output: str,
|
|
46
|
+
strength: float,
|
|
47
|
+
method: str,
|
|
48
|
+
sigma: float,
|
|
49
|
+
) -> None:
|
|
50
|
+
"""Convert a depth map image to a normal map image."""
|
|
51
|
+
convert(input_path, output, strength=strength, method=method, sigma=sigma)
|
|
52
|
+
click.echo(f"Normal map saved to {output}")
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Core depth-to-normal-map conversion logic."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
from numpy.typing import NDArray
|
|
10
|
+
from PIL import Image
|
|
11
|
+
from scipy.ndimage import correlate, gaussian_filter, sobel
|
|
12
|
+
|
|
13
|
+
METHODS = ("gaussian", "sobel", "scharr")
|
|
14
|
+
Method = Literal["gaussian", "sobel", "scharr"]
|
|
15
|
+
|
|
16
|
+
_SCHARR_X = np.array([[-3, 0, 3], [-10, 0, 10], [-3, 0, 3]], dtype=np.float64)
|
|
17
|
+
_SCHARR_Y = _SCHARR_X.T
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _gradients_sobel(depth: NDArray[np.float64]) -> tuple[NDArray, NDArray]:
|
|
21
|
+
return sobel(depth, axis=1), sobel(depth, axis=0)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _gradients_scharr(depth: NDArray[np.float64]) -> tuple[NDArray, NDArray]:
|
|
25
|
+
return correlate(depth, _SCHARR_X), correlate(depth, _SCHARR_Y)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _gradients_gaussian(
|
|
29
|
+
depth: NDArray[np.float64],
|
|
30
|
+
sigma: float,
|
|
31
|
+
) -> tuple[NDArray, NDArray]:
|
|
32
|
+
dx = gaussian_filter(depth, sigma=sigma, order=[0, 1])
|
|
33
|
+
dy = gaussian_filter(depth, sigma=sigma, order=[1, 0])
|
|
34
|
+
return dx, dy
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def depth_to_normal(
|
|
38
|
+
depth: NDArray[np.floating],
|
|
39
|
+
strength: float = 1.0,
|
|
40
|
+
method: Method = "gaussian",
|
|
41
|
+
sigma: float = 1.0,
|
|
42
|
+
) -> NDArray[np.uint8]:
|
|
43
|
+
"""Convert a 2-D depth array to an RGB normal map.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
depth: Grayscale depth image as a 2-D float array. Gradients are
|
|
47
|
+
computed on the raw values so that the original pixel range
|
|
48
|
+
(e.g. 0-255) drives the normal intensity.
|
|
49
|
+
strength: Multiplier applied to the surface gradients. Higher
|
|
50
|
+
values produce more pronounced normals.
|
|
51
|
+
method: Gradient algorithm -- ``"gaussian"`` (smooth, best quality),
|
|
52
|
+
``"sobel"`` (fast, sharp), or ``"scharr"`` (better rotational
|
|
53
|
+
accuracy than Sobel).
|
|
54
|
+
sigma: Standard deviation for the Gaussian derivative kernel.
|
|
55
|
+
Higher values produce smoother normals at the cost of fine
|
|
56
|
+
detail. Only used when *method* is ``"gaussian"``.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
An (H, W, 3) uint8 RGB array where each pixel encodes the
|
|
60
|
+
surface normal mapped to the [0, 255] range.
|
|
61
|
+
|
|
62
|
+
Raises:
|
|
63
|
+
ValueError: If *depth* is not 2-D or *method* is unknown.
|
|
64
|
+
"""
|
|
65
|
+
if depth.ndim != 2:
|
|
66
|
+
raise ValueError(f"Expected a 2-D depth array, got shape {depth.shape}")
|
|
67
|
+
if method not in METHODS:
|
|
68
|
+
raise ValueError(f"Unknown method {method!r}, choose from {METHODS}")
|
|
69
|
+
|
|
70
|
+
depth = depth.astype(np.float64)
|
|
71
|
+
|
|
72
|
+
if method == "gaussian":
|
|
73
|
+
dx, dy = _gradients_gaussian(depth, sigma)
|
|
74
|
+
elif method == "scharr":
|
|
75
|
+
dx, dy = _gradients_scharr(depth)
|
|
76
|
+
else:
|
|
77
|
+
dx, dy = _gradients_sobel(depth)
|
|
78
|
+
|
|
79
|
+
dx *= strength
|
|
80
|
+
dy *= strength
|
|
81
|
+
|
|
82
|
+
normal = np.dstack((-dx, -dy, np.ones_like(dx)))
|
|
83
|
+
|
|
84
|
+
norm = np.linalg.norm(normal, axis=2, keepdims=True)
|
|
85
|
+
norm = np.where(norm == 0, 1, norm)
|
|
86
|
+
normal = normal / norm
|
|
87
|
+
|
|
88
|
+
normal_uint8 = ((normal + 1) * 0.5 * 255).clip(0, 255).astype(np.uint8)
|
|
89
|
+
return normal_uint8
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def load_depth(path: str | Path) -> NDArray[np.floating]:
|
|
93
|
+
"""Load an image file as a 2-D float64 depth array.
|
|
94
|
+
|
|
95
|
+
Preserve native single-channel grayscale depth formats such as 8-bit,
|
|
96
|
+
16-bit, and floating-point images. Multichannel images are converted
|
|
97
|
+
to grayscale.
|
|
98
|
+
"""
|
|
99
|
+
with Image.open(path) as img:
|
|
100
|
+
if len(img.getbands()) != 1 or img.mode == "P":
|
|
101
|
+
img = img.convert("L")
|
|
102
|
+
return np.asarray(img, dtype=np.float64)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def convert(
|
|
106
|
+
input_path: str | Path,
|
|
107
|
+
output_path: str | Path = "normal_map.png",
|
|
108
|
+
strength: float = 1.0,
|
|
109
|
+
method: Method = "gaussian",
|
|
110
|
+
sigma: float = 1.0,
|
|
111
|
+
) -> None:
|
|
112
|
+
"""Convert a depth map image file to a normal map image file.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
input_path: Path to the source depth map image.
|
|
116
|
+
output_path: Destination path for the generated normal map.
|
|
117
|
+
Defaults to ``"normal_map.png"``.
|
|
118
|
+
strength: Gradient multiplier (see :func:`depth_to_normal`).
|
|
119
|
+
method: Gradient algorithm (see :func:`depth_to_normal`).
|
|
120
|
+
sigma: Gaussian sigma (see :func:`depth_to_normal`).
|
|
121
|
+
"""
|
|
122
|
+
depth = load_depth(input_path)
|
|
123
|
+
normal = depth_to_normal(depth, strength=strength, method=method, sigma=sigma)
|
|
124
|
+
Image.fromarray(normal, mode="RGB").save(output_path)
|
depth2normal/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: depth2normal
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Convert depth map images to normal map images
|
|
5
|
+
Project-URL: Homepage, https://github.com/cobanov/depth2normal
|
|
6
|
+
Project-URL: Repository, https://github.com/cobanov/depth2normal
|
|
7
|
+
Project-URL: Issues, https://github.com/cobanov/depth2normal/issues
|
|
8
|
+
Author: Mert Cobanov
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: 3d,computer-vision,depth-map,image-processing,normal-map
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Multimedia :: Graphics
|
|
23
|
+
Classifier: Topic :: Scientific/Engineering :: Image Processing
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Requires-Dist: click
|
|
27
|
+
Requires-Dist: numpy
|
|
28
|
+
Requires-Dist: pillow
|
|
29
|
+
Requires-Dist: scipy
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# depth2normal
|
|
33
|
+
|
|
34
|
+
Convert depth map images to RGB normal maps for shading and 3D workflows.
|
|
35
|
+
|
|
36
|
+
**Requirements:** Python 3.10 or newer. Runtime dependencies are NumPy, SciPy, Pillow, and Click (no OpenCV).
|
|
37
|
+
|
|
38
|
+
| Depth (input) | Normal map (output) |
|
|
39
|
+
| --- | --- |
|
|
40
|
+
|  |  |
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
**From PyPI** (after you publish the package):
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install depth2normal
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
With uv in **another** project (not this repository):
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
uv add depth2normal
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Or without editing that project’s `pyproject.toml`:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
uv pip install depth2normal
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
> **Note:** Inside this repository, do **not** run `uv add depth2normal`—the project name matches the package name, and uv blocks that self-dependency. Use `uv sync` instead (see below).
|
|
63
|
+
|
|
64
|
+
## Usage
|
|
65
|
+
|
|
66
|
+
All CLI forms share the same options (see table at the end of this section).
|
|
67
|
+
|
|
68
|
+
### After `pip install` or `uv pip install`
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
depth2normal path/to/depth.png -o normal.png
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### From a clone (recommended for development)
|
|
75
|
+
|
|
76
|
+
Install the project and dev tools, then use the console script or the helper script:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
git clone https://github.com/cobanov/depth2normal.git
|
|
80
|
+
cd depth2normal
|
|
81
|
+
uv sync
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
uv run depth2normal assets/depth.png -o normal.png
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Or run the repo-root helper without an editable install (still uses deps from `uv sync`):
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
uv run run.py assets/depth.png -o normal.png
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
With plain `python` (dependencies must be available in that environment, e.g. after `pip install -e .`):
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
python run.py assets/depth.png -o normal.png
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Module entry point when `src` is on `PYTHONPATH` or the package is installed:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
PYTHONPATH=src python -m depth2normal assets/depth.png -o normal.png
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Python API
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
import depth2normal
|
|
110
|
+
|
|
111
|
+
depth2normal.convert("depth.png", "normal.png")
|
|
112
|
+
depth2normal.convert("depth.png", "normal.png", strength=2.0, method="gaussian", sigma=1.5)
|
|
113
|
+
|
|
114
|
+
import numpy as np
|
|
115
|
+
|
|
116
|
+
depth_array = np.load("depth.npy")
|
|
117
|
+
normal_map = depth2normal.depth_to_normal(depth_array, method="scharr")
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### CLI options
|
|
121
|
+
|
|
122
|
+
| Option | Default | Description |
|
|
123
|
+
| --- | --- | --- |
|
|
124
|
+
| `-o`, `--output` | `normal_map.png` | Output image path |
|
|
125
|
+
| `-s`, `--strength` | `1.0` | Scales gradients (stronger = more pronounced normals) |
|
|
126
|
+
| `-m`, `--method` | `gaussian` | Gradient algorithm: `gaussian`, `sobel`, or `scharr` |
|
|
127
|
+
| `--sigma` | `1.0` | Gaussian kernel width (only with `--method gaussian`) |
|
|
128
|
+
| `--version` | — | Print version and exit |
|
|
129
|
+
|
|
130
|
+
Positional argument: path to the depth image file.
|
|
131
|
+
|
|
132
|
+
### Gradient methods
|
|
133
|
+
|
|
134
|
+
| Method | Quality | Speed | Notes |
|
|
135
|
+
| --- | --- | --- | --- |
|
|
136
|
+
| `gaussian` | Best | Moderate | Smooth normals via Gaussian derivative. `--sigma` controls the smoothness/detail tradeoff. |
|
|
137
|
+
| `sobel` | Good | Fast | Classic 3x3 Sobel. Sharp but can show staircase artifacts on quantized depth. |
|
|
138
|
+
| `scharr` | Good | Fast | Better rotational accuracy than Sobel, similar speed. |
|
|
139
|
+
|
|
140
|
+
## How it works
|
|
141
|
+
|
|
142
|
+
1. Load depth as grayscale float array.
|
|
143
|
+
2. Estimate surface gradients with the selected filter (Gaussian derivative, Sobel, or Scharr).
|
|
144
|
+
3. Build normals (-dx, -dy, 1), normalize to unit length, then map to 8-bit RGB.
|
|
145
|
+
|
|
146
|
+
## Development
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
uv sync
|
|
150
|
+
uv run pytest
|
|
151
|
+
uv run ruff check .
|
|
152
|
+
uv run ruff format --check .
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## License
|
|
156
|
+
|
|
157
|
+
MIT
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
depth2normal/__init__.py,sha256=Y-4gKbMxh29xFWpPBhRCD7vgPrJOgNVJApZJoK6q3YQ,243
|
|
2
|
+
depth2normal/__main__.py,sha256=1fpfd09B5DBUYn3RSKaGkXxXlhWn2HylVVKOzvjemWU,143
|
|
3
|
+
depth2normal/cli.py,sha256=RCJv0W3s_Q7u6coagh3xFJLhuS86mjXDDpKvzBvmB2U,1237
|
|
4
|
+
depth2normal/converter.py,sha256=zfG5NsWAgbzvrO7fPWK3lnYGc9JGEu1kGBP_MScctOI,4205
|
|
5
|
+
depth2normal/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
depth2normal-1.0.0.dist-info/METADATA,sha256=9p5f8RQ9X33AVkPMq7pV2lBydxhKM07h6sfaFkva3qw,4670
|
|
7
|
+
depth2normal-1.0.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
8
|
+
depth2normal-1.0.0.dist-info/entry_points.txt,sha256=hZ6GRQLuHHy-FhODbq7yehuwphMeGwDnAkK-sortbJM,54
|
|
9
|
+
depth2normal-1.0.0.dist-info/licenses/LICENSE,sha256=oYV_s8A1IS_T54BCvvcrI1qhyXji1eN_Yz2_UHbmru8,1069
|
|
10
|
+
depth2normal-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Mert Cobanov
|
|
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.
|