dfrokido commited on
Commit
eddf0e9
·
verified ·
1 Parent(s): 4f1d121

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +35 -46
app.py CHANGED
@@ -6,7 +6,7 @@ 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')
@@ -14,7 +14,7 @@ tokenizer = open_clip.get_tokenizer('ViT-B-32')
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([
@@ -24,35 +24,38 @@ class PersistentCortex:
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
 
@@ -64,7 +67,6 @@ class PersistentCortex:
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)
@@ -73,17 +75,16 @@ class PersistentCortex:
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).T,
82
- zeros.T, # normals (unused)
83
- self.colors.cpu().numpy().T,
84
- self.opacities.cpu().numpy().reshape(1, -1).T,
85
- log_scales.T,
86
- self.rotations.cpu().numpy().T
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')
@@ -92,7 +93,7 @@ class PersistentCortex:
92
 
93
  def process(image: Image.Image, prompt: str = ""):
94
  if image is None:
95
- raise ValueError("Please upload an image to proceed.")
96
 
97
  cortex = PersistentCortex(num_gaussians=8000)
98
  cortex.evolve_from_image(image, steps=800)
@@ -101,7 +102,6 @@ def process(image: Image.Image, prompt: str = ""):
101
 
102
  ply_path = cortex.export_ply("/tmp/output.ply")
103
 
104
- # In Hugging Face Spaces, /tmp files are automatically served under /files/
105
  viewer_html = f"""
106
  <div id="viewer" style="width:100%; height:600px; background:#000;"></div>
107
  <script type="module">
@@ -121,9 +121,6 @@ def process(image: Image.Image, prompt: str = ""):
121
  controls.autoRotate = false;
122
  controls.enableDamping = true;
123
  controls.dampingFactor = 0.05;
124
- controls.rotateSpeed = 1.0;
125
- controls.zoomSpeed = 1.2;
126
- controls.panSpeed = 0.8;
127
 
128
  await SPLAT.Loader.LoadAsync("/files/output.ply", scene);
129
 
@@ -136,34 +133,26 @@ def process(image: Image.Image, prompt: str = ""):
136
  </script>
137
  """
138
 
139
- status = "3D Gaussian splat generated and evolved from your image"
140
  if prompt:
141
- status += f" with prompt conditioning: '{prompt}'."
142
- else:
143
- status += "."
144
 
145
  return viewer_html, status
146
 
147
  with gr.Blocks(title="Persistent 3D Cortex Demo") as demo:
148
  gr.Markdown("# Persistent 3D Cortex – Interactive Demo")
149
- gr.Markdown("""
150
- Upload an image and optionally add a text prompt.
151
- The system evolves a persistent 3D Gaussian splat representation influenced by the image content and prompt.
152
- """)
153
 
154
  with gr.Row():
155
- img_input = gr.Image(type="pil", label="Input Image")
156
- prompt_input = gr.Textbox(label="Prompt (e.g., 'shiny red apple', 'futuristic city')", placeholder="Optional text prompt")
157
 
158
- generate_btn = gr.Button("Generate & Evolve 3D", variant="primary")
159
 
160
- viewer_output = gr.HTML(label="Interactive 3D Viewer")
161
  status_output = gr.Textbox(label="Status")
162
 
163
- generate_btn.click(
164
- fn=process,
165
- inputs=[img_input, prompt_input],
166
- outputs=[viewer_output, status_output]
167
- )
168
 
169
  demo.launch()
 
6
  import plyfile
7
  import open_clip
8
 
9
+ # Load OpenCLIP for prompt conditioning
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')
 
14
  class PersistentCortex:
15
  def __init__(self, num_gaussians=8000):
16
  self.num = num_gaussians
17
+ # Initialize compact spherical distribution
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([
 
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
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).permute(2, 0, 1) # (3, 256, 256)
34
+
35
+ # Create sampling grid: scatter points across [-1,1] x [-1,1]
36
+ proj = torch.rand(self.num, 2) * 2 - 1 # (N, 2) uniform in view
37
 
38
  for _ in range(steps):
39
+ grid = proj.unsqueeze(0).unsqueeze(0) # (1, 1, N, 2)
 
 
 
40
 
41
  sampled = torch.nn.functional.grid_sample(
42
+ target.unsqueeze(0),
43
  grid,
44
  mode='bilinear',
45
  padding_mode='border',
46
  align_corners=True
47
+ ).squeeze(0).squeeze(0).t() # (N, 3)
48
 
 
49
  brightness = sampled.mean(dim=1)
50
+ attraction_strength = (brightness - brightness.mean()) * 0.02
51
+ proj += attraction_strength.unsqueeze(1) * proj.normalized()
52
+
53
+ # Color adaptation
54
+ avg_color = target.mean(dim=[1,2])
55
+ self.colors = torch.lerp(self.colors, avg_color.repeat(self.num, 1), 0.02)
56
 
57
+ # Map projected points back to 3D sphere surface (simple radial projection)
58
+ self.positions[:, :2] = proj * (self.positions[:, :2].norm(dim=1).unsqueeze(1) + 0.1)
59
 
60
  return self
61
 
 
67
  text_emb = model.encode_text(text_tokens).float()
68
  text_emb = text_emb / text_emb.norm(dim=-1, keepdim=True)
69
 
 
70
  proj = torch.nn.Linear(512, 3, bias=False)
71
  torch.nn.init.normal_(proj.weight, std=0.2)
72
  color_shift = proj(text_emb)
 
75
  return self
76
 
77
  def export_ply(self, path="output.ply"):
 
78
  zeros = np.zeros((self.num, 3), dtype=np.float32)
79
+ log_scales = np.log(self.scales.cpu().numpy())
80
 
81
  vertex_data = np.core.records.fromarrays([
82
+ self.positions.cpu().numpy(),
83
+ zeros,
84
+ self.colors.cpu().numpy(),
85
+ self.opacities.cpu().numpy()[:, np.newaxis],
86
+ log_scales,
87
+ self.rotations.cpu().numpy()
88
  ], 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')
89
 
90
  el = plyfile.PlyElement.describe(vertex_data, 'vertex')
 
93
 
94
  def process(image: Image.Image, prompt: str = ""):
95
  if image is None:
96
+ raise gr.Error("Please upload an image.")
97
 
98
  cortex = PersistentCortex(num_gaussians=8000)
99
  cortex.evolve_from_image(image, steps=800)
 
102
 
103
  ply_path = cortex.export_ply("/tmp/output.ply")
104
 
 
105
  viewer_html = f"""
106
  <div id="viewer" style="width:100%; height:600px; background:#000;"></div>
107
  <script type="module">
 
121
  controls.autoRotate = false;
122
  controls.enableDamping = true;
123
  controls.dampingFactor = 0.05;
 
 
 
124
 
125
  await SPLAT.Loader.LoadAsync("/files/output.ply", scene);
126
 
 
133
  </script>
134
  """
135
 
136
+ status = f"Persistent 3D representation evolved from image"
137
  if prompt:
138
+ status += f" and conditioned on prompt: '{prompt}'"
139
+ status += "."
 
140
 
141
  return viewer_html, status
142
 
143
  with gr.Blocks(title="Persistent 3D Cortex Demo") as demo:
144
  gr.Markdown("# Persistent 3D Cortex – Interactive Demo")
145
+ gr.Markdown("Upload an image and optionally add a text prompt to generate an evolving 3D Gaussian splat representation.")
 
 
 
146
 
147
  with gr.Row():
148
+ img_input = gr.Image(type="pil", label="Input Image (required)")
149
+ prompt_input = gr.Textbox(label="Prompt (e.g., 'glowing blue crystal')", placeholder="Optional descriptive text")
150
 
151
+ generate_btn = gr.Button("Generate Persistent 3D", variant="primary")
152
 
153
+ viewer_output = gr.HTML()
154
  status_output = gr.Textbox(label="Status")
155
 
156
+ generate_btn.click(process, inputs=[img_input, prompt_input], outputs=[viewer_output, status_output])
 
 
 
 
157
 
158
  demo.launch()