jasonod888 commited on
Commit
8d718f3
·
1 Parent(s): 46af2ea

Robust Marigold output unwrap + deps compatible with HF toolkit

Browse files
Files changed (2) hide show
  1. handler.py +82 -46
  2. requirements.txt +1 -2
handler.py CHANGED
@@ -1,6 +1,7 @@
1
  import io
2
  import base64
3
  import inspect
 
4
  from typing import Any, Dict, List, Optional, Union
5
 
6
  import numpy as np
@@ -11,9 +12,9 @@ import requests
11
  from diffusers import MarigoldIntrinsicsPipeline, MarigoldNormalsPipeline
12
 
13
 
14
- # -------------------------
15
  # Image IO helpers
16
- # -------------------------
17
 
18
  def _is_data_url(s: str) -> bool:
19
  return isinstance(s, str) and s.startswith("data:image/") and ";base64," in s
@@ -45,13 +46,12 @@ def _decode_inputs_to_image(payload_inputs: Union[str, Dict[str, str]]) -> Image
45
  raise ValueError("inputs must be a string (url/data-url/base64) or a dict with image_url/image_b64")
46
 
47
  s = payload_inputs.strip()
48
- if s.startswith("http://") or s.startswith("https://"):
49
  return _url_to_pil(s)
50
  if _is_data_url(s):
51
  s = _strip_data_url_header(s)
52
  return _b64_to_pil(s)
53
 
54
- # raw base64
55
  try:
56
  return _b64_to_pil(s)
57
  except Exception as e:
@@ -63,14 +63,13 @@ def _png_b64_from_pil(img: Image.Image) -> str:
63
  return "data:image/png;base64," + base64.b64encode(buf.getvalue()).decode("utf-8")
64
 
65
 
66
- # -------------------------
67
  # Tensor helpers
68
- # -------------------------
69
 
70
  def _to_float_tensor(img: Any) -> torch.Tensor:
71
  """
72
- Accept PIL / numpy / torch; return float32 tensor with last dim=3 when possible.
73
- Scales from [0,255] to [0,1] if needed.
74
  """
75
  if isinstance(img, torch.Tensor):
76
  t = img
@@ -84,9 +83,10 @@ def _to_float_tensor(img: Any) -> torch.Tensor:
84
  raise TypeError(f"Unexpected prediction type: {type(img)}")
85
 
86
  t = t.float()
87
- if t.max() > 1.5: # likely uint8 0..255
88
  t = t / 255.0
89
- # If channels-first, move to HWC
 
90
  if t.ndim == 3 and t.shape[0] in (1, 3) and t.shape[-1] not in (1, 3):
91
  t = t.permute(1, 2, 0).contiguous()
92
  return t
@@ -101,54 +101,76 @@ def _png_b64_from_float_img_neg1to1(img: torch.Tensor) -> str:
101
  return _png_b64_from_float_img01((img + 1.0) * 0.5)
102
 
103
 
104
- # -------------------------
105
  # Output unwrapping
106
- # -------------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
 
108
  def _unwrap_field(obj: Any, names: List[str]) -> Optional[Any]:
109
  """
110
- Try to extract a named field from:
111
- - dataclass-like (attributes)
112
  - dict-like
113
- - tuple-like via known order
114
- - diffusers outputs exposing .images
115
- Returns first match or None.
116
  """
117
- # Attribute access
 
 
 
 
 
 
 
118
  for n in names:
119
  if hasattr(obj, n):
120
  return getattr(obj, n)
121
 
122
- # Dict-like
123
  if isinstance(obj, dict):
124
  for n in names:
125
  if n in obj:
126
  return obj[n]
127
 
128
- # Diffusers common: images list
129
  if hasattr(obj, "images"):
130
  imgs = getattr(obj, "images")
131
  if isinstance(imgs, (list, tuple)) and len(imgs) > 0:
132
- # If caller asked for 'normals'/'albedo' etc but only .images is present,
133
- # best effort is to return images[0].
134
  return imgs[0]
135
 
136
- # Tuple/list fallback for known orders
137
  if isinstance(obj, (list, tuple)):
138
- # Intrinsics often: [albedo, diffuse, residual] (or similar)
139
- if len(obj) >= 3 and names == ["albedo"]:
140
  return obj[0]
141
- if len(obj) >= 2 and names == ["diffuse_shading", "shading"]:
142
  return obj[1]
143
- if len(obj) >= 3 and names == ["residual"]:
144
  return obj[2]
145
 
146
  return None
147
 
148
 
149
- # -------------------------
150
  # Endpoint
151
- # -------------------------
152
 
153
  class EndpointHandler:
154
  def __init__(self, path: Optional[str] = None):
@@ -159,7 +181,7 @@ class EndpointHandler:
159
  self.normals_pipe: Optional[MarigoldNormalsPipeline] = None
160
  self.intrin_pipe: Optional[MarigoldIntrinsicsPipeline] = None
161
 
162
- # Config per our discussion
163
  self.fixed_processing_res = 768
164
  self.default_intr_steps = 4
165
 
@@ -167,7 +189,7 @@ class EndpointHandler:
167
  self.model_id_intrin = "prs-eth/marigold-iid-lighting-v1-1"
168
 
169
  def initialize(self, context: Optional[Dict[str, Any]] = None) -> None:
170
- # Lazy load on first use
171
  pass
172
 
173
  # -------- loaders --------
@@ -188,7 +210,6 @@ class EndpointHandler:
188
  def _run_normals(self, pil_img: Image.Image, processing_res: int) -> Dict[str, Any]:
189
  self._ensure_normals_loaded()
190
 
191
- # Only pass processing_res if supported
192
  sig = inspect.signature(self.normals_pipe.__call__)
193
  kwargs = {"image": pil_img}
194
  if "processing_res" in sig.parameters:
@@ -196,18 +217,25 @@ class EndpointHandler:
196
 
197
  out = self.normals_pipe(**kwargs)
198
 
199
- # Robust extraction
200
- normals = _unwrap_field(out, ["normals"])
201
  if normals is None:
202
- # Some builds expose a single image via .images[0]
203
- normals = _unwrap_field(out, ["images"]) # will give images[0] in our helper
204
-
 
 
 
 
 
 
 
 
205
  if normals is None:
206
- # Last resort: maybe it's a raw tensor/ndarray/PIL already
207
- normals = out
208
 
209
- normals_t = _to_float_tensor(normals) # expect [-1,1] or [0,1]
210
- # Heuristic: if range looks 0..1, map to [-1,1] visualization anyway
211
  maxv, minv = float(normals_t.max().item()), float(normals_t.min().item())
212
  if maxv <= 1.01 and minv >= -0.01:
213
  vis_b64 = _png_b64_from_float_img01(normals_t)
@@ -229,12 +257,20 @@ class EndpointHandler:
229
 
230
  out = self.intrin_pipe(image=pil_img, num_inference_steps=int(steps))
231
 
232
- # Try attributes/dict/tuple/tensor in a robust way
233
- albedo = _unwrap_field(out, ["albedo"])
234
- diffuse = _unwrap_field(out, ["diffuse_shading", "shading"])
235
- residual = _unwrap_field(out, ["residual"])
 
 
 
 
 
 
 
 
236
 
237
- # Tensor stack fallback (e.g., [3,H,W,3] or similar)
238
  if any(v is None for v in (albedo, diffuse, residual)):
239
  try:
240
  t = _to_float_tensor(out)
 
1
  import io
2
  import base64
3
  import inspect
4
+ import dataclasses
5
  from typing import Any, Dict, List, Optional, Union
6
 
7
  import numpy as np
 
12
  from diffusers import MarigoldIntrinsicsPipeline, MarigoldNormalsPipeline
13
 
14
 
15
+ # =======================
16
  # Image IO helpers
17
+ # =======================
18
 
19
  def _is_data_url(s: str) -> bool:
20
  return isinstance(s, str) and s.startswith("data:image/") and ";base64," in s
 
46
  raise ValueError("inputs must be a string (url/data-url/base64) or a dict with image_url/image_b64")
47
 
48
  s = payload_inputs.strip()
49
+ if s.startswith(("http://", "https://")):
50
  return _url_to_pil(s)
51
  if _is_data_url(s):
52
  s = _strip_data_url_header(s)
53
  return _b64_to_pil(s)
54
 
 
55
  try:
56
  return _b64_to_pil(s)
57
  except Exception as e:
 
63
  return "data:image/png;base64," + base64.b64encode(buf.getvalue()).decode("utf-8")
64
 
65
 
66
+ # =======================
67
  # Tensor helpers
68
+ # =======================
69
 
70
  def _to_float_tensor(img: Any) -> torch.Tensor:
71
  """
72
+ Accept PIL / numpy / torch; return float32 tensor HxWxC in [0,1] when possible.
 
73
  """
74
  if isinstance(img, torch.Tensor):
75
  t = img
 
83
  raise TypeError(f"Unexpected prediction type: {type(img)}")
84
 
85
  t = t.float()
86
+ if t.max() > 1.5: # likely 0..255
87
  t = t / 255.0
88
+
89
+ # channels-first -> HWC
90
  if t.ndim == 3 and t.shape[0] in (1, 3) and t.shape[-1] not in (1, 3):
91
  t = t.permute(1, 2, 0).contiguous()
92
  return t
 
101
  return _png_b64_from_float_img01((img + 1.0) * 0.5)
102
 
103
 
104
+ # =======================
105
  # Output unwrapping
106
+ # =======================
107
+
108
+ def _maybe_dataclass_to_dict(obj: Any) -> Optional[Dict[str, Any]]:
109
+ try:
110
+ if dataclasses.is_dataclass(obj):
111
+ return dataclasses.asdict(obj)
112
+ except Exception:
113
+ pass
114
+ # diffusers outputs often have .to_dict()
115
+ if hasattr(obj, "to_dict") and callable(getattr(obj, "to_dict")):
116
+ try:
117
+ return obj.to_dict()
118
+ except Exception:
119
+ pass
120
+ # last resort: use __dict__
121
+ if hasattr(obj, "__dict__") and isinstance(obj.__dict__, dict):
122
+ return dict(obj.__dict__)
123
+ return None
124
 
125
  def _unwrap_field(obj: Any, names: List[str]) -> Optional[Any]:
126
  """
127
+ Extract a named field from:
128
+ - dataclass-like (converted to dict)
129
  - dict-like
130
+ - attribute access
131
+ - diffusers common: .images (first)
132
+ - tuple/list fallbacks by known order
133
  """
134
+ # dataclass / to_dict / __dict__
135
+ asd = _maybe_dataclass_to_dict(obj)
136
+ if isinstance(asd, dict):
137
+ for n in names:
138
+ if n in asd:
139
+ return asd[n]
140
+
141
+ # attribute access
142
  for n in names:
143
  if hasattr(obj, n):
144
  return getattr(obj, n)
145
 
146
+ # dict-like
147
  if isinstance(obj, dict):
148
  for n in names:
149
  if n in obj:
150
  return obj[n]
151
 
152
+ # diffusers: images list
153
  if hasattr(obj, "images"):
154
  imgs = getattr(obj, "images")
155
  if isinstance(imgs, (list, tuple)) and len(imgs) > 0:
 
 
156
  return imgs[0]
157
 
158
+ # list/tuple positional guesses
159
  if isinstance(obj, (list, tuple)):
160
+ # Intrinsics often 3 outputs
161
+ if len(obj) >= 3 and names == ["albedo", "pred_albedo", "albedo_img"]:
162
  return obj[0]
163
+ if len(obj) >= 2 and names == ["diffuse_shading", "shading", "pred_shading", "shading_diffuse"]:
164
  return obj[1]
165
+ if len(obj) >= 3 and names == ["residual", "pred_residual", "specular_residual"]:
166
  return obj[2]
167
 
168
  return None
169
 
170
 
171
+ # =======================
172
  # Endpoint
173
+ # =======================
174
 
175
  class EndpointHandler:
176
  def __init__(self, path: Optional[str] = None):
 
181
  self.normals_pipe: Optional[MarigoldNormalsPipeline] = None
182
  self.intrin_pipe: Optional[MarigoldIntrinsicsPipeline] = None
183
 
184
+ # Config
185
  self.fixed_processing_res = 768
186
  self.default_intr_steps = 4
187
 
 
189
  self.model_id_intrin = "prs-eth/marigold-iid-lighting-v1-1"
190
 
191
  def initialize(self, context: Optional[Dict[str, Any]] = None) -> None:
192
+ # Lazy load at first call
193
  pass
194
 
195
  # -------- loaders --------
 
210
  def _run_normals(self, pil_img: Image.Image, processing_res: int) -> Dict[str, Any]:
211
  self._ensure_normals_loaded()
212
 
 
213
  sig = inspect.signature(self.normals_pipe.__call__)
214
  kwargs = {"image": pil_img}
215
  if "processing_res" in sig.parameters:
 
217
 
218
  out = self.normals_pipe(**kwargs)
219
 
220
+ # Try common field names used across versions
221
+ normals = _unwrap_field(out, ["normals", "pred_normals", "normal_map"])
222
  if normals is None:
223
+ # if still not found, see if the object itself is an image/tensor
224
+ asd = _maybe_dataclass_to_dict(out)
225
+ if isinstance(asd, dict):
226
+ # pick first image-like thing from the dict
227
+ for v in asd.values():
228
+ try:
229
+ _ = _to_float_tensor(v)
230
+ normals = v
231
+ break
232
+ except Exception:
233
+ continue
234
  if normals is None:
235
+ # Last resort: the entire object is not directly convertible; fail clearly
236
+ raise TypeError(f"Unexpected prediction type: {type(out)}")
237
 
238
+ normals_t = _to_float_tensor(normals)
 
239
  maxv, minv = float(normals_t.max().item()), float(normals_t.min().item())
240
  if maxv <= 1.01 and minv >= -0.01:
241
  vis_b64 = _png_b64_from_float_img01(normals_t)
 
257
 
258
  out = self.intrin_pipe(image=pil_img, num_inference_steps=int(steps))
259
 
260
+ # Keys that show up across versions
261
+ albedo = _unwrap_field(out, ["albedo", "pred_albedo", "albedo_img"])
262
+ diffuse = _unwrap_field(out, ["diffuse_shading", "shading", "pred_shading", "shading_diffuse"])
263
+ residual = _unwrap_field(out, ["residual", "pred_residual", "specular_residual"])
264
+
265
+ # If .images[0..2] only:
266
+ if any(v is None for v in (albedo, diffuse, residual)) and hasattr(out, "images"):
267
+ imgs = getattr(out, "images")
268
+ if isinstance(imgs, (list, tuple)) and len(imgs) >= 3:
269
+ albedo = imgs[0] if albedo is None else albedo
270
+ diffuse = imgs[1] if diffuse is None else diffuse
271
+ residual = imgs[2] if residual is None else residual
272
 
273
+ # Tensor stack fallback
274
  if any(v is None for v in (albedo, diffuse, residual)):
275
  try:
276
  t = _to_float_tensor(out)
requirements.txt CHANGED
@@ -1,9 +1,8 @@
1
  torch>=2.1
2
  accelerate>=0.28
3
- diffusers==0.35.2
4
  transformers>=4.44
5
  peft>=0.17.0
6
- huggingface_hub[hf_transfer]==0.30.2
7
  Pillow>=10.3
8
  safetensors>=0.4.2
9
  numpy>=1.26
 
1
  torch>=2.1
2
  accelerate>=0.28
3
+ diffusers==0.33.1
4
  transformers>=4.44
5
  peft>=0.17.0
 
6
  Pillow>=10.3
7
  safetensors>=0.4.2
8
  numpy>=1.26