mirror of
https://github.com/ConsistentlyInconsistentYT/Pixeltovoxelprojector.git
synced 2025-10-12 20:02:06 +00:00
Create voxelmotionviewer.py
This commit is contained in:
parent
3c8421eb85
commit
42dd61d195
1 changed files with 259 additions and 0 deletions
259
voxelmotionviewer.py
Normal file
259
voxelmotionviewer.py
Normal file
|
@ -0,0 +1,259 @@
|
|||
"""
|
||||
pyvista_interactive_view_with_rotation_history.py
|
||||
|
||||
Requirements:
|
||||
pip install pyvista numpy
|
||||
|
||||
Usage:
|
||||
python pyvista_interactive_view_with_rotation_history.py
|
||||
|
||||
Description:
|
||||
1) Loads voxel_grid.bin (written by your C++ code).
|
||||
2) Interprets the 3D array shape as (Z, Y, X).
|
||||
3) Extracts top percentile of brightness.
|
||||
4) Applies an additional Euler rotation to the entire cloud (user-defined).
|
||||
5) Displays them interactively in a PyVista window,
|
||||
so you can orbit, zoom, and pan with the mouse.
|
||||
6) On closing the window, saves a 1920×1080 screenshot named 'voxel_####.png'
|
||||
in a 'screenshots/' folder, so you can keep a history of runs.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import math
|
||||
import numpy as np
|
||||
import pyvista as pv
|
||||
|
||||
|
||||
def load_voxel_grid(filename):
|
||||
"""
|
||||
Reads a voxel grid from a binary file with the following layout:
|
||||
1) int32: N (size of the NxNxN grid)
|
||||
2) float32: voxel_size
|
||||
3) N*N*N float32: the voxel data in row-major order
|
||||
Returns:
|
||||
voxel_grid (N x N x N),
|
||||
voxel_size
|
||||
"""
|
||||
with open(filename, "rb") as f:
|
||||
# read N
|
||||
raw = f.read(4)
|
||||
N = np.frombuffer(raw, dtype=np.int32)[0]
|
||||
|
||||
# read voxel_size
|
||||
raw = f.read(4)
|
||||
voxel_size = np.frombuffer(raw, dtype=np.float32)[0]
|
||||
|
||||
# read the voxel data
|
||||
count = N*N*N
|
||||
raw = f.read(count*4)
|
||||
data = np.frombuffer(raw, dtype=np.float32)
|
||||
voxel_grid = data.reshape((N, N, N))
|
||||
|
||||
return voxel_grid, voxel_size
|
||||
|
||||
|
||||
def extract_top_percentile_z_up(voxel_grid, voxel_size, grid_center,
|
||||
percentile=99.5, use_hard_thresh=False, hard_thresh=700):
|
||||
"""
|
||||
Extract the top 'percentile' bright voxels (or above 'hard_thresh').
|
||||
We interpret the array shape as (Z, Y, X).
|
||||
|
||||
index: voxel[z, y, x]
|
||||
|
||||
We'll produce an Nx3 array of points in (x, y, z).
|
||||
Then we'll produce intensities as a separate array.
|
||||
"""
|
||||
N = voxel_grid.shape[0] # assume shape is (N,N,N)
|
||||
half_side = (N * voxel_size) * 0.5
|
||||
grid_min = grid_center - half_side
|
||||
|
||||
# Flatten to find threshold
|
||||
flat_vals = voxel_grid.ravel()
|
||||
if use_hard_thresh:
|
||||
thresh = hard_thresh
|
||||
else:
|
||||
thresh = np.percentile(flat_vals, percentile)
|
||||
|
||||
coords = np.argwhere(voxel_grid > thresh)
|
||||
if coords.size == 0:
|
||||
print(f"No voxels above threshold {thresh}. Nothing to display.")
|
||||
return None, None
|
||||
|
||||
intensities = voxel_grid[coords[:, 0], coords[:, 1], coords[:, 2]]
|
||||
|
||||
# Because we're now treating 0 -> z, 1 -> y, 2 -> x:
|
||||
z_idx = coords[:, 0] + 0.5
|
||||
y_idx = coords[:, 1] + 0.5
|
||||
x_idx = coords[:, 2] + 0.5
|
||||
|
||||
# Convert to world coords
|
||||
x_world = grid_min[0] + x_idx * voxel_size
|
||||
y_world = grid_min[1] + y_idx * voxel_size
|
||||
z_world = grid_min[2] + z_idx * voxel_size
|
||||
|
||||
points = np.column_stack((x_world, y_world, z_world))
|
||||
return points, intensities
|
||||
|
||||
|
||||
def rotation_matrix_xyz(rx_deg, ry_deg, rz_deg):
|
||||
"""
|
||||
Build a rotation matrix (3x3) for Euler angles (rx, ry, rz) in degrees,
|
||||
applied in X->Y->Z order. That is:
|
||||
R = Rz(rz) * Ry(ry) * Rx(rx)
|
||||
so we rotate first by rx around X, then ry around Y, then rz around Z.
|
||||
"""
|
||||
rx = math.radians(rx_deg)
|
||||
ry = math.radians(ry_deg)
|
||||
rz = math.radians(rz_deg)
|
||||
|
||||
cx, sx = math.cos(rx), math.sin(rx)
|
||||
cy, sy = math.cos(ry), math.sin(ry)
|
||||
cz, sz = math.cos(rz), math.sin(rz)
|
||||
|
||||
# Rx
|
||||
Rx = np.array([
|
||||
[1, 0, 0],
|
||||
[0, cx, -sx],
|
||||
[0, sx, cx]
|
||||
], dtype=np.float32)
|
||||
|
||||
# Ry
|
||||
Ry = np.array([
|
||||
[ cy, 0, sy],
|
||||
[ 0, 1, 0],
|
||||
[-sy, 0, cy]
|
||||
], dtype=np.float32)
|
||||
|
||||
# Rz
|
||||
Rz = np.array([
|
||||
[ cz, -sz, 0],
|
||||
[ sz, cz, 0],
|
||||
[ 0, 0, 1]
|
||||
], dtype=np.float32)
|
||||
|
||||
# Combined: Rz * Ry * Rx
|
||||
Rtemp = Rz @ Ry
|
||||
Rfinal = Rtemp @ Rx
|
||||
return Rfinal
|
||||
|
||||
|
||||
def get_next_image_index(folder, prefix="voxel_", suffix=".png"):
|
||||
"""
|
||||
Scan 'folder' for files named like 'voxel_XXXX.png'.
|
||||
Find the largest XXXX as int, return that + 1.
|
||||
If none found, return 1.
|
||||
"""
|
||||
if not os.path.exists(folder):
|
||||
return 1
|
||||
|
||||
pattern = re.compile(rf"^{prefix}(\d+){suffix}$")
|
||||
max_index = 0
|
||||
for fname in os.listdir(folder):
|
||||
match = pattern.match(fname)
|
||||
if match:
|
||||
idx = int(match.group(1))
|
||||
if idx > max_index:
|
||||
max_index = idx
|
||||
return max_index + 1
|
||||
|
||||
|
||||
def main():
|
||||
# 1) Load the voxel grid
|
||||
voxel_grid, vox_size = load_voxel_grid("voxel_grid.bin")
|
||||
print("Loaded voxel grid:", voxel_grid.shape, "voxel_size=", vox_size)
|
||||
print("Max voxel value:", voxel_grid.max())
|
||||
|
||||
# 2) Define the grid center (x,y,z)
|
||||
grid_center = np.array([30, 0, 14000], dtype=np.float32)
|
||||
|
||||
# 3) Extract top percentile (Z-up)
|
||||
percentile_to_show = 99.9
|
||||
points, intensities = extract_top_percentile_z_up(
|
||||
voxel_grid,
|
||||
voxel_size=vox_size,
|
||||
grid_center=grid_center,
|
||||
percentile=percentile_to_show,
|
||||
use_hard_thresh=False,
|
||||
hard_thresh=700
|
||||
)
|
||||
if points is None:
|
||||
return # nothing to show
|
||||
|
||||
# 4) Optional rotation
|
||||
# e.g. rotate to fix orientation
|
||||
rx_deg = 90
|
||||
ry_deg = 270
|
||||
rz_deg = 0
|
||||
|
||||
R = rotation_matrix_xyz(rx_deg, ry_deg, rz_deg) # shape (3,3)
|
||||
points_rot = points @ R.T
|
||||
# 5) PyVista Plotter (interactive)
|
||||
plotter = pv.Plotter(off_screen=True,)
|
||||
plotter.set_background("white")
|
||||
plotter.enable_terrain_style()
|
||||
|
||||
|
||||
# Convert to PolyData with scalars
|
||||
cloud = pv.PolyData(points_rot)
|
||||
cloud["intensity"] = intensities
|
||||
|
||||
# Add points
|
||||
plotter.add_points(
|
||||
cloud,
|
||||
scalars="intensity",
|
||||
cmap="hot",
|
||||
point_size=4.0,
|
||||
render_points_as_spheres=True,
|
||||
opacity=0.1,
|
||||
)
|
||||
|
||||
plotter.add_scalar_bar(
|
||||
title="Brightness",
|
||||
n_labels=5
|
||||
)
|
||||
|
||||
# 6) Determine the next screenshot index
|
||||
screenshot_folder = "screenshots"
|
||||
if not os.path.exists(screenshot_folder):
|
||||
os.makedirs(screenshot_folder)
|
||||
next_idx = get_next_image_index(screenshot_folder, prefix="voxel_", suffix=".png")
|
||||
out_name = f"voxel_{next_idx:04d}.png"
|
||||
out_path = os.path.join(screenshot_folder, out_name)
|
||||
# 7) Show the interactive window at 1920x1080 and save final screenshot
|
||||
# The screenshot is generated when you close the plot window.
|
||||
plotter.show(window_size=[3840, 2160], auto_close=False, screenshot=out_path)
|
||||
|
||||
|
||||
|
||||
print(f"[Info] Saved screenshot to {out_path}")
|
||||
plotter = pv.Plotter(off_screen=False, )
|
||||
plotter.set_background("white")
|
||||
plotter.enable_terrain_style()
|
||||
|
||||
# Convert to PolyData with scalars
|
||||
cloud = pv.PolyData(points_rot)
|
||||
cloud["intensity"] = intensities
|
||||
|
||||
# Add points
|
||||
plotter.add_points(
|
||||
cloud,
|
||||
scalars="intensity",
|
||||
cmap="hot",
|
||||
point_size=4.0,
|
||||
render_points_as_spheres=True,
|
||||
opacity=0.05,
|
||||
)
|
||||
|
||||
# plotter.add_scalar_bar(
|
||||
# title="Brightness",
|
||||
# n_labels=5
|
||||
# )
|
||||
print("[Done]")
|
||||
plotter.show(window_size=[1920, 1080], auto_close=False)
|
||||
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
Loading…
Add table
Add a link
Reference in a new issue