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.
@@ -0,0 +1,6 @@
1
+ """depth2normal -- convert depth maps to normal maps."""
2
+
3
+ from depth2normal.converter import METHODS, convert, depth_to_normal, load_depth
4
+
5
+ __version__ = "1.0.0"
6
+ __all__ = ["METHODS", "convert", "depth_to_normal", "load_depth", "__version__"]
@@ -0,0 +1,6 @@
1
+ """Allow ``python -m depth2normal`` when the package is importable."""
2
+
3
+ from depth2normal.cli import cli
4
+
5
+ if __name__ == "__main__":
6
+ cli()
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
+ | ![Depth example](https://raw.githubusercontent.com/cobanov/depth2normal/main/assets/depth.png) | ![Normal map example](https://raw.githubusercontent.com/cobanov/depth2normal/main/assets/normal.png) |
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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ depth2normal = depth2normal.cli:cli
@@ -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.