dfrokido commited on
Commit
84aef20
·
verified ·
1 Parent(s): 0c12567

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +139 -69
app.py CHANGED
@@ -2,95 +2,165 @@ import gradio as gr
2
  import torch
3
  import numpy as np
4
  from PIL import Image
5
- import tempfile
6
- import plyfile # Add to requirements.txt
7
-
8
- class SimplePersistentCortex:
9
- def __init__(self, num_gaussians=10000):
10
- self.num_gaussians = num_gaussians
11
- # Initialize clustered Gaussians
12
- self.positions = torch.randn(num_gaussians, 3) * 0.4
13
- self.scales = torch.exp(torch.randn(num_gaussians, 3) * 0.6 - 1.0) # Positive scales
14
- self.colors_dc = torch.rand(num_gaussians, 3) * 0.5 + 0.25 # Base colors (SH degree 0)
15
- self.opacities = torch.sigmoid(torch.randn(num_gaussians) * 1.5 + 2.0) # Mostly visible
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  self.rotations = torch.nn.functional.normalize(torch.randn(num_gaussians, 4), dim=-1)
17
 
18
- def evolve(self, target_image: Image.Image, steps: int = 100):
19
- target_np = np.array(target_image.resize((128, 128))) / 255.0
20
- target_tensor = torch.tensor(target_np, dtype=torch.float32).mean(dim=(0,1)) # Average color
21
 
22
  for _ in range(steps):
23
- # Attract positions to center with noise
24
- self.positions += torch.randn_like(self.positions) * 0.01
25
- self.positions *= 0.99 # Dampen spread
 
 
 
 
 
 
 
 
 
26
 
27
- # Adapt colors toward target
28
- self.colors_dc += (target_tensor - self.colors_dc.mean(dim=0)) * 0.02
29
- self.colors_dc = torch.clamp(self.colors_dc, 0, 1)
 
 
 
 
 
 
30
 
31
- # Vary scales for density
32
- self.scales *= torch.exp(torch.randn_like(self.scales) * 0.03)
 
 
 
 
 
33
 
34
- # Boost opacity
35
- self.opacities = torch.clamp(self.opacities + 0.01, 0, 1)
 
 
36
 
 
37
  return self
38
 
39
- def export_ply(self, path: str):
40
- positions = self.positions.cpu().numpy()
41
- scales = np.log(self.scales.cpu().numpy() + 1e-9)
42
- opacities = np.arctanh(self.opacities.cpu().numpy().clip(1e-6, 1-1e-6))
43
- rot = self.rotations.cpu().numpy()
44
- sh_dc = self.colors_dc.cpu().numpy().reshape(-1, 3) # Simplified SH
45
-
46
- # Create vertex data
47
- vertex_data = np.empty(self.num_gaussians, dtype=[
48
- ('x', 'f4'), ('y', 'f4'), ('z', 'f4'),
49
- ('f_dc_0', 'f4'), ('f_dc_1', 'f4'), ('f_dc_2', 'f4'),
50
- ('opacity', 'f4'),
51
- ('scale_0', 'f4'), ('scale_1', 'f4'), ('scale_2', 'f4'),
52
- ('rot_0', 'f4'), ('rot_1', 'f4'), ('rot_2', 'f4'), ('rot_3', 'f4')
53
- ])
54
-
55
- vertex_data['x'], vertex_data['y'], vertex_data['z'] = positions[:, 0], positions[:, 1], positions[:, 2]
56
- vertex_data['f_dc_0'], vertex_data['f_dc_1'], vertex_data['f_dc_2'] = sh_dc[:, 0], sh_dc[:, 1], sh_dc[:, 2]
57
- vertex_data['opacity'] = opacities
58
- vertex_data['scale_0'], vertex_data['scale_1'], vertex_data['scale_2'] = scales[:, 0], scales[:, 1], scales[:, 2]
59
- vertex_data['rot_0'], vertex_data['rot_1'], vertex_data['rot_2'], vertex_data['rot_3'] = rot[:, 0], rot[:, 1], rot[:, 2], rot[:, 3]
60
 
61
  el = plyfile.PlyElement.describe(vertex_data, 'vertex')
62
- plyfile.PlyData([el]).write(path)
 
63
 
64
  def process(image: Image.Image, prompt: str = ""):
65
- if image is None:
66
- return None, "Error: Please upload an image."
67
-
68
- cortex = SimplePersistentCortex()
69
- cortex.evolve(image)
70
-
71
- with tempfile.NamedTemporaryFile(delete=False, suffix=".ply") as tmp:
72
- cortex.export_ply(tmp.name)
73
- ply_path = tmp.name
74
-
75
- status = "Persistent 3D Gaussian splat generated. Use mouse to orbit/zoom in viewer."
76
  if prompt:
77
- status += f" (Prompt: {prompt})"
78
-
79
- return ply_path, status
80
 
81
- with gr.Blocks(title="Persistent 3D Cortex Demo v3") as demo:
82
- gr.Markdown("# Persistent 3D Cortex Demo")
83
- gr.Markdown("Upload an image to generate an evolving 3D Gaussian representation.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
 
85
  with gr.Row():
86
  img_input = gr.Image(type="pil", label="Input Image")
87
- prompt_input = gr.Textbox(label="Prompt (optional)")
88
 
89
- btn = gr.Button("Generate 3D")
90
 
91
- model_output = gr.Model3D(label="Interactive Gaussian Splat Viewer", clear_color=[0.0, 0.0, 0.0, 1.0])
92
- text_output = gr.Textbox(label="Status")
93
-
94
- btn.click(process, inputs=[img_input, prompt_input], outputs=[model_output, text_output])
 
 
 
 
95
 
96
  demo.launch()
 
2
  import torch
3
  import numpy as np
4
  from PIL import Image
5
+ import os
6
+ import plyfile
7
+ import open_clip
8
+
9
+ # Load OpenCLIP for prompt conditioning (runs efficiently on CPU or GPU)
10
+ model, _, preprocess = open_clip.create_model_and_transforms('ViT-B-32', pretrained='laion2b_s34b_b79K')
11
+ model.eval()
12
+ tokenizer = open_clip.get_tokenizer('ViT-B-32')
13
+
14
+ class PersistentCortex:
15
+ def __init__(self, num_gaussians=8000):
16
+ self.num = num_gaussians
17
+ # Initialize a compact spherical distribution (similar to your working PLY)
18
+ angles = torch.rand(num_gaussians, 2) * 2 * np.pi
19
+ radius = torch.rand(num_gaussians).pow(1/3) * 0.6
20
+ self.positions = torch.stack([
21
+ radius * torch.sin(angles[:, 0]) * torch.cos(angles[:, 1]),
22
+ radius * torch.sin(angles[:, 0]) * torch.sin(angles[:, 1]),
23
+ radius * torch.cos(angles[:, 0])
24
+ ], dim=1)
25
+
26
+ self.scales = torch.exp(torch.randn(num_gaussians, 3) * -2.5 - 2.0)
27
+ self.colors = torch.rand(num_gaussians, 3) * 0.7 + 0.3 # neutral gray start
28
+ self.opacities = torch.sigmoid(torch.randn(num_gaussians) * 1.5 + 2.0)
29
  self.rotations = torch.nn.functional.normalize(torch.randn(num_gaussians, 4), dim=-1)
30
 
31
+ def evolve_from_image(self, image: Image.Image, steps=800):
32
+ img_array = np.array(image.resize((256, 256))) / 255.0
33
+ target = torch.tensor(img_array, dtype=torch.float32)
34
 
35
  for _ in range(steps):
36
+ # Simple orthographic projection to 2D for guidance
37
+ proj = self.positions[:, :2].clone()
38
+ proj = proj / proj.abs().max() # normalize to [-1, 1]
39
+ grid = proj.unsqueeze(0).unsqueeze(0) # (1,1,N,2)
40
+
41
+ sampled = torch.nn.functional.grid_sample(
42
+ target.permute(2, 0, 1).unsqueeze(0),
43
+ grid,
44
+ mode='bilinear',
45
+ padding_mode='border',
46
+ align_corners=True
47
+ ).squeeze(0).squeeze(0) # (N, 3)
48
 
49
+ # Attract positions toward brighter areas
50
+ brightness = sampled.mean(dim=1)
51
+ attraction = (brightness - brightness.mean()) * 0.015
52
+ self.positions[:, :2] += attraction.unsqueeze(1) * proj
53
+
54
+ # Gradually adopt average color from target
55
+ self.colors = torch.lerp(self.colors, target.mean(dim=[0,1]).repeat(self.num, 1), 0.02)
56
+
57
+ return self
58
 
59
+ def condition_on_prompt(self, prompt: str):
60
+ if not prompt.strip():
61
+ return self
62
+ text_tokens = tokenizer([prompt])
63
+ with torch.no_grad():
64
+ text_emb = model.encode_text(text_tokens).float()
65
+ text_emb = text_emb / text_emb.norm(dim=-1, keepdim=True)
66
 
67
+ # Simple linear projection to RGB shift (deterministic for reproducibility)
68
+ proj = torch.nn.Linear(512, 3, bias=False)
69
+ torch.nn.init.normal_(proj.weight, std=0.2)
70
+ color_shift = proj(text_emb)
71
 
72
+ self.colors = torch.clamp(self.colors + color_shift.repeat(self.num, 1) * 0.3, 0, 1)
73
  return self
74
 
75
+ def export_ply(self, path="output.ply"):
76
+ # Prepare vertex data matching gsplat.js expected properties
77
+ zeros = np.zeros((self.num, 3), dtype=np.float32)
78
+ log_scales = np.log(self.scales.cpu().numpy()).astype(np.float32)
79
+
80
+ vertex_data = np.core.records.fromarrays([
81
+ self.positions.cpu().numpy().astype(np.float32),
82
+ zeros, # normals (unused)
83
+ self.colors.cpu().numpy().astype(np.float32),
84
+ self.opacities.cpu().numpy().astype(np.float32),
85
+ log_scales,
86
+ self.rotations.cpu().numpy().astype(np.float32)
87
+ ], names='x,y,z,nx,ny,nz,f_dc_0,f_dc_1,f_dc_2,opacity,scale_0,scale_1,scale_2,rot_0,rot_1,rot_2,rot_3')
 
 
 
 
 
 
 
 
88
 
89
  el = plyfile.PlyElement.describe(vertex_data, 'vertex')
90
+ plyfile.PlyData([el], text=True).write(path)
91
+ return os.path.abspath(path)
92
 
93
  def process(image: Image.Image, prompt: str = ""):
94
+ cortex = PersistentCortex(num_gaussians=8000)
95
+ cortex.evolve_from_image(image, steps=800)
 
 
 
 
 
 
 
 
 
96
  if prompt:
97
+ cortex.condition_on_prompt(prompt)
98
+
99
+ ply_path = cortex.export_ply("/tmp/output.ply")
100
 
101
+ # In Hugging Face Spaces, /tmp files are automatically served under /files/
102
+ viewer_html = f"""
103
+ <div id="viewer" style="width:100%; height:600px; background:#000;"></div>
104
+ <script type="module">
105
+ import * as SPLAT from "https://cdn.jsdelivr.net/npm/gsplat@latest";
106
+
107
+ const container = document.getElementById('viewer');
108
+ const canvas = document.createElement('canvas');
109
+ canvas.style.width = '100%';
110
+ canvas.style.height = '100%';
111
+ container.appendChild(canvas);
112
+
113
+ const scene = new SPLAT.Scene();
114
+ const camera = new SPLAT.Camera();
115
+ const renderer = new SPLAT.WebGLRenderer({{ canvas }});
116
+ const controls = new SPLAT.OrbitControls(camera, canvas);
117
+
118
+ controls.autoRotate = false;
119
+ controls.enableDamping = true;
120
+ controls.dampingFactor = 0.05;
121
+ controls.rotateSpeed = 1.0;
122
+ controls.zoomSpeed = 1.2;
123
+ controls.panSpeed = 0.8;
124
+
125
+ await SPLAT.Loader.LoadAsync("/files/output.ply", scene);
126
+
127
+ function animate() {{
128
+ controls.update();
129
+ renderer.render(scene, camera);
130
+ requestAnimationFrame(animate);
131
+ }}
132
+ animate();
133
+ </script>
134
+ """
135
+
136
+ status = "3D Gaussian splat generated and evolved from your image"
137
+ if prompt:
138
+ status += f" with prompt conditioning: '{prompt}'."
139
+ else:
140
+ status += "."
141
+
142
+ return viewer_html, status
143
+
144
+ with gr.Blocks(title="Persistent 3D Cortex Demo") as demo:
145
+ gr.Markdown("# Persistent 3D Cortex – Interactive Demo")
146
+ gr.Markdown("""
147
+ Upload an image and optionally add a text prompt.
148
+ The system evolves a persistent 3D Gaussian splat representation influenced by the image content and prompt.
149
+ """)
150
 
151
  with gr.Row():
152
  img_input = gr.Image(type="pil", label="Input Image")
153
+ prompt_input = gr.Textbox(label="Prompt (e.g., 'shiny red apple', 'futuristic city')", placeholder="Optional text prompt")
154
 
155
+ generate_btn = gr.Button("Generate & Evolve 3D", variant="primary")
156
 
157
+ viewer_output = gr.HTML(label="Interactive 3D Viewer")
158
+ status_output = gr.Textbox(label="Status")
159
+
160
+ generate_btn.click(
161
+ fn=process,
162
+ inputs=[img_input, prompt_input],
163
+ outputs=[viewer_output, status_output]
164
+ )
165
 
166
  demo.launch()