Initial commit: Speckle-Scanner 3D pipeline with setup README

This commit is contained in:
2026-06-10 03:09:05 +05:00
commit 1765934846
375 changed files with 123081 additions and 0 deletions
+137
View File
@@ -0,0 +1,137 @@
import cv2
import numpy as np
def load_ascii_ply(ply_path):
print(f"Loading point cloud from: {ply_path}")
with open(ply_path, 'r') as f:
lines = f.readlines()
header_end = 0
for i, line in enumerate(lines):
if line.strip() == "end_header":
header_end = i + 1
break
data = np.loadtxt(lines[header_end:])
points_3d = data[:, :3].astype(np.float32)
return points_3d
def save_master_ply(filename, points, rgb_colors, ir_colors):
print(f"Writing Master Binary PLY file to: {filename}...")
# Create a structured numpy array for fast binary writing
vertex_type = [
('x', 'f4'), ('y', 'f4'), ('z', 'f4'),
('red', 'u1'), ('green', 'u1'), ('blue', 'u1'),
('ir_red', 'u1'), ('ir_green', 'u1'), ('ir_blue', 'u1')
]
vertices_struct = np.empty(len(points), dtype=vertex_type)
vertices_struct['x'] = points[:, 0]
vertices_struct['y'] = points[:, 1]
vertices_struct['z'] = points[:, 2]
vertices_struct['red'] = rgb_colors[:, 0]
vertices_struct['green'] = rgb_colors[:, 1]
vertices_struct['blue'] = rgb_colors[:, 2]
vertices_struct['ir_red'] = ir_colors[:, 0]
vertices_struct['ir_green'] = ir_colors[:, 1]
vertices_struct['ir_blue'] = ir_colors[:, 2]
with open(filename, 'wb') as f:
header = (
"ply\n"
"format binary_little_endian 1.0\n"
f"element vertex {len(points)}\n"
"property float x\n"
"property float y\n"
"property float z\n"
"property uchar red\n"
"property uchar green\n"
"property uchar blue\n"
"property uchar ir_red\n"
"property uchar ir_green\n"
"property uchar ir_blue\n"
"end_header\n"
)
f.write(header.encode('ascii'))
f.write(vertices_struct.tobytes())
print("Done! Master PLY is ready for the client & CloudCompare.")
def colorize_combined_pointcloud(points_3d, rgb_img_path, ir_img_path, lc_rgb_yaml, lc_ir_yaml, lc_rc_yaml, output_ply_path):
print(f"\nLoading RGB scan image...")
rgb_img = cv2.imread(rgb_img_path)
if rgb_img.shape[:2] != (2044, 2040):
rgb_img = cv2.resize(rgb_img, (2040, 2044), interpolation=cv2.INTER_LINEAR)
print(f"Loading IR scan image...")
ir_img = cv2.imread(ir_img_path)
if ir_img.shape[:2] != (480, 640):
ir_img = cv2.resize(ir_img, (640, 480), interpolation=cv2.INTER_LINEAR)
# Rectify & Un-rotate 3D points
fs_rc = cv2.FileStorage(lc_rc_yaml, cv2.FILE_STORAGE_READ)
R1, _, _, _, _, _, _ = cv2.stereoRectify(
fs_rc.getNode("L_Intrinsic").mat(), fs_rc.getNode("L_Distortion").mat(),
fs_rc.getNode("R_Intrinsic").mat(), fs_rc.getNode("R_Distortion").mat(),
(1920, 1464), fs_rc.getNode("Rotation").mat(), fs_rc.getNode("Translation").mat(),
flags=cv2.CALIB_ZERO_DISPARITY, alpha=1
)
fs_rc.release()
points_3d_raw = np.dot(R1.T, points_3d.T).T
# Project to RGB camera
fs_rgb = cv2.FileStorage(lc_rgb_yaml, cv2.FILE_STORAGE_READ)
rvec_rgb, _ = cv2.Rodrigues(fs_rgb.getNode("Rotation").mat())
pixels_rgb, _ = cv2.projectPoints(points_3d_raw, rvec_rgb, fs_rgb.getNode("Translation").mat(), fs_rgb.getNode("R_Intrinsic").mat(), fs_rgb.getNode("R_Distortion").mat())
pixels_rgb = pixels_rgb.reshape(-1, 2)
fs_rgb.release()
# Project to IR camera
fs_ir = cv2.FileStorage(lc_ir_yaml, cv2.FILE_STORAGE_READ)
rvec_ir, _ = cv2.Rodrigues(fs_ir.getNode("Rotation").mat())
pixels_ir, _ = cv2.projectPoints(points_3d_raw, rvec_ir, fs_ir.getNode("Translation").mat(), fs_ir.getNode("R_Intrinsic").mat(), fs_ir.getNode("R_Distortion").mat())
pixels_ir = pixels_ir.reshape(-1, 2)
fs_ir.release()
points_list, rgb_list, ir_list = [], [], []
# Apply your tuned offsets
rgb_x, rgb_y = 30, 5
ir_x, ir_y = -22, 9
print("Extracting combined colors...")
for i in range(len(points_3d)):
x_r, y_r = int(np.round(pixels_rgb[i][0])) + rgb_x, int(np.round(pixels_rgb[i][1])) + rgb_y
x_i, y_i = int(np.round(pixels_ir[i][0])) + ir_x, int(np.round(pixels_ir[i][1])) + ir_y
# Keep only points visible to BOTH cameras
if (0 <= x_r < 2040 and 0 <= y_r < 2044) and (0 <= x_i < 640 and 0 <= y_i < 480):
b, g, r = rgb_img[y_r, x_r]
ir_b, ir_g, ir_r = ir_img[y_i, x_i]
points_list.append(points_3d[i])
rgb_list.append([r, g, b])
# Store true IR RGB values (0-255) instead of converting to normals
ir_list.append([ir_r, ir_g, ir_b])
print(f"Successfully encoded dual-textures to {len(points_list)} points!")
save_master_ply(output_ply_path, np.array(points_list), np.array(rgb_list), np.array(ir_list))
if __name__ == "__main__":
# Your exact file paths
input_cloud = r"/mnt/e/jost/color-maping(Asad)/input/Point_cloud.ply"
raw_rgb = r"/mnt/e/jost/color-maping(Asad)/input/rgb_1.bmp"
raw_ir = r"/mnt/e/jost/color-maping(Asad)/input/IR_scan_000001.png"
lc_rgb_yaml = r"/mnt/e/jost/color-maping(Asad)/calibration-results lc-rgb/stereo_cam_model.yaml"
lc_ir_yaml = r"/mnt/e/jost/color-maping(Asad)/calibration-results lc-ir/stereo_cam_model.yaml"
lc_rc_yaml = r"/mnt/e/jost/color-maping(Asad)/calibration-results lc-rc/stereo_cam_model.yaml"
# Save the Client Master PLY
out_master = r"/mnt/e/jost/color-maping(Asad)/output/CLIENT_MASTER_COMBINED.ply"
pts = load_ascii_ply(input_cloud)
colorize_combined_pointcloud(pts, raw_rgb, raw_ir, lc_rgb_yaml, lc_ir_yaml, lc_rc_yaml, out_master)