refactor
Browse files- .gitignore +2 -0
- app.py +5 -15
- llm_service.py +2 -6
- mv_utils_zs.py +60 -93
- test.py +73 -0
.gitignore
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.venv/*
|
| 2 |
+
.env/*
|
app.py
CHANGED
|
@@ -1,10 +1,10 @@
|
|
| 1 |
-
import asyncio
|
| 2 |
import os
|
| 3 |
import platform
|
| 4 |
-
import
|
| 5 |
import subprocess # used to connect to FreeCAD via terminal sub process
|
| 6 |
-
import sys
|
| 7 |
import tempfile
|
|
|
|
|
|
|
| 8 |
from typing import Any, Dict, List, Tuple
|
| 9 |
|
| 10 |
import gradio as gr # demo with gradio
|
|
@@ -52,6 +52,8 @@ def convert_step_to_obj_with_freecad(step_path, obj_path):
|
|
| 52 |
freecad_executable = "/usr/bin/freecadcmd" # freecadcmd
|
| 53 |
elif os_name == "Darwin":
|
| 54 |
freecad_executable = "/Applications/FreeCAD.app/Contents/MacOS/FreeCAD"
|
|
|
|
|
|
|
| 55 |
# Python script to be executed by FreeCAD
|
| 56 |
_, ext = os.path.splitext(step_path)
|
| 57 |
ext = ext.lower()
|
|
@@ -229,10 +231,6 @@ def query_3D_object(query: str, embedding_dict: dict, top_k: int = 4):
|
|
| 229 |
# Metadata Extraction
|
| 230 |
####################################################################################################################
|
| 231 |
|
| 232 |
-
import os
|
| 233 |
-
import xml.etree.ElementTree as ET
|
| 234 |
-
import zipfile
|
| 235 |
-
|
| 236 |
|
| 237 |
def extract_header_from_3dxml(file_path):
|
| 238 |
header_info = {}
|
|
@@ -267,8 +265,6 @@ def extract_header_from_3dxml(file_path):
|
|
| 267 |
|
| 268 |
#######################################################################################################################
|
| 269 |
|
| 270 |
-
import re
|
| 271 |
-
|
| 272 |
|
| 273 |
def extract_step_metadata(file_path):
|
| 274 |
metadata = {}
|
|
@@ -313,16 +309,10 @@ def extract_step_metadata(file_path):
|
|
| 313 |
return metadata
|
| 314 |
|
| 315 |
|
| 316 |
-
#######################################################################################################################
|
| 317 |
-
|
| 318 |
-
|
| 319 |
def dict_to_markdown(metadata: dict) -> str:
|
| 320 |
return "\n".join(f"{key}: {value}" for key, value in metadata.items())
|
| 321 |
|
| 322 |
|
| 323 |
-
#######################################################################################################################
|
| 324 |
-
|
| 325 |
-
|
| 326 |
# Dummy parser - Replace with real parser
|
| 327 |
def parse_3d_file(original_filepath: str):
|
| 328 |
if original_filepath is None:
|
|
|
|
|
|
|
| 1 |
import os
|
| 2 |
import platform
|
| 3 |
+
import re
|
| 4 |
import subprocess # used to connect to FreeCAD via terminal sub process
|
|
|
|
| 5 |
import tempfile
|
| 6 |
+
import xml.etree.ElementTree as ET
|
| 7 |
+
import zipfile
|
| 8 |
from typing import Any, Dict, List, Tuple
|
| 9 |
|
| 10 |
import gradio as gr # demo with gradio
|
|
|
|
| 52 |
freecad_executable = "/usr/bin/freecadcmd" # freecadcmd
|
| 53 |
elif os_name == "Darwin":
|
| 54 |
freecad_executable = "/Applications/FreeCAD.app/Contents/MacOS/FreeCAD"
|
| 55 |
+
else:
|
| 56 |
+
raise Exception("Unsupported OS for FreeCAD execution: " + os_name)
|
| 57 |
# Python script to be executed by FreeCAD
|
| 58 |
_, ext = os.path.splitext(step_path)
|
| 59 |
ext = ext.lower()
|
|
|
|
| 231 |
# Metadata Extraction
|
| 232 |
####################################################################################################################
|
| 233 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 234 |
|
| 235 |
def extract_header_from_3dxml(file_path):
|
| 236 |
header_info = {}
|
|
|
|
| 265 |
|
| 266 |
#######################################################################################################################
|
| 267 |
|
|
|
|
|
|
|
| 268 |
|
| 269 |
def extract_step_metadata(file_path):
|
| 270 |
metadata = {}
|
|
|
|
| 309 |
return metadata
|
| 310 |
|
| 311 |
|
|
|
|
|
|
|
|
|
|
| 312 |
def dict_to_markdown(metadata: dict) -> str:
|
| 313 |
return "\n".join(f"{key}: {value}" for key, value in metadata.items())
|
| 314 |
|
| 315 |
|
|
|
|
|
|
|
|
|
|
| 316 |
# Dummy parser - Replace with real parser
|
| 317 |
def parse_3d_file(original_filepath: str):
|
| 318 |
if original_filepath is None:
|
llm_service.py
CHANGED
|
@@ -1,16 +1,12 @@
|
|
| 1 |
# %%writefile llm_service.py
|
| 2 |
import asyncio
|
| 3 |
-
import base64
|
| 4 |
-
import io
|
| 5 |
import os
|
| 6 |
-
from
|
| 7 |
-
from typing import List, Tuple, Union, cast
|
| 8 |
|
| 9 |
-
import cv2
|
| 10 |
import numpy as np
|
|
|
|
| 11 |
from openai import AsyncOpenAI
|
| 12 |
from PIL import Image
|
| 13 |
-
from loguru import logger
|
| 14 |
|
| 15 |
from encode_image import encode_image
|
| 16 |
from string_utils import StringUtils
|
|
|
|
| 1 |
# %%writefile llm_service.py
|
| 2 |
import asyncio
|
|
|
|
|
|
|
| 3 |
import os
|
| 4 |
+
from typing import Union
|
|
|
|
| 5 |
|
|
|
|
| 6 |
import numpy as np
|
| 7 |
+
from loguru import logger
|
| 8 |
from openai import AsyncOpenAI
|
| 9 |
from PIL import Image
|
|
|
|
| 10 |
|
| 11 |
from encode_image import encode_image
|
| 12 |
from string_utils import StringUtils
|
mv_utils_zs.py
CHANGED
|
@@ -71,7 +71,7 @@ class Grid2Image(nn.Module):
|
|
| 71 |
zsigma=params["convsigmaz"],
|
| 72 |
)
|
| 73 |
self.conv.weight.data = torch.Tensor(kn3d).repeat(1, 1, 1, 1, 1)
|
| 74 |
-
self.conv.bias.data.fill_(0)
|
| 75 |
|
| 76 |
def forward(self, x):
|
| 77 |
x = self.maxpool(x.unsqueeze(1))
|
|
@@ -138,50 +138,48 @@ def points_to_2d_grid(
|
|
| 138 |
points, grid_h=params["grid_height"], grid_w=params["grid_width"]
|
| 139 |
):
|
| 140 |
"""
|
| 141 |
-
|
| 142 |
-
|
| 143 |
|
| 144 |
Args:
|
| 145 |
-
points (torch.tensor): Tensor
|
| 146 |
-
(B: batch size, P:
|
| 147 |
-
grid_h (int):
|
| 148 |
-
grid_w (int):
|
| 149 |
|
| 150 |
Returns:
|
| 151 |
-
grid (torch.tensor):
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
"""
|
| 156 |
batch, pnum, _ = points.shape
|
| 157 |
device = points.device
|
| 158 |
|
| 159 |
-
# ---
|
| 160 |
-
#
|
| 161 |
pmax_xy = points[:, :, :2].max(dim=1)[0]
|
| 162 |
pmin_xy = points[:, :, :2].min(dim=1)[0]
|
| 163 |
|
| 164 |
-
#
|
| 165 |
pcent_xy = (pmax_xy + pmin_xy) / 2
|
| 166 |
-
pcent_xy = pcent_xy[:, None, :] #
|
| 167 |
|
| 168 |
-
#
|
| 169 |
prange_xy = (pmax_xy - pmin_xy).max(dim=-1)[0][:, None, None] # [B, 1, 1]
|
| 170 |
|
| 171 |
-
#
|
| 172 |
epsilon = 1e-8
|
| 173 |
-
#
|
| 174 |
-
# (points[:, :, :2] - pcent_xy) -> [B, P, 2]
|
| 175 |
-
# prange_xy -> [B, 1, 1]
|
| 176 |
points_normalized_xy = (points[:, :, :2] - pcent_xy) / (prange_xy + epsilon) * 2.0
|
| 177 |
|
| 178 |
-
#
|
| 179 |
points_normalized_xy = points_normalized_xy * params["obj_ratio"]
|
| 180 |
|
| 181 |
-
# ---
|
| 182 |
-
#
|
| 183 |
-
#
|
| 184 |
-
#
|
| 185 |
_x = (
|
| 186 |
(points_normalized_xy[:, :, 0] + params["obj_ratio"])
|
| 187 |
/ (2 * params["obj_ratio"])
|
|
@@ -193,32 +191,32 @@ def points_to_2d_grid(
|
|
| 193 |
* grid_h
|
| 194 |
)
|
| 195 |
|
| 196 |
-
#
|
| 197 |
_x = torch.floor(_x).long()
|
| 198 |
_y = torch.floor(_y).long()
|
| 199 |
|
| 200 |
-
# ---
|
| 201 |
-
# Clip _x
|
| 202 |
-
# Clip _y
|
| 203 |
_x = torch.clip(_x, 0, grid_w - 1)
|
| 204 |
_y = torch.clip(_y, 0, grid_h - 1)
|
| 205 |
|
| 206 |
-
# ---
|
| 207 |
-
#
|
| 208 |
grid = torch.full(
|
| 209 |
(batch, grid_h, grid_w), params["bg_clr"], dtype=torch.float32, device=device
|
| 210 |
)
|
| 211 |
|
| 212 |
-
#
|
| 213 |
batch_indices = torch.arange(batch, device=device).view(-1, 1).repeat(1, pnum)
|
| 214 |
|
| 215 |
-
# Flatten
|
| 216 |
batch_idx_flat = batch_indices.view(-1)
|
| 217 |
y_idx_flat = _y.view(-1)
|
| 218 |
x_idx_flat = _x.view(-1)
|
| 219 |
|
| 220 |
-
#
|
| 221 |
-
#
|
| 222 |
grid[batch_idx_flat, y_idx_flat, x_idx_flat] = 1.0
|
| 223 |
|
| 224 |
return grid
|
|
@@ -262,55 +260,27 @@ def points2grid(points, resolution=params["resolution"], depth=params["depth"]):
|
|
| 262 |
* params["bg_clr"]
|
| 263 |
)
|
| 264 |
|
| 265 |
-
# # *** THAY ĐỔI CHÍNH Ở ĐÂY ***
|
| 266 |
-
# # Tạo tensor nguồn (src) chứa giá trị 1.0 cho mỗi điểm
|
| 267 |
-
# # Kích thước phải phù hợp với coordinates khi flatten: [B * pnum]
|
| 268 |
-
# values_to_scatter = torch.ones(batch * pnum, dtype=torch.float32, device=points.device)
|
| 269 |
-
|
| 270 |
-
# # Scatter giá trị 1.0 vào grid tại các vị trí `coordinates`
|
| 271 |
-
# # Sử dụng reduce="max". Nếu ô có ít nhất một điểm, max(1.0, bg_clr) sẽ là 1.0 (nếu bg_clr <= 1)
|
| 272 |
-
# # Nếu muốn chắc chắn là 1 bất kể bg_clr, có thể dùng reduce khác hoặc xử lý sau scatter.
|
| 273 |
-
# # Lựa chọn an toàn hơn nếu bg_clr có thể > 1 là khởi tạo grid bằng 0 và dùng reduce='max'/'mean'
|
| 274 |
-
# # Hoặc khởi tạo bằng bg_clr và xử lý sau scatter.
|
| 275 |
-
# # Giả định bg_clr = 0.0 là phổ biến nhất cho occupancy grid.
|
| 276 |
-
|
| 277 |
-
# grid = scatter(
|
| 278 |
-
# values_to_scatter,
|
| 279 |
-
# coordinates.view(-1).long(), # Flatten coordinates thành [B*pnum]
|
| 280 |
-
# dim=0, # Scatter trên chiều 0 của grid đã flatten [B*D*R*R]
|
| 281 |
-
# # Cần chỉ số batch tương ứng nếu grid chưa flatten theo batch
|
| 282 |
-
# out=grid.view(-1), # Flatten grid thành [B*D*R*R] để scatter trên dim 0
|
| 283 |
-
# reduce="max",
|
| 284 |
-
# ) # Nếu có điểm -> giá trị ô là 1, nếu không là bg_clr
|
| 285 |
-
# # **********************************
|
| 286 |
-
|
| 287 |
grid = scatter(_z, coordinates.long(), dim=1, out=grid, reduce="max")
|
| 288 |
grid = grid.reshape((batch, depth, resolution, resolution)).permute((0, 1, 3, 2))
|
| 289 |
|
| 290 |
return grid
|
| 291 |
|
| 292 |
|
| 293 |
-
# Giả sử bạn có thư viện scatter, ví dụ: from torch_scatter import scatter
|
| 294 |
-
# Hoặc hàm scatter tương đương
|
| 295 |
-
# import torch # Đảm bảo đã import torch
|
| 296 |
-
# from torch_scatter import scatter # Ví dụ
|
| 297 |
-
|
| 298 |
-
|
| 299 |
def points_to_occupancy_grid(
|
| 300 |
points, resolution=params["resolution"], depth=params["depth"]
|
| 301 |
):
|
| 302 |
-
"""Quantize each point cloud
|
| 303 |
|
| 304 |
batch, pnum, _ = points.shape
|
| 305 |
-
device = points.device #
|
| 306 |
|
| 307 |
-
# ---
|
| 308 |
pmax, pmin = points.max(dim=1)[0], points.min(dim=1)[0]
|
| 309 |
pcent = (pmax + pmin) / 2
|
| 310 |
pcent = pcent[:, None, :]
|
| 311 |
prange = (pmax - pmin).max(dim=-1)[0][
|
| 312 |
:, None, None
|
| 313 |
-
] + 1e-8 #
|
| 314 |
points_norm = (points - pcent) / prange * 2.0
|
| 315 |
points_norm[:, :, :2] = points_norm[:, :, :2] * params["obj_ratio"]
|
| 316 |
|
|
@@ -325,35 +295,33 @@ def points_to_occupancy_grid(
|
|
| 325 |
|
| 326 |
_x = torch.clip(_x, 1, resolution - 2)
|
| 327 |
_y = torch.clip(_y, 1, resolution - 2)
|
| 328 |
-
# z_int
|
| 329 |
z_int = torch.clip(z_int, 1, depth - 2)
|
| 330 |
|
| 331 |
-
# ---
|
| 332 |
coordinates = z_int * resolution * resolution + _y * resolution + _x
|
| 333 |
-
coordinates = coordinates.long() #
|
| 334 |
|
| 335 |
-
# ---
|
| 336 |
-
#
|
| 337 |
-
#
|
| 338 |
-
bg_clr_value = params.get("bg_clr", 0.0) #
|
| 339 |
grid = torch.full(
|
| 340 |
(batch, depth * resolution * resolution),
|
| 341 |
bg_clr_value,
|
| 342 |
-
dtype=torch.float32, #
|
| 343 |
device=device,
|
| 344 |
)
|
| 345 |
|
| 346 |
-
#
|
| 347 |
-
#
|
| 348 |
-
# Kích thước phải phù hợp với coordinates khi flatten: [B * pnum]
|
| 349 |
values_to_scatter = torch.ones(batch * pnum, dtype=torch.float32, device=device)
|
| 350 |
|
| 351 |
-
# Scatter
|
| 352 |
-
#
|
| 353 |
-
#
|
| 354 |
-
#
|
| 355 |
-
#
|
| 356 |
-
# Giả định bg_clr = 0.0 là phổ biến nhất cho occupancy grid.
|
| 357 |
if bg_clr_value != 0.0:
|
| 358 |
print(
|
| 359 |
"Warning: bg_clr is not 0.0, occupancy grid might not be strictly binary 0/1 with reduce='max'. Consider initializing grid with 0."
|
|
@@ -361,17 +329,16 @@ def points_to_occupancy_grid(
|
|
| 361 |
|
| 362 |
grid = scatter(
|
| 363 |
values_to_scatter,
|
| 364 |
-
coordinates.view(-1), # Flatten coordinates
|
| 365 |
-
dim=0, # Scatter
|
| 366 |
-
#
|
| 367 |
-
out=grid.view(-1), # Flatten grid thành [B*D*R*R] để scatter trên dim 0
|
| 368 |
reduce="max",
|
| 369 |
-
) #
|
| 370 |
|
| 371 |
-
# --- Reshape
|
| 372 |
-
# Reshape
|
| 373 |
-
#
|
| 374 |
-
grid = grid.view(batch, depth, resolution, resolution) # Reshape
|
| 375 |
grid = grid.permute((0, 1, 3, 2))
|
| 376 |
|
| 377 |
return grid
|
|
|
|
| 71 |
zsigma=params["convsigmaz"],
|
| 72 |
)
|
| 73 |
self.conv.weight.data = torch.Tensor(kn3d).repeat(1, 1, 1, 1, 1)
|
| 74 |
+
self.conv.bias.data.fill_(0) # type: ignore
|
| 75 |
|
| 76 |
def forward(self, x):
|
| 77 |
x = self.maxpool(x.unsqueeze(1))
|
|
|
|
| 138 |
points, grid_h=params["grid_height"], grid_w=params["grid_width"]
|
| 139 |
):
|
| 140 |
"""
|
| 141 |
+
Converts a point cloud into a 2D grid based on X, Y coordinates.
|
| 142 |
+
Points are projected onto a plane and quantized into grid cells.
|
| 143 |
|
| 144 |
Args:
|
| 145 |
+
points (torch.tensor): Tensor containing points, shape [B, P, 3]
|
| 146 |
+
(B: batch size, P: number of points, 3: x, y, z coordinates)
|
| 147 |
+
grid_h (int): Height of the output 2D grid.
|
| 148 |
+
grid_w (int): Width of the output 2D grid.
|
| 149 |
|
| 150 |
Returns:
|
| 151 |
+
grid (torch.tensor): 2D grid representing the occupancy of points,
|
| 152 |
+
shape [B, grid_h, grid_w].
|
| 153 |
+
Value 1.0 at cell (y, x) if at least one point falls into it,
|
| 154 |
+
otherwise the background value (params["bg_clr"]).
|
| 155 |
"""
|
| 156 |
batch, pnum, _ = points.shape
|
| 157 |
device = points.device
|
| 158 |
|
| 159 |
+
# --- Step 1: Normalize point coordinates ---
|
| 160 |
+
# Find min/max for each point cloud in the batch (considering only X, Y for better 2D normalization)
|
| 161 |
pmax_xy = points[:, :, :2].max(dim=1)[0]
|
| 162 |
pmin_xy = points[:, :, :2].min(dim=1)[0]
|
| 163 |
|
| 164 |
+
# Compute the center and range based on X, Y
|
| 165 |
pcent_xy = (pmax_xy + pmin_xy) / 2
|
| 166 |
+
pcent_xy = pcent_xy[:, None, :] # Add P dimension for broadcasting [B, 1, 2]
|
| 167 |
|
| 168 |
+
# Use the larger range between X and Y to maintain aspect ratio
|
| 169 |
prange_xy = (pmax_xy - pmin_xy).max(dim=-1)[0][:, None, None] # [B, 1, 1]
|
| 170 |
|
| 171 |
+
# Add a small epsilon to avoid division by zero if all points overlap
|
| 172 |
epsilon = 1e-8
|
| 173 |
+
# Normalize X, Y into the range [-1, 1] based on the X, Y range
|
|
|
|
|
|
|
| 174 |
points_normalized_xy = (points[:, :, :2] - pcent_xy) / (prange_xy + epsilon) * 2.0
|
| 175 |
|
| 176 |
+
# Adjust the scale according to obj_ratio (if needed)
|
| 177 |
points_normalized_xy = points_normalized_xy * params["obj_ratio"]
|
| 178 |
|
| 179 |
+
# --- Step 2: Map normalized coordinates to 2D grid indices ---
|
| 180 |
+
# Map X from the range [-obj_ratio, obj_ratio] -> [0, grid_w]
|
| 181 |
+
# Map Y from the range [-obj_ratio, obj_ratio] -> [0, grid_h]
|
| 182 |
+
# General formula: (normalized_coord + scale) / (2 * scale) * grid_dim
|
| 183 |
_x = (
|
| 184 |
(points_normalized_xy[:, :, 0] + params["obj_ratio"])
|
| 185 |
/ (2 * params["obj_ratio"])
|
|
|
|
| 191 |
* grid_h
|
| 192 |
)
|
| 193 |
|
| 194 |
+
# Round down to determine the grid cell indices
|
| 195 |
_x = torch.floor(_x).long()
|
| 196 |
_y = torch.floor(_y).long()
|
| 197 |
|
| 198 |
+
# --- Step 3: Clamp indices to valid grid range ---
|
| 199 |
+
# Clip _x to [0, grid_w - 1]
|
| 200 |
+
# Clip _y to [0, grid_h - 1]
|
| 201 |
_x = torch.clip(_x, 0, grid_w - 1)
|
| 202 |
_y = torch.clip(_y, 0, grid_h - 1)
|
| 203 |
|
| 204 |
+
# --- Step 4: Create a 2D grid and mark occupied cells ---
|
| 205 |
+
# Initialize the 2D grid with the background value
|
| 206 |
grid = torch.full(
|
| 207 |
(batch, grid_h, grid_w), params["bg_clr"], dtype=torch.float32, device=device
|
| 208 |
)
|
| 209 |
|
| 210 |
+
# Create batch indices corresponding to each point
|
| 211 |
batch_indices = torch.arange(batch, device=device).view(-1, 1).repeat(1, pnum)
|
| 212 |
|
| 213 |
+
# Flatten indices for easier assignment
|
| 214 |
batch_idx_flat = batch_indices.view(-1)
|
| 215 |
y_idx_flat = _y.view(-1)
|
| 216 |
x_idx_flat = _x.view(-1)
|
| 217 |
|
| 218 |
+
# Assign a value of 1.0 to grid cells (y, x) corresponding to point positions
|
| 219 |
+
# If multiple points fall into the same cell, the cell still has a value of 1.0
|
| 220 |
grid[batch_idx_flat, y_idx_flat, x_idx_flat] = 1.0
|
| 221 |
|
| 222 |
return grid
|
|
|
|
| 260 |
* params["bg_clr"]
|
| 261 |
)
|
| 262 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 263 |
grid = scatter(_z, coordinates.long(), dim=1, out=grid, reduce="max")
|
| 264 |
grid = grid.reshape((batch, depth, resolution, resolution)).permute((0, 1, 3, 2))
|
| 265 |
|
| 266 |
return grid
|
| 267 |
|
| 268 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 269 |
def points_to_occupancy_grid(
|
| 270 |
points, resolution=params["resolution"], depth=params["depth"]
|
| 271 |
):
|
| 272 |
+
"""Quantize each point cloud into a 3D occupancy grid."""
|
| 273 |
|
| 274 |
batch, pnum, _ = points.shape
|
| 275 |
+
device = points.device # Get device to create new tensors
|
| 276 |
|
| 277 |
+
# --- Normalization and coordinate mapping remain unchanged ---
|
| 278 |
pmax, pmin = points.max(dim=1)[0], points.min(dim=1)[0]
|
| 279 |
pcent = (pmax + pmin) / 2
|
| 280 |
pcent = pcent[:, None, :]
|
| 281 |
prange = (pmax - pmin).max(dim=-1)[0][
|
| 282 |
:, None, None
|
| 283 |
+
] + 1e-8 # Add epsilon to avoid division by zero
|
| 284 |
points_norm = (points - pcent) / prange * 2.0
|
| 285 |
points_norm[:, :, :2] = points_norm[:, :, :2] * params["obj_ratio"]
|
| 286 |
|
|
|
|
| 295 |
|
| 296 |
_x = torch.clip(_x, 1, resolution - 2)
|
| 297 |
_y = torch.clip(_y, 1, resolution - 2)
|
| 298 |
+
# z_int should also be clipped if used as coordinate indices
|
| 299 |
z_int = torch.clip(z_int, 1, depth - 2)
|
| 300 |
|
| 301 |
+
# --- Compute flattened coordinates ---
|
| 302 |
coordinates = z_int * resolution * resolution + _y * resolution + _x
|
| 303 |
+
coordinates = coordinates.long() # Convert to Long
|
| 304 |
|
| 305 |
+
# --- Create Grid and Scatter ---
|
| 306 |
+
# Initialize the grid with the background value (e.g., 0)
|
| 307 |
+
# Use torch.zeros instead of torch.ones and multiply by bg_clr
|
| 308 |
+
bg_clr_value = params.get("bg_clr", 0.0) # Get bg_clr, default is 0
|
| 309 |
grid = torch.full(
|
| 310 |
(batch, depth * resolution * resolution),
|
| 311 |
bg_clr_value,
|
| 312 |
+
dtype=torch.float32, # Or appropriate dtype
|
| 313 |
device=device,
|
| 314 |
)
|
| 315 |
|
| 316 |
+
# Create a source tensor (src) containing a value of 1.0 for each point
|
| 317 |
+
# The size must match the flattened coordinates: [B * pnum]
|
|
|
|
| 318 |
values_to_scatter = torch.ones(batch * pnum, dtype=torch.float32, device=device)
|
| 319 |
|
| 320 |
+
# Scatter the value 1.0 into the grid at the positions `coordinates`
|
| 321 |
+
# Use reduce="max". If a cell has at least one point, max(1.0, bg_clr) will be 1.0 (if bg_clr <= 1)
|
| 322 |
+
# To ensure the value is always 1 regardless of bg_clr, use a different reduce or post-process after scatter.
|
| 323 |
+
# A safer choice if bg_clr can be > 1 is to initialize the grid with 0 and use reduce='max'/'mean'
|
| 324 |
+
# Or initialize with bg_clr and process after scatter.
|
|
|
|
| 325 |
if bg_clr_value != 0.0:
|
| 326 |
print(
|
| 327 |
"Warning: bg_clr is not 0.0, occupancy grid might not be strictly binary 0/1 with reduce='max'. Consider initializing grid with 0."
|
|
|
|
| 329 |
|
| 330 |
grid = scatter(
|
| 331 |
values_to_scatter,
|
| 332 |
+
coordinates.view(-1), # Flatten coordinates to [B*pnum]
|
| 333 |
+
dim=0, # Scatter along dimension 0 of the flattened grid [B*D*R*R]
|
| 334 |
+
out=grid.view(-1), # Flatten grid to [B*D*R*R] for scatter along dim 0
|
|
|
|
| 335 |
reduce="max",
|
| 336 |
+
) # If a point exists -> cell value is 1, otherwise bg_clr
|
| 337 |
|
| 338 |
+
# --- Reshape and Permute remain unchanged ---
|
| 339 |
+
# Reshape the grid back to the correct 3D + batch size
|
| 340 |
+
# Note: scatter into a flattened grid requires careful reshaping
|
| 341 |
+
grid = grid.view(batch, depth, resolution, resolution) # Reshape back
|
| 342 |
grid = grid.permute((0, 1, 3, 2))
|
| 343 |
|
| 344 |
return grid
|
test.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
+
from loguru import logger
|
| 5 |
+
|
| 6 |
+
from app import convert_to_obj, embedding_3d_object, parse_3d_file
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class ModuleTest:
|
| 10 |
+
@staticmethod
|
| 11 |
+
async def test_process_file(file_path: str, debug_mode: bool = False) -> dict:
|
| 12 |
+
report = {}
|
| 13 |
+
try:
|
| 14 |
+
obj_path = convert_to_obj(file_path)
|
| 15 |
+
report["CONV_TO_OBJ"] = "PASSED"
|
| 16 |
+
if debug_mode:
|
| 17 |
+
logger.info(f"Obj path: {obj_path}")
|
| 18 |
+
except Exception:
|
| 19 |
+
report["CONV_TO_OBJ"] = "FAILED"
|
| 20 |
+
|
| 21 |
+
if report["CONV_TO_OBJ"] == "PASSED":
|
| 22 |
+
obj_path = locals().get("obj_path", None)
|
| 23 |
+
assert obj_path is not None, "Conversion to OBJ failed"
|
| 24 |
+
assert os.path.exists(obj_path), "Converted OBJ file does not exist"
|
| 25 |
+
|
| 26 |
+
try:
|
| 27 |
+
embeddings = await embedding_3d_object(obj_path)
|
| 28 |
+
report["EMBEDDING_3D_OBJ"] = "PASSED"
|
| 29 |
+
if debug_mode:
|
| 30 |
+
logger.info(f"Description: {embeddings['description']}")
|
| 31 |
+
except Exception:
|
| 32 |
+
report["EMBEDDING_3D_OBJ"] = "FAILED"
|
| 33 |
+
else:
|
| 34 |
+
report["EMBEDDING_3D_OBJ"] = "FAILED"
|
| 35 |
+
|
| 36 |
+
try:
|
| 37 |
+
metadata = parse_3d_file(file_path)
|
| 38 |
+
report["PARSE_METADATA"] = "PASSED"
|
| 39 |
+
if debug_mode:
|
| 40 |
+
logger.info(f"Parsed metadata: {metadata}")
|
| 41 |
+
except Exception:
|
| 42 |
+
report["PARSE_METADATA"] = "FAILED"
|
| 43 |
+
return report
|
| 44 |
+
|
| 45 |
+
async def test(self, file_paths: list[str], debug_mode: bool = False) -> None:
|
| 46 |
+
for file_path in file_paths:
|
| 47 |
+
basename = os.path.basename(file_path)
|
| 48 |
+
response = await self.test_process_file(
|
| 49 |
+
file_path=file_path, debug_mode=debug_mode
|
| 50 |
+
)
|
| 51 |
+
for key, value in response.items():
|
| 52 |
+
if value == "FAILED":
|
| 53 |
+
logger.error(f"Processed file `{basename}` failed {key}")
|
| 54 |
+
else:
|
| 55 |
+
logger.info(f"Processed file `{basename}` successfully {key}!")
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
if __name__ == "__main__":
|
| 59 |
+
asyncio.run(
|
| 60 |
+
ModuleTest().test(
|
| 61 |
+
file_paths=[
|
| 62 |
+
# "/Users/tridoan/Spartan/Datum/service-ai/poc/resources/notebooks/3d_files/c5-corvette-knuckle-1.snapshot.1/C5 Knuckle Mesh.stl", # ok
|
| 63 |
+
# "/Users/tridoan/Spartan/Datum/service-ai/poc/resources/notebooks/3d_files/c5-corvette-knuckle-1.snapshot.1/C5 Knuckle 3mf.3mf", # ok
|
| 64 |
+
# "/Users/tridoan/Spartan/Datum/service-ai/poc/resources/notebooks/3d_files/nema-17-stepper-motors-coaxial-60-48-39-23mm-1.snapshot.3/NEMA 17 Stepper Motor 23mm-NEMA 17 Stepper Motor 23mm.obj", # ok
|
| 65 |
+
# "/Users/tridoan/Spartan/Datum/service-ai/poc/resources/notebooks/3d_files/nema-17-stepper-motors-coaxial-60-48-39-23mm-1.snapshot.3/NEMA 17 Stepper Motor 48mm-NEMA 17 Stepper Motor 48mm.step", # ok
|
| 66 |
+
# "/Users/tridoan/Spartan/Datum/service-ai/poc/resources/notebooks/3d_files/engrenagens-5.snapshot.6/Engre_con_Z16_mod_1_5.FCStd", # ok
|
| 67 |
+
# "/Users/tridoan/Spartan/Datum/service-ai/poc/resources/notebooks/3d_files/radial engine.3dxml",
|
| 68 |
+
# "/Users/tridoan/Spartan/Datum/service-ai/poc/resources/notebooks/3d_files/ARBOR GEAR.dwg",
|
| 69 |
+
# "/Users/tridoan/Spartan/Datum/service-ai/poc/resources/notebooks/3d_files/electrical-switch-1.snapshot.3/Electrical switch.IGS"
|
| 70 |
+
],
|
| 71 |
+
debug_mode=True,
|
| 72 |
+
)
|
| 73 |
+
)
|