Skip to content

Commit 5e48b8b

Browse files
committed
Initialize
1 parent a85fc8b commit 5e48b8b

File tree

6 files changed

+166
-1
lines changed

6 files changed

+166
-1
lines changed

README.md

+35-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,35 @@
1-
# depth2normal
1+
# Depth to Normal Map Converter
2+
3+
This script converts a depth map image to a normal map image.
4+
5+
</img><img src='assets/depth.png' width=200> </img><img src='assets/normal.png' width=200></img>
6+
7+
## Requirements
8+
9+
- Python 3.x
10+
- OpenCV (`pip install opencv-python`)
11+
- Numpy
12+
13+
## Usage
14+
15+
To run the script, use the following command:
16+
17+
```bash
18+
python depth_to_normal_map.py --input /path/to/input_image --output /path/to/output_image --max_depth 255
19+
```
20+
21+
The following arguments are available:
22+
23+
- `--input` (`-i`): Path to the input depth map image.
24+
- `--output` (`-o`): Path to save the output normal map image.
25+
- `--max_depth` (`-m`): Maximum depth value of the input depth map image (default: 255).
26+
27+
## Example
28+
29+
```bash
30+
python depth_to_normal_map.py --input assets/depth.png --output normal_map.png --max_depth 255
31+
```
32+
33+
## License
34+
35+
This script is licensed under the [MIT License](https://opensource.org/licenses/MIT).

assets/depth.png

255 KB
Loading

assets/normal.png

910 KB
Loading

depth_to_normal_map.py

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import cv2
2+
import numpy as np
3+
import argparse
4+
5+
6+
class DepthToNormalMap:
7+
"""A class for converting a depth map image to a normal map image.
8+
9+
10+
Attributes:
11+
depth_map (ndarray): A numpy array representing the depth map image.
12+
max_depth (int): The maximum depth value in the depth map image.
13+
"""
14+
15+
def __init__(self, depth_map_path: str, max_depth: int = 255) -> None:
16+
"""Constructs a DepthToNormalMap object.
17+
18+
Args:
19+
depth_map_path (str): The path to the depth map image file.
20+
max_depth (int, optional): The maximum depth value in the depth map image.
21+
Defaults to 255.
22+
23+
Raises:
24+
ValueError: If the depth map image file cannot be read.
25+
26+
"""
27+
self.depth_map = cv2.imread(depth_map_path, cv2.IMREAD_UNCHANGED)
28+
29+
if self.depth_map is None:
30+
raise ValueError(
31+
f"Could not read the depth map image file at {depth_map_path}"
32+
)
33+
self.max_depth = max_depth
34+
35+
def convert(self, output_path: str) -> None:
36+
"""Converts the depth map image to a normal map image.
37+
38+
Args:
39+
output_path (str): The path to save the normal map image file.
40+
41+
"""
42+
rows, cols = self.depth_map.shape
43+
44+
x, y = np.meshgrid(np.arange(cols), np.arange(rows))
45+
x = x.astype(np.float32)
46+
y = y.astype(np.float32)
47+
48+
# Calculate the partial derivatives of depth with respect to x and y
49+
dx = cv2.Sobel(self.depth_map, cv2.CV_32F, 1, 0)
50+
dy = cv2.Sobel(self.depth_map, cv2.CV_32F, 0, 1)
51+
52+
# Compute the normal vector for each pixel
53+
normal = np.dstack((-dx, -dy, np.ones((rows, cols))))
54+
norm = np.sqrt(np.sum(normal**2, axis=2, keepdims=True))
55+
normal = np.divide(normal, norm, out=np.zeros_like(normal), where=norm != 0)
56+
57+
# Map the normal vectors to the [0, 255] range and convert to uint8
58+
normal = (normal + 1) * 127.5
59+
normal = normal.clip(0, 255).astype(np.uint8)
60+
61+
# Save the normal map to a file
62+
normal_bgr = cv2.cvtColor(normal, cv2.COLOR_RGB2BGR)
63+
cv2.imwrite(output_path, normal_bgr)
64+
65+
66+
if __name__ == "__main__":
67+
parser = argparse.ArgumentParser(description="Convert depth map to normal map")
68+
parser.add_argument("--input", type=str, help="Path to depth map image")
69+
parser.add_argument(
70+
"--max_depth", type=int, default=255, help="Maximum depth value (default: 255)"
71+
)
72+
parser.add_argument(
73+
"--output_path",
74+
type=str,
75+
default="normal_map.png",
76+
help="Output path for normal map image (default: normal_map.png)",
77+
)
78+
args = parser.parse_args()
79+
80+
converter = DepthToNormalMap(args.input, max_depth=args.max_depth)
81+
converter.convert(args.output_path)

requirements.txt

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
opencv-python
2+
numpy

test.py

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import unittest
2+
import cv2
3+
import numpy as np
4+
import os
5+
from depth_to_normal_map import DepthToNormalMap
6+
7+
8+
class TestDepthToNormalMap(unittest.TestCase):
9+
def setUp(self):
10+
self.depth_map_path = "test_depth_map.png"
11+
self.normal_map_path = "test_normal_map.png"
12+
self.max_depth = 255
13+
self.expected_shape = (480, 640, 3)
14+
15+
# create a sample depth map image
16+
self.depth_map = np.random.randint(
17+
low=0, high=self.max_depth, size=self.expected_shape[:2], dtype=np.uint16
18+
)
19+
cv2.imwrite(self.depth_map_path, self.depth_map)
20+
21+
def test_converting_depth_map_to_normal_map(self):
22+
# create an instance of DepthToNormalMap
23+
converter = DepthToNormalMap(self.depth_map_path, max_depth=self.max_depth)
24+
25+
# convert the depth map to normal map
26+
converter.convert(self.normal_map_path)
27+
28+
# assert the output file exists
29+
self.assertTrue(os.path.isfile(self.normal_map_path))
30+
31+
# read the output normal map
32+
normal_map = cv2.imread(self.normal_map_path)
33+
34+
# assert the shape and data type of the normal map
35+
self.assertEqual(normal_map.shape, self.expected_shape)
36+
self.assertEqual(normal_map.dtype, np.uint8)
37+
38+
def tearDown(self):
39+
# remove the test depth and normal map images
40+
if os.path.isfile(self.depth_map_path):
41+
os.remove(self.depth_map_path)
42+
43+
if os.path.isfile(self.normal_map_path):
44+
os.remove(self.normal_map_path)
45+
46+
47+
if __name__ == "__main__":
48+
unittest.main()

0 commit comments

Comments
 (0)