bdck commited on
Commit
99a77de
·
verified ·
1 Parent(s): f25b717

Upload depthpro_wrapper/depth_estimator.py

Browse files
Files changed (1) hide show
  1. depthpro_wrapper/depth_estimator.py +241 -0
depthpro_wrapper/depth_estimator.py ADDED
@@ -0,0 +1,241 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Core DepthPro depth estimation wrapper.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+ from typing import Optional, Union
10
+
11
+ import numpy as np
12
+ import torch
13
+ from PIL import Image
14
+
15
+
16
+ try:
17
+ from transformers import DepthProImageProcessorFast, DepthProForDepthEstimation
18
+ except ImportError as exc:
19
+ raise ImportError(
20
+ "transformers>=4.40.0 is required. "
21
+ "Install it with: pip install transformers[torch]"
22
+ ) from exc
23
+
24
+
25
+ @dataclass
26
+ class DepthResult:
27
+ """Container for depth estimation results."""
28
+
29
+ depth: np.ndarray
30
+ """(H, W) float32 metric depth map in meters."""
31
+
32
+ focal_length: float
33
+ """Estimated focal length in pixels (for the original image resolution)."""
34
+
35
+ field_of_view: float
36
+ """Estimated horizontal field of view in degrees."""
37
+
38
+ image: np.ndarray
39
+ """(H, W, 3) uint8 RGB image at original resolution."""
40
+
41
+ confidence: Optional[np.ndarray] = None
42
+ """(H, W) optional confidence / uncertainty map."""
43
+
44
+ @property
45
+ def height(self) -> int:
46
+ return self.depth.shape[0]
47
+
48
+ @property
49
+ def width(self) -> int:
50
+ return self.depth.shape[1]
51
+
52
+
53
+ class DepthProEstimator:
54
+ """
55
+ High-level wrapper around Apple's DepthPro for metric depth estimation.
56
+
57
+ DepthPro is a single-image zero-shot metric depth estimator. Unlike
58
+ relative-depth models (e.g. Depth Anything), it outputs **absolute-scale
59
+ metric depth in meters** and also estimates the **camera focal length**
60
+ and **field of view** automatically — no calibration required.
61
+
62
+ Parameters
63
+ ----------
64
+ model_name : str, default "apple/DepthPro-hf"
65
+ HuggingFace model id or local path.
66
+ device : str or torch.device, default "cuda:0"
67
+ PyTorch device. CUDA strongly recommended for 952M-parameter ViT-L.
68
+ dtype : torch.dtype, default torch.float16
69
+ Inference dtype. fp16 halves memory and is the default the model
70
+ was trained with; fp32 gives marginally higher precision.
71
+ """
72
+
73
+ _MODEL_INPUT_SIZE: int = 1536 # DepthPro always processes at 1536×1536
74
+
75
+ def __init__(
76
+ self,
77
+ model_name: str = "apple/DepthPro-hf",
78
+ device: Union[str, torch.device] = "cuda:0",
79
+ dtype: torch.dtype = torch.float16,
80
+ ):
81
+ self.device = torch.device(device)
82
+ self.dtype = dtype
83
+ self.model_name = model_name
84
+
85
+ if not torch.cuda.is_available() and self.device.type == "cuda":
86
+ raise RuntimeError(
87
+ "CUDA is not available but device='cuda' was requested. "
88
+ "DepthPro is a 952M ViT-L model; CPU inference will be extremely slow. "
89
+ "Pass device='cpu' explicitly if you really want this."
90
+ )
91
+
92
+ self._load_model()
93
+
94
+ # ------------------------------------------------------------------ #
95
+ # Loading #
96
+ # ------------------------------------------------------------------ #
97
+
98
+ def _load_model(self) -> None:
99
+ """Load the processor and model from HF."""
100
+ self.processor = DepthProImageProcessorFast.from_pretrained(self.model_name)
101
+ self.model = DepthProForDepthEstimation.from_pretrained(
102
+ self.model_name,
103
+ torch_dtype=self.dtype,
104
+ ).to(self.device)
105
+ self.model.eval()
106
+
107
+ # ------------------------------------------------------------------ #
108
+ # Public API #
109
+ # ------------------------------------------------------------------ #
110
+
111
+ @torch.no_grad()
112
+ def estimate(
113
+ self,
114
+ image: Union[str, Path, Image.Image, np.ndarray],
115
+ *,
116
+ return_confidence: bool = False,
117
+ ) -> DepthResult:
118
+ """
119
+ Run metric depth estimation on a single RGB image.
120
+
121
+ Parameters
122
+ ----------
123
+ image : str, Path, PIL.Image, or np.ndarray
124
+ Input RGB image. If a path, loaded with PIL.
125
+ return_confidence : bool, default False
126
+ If True and the model provides a confidence map, include it in
127
+ the result.
128
+
129
+ Returns
130
+ -------
131
+ DepthResult
132
+ Dataclass containing:
133
+ * ``depth`` — (H, W) metric depth in meters
134
+ * ``focal_length`` — estimated focal length (px) at original res
135
+ * ``field_of_view`` — estimated horizontal FOV in degrees
136
+ * ``image`` — original RGB image as (H, W, 3) uint8
137
+ """
138
+ # ---- load / normalise image ---------------------------------------
139
+ pil_image = self._to_pil(image)
140
+ rgb_array = np.array(pil_image.convert("RGB"), dtype=np.uint8)
141
+
142
+ # ---- preprocess & forward pass ------------------------------------
143
+ inputs = self.processor(images=pil_image, return_tensors="pt")
144
+ inputs = {k: v.to(self.device) for k, v in inputs.items()}
145
+
146
+ outputs = self.model(**inputs)
147
+
148
+ # ---- post-process back to original resolution ---------------------
149
+ post = self.processor.post_process_depth_estimation(
150
+ outputs,
151
+ target_sizes=[(pil_image.height, pil_image.width)],
152
+ )[0]
153
+
154
+ depth = post["predicted_depth"] # torch.Tensor [H, W]
155
+ focal_length = post["focal_length"]
156
+ fov = post["field_of_view"]
157
+
158
+ # DepthPro returns focal_length for the 1536×1536 processed image.
159
+ # Scale it to the original image resolution.
160
+ focal_original = focal_length * (pil_image.width / self._MODEL_INPUT_SIZE)
161
+
162
+ depth_np = depth.cpu().float().numpy()
163
+ confidence_np = None
164
+ if return_confidence and "confidence" in post:
165
+ confidence_np = post["confidence"].cpu().float().numpy()
166
+
167
+ return DepthResult(
168
+ depth=depth_np,
169
+ focal_length=focal_original.item(),
170
+ field_of_view=fov.item(),
171
+ image=rgb_array,
172
+ confidence=confidence_np,
173
+ )
174
+
175
+ @torch.no_grad()
176
+ def estimate_batch(
177
+ self,
178
+ images: list,
179
+ *,
180
+ return_confidence: bool = False,
181
+ ) -> list[DepthResult]:
182
+ """
183
+ Run depth estimation on a batch of images.
184
+
185
+ Parameters
186
+ ----------
187
+ images : list of str, Path, PIL.Image, or np.ndarray
188
+ return_confidence : bool
189
+
190
+ Returns
191
+ -------
192
+ list[DepthResult]
193
+ """
194
+ pil_images = [self._to_pil(img) for img in images]
195
+ rgb_arrays = [np.array(p.convert("RGB"), dtype=np.uint8) for p in pil_images]
196
+
197
+ # Processor batch handling
198
+ inputs = self.processor(images=pil_images, return_tensors="pt")
199
+ inputs = {k: v.to(self.device) for k, v in inputs.items()}
200
+
201
+ outputs = self.model(**inputs)
202
+
203
+ posts = self.processor.post_process_depth_estimation(
204
+ outputs,
205
+ target_sizes=[(p.height, p.width) for p in pil_images],
206
+ )
207
+
208
+ results = []
209
+ for post, pil_img, rgb in zip(posts, pil_images, rgb_arrays):
210
+ depth = post["predicted_depth"].cpu().float().numpy()
211
+ focal = post["focal_length"] * (pil_img.width / self._MODEL_INPUT_SIZE)
212
+ fov = post["field_of_view"]
213
+ conf = None
214
+ if return_confidence and "confidence" in post:
215
+ conf = post["confidence"].cpu().float().numpy()
216
+
217
+ results.append(DepthResult(
218
+ depth=depth,
219
+ focal_length=focal.item(),
220
+ field_of_view=fov.item(),
221
+ image=rgb,
222
+ confidence=conf,
223
+ ))
224
+ return results
225
+
226
+ # ------------------------------------------------------------------ #
227
+ # Helpers #
228
+ # ------------------------------------------------------------------ #
229
+
230
+ @staticmethod
231
+ def _to_pil(image: Union[str, Path, Image.Image, np.ndarray]) -> Image.Image:
232
+ """Normalise input to a PIL RGB image."""
233
+ if isinstance(image, (str, Path)):
234
+ return Image.open(str(image)).convert("RGB")
235
+ if isinstance(image, np.ndarray):
236
+ if image.dtype != np.uint8:
237
+ image = (image * 255).clip(0, 255).astype(np.uint8)
238
+ return Image.fromarray(image).convert("RGB")
239
+ if isinstance(image, Image.Image):
240
+ return image.convert("RGB")
241
+ raise TypeError(f"Unsupported image type: {type(image)}")