Tremick commited on
Commit
ddbf47c
·
1 Parent(s): 84aa9d2

Fix ZeroGPU: add @spaces.GPU, import spaces first, clean up requirements

Browse files
Files changed (2) hide show
  1. app.py +99 -75
  2. requirements.txt +5 -5
app.py CHANGED
@@ -1,5 +1,6 @@
 
 
1
  import io
2
- import os
3
  import numpy as np
4
  from PIL import Image, UnidentifiedImageError
5
  import torch
@@ -9,85 +10,59 @@ from fastapi.responses import Response
9
  from transformers import DepthProImageProcessor, DepthProForDepthEstimation
10
 
11
 
12
- # 1. Initialize FastAPI & Load Apple Depth Pro
13
- fastapi_app = FastAPI(title="True Metric ToF Server")
 
 
14
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
15
-
16
- # Updated to avoid the transformers deprecation crash
17
 
18
  print("Loading Apple Depth Pro components...")
19
  image_processor = DepthProImageProcessor.from_pretrained("apple/DepthPro-hf")
20
- model = DepthProForDepthEstimation.from_pretrained("apple/DepthPro-hf").to(device)
21
-
 
 
22
  model.eval()
23
  print(f"Model successfully loaded on target device: {device}")
24
 
25
 
26
- @fastapi_app.get("/")
27
- def home():
28
- return {"status": "running"}
29
-
30
-
31
- @fastapi_app.post("/get_metric_depth")
32
- def get_metric_depth(file: UploadFile = File(...)):
33
- if not (file.content_type or "").startswith("image/"):
34
- raise HTTPException(status_code=400, detail="Uploaded file must be an image.")
35
-
36
- try:
37
- image_bytes = file.file.read()
38
- image = Image.open(io.BytesIO(image_bytes)).convert("RGB")
39
- except (UnidentifiedImageError, OSError):
40
- raise HTTPException(status_code=400, detail="Could not decode image.")
41
-
42
- try:
43
- inputs = image_processor(images=image, return_tensors="pt").to(device)
44
- with torch.no_grad():
45
- outputs = model(**inputs)
46
-
47
- post_processed = image_processor.post_process_depth_estimation(
48
- outputs, target_sizes=[(image.height, image.width)],
49
- )
50
-
51
- metric_depth_map = (
52
- post_processed["predicted_depth"]
53
- .detach().cpu().numpy().astype(np.float32)
54
- )
55
-
56
- buffer = io.BytesIO()
57
- np.save(buffer, metric_depth_map)
58
- return Response(content=buffer.getvalue(), media_type="application/octet-stream")
59
-
60
- except Exception as e:
61
- raise HTTPException(status_code=500, detail=f"Inference error: {str(e)}")
62
 
63
 
64
  # =====================================================================
65
- # 2. GRADIO INTERACTIVE INTERFACE (Does the 3D Projection in the Cloud)
 
66
  # =====================================================================
67
-
68
  def create_3d_point_cloud(input_pil_image):
69
  """Processes the image, calculates 3D vertices, and writes a .ply file."""
70
  if input_pil_image is None:
71
  return None
72
 
73
- # Step A: Run inference locally inside the space
74
  image = input_pil_image.convert("RGB")
75
- inputs = image_processor(images=image, return_tensors="pt").to(device)
76
-
77
- with torch.no_grad():
78
- outputs = model(**inputs)
79
-
80
- post_processed = image_processor.post_process_depth_estimation(
81
- outputs, target_sizes=[(image.height, image.width)],
82
- )
83
- # Your list index fix: grabbing the first dictionary element out of the post-processed array
84
- depth_matrix = post_processed[0]["predicted_depth"].detach().cpu().numpy().astype(np.float32)
85
 
86
-
87
- # Step B: Pinhole camera math projection (u, v, depth) -> (X, Y, Z)
88
  height, width = depth_matrix.shape
89
  color_np = np.array(image) / 255.0
90
-
91
  focal_length = max(width, height)
92
  cx, cy = width / 2.0, height / 2.0
93
  u, v = np.meshgrid(np.arange(width), np.arange(height))
@@ -95,18 +70,18 @@ def create_3d_point_cloud(input_pil_image):
95
  z = depth_matrix.flatten()
96
  x = ((u.flatten() - cx) * z) / focal_length
97
  y = ((v.flatten() - cy) * z) / focal_length
98
-
99
- # Clean up infinite backgrounds (keep objects between 10cm and 30 meters)
100
  valid_mask = (z > 0.1) & (z < 30.0)
101
  x, y, z = x[valid_mask], y[valid_mask], z[valid_mask]
102
-
103
  rgb = color_np.reshape(-1, 3)[valid_mask]
104
  rgb_ints = (rgb * 255).astype(np.uint8)
105
 
106
- # Step C: Write raw points directly to a standard .ply file format
107
  ply_filename = "output_scene.ply"
108
  num_points = len(z)
109
-
110
  with open(ply_filename, "w") as f:
111
  f.write("ply\n")
112
  f.write("format ascii 1.0\n")
@@ -118,29 +93,78 @@ def create_3d_point_cloud(input_pil_image):
118
  f.write("property uchar green\n")
119
  f.write("property uchar blue\n")
120
  f.write("end_header\n")
121
-
122
  for i in range(num_points):
123
- # Invert Y axis slightly so the 3D model loads right-side up
124
- f.write(f"{x[i]:.4f} {-y[i]:.4f} {z[i]:.4f} {rgb_ints[i][0]} {rgb_ints[i][1]} {rgb_ints[i][2]}\n")
 
 
 
125
 
126
  return ply_filename
127
 
128
 
129
- # Define web dashboard interface layout
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
  with gr.Blocks(title="Software-ToF 3D Cloud Visualizer") as demo:
131
  gr.Markdown("# 📷 Software-ToF Camera Simulation")
132
- gr.Markdown("Upload any image to calculate absolute metric depths and generate an interactive 3D Point Cloud visualization.")
133
-
 
 
 
134
  with gr.Row():
135
  with gr.Column():
136
- input_img = gr.Image(type="pil", label="Input Scene Photo")
137
  submit_btn = gr.Button("Generate ToF 3D Structure", variant="primary")
138
-
139
  with gr.Column():
140
- # Model3D renders an interactive canvas in the web browser
141
- output_3d = gr.Model3D(label="Interactive 3D Point Cloud Canvas", display_mode="point_cloud")
 
 
142
 
143
  submit_btn.click(fn=create_3d_point_cloud, inputs=[input_img], outputs=[output_3d])
144
 
145
- # Mount your unified architecture onto Hugging Face port 7860
 
 
 
 
 
146
  app = gr.mount_gradio_app(fastapi_app, demo, path="/")
 
1
+ import spaces # MUST be the very first import for ZeroGPU to work
2
+
3
  import io
 
4
  import numpy as np
5
  from PIL import Image, UnidentifiedImageError
6
  import torch
 
10
  from transformers import DepthProImageProcessor, DepthProForDepthEstimation
11
 
12
 
13
+ # =====================================================================
14
+ # 1. Load Apple Depth Pro at module level.
15
+ # ZeroGPU safely emulates CUDA during startup so .to("cuda") works here.
16
+ # =====================================================================
17
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
18
+ dtype = torch.float16 if torch.cuda.is_available() else torch.float32
 
19
 
20
  print("Loading Apple Depth Pro components...")
21
  image_processor = DepthProImageProcessor.from_pretrained("apple/DepthPro-hf")
22
+ model = DepthProForDepthEstimation.from_pretrained(
23
+ "apple/DepthPro-hf",
24
+ torch_dtype=dtype,
25
+ ).to(device)
26
  model.eval()
27
  print(f"Model successfully loaded on target device: {device}")
28
 
29
 
30
+ # =====================================================================
31
+ # 2. Core inference helper (shared by Gradio UI and the REST endpoint)
32
+ # =====================================================================
33
+ def _run_depth_inference(image: Image.Image):
34
+ """Returns post-processed depth output list. Caller handles indexing."""
35
+ inputs = image_processor(images=image, return_tensors="pt").to(device)
36
+ with torch.no_grad():
37
+ outputs = model(**inputs)
38
+ return image_processor.post_process_depth_estimation(
39
+ outputs,
40
+ target_sizes=[(image.height, image.width)],
41
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
 
43
 
44
  # =====================================================================
45
+ # 3. Gradio function - decorated with @spaces.GPU so ZeroGPU allocates
46
+ # a real GPU for this call.
47
  # =====================================================================
48
+ @spaces.GPU(duration=120)
49
  def create_3d_point_cloud(input_pil_image):
50
  """Processes the image, calculates 3D vertices, and writes a .ply file."""
51
  if input_pil_image is None:
52
  return None
53
 
54
+ # Step A: Depth inference
55
  image = input_pil_image.convert("RGB")
56
+ post_processed = _run_depth_inference(image)
57
+ depth_matrix = (
58
+ post_processed[0]["predicted_depth"]
59
+ .detach().cpu().numpy().astype(np.float32)
60
+ )
 
 
 
 
 
61
 
62
+ # Step B: Pinhole camera projection (u, v, depth) -> (X, Y, Z)
 
63
  height, width = depth_matrix.shape
64
  color_np = np.array(image) / 255.0
65
+
66
  focal_length = max(width, height)
67
  cx, cy = width / 2.0, height / 2.0
68
  u, v = np.meshgrid(np.arange(width), np.arange(height))
 
70
  z = depth_matrix.flatten()
71
  x = ((u.flatten() - cx) * z) / focal_length
72
  y = ((v.flatten() - cy) * z) / focal_length
73
+
74
+ # Keep objects between 10 cm and 30 m; discard sky / infinite background
75
  valid_mask = (z > 0.1) & (z < 30.0)
76
  x, y, z = x[valid_mask], y[valid_mask], z[valid_mask]
77
+
78
  rgb = color_np.reshape(-1, 3)[valid_mask]
79
  rgb_ints = (rgb * 255).astype(np.uint8)
80
 
81
+ # Step C: Write ASCII PLY file
82
  ply_filename = "output_scene.ply"
83
  num_points = len(z)
84
+
85
  with open(ply_filename, "w") as f:
86
  f.write("ply\n")
87
  f.write("format ascii 1.0\n")
 
93
  f.write("property uchar green\n")
94
  f.write("property uchar blue\n")
95
  f.write("end_header\n")
 
96
  for i in range(num_points):
97
+ # Invert Y axis so the model loads right-side up
98
+ f.write(
99
+ f"{x[i]:.4f} {-y[i]:.4f} {z[i]:.4f} "
100
+ f"{rgb_ints[i][0]} {rgb_ints[i][1]} {rgb_ints[i][2]}\n"
101
+ )
102
 
103
  return ply_filename
104
 
105
 
106
+ # =====================================================================
107
+ # 4. FastAPI backend for the REST /get_metric_depth endpoint
108
+ # =====================================================================
109
+ fastapi_app = FastAPI(title="True Metric ToF Server")
110
+
111
+
112
+ @fastapi_app.get("/status")
113
+ def status():
114
+ return {"status": "running", "device": str(device)}
115
+
116
+
117
+ @fastapi_app.post("/get_metric_depth")
118
+ async def get_metric_depth(file: UploadFile = File(...)):
119
+ if not (file.content_type or "").startswith("image/"):
120
+ raise HTTPException(status_code=400, detail="Uploaded file must be an image.")
121
+
122
+ try:
123
+ image_bytes = await file.read()
124
+ image = Image.open(io.BytesIO(image_bytes)).convert("RGB")
125
+ except (UnidentifiedImageError, OSError):
126
+ raise HTTPException(status_code=400, detail="Could not decode image.")
127
+
128
+ try:
129
+ post_processed = _run_depth_inference(image)
130
+ metric_depth_map = (
131
+ post_processed[0]["predicted_depth"]
132
+ .detach().cpu().numpy().astype(np.float32)
133
+ )
134
+ buffer = io.BytesIO()
135
+ np.save(buffer, metric_depth_map)
136
+ return Response(content=buffer.getvalue(), media_type="application/octet-stream")
137
+
138
+ except Exception as e:
139
+ raise HTTPException(status_code=500, detail=f"Inference error: {str(e)}")
140
+
141
+
142
+ # =====================================================================
143
+ # 5. Gradio UI
144
+ # =====================================================================
145
  with gr.Blocks(title="Software-ToF 3D Cloud Visualizer") as demo:
146
  gr.Markdown("# 📷 Software-ToF Camera Simulation")
147
+ gr.Markdown(
148
+ "Upload any image to calculate absolute metric depths and generate "
149
+ "an interactive 3D Point Cloud visualization."
150
+ )
151
+
152
  with gr.Row():
153
  with gr.Column():
154
+ input_img = gr.Image(type="pil", label="Input Scene Photo")
155
  submit_btn = gr.Button("Generate ToF 3D Structure", variant="primary")
 
156
  with gr.Column():
157
+ output_3d = gr.Model3D(
158
+ label="Interactive 3D Point Cloud Canvas",
159
+ display_mode="point_cloud",
160
+ )
161
 
162
  submit_btn.click(fn=create_3d_point_cloud, inputs=[input_img], outputs=[output_3d])
163
 
164
+
165
+ # =====================================================================
166
+ # 6. Mount Gradio onto FastAPI.
167
+ # Hugging Face (sdk: gradio) auto-discovers this `app` variable and
168
+ # serves it via uvicorn on port 7860 - no manual uvicorn.run() needed.
169
+ # =====================================================================
170
  app = gr.mount_gradio_app(fastapi_app, demo, path="/")
requirements.txt CHANGED
@@ -1,10 +1,10 @@
1
- fastapi
2
- uvicorn[standard]
3
- python-multipart
4
- torch --index-url https://pytorch.org
5
- torchvision --index-url https://pytorch.org
6
  transformers
7
  accelerate
8
  numpy
9
  pillow
10
  gradio
 
 
 
 
1
+ torch
2
+ torchvision
 
 
 
3
  transformers
4
  accelerate
5
  numpy
6
  pillow
7
  gradio
8
+ fastapi
9
+ uvicorn[standard]
10
+ python-multipart