This document describes every package and module in src/ and the executable scripts in scripts/, in pipeline order — from raw mesh files on disk to trained model and visualisation.
.off files on disk
└─► src/builders/ parse mesh
└─► src/geometry/ represent + sample point cloud
└─► src/dataset/ batch + cache
└─► src/deep_learning/ train + evaluate
└─► results/ + models/
└─► scripts/ visualise + run inference
Central location for all project-wide path constants. Import these instead of hard-coding paths anywhere else.
| Constant | Resolved path |
|---|---|
PROJECT_ROOT |
Repository root |
DATA_DIR |
data/ModelNet10/models/ |
MODELNET40_DIR |
data/ModelNet40/models/ |
MODELS_DIR |
models/ |
RESULTS_DIR |
results/ |
Low-level 3D geometry primitives.
Three point-cloud sampling strategies, passed wherever a sampling method is required:
| Value | Open3D method | Behaviour |
|---|---|---|
UNIFORM |
sample_points_uniformly |
Fast random surface samples |
FARTHEST_POINT |
farthest_point_down_sample |
Greedy max-coverage (FPS) |
POISSON |
sample_points_poisson_disk |
Evenly-spaced disk sampling |
Thin wrapper around an Open3D TriangleMesh.
mesh = Mesh3D(vertices, faces) # np.ndarray (V,3), (F,3)
pts = mesh.sample_points(n, method) # → np.ndarray (n, 3), float32sample_points caches the result in memory keyed on (n_points, method). Pass force_resample=True to discard the cache (used when building the training set disk cache).
If the mesh has no faces (vertex-only OFF file), sample_points automatically falls back to sampling from vertex positions via the private _sample_from_vertices method — FPS uses Open3D's PointCloud.farthest_point_down_sample; UNIFORM and POISSON use numpy.random.choice. A UserWarning is emitted so the caller knows the fallback was triggered.
Converts raw .off files into Mesh3D objects.
vertices, faces = OffMeshParser.parse_off(lines)Handles both OFF header variants:
- Standard (ModelNet10):
"OFF\n3514 3546 0\n…" - Compact (ModelNet40):
"OFF3514 3546 0\n…"
mesh = Mesh3DBuilder.from_off_file(path) # Path → Mesh3DReads the file, delegates parsing to OffMeshParser, and wraps the result in a Mesh3D.
PyTorch Dataset implementations with split management and disk caching.
Abstract base class. Handles:
- Class discovery — scans
root_dir/for subdirectories; buildsclass_to_idxandidx_to_classmappings dynamically (no hardcoded class list). - Split strategy —
use_existing_split=Truereads{class}/train/and{class}/test/folders;use_existing_split=Falsecreates a random train/test split from all files. - Caching hook — calls
_build_cache()ifcache_processed=True; subclasses override this. __getitem__— returns(tensor, label_int)and applies an optionaltransform.
Concrete subclass. Adds:
- Disk cache at
data/{dataset}/cache/pointcloud_{split}_{n}pts_{method}/— one.npyfile per mesh. Reused automatically if count matches. - Train/test asymmetry — test set is always cached (
cache_processed=Trueby default); training set uses dynamic re-sampling each call for implicit data augmentation. - Unit-sphere normalisation — every point cloud is centred at the origin and scaled so
max_norm = 1before being returned. Applied in-memory; the on-disk.npyfiles retain raw coordinates.
ds = PointCloudDataset(
root_dir=DATA_DIR,
split='train',
n_points=1024,
sampling_method=Sampling.FARTHEST_POINT,
)
points, label = ds[0] # torch.Tensor (1024, 3), inttrain_ds, test_ds = make_datasets(n_points, sampling_method, data_dir)Convenience function used by the sequential trainer to avoid boilerplate.
Per-model configuration passed to run_sequential(). Only sampling is required; all other fields fall back to the global values supplied to run_sequential() when set to None.
ModelConfig(
sampling="fps", # required: "uniform" | "fps" | "poisson"
lr=0.001, # optional override
patience=25, # optional override (epochs without improvement)
early_stop_metric="f1", # optional override: "accuracy" | "f1" | "loss"
epochs=200, # optional override
optimizer_factory=lambda params, lr: AdamW(params, lr=lr, weight_decay=1e-4),
scheduler_factory=lambda opt, _: StepLR(opt, step_size=20, gamma=0.7),
)optimizer_factory signature: (parameters, lr) → Optimizer.
scheduler_factory signature: (optimizer, epochs_remaining) → LRScheduler.
The core training loop. Accepts a model, two datasets, and configuration; returns a TrainingResults TypedDict.
Key behaviours:
- Optimiser — Adam with auto LR
0.001 × (batch_size / 32)by default; override withoptimizer_factory. - Scheduler —
CosineAnnealingLRby default; override withscheduler_factory. - Early stopping — monitors
accuracy,f1(macro), orloss; stops afterpatienceepochs without improvement. - TensorBoard — logs train/test loss, accuracy, per-class precision/recall/F1, and learning rate to
runs/{experiment_name}/. - Checkpoints — saves
{name}.pth(latest) and{name}_best.pth(best metric) to the configuredsave_modelpath.
trainer = ModelTrainer(train_dataset, test_dataset, model, save_model=path, ...)
results = trainer.train(epochs=200) # → TrainingResults TypedDictTrains every model in configs one after another. Key details:
- Dataset cache — datasets are shared across models that use the same
(n_points, sampling)pair, avoiding redundant re-building. num_classesis dynamic — inferred aslen(train_ds.class_to_idx)after dataset construction; no hardcoding.- Output — saves
sequential_results.jsonand four comparison plots (model comparison, per-class accuracy, per-class F1, training efficiency) toresults_dir. - Checkpoint paths —
models_dir / f"{run_name}.pth".
The entry point scripts/sequential_training.py builds timestamped paths:
results/sequential/{dataset}/YYYY-MM-DD_HHMMSS/
models/sequential/{dataset}/YYYY-MM-DD_HHMMSS/
Defines the Cartesian product to explore:
GridSearchConfig(
model_classes=[PointNet, DGCNN],
sampling_methods=[Sampling.UNIFORM, Sampling.FARTHEST_POINT],
n_points_list=[512, 1024],
batch_sizes=[16, 32],
)Iterates every combination, caches datasets by (n_points, sampling), and saves results after each run for crash recovery.
The entry point scripts/grid_training.py builds the dataset factory with functools.partial(make_datasets, data_dir=data_dir) so the correct dataset root (ModelNet10 or ModelNet40) is pre-bound while keeping GridSearch's DatasetFactory signature (n_points, Sampling) → datasets unchanged. Timestamped output paths are constructed the same way as sequential_training.py:
results/grid/{dataset}/YYYY-MM-DD_HHMMSS/
models/grid/{dataset}/YYYY-MM-DD_HHMMSS/
| Function | Output files | Used by |
|---|---|---|
plot_sequential_results(results_path) |
sequential_model_comparison.png, sequential_per_class_accuracy.png, sequential_per_class_f1.png, sequential_training_efficiency.png |
run_sequential(), rebuild_figures.py |
plot_training_efficiency(runs, output_dir, plt, *, filename) |
sequential_training_efficiency.png (default) or ablation_training_efficiency.png |
called by plot_sequential_results() and create_ablation_plots() |
create_ablation_plots(results_path) |
accuracy_comparison.png, npoints_effect.png, batchsize_effect.png, sampling_comparison.png, model_heatmap.png, ablation_training_efficiency.png |
grid_training.py |
All plots are written to the same directory as the JSON file, or to output_dir if provided.
Shared inference helpers used by both scripts/visualize_inference.py and scripts/infer_single.py. Extracted into a library module so neither script depends on the other.
| Symbol | Kind | Description |
|---|---|---|
SAMPLING_MAP |
dict[str, Sampling] |
Maps string keys ("uniform", "fps", "poisson") to Sampling enum values |
_CKPT_PATTERN |
re.Pattern |
Regex that parses {Model}_{sampling}_pts{N}_bs{B}[_best].pth filenames |
_DATASET_MAP |
dict[str, (Path, int)] |
Maps "modelnet10" / "modelnet40" to (data_dir, num_classes) |
detect_dataset_from_path(path) |
function | Walks path components to find "modelnet10" or "modelnet40" |
parse_checkpoint_config(path) |
function | Returns (model_class, n_points, Sampling) from a checkpoint filename, or None |
load_model_from_checkpoint(path, model_class, num_classes, device) |
function | Instantiates the model, loads weights, sets eval() mode |
run_inference(model, points_np, device) |
function | Unit-sphere normalises the point cloud, runs a forward pass, returns (pred_idx, confidence) |
All scripts are run from the project root with python -m scripts.<name>.
| Script | Invocation | Purpose |
|---|---|---|
main.py |
python -m scripts.main |
Interactive Open3D viewer — browse all meshes (N/Right = next, P/Left = prev) |
sequential_training.py |
python -m scripts.sequential_training [--dataset modelnet10|modelnet40] |
Train all models sequentially with curated hyperparameters |
grid_training.py |
python -m scripts.grid_training [--dataset modelnet10|modelnet40] |
Full ablation over model × sampling × n_points × batch_size |
visualize_inference.py |
python -m scripts.visualize_inference |
Load a trained checkpoint, run inference on test meshes, visualise in 3D |
infer_single.py |
python -m scripts.infer_single |
Run one .off file through a checkpoint and print the predicted class; edit MODEL_PATH / OBJECT_PATH at the top |
view_mesh.py |
python -m scripts.view_mesh |
File-picker dialog to open any .off mesh; prints vertex/face counts and shows geometry in Open3D |
rebuild_figures.py |
python -m scripts.rebuild_figures |
Re-run plot_sequential_results() on any past JSON without retraining — useful after editing plotting.py |