jasonod888 commited on
Commit
e86d966
·
1 Parent(s): 8d53463

Fix: remove unsupported target_properties kwarg; add previews; robust inputs

Browse files
Files changed (1) hide show
  1. handler.py +99 -128
handler.py CHANGED
@@ -1,153 +1,124 @@
1
  import base64
2
  import io
 
3
  import os
4
- from typing import Any, Dict, List, Optional
5
 
6
- import numpy as np
7
- from PIL import Image
8
  import torch
9
- from diffusers import MarigoldIntrinsicsPipeline
10
-
11
-
12
- def _b64_to_pil(b64: str) -> Image.Image:
13
- """Decode base64 (optionally data URL) → PIL RGB."""
14
- s = b64.strip()
15
- if s.startswith("data:image"):
16
- s = s.split(",", 1)[1]
17
- raw = base64.b64decode(s, validate=False)
18
- return Image.open(io.BytesIO(raw)).convert("RGB")
19
-
20
-
21
- def _pil_to_b64_png(img: Image.Image) -> str:
22
- """PIL → base64 PNG (no data URL prefix)."""
23
- buf = io.BytesIO()
24
- img.save(buf, format="PNG")
25
- return base64.b64encode(buf.getvalue()).decode("utf-8")
26
 
27
 
28
  class EndpointHandler:
29
  """
30
- Hugging Face Inference Toolkit-compatible handler.
31
-
32
- Expects request JSON:
33
- {
34
- "inputs": "<base64 image or data URL>",
35
- "parameters": {
36
- "processing_res": 768,
37
- "targets": ["albedo","diffuse_shading","residual"], # optional subset
38
- "num_inference_steps": <int>, # optional
39
- "match_input_resolution": true # default true
40
- }
41
- }
42
-
43
- Returns:
44
- {
45
- "outputs": {
46
- "targets": [...],
47
- "tensors_info": {"shape":[H,W,C], "dtype":"float32"},
48
- "uncertainty_available": <bool>,
49
- "visualizations": ["<b64_png_albedo>", "<b64_png_diffuse>", "<b64_png_residual>"],
50
- "visualizations_error": null | "<msg>"
51
- }
52
- }
53
  """
54
 
55
- def __init__(self, model_dir: Optional[str] = None):
56
- # The toolkit passes model_dir but we load by model id to ensure the correct subtask.
57
- self.model_id = os.environ.get("MARIGOLD_MODEL_ID", "prs-eth/marigold-iid-lighting-v1-1")
58
- self.device = "cuda" if torch.cuda.is_available() else "cpu"
59
- self.dtype = torch.float16 if self.device == "cuda" else torch.float32
60
- self.pipe: Optional[MarigoldIntrinsicsPipeline] = None
61
- self.supported_targets: List[str] = ["albedo", "diffuse_shading", "residual"]
62
-
63
- def setup(self):
64
- # Load once
65
- self.pipe = MarigoldIntrinsicsPipeline.from_pretrained(self.model_id, torch_dtype=self.dtype)
66
- self.pipe = self.pipe.to(self.device)
67
- # figure out supported target names from the checkpoint if provided
68
- try:
69
- if getattr(self.pipe, "target_properties", None):
70
- names = self.pipe.target_properties.get("target_names")
71
- if isinstance(names, list) and names:
72
- self.supported_targets = names
73
- except Exception:
74
- pass
75
- try:
76
- self.pipe.set_progress_bar_config(disable=True)
77
- except Exception:
78
- pass
79
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  def __call__(self, data: Dict[str, Any]) -> Dict[str, Any]:
81
- if self.pipe is None:
82
- self.setup()
 
83
 
84
- # ---- Parse inputs ----
85
- raw_inputs = data.get("inputs")
86
- params = data.get("parameters") or {}
87
 
88
- if not isinstance(raw_inputs, (str, bytes, bytearray)):
89
- return {"error": "Send the image in 'inputs' as base64 string (or data URL)."}
90
 
91
- # decode base64 image
92
- try:
93
- if isinstance(raw_inputs, (bytes, bytearray)):
94
- raw_inputs = raw_inputs.decode("utf-8")
95
- image = _b64_to_pil(raw_inputs)
96
- except Exception as e:
97
- return {"error": f"Failed to decode 'inputs': {e}"}
98
-
99
- processing_res = int(params.get("processing_res", getattr(self.pipe, "default_processing_resolution", 768) or 768))
100
- num_steps = params.get("num_inference_steps", None)
101
- match_input_resolution = bool(params.get("match_input_resolution", True))
102
-
103
- # requested targets, keep order, intersect with supported
104
- req_targets = params.get("targets", self.supported_targets)
105
- targets = [t for t in req_targets if t in self.supported_targets]
106
- if not targets:
107
- targets = self.supported_targets
108
-
109
- # target_properties override so the pipeline outputs only what we want, in order
110
- target_props = dict(self.pipe.target_properties) if getattr(self.pipe, "target_properties", None) else {}
111
- target_props["target_names"] = targets
112
-
113
- # ---- Inference ----
114
  with torch.inference_mode():
115
- out = self.pipe(
116
- image=image,
117
- processing_resolution=processing_res,
118
- match_input_resolution=match_input_resolution,
119
  num_inference_steps=num_steps,
120
- target_properties=target_props,
121
- output_type="np",
122
- return_dict=True,
 
123
  )
124
 
125
- pred = out.prediction # numpy HxWxC float32
126
- h, w, c = pred.shape
127
- dtype_str = str(pred.dtype)
128
- unc_avail = bool(getattr(out, "uncertainty", None) is not None)
 
 
 
129
 
130
- # ---- Visualize ----
131
- vis_b64 = None
132
- vis_err = None
133
  try:
134
- # Uses the built-in visualizer for intrinsics
135
- from diffusers.pipelines.marigold.image_processor import MarigoldImageProcessor
136
- vis_imgs = MarigoldImageProcessor.visualize_intrinsics(
137
- prediction=pred, target_properties=target_props, color_map="binary"
138
- )
139
- # to base64 PNGs (no data URL prefix to keep payload compact)
140
- vis_b64 = [_pil_to_b64_png(im) for im in vis_imgs]
141
- except Exception as e:
142
- vis_b64 = None
143
- vis_err = f"Failed to visualize: {e}"
144
 
145
- return {
146
  "outputs": {
147
- "targets": targets,
148
- "tensors_info": {"shape": [h, w, c], "dtype": dtype_str},
149
- "uncertainty_available": unc_avail,
150
- "visualizations": vis_b64,
151
- "visualizations_error": vis_err,
 
152
  }
153
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import base64
2
  import io
3
+ import json
4
  import os
5
+ from typing import Any, Dict, Tuple, Optional
6
 
 
 
7
  import torch
8
+ from PIL import Image
9
+ from diffusers import MarigoldIntrinsicsPipeline, utils as diffusers_utils
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
 
12
  class EndpointHandler:
13
  """
14
+ Hugging Face Inference Endpoints custom handler for Marigold Intrinsics (Lighting).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  """
16
 
17
+ def __init__(self, path: str = "."):
18
+ # Load the Lighting checkpoint
19
+ self.pipe = MarigoldIntrinsicsPipeline.from_pretrained(
20
+ "prs-eth/marigold-iid-lighting-v1-1",
21
+ variant="fp16",
22
+ torch_dtype=torch.float16,
23
+ ).to("cuda" if torch.cuda.is_available() else "cpu")
24
+
25
+ # Optional: reduced steps (user can override via inputs)
26
+ self.default_steps = 4
27
+
28
+ # ------- helpers --------
29
+ def _load_image_from_any(self, obj: Any) -> Image.Image:
30
+ """
31
+ Accepts:
32
+ - str URL (http/https)
33
+ - base64 string (png/jpg)
34
+ - dict {"image_url": "..."}
35
+ Returns PIL.Image
36
+ """
37
+ if isinstance(obj, dict):
38
+ if "image_url" in obj:
39
+ return diffusers_utils.load_image(obj["image_url"])
40
+ # if someone sent {"image_b64": "..."}:
41
+ if "image_b64" in obj:
42
+ return Image.open(io.BytesIO(base64.b64decode(obj["image_b64"]))).convert("RGB")
43
+ raise ValueError("Unsupported dict format for 'inputs'. Provide 'image_url' or 'image_b64'.")
44
+
45
+ if isinstance(obj, str):
46
+ s = obj.strip()
47
+ if s.startswith("http://") or s.startswith("https://"):
48
+ return diffusers_utils.load_image(s)
49
+ # else assume base64 image
50
+ return Image.open(io.BytesIO(base64.b64decode(s))).convert("RGB")
51
+
52
+ raise ValueError("Unsupported 'inputs' type. Provide URL string, base64 string, or {'image_url': ...} dict.")
53
+
54
+ def _png_b64(self, pil_img: Image.Image) -> str:
55
+ buf = io.BytesIO()
56
+ pil_img.save(buf, format="PNG")
57
+ return base64.b64encode(buf.getvalue()).decode("utf-8")
58
+
59
+ # ------- main entry --------
60
  def __call__(self, data: Dict[str, Any]) -> Dict[str, Any]:
61
+ # Inference Endpoints expects the request JSON to have an "inputs" key.
62
+ if "inputs" not in data:
63
+ raise ValueError("Body must include an 'inputs' key")
64
 
65
+ # Parse options (all optional)
66
+ params = data if isinstance(data, dict) else {}
67
+ num_steps = int(params.get("num_inference_steps", self.default_steps))
68
 
69
+ # Note: do NOT pass unsupported kwargs like target_properties or processing_res
70
+ image = self._load_image_from_any(data["inputs"])
71
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  with torch.inference_mode():
73
+ intrinsics = self.pipe(
74
+ image,
 
 
75
  num_inference_steps=num_steps,
76
+ # If you later want to expose these (and they’re supported),
77
+ # you can add conditionals:
78
+ # processing_resolution=params.get("processing_resolution", None),
79
+ # match_input_resolution=params.get("match_input_resolution", None),
80
  )
81
 
82
+ # intrinsics.prediction is a tensor; lighting model yields 3 targets
83
+ pred = intrinsics.prediction # shape: (T, H, W, 3) typically; T=3 for lighting
84
+ if isinstance(pred, torch.Tensor):
85
+ t = pred
86
+ else:
87
+ # Just in case (diffusers usually returns a dataclass with .prediction tensor)
88
+ raise RuntimeError("Unexpected intrinsics.prediction type")
89
 
90
+ # Gather target names from pipeline properties
91
+ # For lighting these should be: ["albedo", "shading", "residual"]
 
92
  try:
93
+ target_names = list(self.pipe.target_properties.keys())
94
+ except Exception:
95
+ # Fallback (expected order for lighting model)
96
+ target_names = ["albedo", "shading", "residual"]
 
 
 
 
 
 
97
 
98
+ out: Dict[str, Any] = {
99
  "outputs": {
100
+ "targets": target_names,
101
+ "tensors_info": {
102
+ "shape": list(t.shape),
103
+ "dtype": str(t.dtype).replace("torch.", ""),
104
+ },
105
+ "uncertainty_available": False,
106
  }
107
  }
108
+
109
+ # Try to attach base64 previews per target
110
+ try:
111
+ vis_list = self.pipe.image_processor.visualize_intrinsics(t, self.pipe.target_properties)
112
+ # vis_list is a list over batch (we run 1 image), where each item is a dict of PIL images per target
113
+ previews: Dict[str, str] = {}
114
+ if isinstance(vis_list, (list, tuple)) and len(vis_list) >= 1 and isinstance(vis_list[0], dict):
115
+ for name, pil_img in vis_list[0].items():
116
+ # only include known target previews to avoid surprises
117
+ if name in target_names and isinstance(pil_img, Image.Image):
118
+ previews[name] = self._png_b64(pil_img)
119
+ if previews:
120
+ out["outputs"]["visualizations_png_b64"] = previews
121
+ except Exception as e:
122
+ out["outputs"]["visualizations_error"] = f"Failed to visualize: {e.__class__.__name__}: {e}"
123
+
124
+ return out