Files
FishServer/FishMeasure/utils/evaluate_flatness.py

245 lines
8.6 KiB
Python
Raw Normal View History

"""
Evaluate the flatness/curvature of a point cloud using RANSAC plane fitting.
The score is the percentage of points that lie on the fitted plane.
Higher percentage means the point cloud is flatter (better for flat objects like fish).
"""
import numpy as np
from pathlib import Path
from typing import Tuple, Optional
import open3d as o3d
def evaluate_flatness_ransac(points: np.ndarray,
distance_threshold: float = 5.0,
ransac_n: int = 3,
num_iterations: int = 1000) -> Tuple[float, dict]:
"""
Evaluate flatness of a point cloud using RANSAC plane fitting.
Args:
points: Point cloud array (N, 3)
distance_threshold: Maximum distance from a point to the plane to be considered inlier (in mm)
ransac_n: Number of points to sample for plane fitting
num_iterations: Number of RANSAC iterations
Returns:
tuple: (flatness_score: float, info_dict: dict)
- flatness_score: Percentage of points on the plane (0-100, higher is flatter)
- info_dict: Contains plane equation, inlier count, total points, etc.
"""
if len(points) < 3:
return 0.0, {"error": "Not enough points (need at least 3)"}
if len(points) < ransac_n:
return 0.0, {"error": f"Not enough points for RANSAC (need at least {ransac_n})"}
# Convert to Open3D point cloud
pcd = o3d.geometry.PointCloud()
pcd.points = o3d.utility.Vector3dVector(points.astype(np.float64))
# Fit plane using RANSAC
plane_model, inliers = pcd.segment_plane(
distance_threshold=distance_threshold,
ransac_n=ransac_n,
num_iterations=num_iterations
)
# Calculate flatness score (percentage of inliers)
num_inliers = len(inliers)
num_total = len(points)
flatness_score = (num_inliers / num_total) * 100.0
# Extract plane equation: ax + by + cz + d = 0
[a, b, c, d] = plane_model
# Calculate plane normal and distance
normal = np.array([a, b, c])
normal_norm = np.linalg.norm(normal)
if normal_norm > 0:
normal = normal / normal_norm
plane_distance = d / normal_norm
else:
normal = np.array([0, 0, 1])
plane_distance = 0.0
info = {
"flatness_score": flatness_score,
"num_inliers": num_inliers,
"num_total": num_total,
"num_outliers": num_total - num_inliers,
"plane_equation": [a, b, c, d],
"plane_normal": normal.tolist(),
"plane_distance": float(plane_distance),
"distance_threshold": distance_threshold,
"inlier_indices": inliers
}
return flatness_score, info
def evaluate_flatness_from_ply(ply_path: str,
distance_threshold: float = 5.0,
ransac_n: int = 3,
num_iterations: int = 1000) -> Tuple[float, dict]:
"""
Evaluate flatness of a point cloud from a PLY file.
Args:
ply_path: Path to PLY file
distance_threshold: Maximum distance from a point to the plane to be considered inlier (in mm)
ransac_n: Number of points to sample for plane fitting
num_iterations: Number of RANSAC iterations
Returns:
tuple: (flatness_score: float, info_dict: dict)
"""
ply_path = Path(ply_path).expanduser().resolve()
if not ply_path.exists():
return 0.0, {"error": f"PLY file not found: {ply_path}"}
# Load point cloud
pcd = o3d.io.read_point_cloud(str(ply_path))
if len(pcd.points) == 0:
return 0.0, {"error": "Empty point cloud"}
# Convert to numpy array
points = np.asarray(pcd.points)
return evaluate_flatness_ransac(points, distance_threshold, ransac_n, num_iterations)
def batch_evaluate_flatness(ply_files: list,
distance_threshold: float = 5.0,
ransac_n: int = 3,
num_iterations: int = 1000) -> list:
"""
Evaluate flatness for multiple PLY files.
Args:
ply_files: List of paths to PLY files
distance_threshold: Maximum distance from a point to the plane to be considered inlier (in mm)
ransac_n: Number of points to sample for plane fitting
num_iterations: Number of RANSAC iterations
Returns:
list: List of dicts, each containing file path and flatness evaluation results
"""
results = []
for ply_file in ply_files:
ply_path = Path(ply_file)
score, info = evaluate_flatness_from_ply(
str(ply_path),
distance_threshold=distance_threshold,
ransac_n=ransac_n,
num_iterations=num_iterations
)
result = {
"file": str(ply_path),
"flatness_score": score,
**info
}
results.append(result)
return results
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="Evaluate flatness of point clouds using RANSAC plane fitting")
parser.add_argument("--ply", type=str, help="Path to single PLY file")
parser.add_argument("--folder", type=str, help="Path to folder containing PLY files")
parser.add_argument("--distance-threshold", type=float, default=5.0,
help="Maximum distance from point to plane to be considered inlier (mm, default: 5.0)")
parser.add_argument("--ransac-n", type=int, default=3,
help="Number of points to sample for plane fitting (default: 3)")
parser.add_argument("--num-iterations", type=int, default=1000,
help="Number of RANSAC iterations (default: 1000)")
parser.add_argument("--output", type=str, help="Output JSON file to save results")
args = parser.parse_args()
import json
if args.ply:
# Single file
score, info = evaluate_flatness_from_ply(
args.ply,
distance_threshold=args.distance_threshold,
ransac_n=args.ransac_n,
num_iterations=args.num_iterations
)
result = {
"file": args.ply,
"flatness_score": score,
**info
}
print(f"File: {args.ply}")
print(f"Flatness Score: {score:.2f}%")
print(f"Inliers: {info.get('num_inliers', 0)}/{info.get('num_total', 0)}")
print(f"Plane Normal: {info.get('plane_normal', [0,0,0])}")
if args.output:
with open(args.output, 'w') as f:
json.dump(result, f, indent=2)
print(f"\nResults saved to: {args.output}")
elif args.folder:
# Folder of files
folder = Path(args.folder)
ply_files = list(folder.glob("*.ply"))
if not ply_files:
print(f"No PLY files found in {folder}")
exit(1)
print(f"Evaluating {len(ply_files)} PLY files...")
results = batch_evaluate_flatness(
ply_files,
distance_threshold=args.distance_threshold,
ransac_n=args.ransac_n,
num_iterations=args.num_iterations
)
# Print summary
scores = [r["flatness_score"] for r in results if "error" not in r]
if scores:
print(f"\nSummary:")
print(f" Average flatness score: {np.mean(scores):.2f}%")
print(f" Min flatness score: {np.min(scores):.2f}%")
print(f" Max flatness score: {np.max(scores):.2f}%")
print(f" Std flatness score: {np.std(scores):.2f}%")
if args.output:
output_data = {
"summary": {
"num_files": len(results),
"average_score": float(np.mean(scores)) if scores else 0.0,
"min_score": float(np.min(scores)) if scores else 0.0,
"max_score": float(np.max(scores)) if scores else 0.0,
"std_score": float(np.std(scores)) if scores else 0.0
},
"results": results
}
with open(args.output, 'w') as f:
json.dump(output_data, f, indent=2)
print(f"\nResults saved to: {args.output}")
else:
# Print all results
print("\nDetailed Results:")
for r in results:
if "error" in r:
print(f" {r['file']}: ERROR - {r['error']}")
else:
print(f" {r['file']}: {r['flatness_score']:.2f}% ({r['num_inliers']}/{r['num_total']} inliers)")
else:
parser.print_help()