Solaria1.0 / pointcloud.py
CronosSa's picture
Update pointcloud.py
f531e56 verified
Raw History Blame Contribute Delete
6.01 kB
import numpy as np
from PIL import Image
def depth_to_pointcloud(
depth,
image=None,
mask=None,
stride=4
):
"""
Converte mapa de profundidade em nuvem de pontos 3D.
"""
# Depth precisa ser uma matriz 2D
depth = np.asarray(
depth,
dtype=np.float32
)
depth = np.squeeze(depth)
if depth.ndim != 2:
raise ValueError(
f"Formato de depth inesperado: {depth.shape}"
)
height, width = depth.shape
# Grid de pixels
y, x = np.mgrid[
0:height:stride,
0:width:stride
]
z = depth[
::stride,
::stride
]
# Pontos válidos
valid = np.isfinite(z)
# ==========================================
# MÁSCARA DO OBJETO
# ==========================================
if mask is not None:
if isinstance(mask, Image.Image):
mask_image = mask
else:
mask_array = np.asarray(mask)
# Se vier RGB/RGBA, converte corretamente
if mask_array.ndim == 3:
mask_array = mask_array[:, :, 0]
mask_image = Image.fromarray(
mask_array.astype(np.uint8)
)
# Sempre transforma em escala de cinza
mask_image = mask_image.convert("L")
# Mesmo tamanho da profundidade
mask_image = mask_image.resize(
(width, height),
Image.Resampling.LANCZOS
)
mask_array = np.asarray(
mask_image
)
print(
"DEBUG máscara:",
mask_array.shape,
flush=True
)
object_mask = (
mask_array[
::stride,
::stride
] > 128
)
print(
"DEBUG object_mask:",
object_mask.shape,
flush=True
)
print(
"DEBUG valid:",
valid.shape,
flush=True
)
valid = (
valid
& object_mask
)
# ==========================================
# VERIFICA PONTOS
# ==========================================
if not np.any(valid):
raise ValueError(
"Nenhum ponto 3D válido foi encontrado. "
"A máscara pode ter removido todo o objeto."
)
# ==========================================
# FILTRA PONTOS
# ==========================================
x = x[valid].astype(
np.float32
)
y = y[valid].astype(
np.float32
)
z = z[valid].astype(
np.float32
)
# ==========================================
# NORMALIZA PROFUNDIDADE
# ==========================================
z_min = np.min(z)
z_max = np.max(z)
if z_max - z_min > 1e-8:
z = (
z - z_min
) / (
z_max - z_min
)
else:
z[:] = 0.5
# ==========================================
# CENTRALIZA X/Y
# ==========================================
x = (
x - width / 2
) / width
y = -(
y - height / 2
) / height
# ==========================================
# ESCALA Z
# ==========================================
z = z * 2.0
# ==========================================
# XYZ
# ==========================================
points = np.stack(
[
x,
y,
z
],
axis=1
)
# ==========================================
# CORES
# ==========================================
colors = None
if image is not None:
image = image.convert("RGB")
image = image.resize(
(width, height)
)
image_array = np.asarray(
image
)
sampled_colors = (
image_array[
::stride,
::stride
]
)
colors = sampled_colors[
valid
]
return points, colors
def save_pointcloud_ply(
filename,
points,
colors=None
):
"""
Salva a nuvem de pontos no formato PLY.
"""
points = np.asarray(
points,
dtype=np.float32
)
if (
points.ndim != 2
or points.shape[1] != 3
):
raise ValueError(
"points precisa ter formato (N, 3)."
)
if colors is not None:
colors = np.asarray(colors)
if len(colors) != len(points):
raise ValueError(
"A quantidade de cores precisa "
"ser igual à quantidade de pontos."
)
with open(
filename,
"w",
encoding="utf-8"
) as file:
file.write("ply\n")
file.write("format ascii 1.0\n")
file.write(
f"element vertex {len(points)}\n"
)
file.write("property float x\n")
file.write("property float y\n")
file.write("property float z\n")
if colors is not None:
file.write(
"property uchar red\n"
)
file.write(
"property uchar green\n"
)
file.write(
"property uchar blue\n"
)
file.write("end_header\n")
if colors is not None:
for point, color in zip(
points,
colors
):
x, y, z = point
r, g, b = color
file.write(
f"{x:.6f} "
f"{y:.6f} "
f"{z:.6f} "
f"{int(r)} "
f"{int(g)} "
f"{int(b)}\n"
)
else:
for point in points:
x, y, z = point
file.write(
f"{x:.6f} "
f"{y:.6f} "
f"{z:.6f}\n"
)