Nekochu commited on
Commit
d65d5b5
·
1 Parent(s): 2aca4a6

fixes: cudagc guard, rm conditioner.py, turbo depth colormap, proper normal viz, compact UI, example

Browse files
README.md CHANGED
@@ -21,6 +21,7 @@ tags:
21
 
22
  Estimate depth and surface normals from a single image using [FE2E](https://github.com/AMAP-ML/FE2E) (CVPR 2026).
23
 
24
- - Model: Step1X-Edit DiT + LDRN LoRA (FP8 -> dynamic INT8 on CPU)
25
  - Single denoise step (not iterative diffusion)
26
  - Outputs both depth AND normal maps simultaneously
 
 
21
 
22
  Estimate depth and surface normals from a single image using [FE2E](https://github.com/AMAP-ML/FE2E) (CVPR 2026).
23
 
24
+ - Model: Step1X-Edit DiT + LDRN LoRA, pre-quantized INT8 (12 GB)
25
  - Single denoise step (not iterative diffusion)
26
  - Outputs both depth AND normal maps simultaneously
27
+ - Takes ~29 min for 768x1024 on free CPU Basic
app.py CHANGED
@@ -114,9 +114,24 @@ def generate(image):
114
 
115
  elapsed = time.time() - t0
116
 
117
- normal_map = images[0] if images else None
118
- Lpred_img = Lpred[0].clamp(0, 1).cpu()
119
- depth_map = F.to_pil_image(Lpred_img)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
 
121
  status = f"Generated in {elapsed:.1f}s ({image.size[0]}x{image.size[1]}, single denoise, INT8)"
122
  print(f"[gen] {status}")
@@ -128,16 +143,15 @@ import gradio as gr
128
  with gr.Blocks(title="FE2E: Depth + Normal (CPU)") as demo:
129
  gr.Markdown(
130
  "**[FE2E](https://github.com/AMAP-ML/FE2E)** Depth + Normal from a single image (CVPR 2026). "
131
- "Single denoise step, Step1X-Edit DiT + LDRN LoRA (pre-merged), INT8 quantized on CPU."
132
  )
 
 
 
 
133
  with gr.Row():
134
- with gr.Column():
135
- input_img = gr.Image(label="Input Image", type="pil")
136
- run_btn = gr.Button("Estimate Depth + Normal", variant="primary")
137
- with gr.Column():
138
- depth_out = gr.Image(label="Depth Map", type="pil")
139
- normal_out = gr.Image(label="Normal Map", type="pil")
140
- status_out = gr.Textbox(label="Status", interactive=False)
141
 
142
  run_btn.click(
143
  fn=generate,
@@ -147,6 +161,15 @@ with gr.Blocks(title="FE2E: Depth + Normal (CPU)") as demo:
147
  api_name="generate",
148
  )
149
 
 
 
 
 
 
 
 
 
 
150
  demo.queue(default_concurrency_limit=1)
151
 
152
  if __name__ == "__main__":
 
114
 
115
  elapsed = time.time() - t0
116
 
117
+ import numpy as np
118
+ import matplotlib.cm as cm
119
+
120
+ # Normal map: normalize vectors, map [-1,1] to [0,255] RGB
121
+ normal_np = Rpred[0].cpu().float().numpy().transpose(1, 2, 0) # (H, W, 3)
122
+ normal_norm = np.linalg.norm(normal_np, axis=-1, keepdims=True)
123
+ normal_norm[normal_norm < 1e-12] = 1e-12
124
+ normal_np = normal_np / normal_norm
125
+ normal_rgb = (((normal_np + 1) * 0.5) * 255).clip(0, 255).astype(np.uint8)
126
+ normal_map = Image.fromarray(normal_rgb)
127
+ normal_map = normal_map.resize(image.size)
128
+
129
+ # Depth map: apply turbo colormap for visualization
130
+ depth_np = Lpred[0].cpu().float().mean(dim=0).numpy() # (H, W) average channels
131
+ depth_np = (depth_np - depth_np.min()) / (depth_np.max() - depth_np.min() + 1e-8)
132
+ depth_colored = (cm.turbo(depth_np)[:, :, :3] * 255).astype(np.uint8)
133
+ depth_map = Image.fromarray(depth_colored)
134
+ depth_map = depth_map.resize(image.size)
135
 
136
  status = f"Generated in {elapsed:.1f}s ({image.size[0]}x{image.size[1]}, single denoise, INT8)"
137
  print(f"[gen] {status}")
 
143
  with gr.Blocks(title="FE2E: Depth + Normal (CPU)") as demo:
144
  gr.Markdown(
145
  "**[FE2E](https://github.com/AMAP-ML/FE2E)** Depth + Normal from a single image (CVPR 2026). "
146
+ "Takes ~29 min for 768x1024, 1 step, Step1X-Edit DiT + LDRN LoRA (pre-merged), INT8 quantized on CPU."
147
  )
148
+ with gr.Row(equal_height=True):
149
+ input_img = gr.Image(label="Input", type="pil", height=256)
150
+ depth_out = gr.Image(label="Depth", type="pil", height=256)
151
+ normal_out = gr.Image(label="Normal", type="pil", height=256)
152
  with gr.Row():
153
+ run_btn = gr.Button("Estimate Depth + Normal", variant="primary", size="lg")
154
+ status_out = gr.Textbox(label="Status", interactive=False, scale=2)
 
 
 
 
 
155
 
156
  run_btn.click(
157
  fn=generate,
 
161
  api_name="generate",
162
  )
163
 
164
+ gr.Examples(
165
+ examples=["assets/example.jpg"],
166
+ inputs=[input_img],
167
+ outputs=[depth_out, normal_out, status_out],
168
+ fn=generate,
169
+ cache_examples=False,
170
+ label="Examples",
171
+ )
172
+
173
  demo.queue(default_concurrency_limit=1)
174
 
175
  if __name__ == "__main__":
infer/inference.py CHANGED
@@ -29,8 +29,9 @@ EMPTY_PROMPT_LATENT_PATH = REPO_ROOT / "latent" / "no_info.npz"
29
 
30
 
31
  def cudagc():
32
- torch.cuda.empty_cache()
33
- torch.cuda.ipc_collect()
 
34
 
35
 
36
  def load_state_dict(model, ckpt_path, device="cuda", strict=False, assign=True):
 
29
 
30
 
31
  def cudagc():
32
+ if torch.cuda.is_available():
33
+ torch.cuda.empty_cache()
34
+ torch.cuda.ipc_collect()
35
 
36
 
37
  def load_state_dict(model, ckpt_path, device="cuda", strict=False, assign=True):
library/lora_module.py CHANGED
@@ -10,7 +10,10 @@ import math
10
  import os
11
  from contextlib import contextmanager
12
  from typing import Dict, List, Optional, Tuple, Type, Union
13
- from diffusers import AutoencoderKL
 
 
 
14
  import numpy as np
15
  import torch
16
  from torch import Tensor
 
10
  import os
11
  from contextlib import contextmanager
12
  from typing import Dict, List, Optional, Tuple, Type, Union
13
+ try:
14
+ from diffusers import AutoencoderKL
15
+ except ImportError:
16
+ AutoencoderKL = None
17
  import numpy as np
18
  import torch
19
  from torch import Tensor
modules/conditioner.py DELETED
@@ -1,239 +0,0 @@
1
- import torch
2
- from qwen_vl_utils import process_vision_info
3
- from transformers import (
4
- AutoProcessor,
5
- Qwen2VLForConditionalGeneration,
6
- Qwen2_5_VLForConditionalGeneration,
7
- )
8
- from torchvision.transforms import ToPILImage
9
-
10
- to_pil = ToPILImage()
11
-
12
- Qwen25VL_7b_PREFIX = '''You are an expert image analyst specializing in 3D scene understanding. Your goal is to describe the 3D structure and layout of the scene in the image to aid in depth estimation tasks.
13
- - Focus your description on elements crucial for 3D understanding. Incorporate observations about key **objects**, their **spatial relationships** (like positions, relative distances, and relative sizes using real-world orientation), the overall **layout**, potential **camera perspective**, and visible **depth cues** (like occlusion or perspective lines) into a unified description.
14
- - Be concise but informative, and assume it is a real-world image. Only describe what you are very confident about.
15
- User Prompt:'''
16
-
17
- Qwen25VL_7b_PREFIX2 = '''Given a user prompt, generate an "Enhanced prompt" that provides detailed visual descriptions suitable for image generation. Evaluate the level of detail in the user prompt:
18
- - If the prompt is simple, focus on adding specifics about colors, shapes, sizes, textures, and spatial relationships to create vivid and concrete scenes.
19
- - If the prompt is already detailed, refine and enhance the existing details slightly without overcomplicating.\n
20
- Here are examples of how to transform or refine prompts:
21
- - User Prompt: A cat sleeping -> Enhanced: A small, fluffy white cat curled up in a round shape, sleeping peacefully on a warm sunny windowsill, surrounded by pots of blooming red flowers.
22
- - User Prompt: A busy city street -> Enhanced: A bustling city street scene at dusk, featuring glowing street lamps, a diverse crowd of people in colorful clothing, and a double-decker bus passing by towering glass skyscrapers.\n
23
- Please generate only the enhanced description for the prompt below and avoid including any additional commentary or evaluations:
24
- User Prompt:'''
25
-
26
- Qwen25VL_7b_PREFIX3 = '''You are a helpful assistant. Describe the image in detail. User Prompt:'''
27
-
28
- def split_string(s):
29
- # 将中文引号替换为英文引号
30
- s = s.replace("“", '"').replace("”", '"') # use english quotes
31
- result = []
32
- # 标记是否在引号内
33
- in_quotes = False
34
- temp = ""
35
-
36
- # 遍历字符串中的每个字符及其索引
37
- for idx, char in enumerate(s):
38
- # 如果字符是引号且索引大于 155
39
- if char == '"' and idx > 155:
40
- # 将引号添加到临时字符串
41
- temp += char
42
- # 如果不在引号内
43
- if not in_quotes:
44
- # 将临时字符串添加到结果列表
45
- result.append(temp)
46
- # 清空临时字符串
47
- temp = ""
48
-
49
- # 切换引号状态
50
- in_quotes = not in_quotes
51
- continue
52
- # 如果在引号内
53
- if in_quotes:
54
- # 如果字符是空格
55
- if char.isspace():
56
- pass # have space token
57
-
58
- # 将字符用中文引号包裹后添加到结果列表
59
- result.append("“" + char + "”")
60
- else:
61
- # 将字符添加到临时字符串
62
- temp += char
63
-
64
- # 如果临时字符串不为空
65
- if temp:
66
- # 将临时字符串添加到结果列表
67
- result.append(temp)
68
-
69
- return result
70
-
71
-
72
- class Qwen25VL_7b_Embedder(torch.nn.Module):
73
- def __init__(self, model_path, max_length=640, dtype=torch.bfloat16, device="cuda",args=None):
74
- super(Qwen25VL_7b_Embedder, self).__init__()
75
- self.max_length = max_length
76
-
77
- self.model = Qwen2_5_VLForConditionalGeneration.from_pretrained(
78
- model_path,
79
- torch_dtype=dtype,
80
- attn_implementation="flash_attention_2",
81
- ).to(torch.cuda.current_device())
82
-
83
- self.model.requires_grad_(False)
84
- self.processor = AutoProcessor.from_pretrained(
85
- model_path, min_pixels=256 * 28 * 28, max_pixels=324 * 28 * 28
86
- )
87
-
88
- self.prefix = Qwen25VL_7b_PREFIX if (args is not None) and (not args.old_prompt) else Qwen25VL_7b_PREFIX2
89
- self.prefix = Qwen25VL_7b_PREFIX3 if (args is not None) and (args.prompt_type == "rgb" or args.prompt_type == "empty") else self.prefix
90
- self.prefix_len = self.calculate_len_of_prefix()
91
-
92
- @property
93
- def device(self) -> torch.device:
94
- return next(self.parameters()).device
95
-
96
- @property
97
- def dtype(self) -> torch.dtype:
98
- return next(self.parameters()).dtype
99
-
100
- def calculate_len_of_prefix(self):
101
- messages_specific = [{"role": "user", "content": []}]
102
- messages_specific[0]["content"].append({"type": "text", "text": f"{self.prefix}"})
103
-
104
- text_specific = self.processor.apply_chat_template(
105
- messages_specific, tokenize=False, add_generation_prompt=True, add_vision_id=True
106
- )
107
-
108
-
109
- inputs_specific = self.processor(
110
- text=[text_specific],
111
- padding=True,
112
- # return_tensors="pt",
113
- )
114
- len_specific = len(inputs_specific["input_ids"][0]) - 6 # 223 - 6 = self.prefix_len
115
- return len_specific
116
-
117
- def forward(self, caption, ref_images):
118
- text_list = caption
119
- embs = torch.zeros(
120
- len(text_list),
121
- self.max_length,
122
- self.model.config.hidden_size,
123
- dtype=torch.bfloat16,
124
- device=torch.cuda.current_device(),
125
- )
126
- hidden_states = torch.zeros(
127
- len(text_list),
128
- self.max_length,
129
- self.model.config.hidden_size,
130
- dtype=torch.bfloat16,
131
- device=torch.cuda.current_device(),
132
- )
133
- masks = torch.zeros(
134
- len(text_list),
135
- self.max_length,
136
- dtype=torch.long,
137
- device=torch.cuda.current_device(),
138
- )
139
- input_ids_list = []
140
- attention_mask_list = []
141
- emb_list = []
142
-
143
- def split_string(s):
144
- s = s.replace("“", '"').replace("”", '"').replace("'", '''"''') # use english quotes
145
- result = []
146
- in_quotes = False
147
- temp = ""
148
-
149
- for idx,char in enumerate(s):
150
- if char == '"' and idx>155:
151
- temp += char
152
- if not in_quotes:
153
- result.append(temp)
154
- temp = ""
155
-
156
- in_quotes = not in_quotes
157
- continue
158
- if in_quotes:
159
- if char.isspace():
160
- pass # have space token
161
-
162
- result.append("“" + char + "”")
163
- else:
164
- temp += char
165
-
166
- if temp:
167
- result.append(temp)
168
-
169
- return result
170
-
171
-
172
- for idx, (txt, imgs) in enumerate(zip(text_list, ref_images)):
173
-
174
- messages = [{"role": "user", "content": []}]
175
- messages[0]["content"].append({"type": "text", "text": f"{self.prefix}"})
176
- messages[0]["content"].append({"type": "image", "image": to_pil(imgs)})
177
- # 再添加 text
178
- messages[0]["content"].append({"type": "text", "text": f"{txt}"})
179
- # Preparation for inference
180
- text = self.processor.apply_chat_template(
181
- messages, tokenize=False, add_generation_prompt=True, add_vision_id=True
182
- )
183
- image_inputs, video_inputs = process_vision_info(messages)
184
- inputs = self.processor(
185
- text=[text],
186
- images=image_inputs,
187
- padding=True,
188
- return_tensors="pt",
189
- )
190
-
191
- old_inputs_ids = inputs.input_ids
192
- text_split_list = split_string(text)
193
- token_list = []
194
- for text_each in text_split_list:
195
- txt_inputs = self.processor(
196
- text=text_each,
197
- images=None,
198
- videos=None,
199
- padding=True,
200
- return_tensors="pt",
201
- )
202
- token_each = txt_inputs.input_ids
203
- if token_each[0][0] == 2073 and token_each[0][-1] == 854:
204
- token_each = token_each[:, 1:-1]
205
- token_list.append(token_each)
206
- else:
207
- token_list.append(token_each)
208
-
209
- new_txt_ids = torch.cat(token_list, dim=1).to("cuda")
210
-
211
- new_txt_ids = new_txt_ids.to(old_inputs_ids.device)
212
-
213
- idx1 = (old_inputs_ids == 151653).nonzero(as_tuple=True)[1][0]
214
- idx2 = (new_txt_ids == 151653).nonzero(as_tuple=True)[1][0]
215
- inputs.input_ids = (
216
- torch.cat([old_inputs_ids[0, :idx1], new_txt_ids[0, idx2:]], dim=0)
217
- .unsqueeze(0)
218
- .to("cuda")
219
- )
220
- inputs.attention_mask = (inputs.input_ids > 0).long().to("cuda")
221
- outputs = self.model(
222
- input_ids=inputs.input_ids,
223
- attention_mask=inputs.attention_mask,
224
- pixel_values=inputs.pixel_values.to("cuda"),
225
- image_grid_thw=inputs.image_grid_thw.to("cuda"),
226
- output_hidden_states=True,
227
- )
228
-
229
- emb = outputs["hidden_states"][-1]
230
-
231
- embs[idx, : min(self.max_length, emb.shape[1] - self.prefix_len)] = emb[0, self.prefix_len:][:self.max_length] #2,640,3584
232
-
233
- masks[idx, : min(self.max_length, emb.shape[1] - self.prefix_len)] = torch.ones(
234
- (min(self.max_length, emb.shape[1] - self.prefix_len)),
235
- dtype=torch.long,
236
- device=torch.cuda.current_device(),
237
- )
238
-
239
- return embs, masks
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
requirements.txt CHANGED
@@ -7,5 +7,4 @@ safetensors
7
  einops
8
  numpy
9
  tqdm
10
- diffusers[torch]
11
- transformers
 
7
  einops
8
  numpy
9
  tqdm
10
+ matplotlib