Skip to content
Search

Pointcept:Examples:Weld:LVS

lastmod 2026-04-21created 2026-04-1716 edits

A sample of training on data acquired with LVS.

Pointcept is a framework specialized mainly in Semantic Segmentation and Instance Segmentation (segmentation of individual objects) of 3D point clouds. Here is a concrete way to use Pointcept for weld surface quality inspection (porosity and spatter detection).

Acquiring sample data

See Welding:SurfaceSampleDataGenerator for details.

From LVS, a single line of csv data with only the (x, z) axes is extended with multiple lines along the y axis, and the resulting virtual "Surface" data is generated as ply.

The files created as ply were compressed: 20260415-butt_bead_profile_sample-all.zip

Point cloud labeling

Performed 3D Semantic Segmentation using CloudCompare: 20260415-butt_bead_profile_sample-all-segs.zip

  • bead.ply - weld bead
  • face1.ply - surface 1
  • face1.ply - surface 2

Task Formulation

Weld defect detection is usually formulated as a Semantic Segmentation problem. The model is trained to classify each point as one of the following.

  • Class 0: Normal Bead
  • Class 1: Porosity - concave, pitted shape feature
  • Class 2: Spatter - protruding, spherical feature
  • Class 3: Base Metal

Data preparation and building a custom dataset

The .ply files you provided contain only shape information ($x, y, z$). To train on them in Pointcept, the following preparation is needed.

  1. Data annotation: use a tool such as LabelCloud or CloudCompare to assign a defect-type label to each point in the .ply files.
  2. Create a dataset class: write a custom Dataset class in the pointcept/datasets directory that loads the weld data. (e.g. WeldDataset)
    • Add a custom config under configs/_base_/dataset/ containing the data path and preprocessing settings.

Model Selection

Because precise geometric features (curvature, height changes) matter for weld data, the following models are recommended.

  • PTv3 (Point Transformer v3) - Currently Pointcept's flagship model; it can be used after adapting the configs/scannet/semseg-ptv3-base-v1m1-0-base.py config to weld data. Its strong context understanding is an advantage for detecting fine defects.
  • SpUNet (Sparse UNet) - Computationally efficient and strong at feature extraction on grid-like structures, making it a good fit for the regular scan data captured with LVS.

Quality inspection process (Workflow)

1. Train: train the model using data that includes defect labels.

python tools/train.py --config-file configs/weld/semseg-ptv3-weld.py

2. Inference and inspection (Inference): feed a new .ply file as input to predict defect regions.

python tools/test.py --config-file configs/weld/semseg-ptv3-weld.py --model-path model_best.pth

3. Result analysis: from the Segmentation Map output by the model, compute the cluster sizes of the points classified as porosity or spatter, and make a pass/fail quality decision.

Tips for approaching this project

  • Normalization: for weld beads, the height change along the $z$ axis is the key. It is important to match the data scale using preprocessing such as GridSample or Normalization in Pointcept.
  • RPE (Relative Position Encoding): turning on the RPE option in PointTransformerV3 helps catch fine indentations (porosity) better.

Writing the WeldDataset class

Let's write a WeldDataset class that can load the provided face1.ply, face2.ply, and bead.ply files, labeled per class.

Following Pointcept's structure, create the file pointcept/datasets/weld.py, including logic that merges the point clouds into a single Scene and assigns labels.

Writing pointcept/datasets/weld.py

This class reads each .ply file and produces (N, 3) coordinates and (N,) labels.

import os
import glob
import numpy as np
from torch.utils.data import Dataset
from plyfile import PlyData
from .builder import DATASETS
from .transform import Compose

def read_ply(path):
    plydata = PlyData.read(path)
    x = np.array(plydata['vertex']['x'])
    y = np.array(plydata['vertex']['y'])
    z = np.array(plydata['vertex']['z'])
    coords = np.stack([x, y, z], axis=1)
    return coords

@DATASETS.register_module()
class WeldDataset(Dataset):
    def __init__(self,
                 split='train',
                 data_root='data/weld',
                 transform=None,
                 test_mode=False,
                 loop=1):
        super(WeldDataset, self).__init__()
        self.split = split
        self.data_root = data_root
        self.transform = Compose(transform)
        self.loop = loop
        self.test_mode = test_mode

        # 클래스 매핑 정의
        self.class_map = {
            "face1": 0,
            "face2": 1,
            "bead": 2,
        }

        self.data_list = self.get_data_list()

    def get_data_list(self):
        split_dir = os.path.join(self.data_root, self.split)
        if not os.path.exists(split_dir):
            return []
        return sorted(glob.glob(os.path.join(split_dir, "*")))

    def __getitem__(self, idx):
        scene_path = self.data_list[idx % len(self.data_list)]

        all_coords = []
        all_labels = []

        for file_name, label in self.class_map.items():
            file_path = os.path.join(scene_path, f"{file_name}.ply")
            if os.path.exists(file_path):
                coords = read_ply(file_path)
                all_coords.append(coords)
                all_labels.append(np.full(len(coords), label, dtype=np.int64))

        if len(all_coords) == 0:
            # 빈 데이터 처리 (에러 방지)
            coords = np.zeros((0, 3), dtype=np.float32)
            labels = np.zeros((0,), dtype=np.int64)
        else:
            coords = np.concatenate(all_coords, axis=0).astype(np.float32)
            labels = np.concatenate(all_labels, axis=0).astype(np.int64)

        # Pointcept standard keys: coord, segment (instead of label)
        data_dict = dict(coord=coords, segment=labels, name=os.path.basename(scene_path))

        if self.transform is not None:
            data_dict = self.transform(data_dict)

        return data_dict

    def __len__(self):
        return len(self.data_list) * self.loop

For the code above to work, the training data must be laid out as follows.

data/weld/
├── train/
│   ├── scene_001/
│   │   ├── face1.ply
│   │   ├── face2.ply
│   │   └── bead.ply
│   └── scene_002/ ...
└── val/
    └── scene_003/ ...

Then register the new dataset in the pointcept/datasets/__init__.py file so that it can be recognized:

# pointcept/datasets/__init__.py 에 추가
from .weld import WeldDataset

Config file settings (configs/base/dataset/weld.py)

Add settings so that this dataset is used when training the model.

dataset_type = "WeldDataset"
data_root = "data/weld"

data = dict(
    train=dict(
        type=dataset_type,
        split="train",
        data_root=data_root,
        transform=[
            dict(type="CenterShift", apply_z=True),
            dict(type="RandomRotate", angle=[-1, 1], axis="z", center=[0, 0, 0], p=0.5),
            dict(type="RandomScale", scale=[0.9, 1.1]),
            dict(type="DefaultPredictDoNothing"), # placeholder
            dict(type="Collect", keys=("coord", "label"), feat_keys=("coord")),
            dict(type="ToTensor"),
        ],
        loop=10, # 데이터가 적을 경우 반복 학습
    ),
    val=dict(dataset_type,
        split="val",
        data_root=data_root,
        transform=[
            dict(type="CenterShift", apply_z=True),
            dict(type="Collect", keys=("coord", "label"), feat_keys=("coord")),
            dict(type="ToTensor"),
        ],
    ),
)

Writing the configs/weld/semseg-pt-v3m1-0-base.py file:

_base_ = ["../_base_/default_runtime.py"]

# misc custom setting
batch_size = 1  # bs: total bs in all gpus
num_worker = 4
mix_prob = 0.0
empty_cache = False
enable_amp = True

# model settings
model = dict(
    type="DefaultSegmentorV2",
    num_classes=3,
    backbone_out_channels=64,
    backbone=dict(
        type="PT-v3m1",
        in_channels=3,
        order=("z", "z-trans", "hilbert", "hilbert-trans"),
        stride=(2, 2, 2, 2),
        enc_depths=(2, 2, 2, 6, 2),
        enc_channels=(32, 64, 128, 256, 512),
        enc_num_head=(2, 4, 8, 16, 32),
        enc_patch_size=(1024, 1024, 1024, 1024, 1024),
        dec_depths=(2, 2, 2, 2),
        dec_channels=(64, 64, 128, 256),
        dec_num_head=(4, 4, 8, 16),
        dec_patch_size=(1024, 1024, 1024, 1024),
        mlp_ratio=4,
        qkv_bias=True,
        qk_scale=None,
        attn_drop=0.0,
        proj_drop=0.0,
        drop_path=0.3,
        shuffle_orders=True,
        pre_norm=True,
        enable_rpe=False,
        enable_flash=False,
        upcast_attention=True,
        upcast_softmax=True,
        enc_mode=False,
        pdnorm_bn=False,
        pdnorm_ln=False,
        pdnorm_decouple=True,
        pdnorm_adaptive=False,
        pdnorm_affine=True,
        pdnorm_conditions=("ScanNet", "S3DIS", "Structured3D"),
    ),
    criteria=[
        dict(type="CrossEntropyLoss", loss_weight=1.0, ignore_index=-1),
    ],
)

# scheduler settings
epoch = 100
optimizer = dict(type="AdamW", lr=0.006, weight_decay=0.05)
scheduler = dict(
    type="OneCycleLR",
    max_lr=0.006,
    pct_start=0.05,
    anneal_strategy="cos",
    div_factor=10.0,
    final_div_factor=1000.0,
)

# dataset settings
dataset_type = "WeldDataset"
data_root = "data/weld"

data = dict(
    num_classes=3,
    ignore_index=-1,
    names=["face1", "face2", "bead"],
    train=dict(
        type=dataset_type,
        split="train",
        data_root=data_root,
        transform=[
            dict(type="CenterShift", apply_z=True),
            dict(type="RandomRotate", angle=[-1, 1], axis="z", center=[0, 0, 0], p=0.5),
            dict(type="RandomScale", scale=[0.9, 1.1]),
            dict(
                type="GridSample",
                grid_size=0.02,
                hash_type="fnv",
                mode="train",
                return_grid_coord=True,
            ),
            dict(type="SphereCrop", point_max=102400, mode="random"),
            dict(type="ToTensor"),
            dict(
                type="Collect",
                keys=("coord", "grid_coord", "segment"),
                feat_keys=["coord"],
            ),
        ],
        loop=10,
    ),
    val=dict(
        type=dataset_type,
        split="train",  # Only have one scene for now
        data_root=data_root,
        transform=[
            dict(type="CenterShift", apply_z=True),
            dict(type="Copy", keys_dict={"segment": "origin_segment"}),
            dict(
                type="GridSample",
                grid_size=0.02,
                hash_type="fnv",
                mode="train",
                return_grid_coord=True,
                return_inverse=True,
            ),
            dict(type="ToTensor"),
            dict(
                type="Collect",
                keys=("coord", "grid_coord", "segment", "origin_segment", "inverse"),
                feat_keys=["coord"],
            ),
        ],
    ),
)

Starting training

PYTHONPATH=$PWD:$PYTHONPATH uv run --with open3d --no-sync python tools/train.py --config-file configs/weld/semseg-pt-v3m1-0-base.py --num-gpus 1

A train.sh script file for repeated use:

#!/usr/bin/env bash

ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" || exit; pwd)

export PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True
export PYTHONPATH=$PWD:$PYTHONPATH

PYTHON_EXE="$ROOT_DIR/.venv/bin/python"
TRAIN_FILE="$ROOT_DIR/tools/train.py"
CONFIG_FILE="$ROOT_DIR/configs/weld/semseg-pt-v3m1-0-base.py"

ARGS=(
    "$TRAIN_FILE"
    --config-file "$CONFIG_FILE"
    --num-gpus 1
)

function print_error
{
    # shellcheck disable=SC2145
    echo -e "\033[31m$@\033[0m" 1>&2
}

function print_message
{
    # shellcheck disable=SC2145
    echo -e "\033[32m$@\033[0m"
}

function on_interrupt_trap
{
    print_error "An interrupt signal was detected."
    exit 1
}

trap on_interrupt_trap INT

print_message "$PYTHON_EXE" "${ARGS[*]}"

"$PYTHON_EXE" "${ARGS[@]}"

How to make the model lighter

configs/weld/semseg-pt-v3m1-0-base.py is PTv3 base (channels 32 → 512, 6.5M+ params), so it can be a burden on 8GB.

Lightweight alternatives (built into Pointcept):

  1. LitePT-v1 (pointcept/models/litept) — a lightweight version of PTv3. See configs/scannet/semseg-litept-v1m1-0-small.py. A Conv+Attn hybrid, favorable in both memory and speed.
  2. SpUNet / MinkUNet (sparse_unet) — the lightest baseline. configs/scannet/semseg-spunet-v1m1-0-base.py, semseg-minkunet34c-0-base.py.
  3. SPVCNN, OACNNs — medium scale.

The easiest way

Keep the current PTv3 config but reduce the following:

  • enc_channels=(32,64,128,256,512) → (16,32,64,128,256)
  • enc_depths=(2,2,2,6,2) → (1,1,1,3,1)
  • SphereCrop point_max=102400 → 40960
  • grid_size=0.02 → 0.03 (larger voxels sharply reduce the point count)

Which direction loses the least performance?

Ordered by size of accuracy loss:

model capacity(채널·depth) >> grid_size(voxel) > patch_size > point_max > upcast flags.

So the minimum-loss strategy is a staged approach that applies changes "gradually, starting with the free ones":

Priority 0 — free (almost no accuracy loss)

upcast_attention=False,  # fp32 upcast 해제
upcast_softmax=False,

When running:

PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True ...
  • upcast_*: a safeguard that promotes attention to fp32 under AMP (fp16), but has almost no effect on a numerically benign task like 3-class weld seg
  • expandable_segments: only mitigates memory fragmentation, unrelated to accuracy
  • The error message "1.56 GiB at attn.float()" points exactly at the upcast — likely to pass with this alone

Priority 1 — very small (slightly shrinks the receptive field)

enc_patch_size=(512, 512, 512, 512, 512),  # 1024 → 512
dec_patch_size=(512, 512, 512, 512),
  • PTv3 computes attention only within serialized patches. Weld scenes are small, so a 512-point context is enough
  • 1024 effectively covered the whole scene, so dropping to 512 causes negligible real receptive field loss

Priority 2 — small (training slows down, final accuracy recoverable)

# SphereCrop point_max=102400 → 51200
  • It is a random crop during training, so everything gets covered as epochs go by
  • loop=10 is already set, so diversity actually goes up

Priority 3 — medium (last resort)

# grid_size 0.02 → 0.025
  • Affects bead thickness and boundary precision. Avoid if real-world measurement precision is required.

Absolute last — large loss

  • Reduce channels/depth, replace the model
  1. First apply only priority 0 and run → if it passes, done (accuracy loss ≈ 0)
  2. If still OOM, add priority 1 (negligible loss)
  3. If still OOM, add priority 2
  4. Up to here there is almost no accuracy loss. Beyond this, the trade-off is real.

Lightweight config file for Weld

(e.g. semseg-litept-v1m1-0-weld.py or semseg-spunet-v1m1-0-weld.py) Shall I create a new one for you? Please choose which model you would prefer.

Troubleshooting

ImportError torch/lib/libtorch_cuda.so: undefined symbol: ncclCommWindowDeregister

$ PYTHONPATH=$PWD:$PYTHONPATH ./.venv/bin/python tools/train.py --config-file configs/weld/semseg-pt-v3m1-0-base.py --num-gpus 1
Traceback (most recent call last):
  File "/home/user/Projects/Pointcept/tools/train.py", line 8, in <module>
    from pointcept.engines.defaults import (
  File "/home/user/Projects/Pointcept/pointcept/engines/defaults.py", line 14, in <module>
    from torch.nn.parallel import DistributedDataParallel
  File "/home/user/Projects/Pointcept/.venv/lib/python3.10/site-packages/torch/__init__.py", line 442, in <module>
    from torch._C import *  # noqa: F403
ImportError: /home/user/Projects/Pointcept/.venv/lib/python3.10/site-packages/torch/lib/libtorch_cuda.so: undefined symbol: ncclCommWindowDeregister

The library version is wrong. Reinstall it.

torch.OutOfMemoryError: CUDA out of memory

[2026-04-20 11:12:58,053 INFO train.py line 141 221563] => Building model ...
[2026-04-20 11:12:58,241 INFO train.py line 261 221563] Num params: 46154467
[2026-04-20 11:12:58,430 INFO train.py line 143 221563] => Building writer ...
[2026-04-20 11:12:58,431 INFO train.py line 271 221563] Tensorboard writer logging dir: exp/default
wandb: [wandb.login()] Loaded credentials for https://api.wandb.ai from /home/zer0/.netrc.
wandb: Currently logged in as: osom8979 (osom8979-blackhole) to https://api.wandb.ai. Use `wandb login --relogin` to force relogin
wandb: Tracking run with wandb version 0.26.0
wandb: Run data is saved locally in exp/default/wandb/run-20260420_111259-oibzqe5r
wandb: Run `wandb offline` to turn off syncing.
wandb: Syncing run exp/default
wandb: ⭐️ View project at https://wandb.ai/osom8979-blackhole/pointcept
wandb: 🚀 View run at https://wandb.ai/osom8979-blackhole/pointcept/runs/oibzqe5r
[2026-04-20 11:13:00,771 INFO train.py line 145 221563] => Building train dataset & dataloader ...
[2026-04-20 11:13:00,771 INFO train.py line 147 221563] => Building val dataset & dataloader ...
[2026-04-20 11:13:00,772 INFO train.py line 149 221563] => Building optimize, scheduler, scaler(amp) ...
[2026-04-20 11:13:00,773 INFO train.py line 153 221563] => Building hooks ...
[2026-04-20 11:13:00,773 INFO misc.py line 237 221563] => Loading checkpoint & weight ...
[2026-04-20 11:13:00,773 INFO misc.py line 274 221563] No weight found at: None
[2026-04-20 11:13:00,774 INFO train.py line 161 221563] >>>>>>>>>>>>>>>> Start Training >>>>>>>>>>>>>>>>
[2026-04-20 11:13:03,543 ERROR events.py line 611 221563] Traceback (most recent call last):
  File "/home/zer0/Projects/Pointcept/pointcept/engines/train.py", line 177, in train
    self.run_step()
  File "/home/zer0/Projects/Pointcept/pointcept/engines/train.py", line 205, in run_step
    output_dict = self.model(input_dict)
  File "/home/zer0/Projects/Pointcept/.venv/lib/python3.10/site-packages/torch/nn/modules/module.py", line 1751, in _wrapped_call_impl
    return self._call_impl(*args, **kwargs)
  File "/home/zer0/Projects/Pointcept/.venv/lib/python3.10/site-packages/torch/nn/modules/module.py", line 1762, in _call_impl
    return forward_call(*args, **kwargs)
  File "/home/zer0/Projects/Pointcept/pointcept/models/default.py", line 65, in forward
    point = self.backbone(point)
  File "/home/zer0/Projects/Pointcept/.venv/lib/python3.10/site-packages/torch/nn/modules/module.py", line 1751, in _wrapped_call_impl
    return self._call_impl(*args, **kwargs)
  File "/home/zer0/Projects/Pointcept/.venv/lib/python3.10/site-packages/torch/nn/modules/module.py", line 1762, in _call_impl
    return forward_call(*args, **kwargs)
  File "/home/zer0/Projects/Pointcept/pointcept/models/point_transformer_v3/point_transformer_v3m1_base.py", line 705, in forward
    point = self.enc(point)
  File "/home/zer0/Projects/Pointcept/.venv/lib/python3.10/site-packages/torch/nn/modules/module.py", line 1751, in _wrapped_call_impl
    return self._call_impl(*args, **kwargs)
  File "/home/zer0/Projects/Pointcept/.venv/lib/python3.10/site-packages/torch/nn/modules/module.py", line 1762, in _call_impl
    return forward_call(*args, **kwargs)
  File "/home/zer0/Projects/Pointcept/pointcept/models/modules.py", line 82, in forward
    input = module(input)
  File "/home/zer0/Projects/Pointcept/.venv/lib/python3.10/site-packages/torch/nn/modules/module.py", line 1751, in _wrapped_call_impl
    return self._call_impl(*args, **kwargs)
  File "/home/zer0/Projects/Pointcept/.venv/lib/python3.10/site-packages/torch/nn/modules/module.py", line 1762, in _call_impl
    return forward_call(*args, **kwargs)
  File "/home/zer0/Projects/Pointcept/pointcept/models/modules.py", line 82, in forward
    input = module(input)
  File "/home/zer0/Projects/Pointcept/.venv/lib/python3.10/site-packages/torch/nn/modules/module.py", line 1751, in _wrapped_call_impl
    return self._call_impl(*args, **kwargs)
  File "/home/zer0/Projects/Pointcept/.venv/lib/python3.10/site-packages/torch/nn/modules/module.py", line 1762, in _call_impl
    return forward_call(*args, **kwargs)
  File "/home/zer0/Projects/Pointcept/pointcept/models/point_transformer_v3/point_transformer_v3m1_base.py", line 325, in forward
    point = self.drop_path(self.attn(point))
  File "/home/zer0/Projects/Pointcept/.venv/lib/python3.10/site-packages/torch/nn/modules/module.py", line 1751, in _wrapped_call_impl
    return self._call_impl(*args, **kwargs)
  File "/home/zer0/Projects/Pointcept/.venv/lib/python3.10/site-packages/torch/nn/modules/module.py", line 1762, in _call_impl
    return forward_call(*args, **kwargs)
  File "/home/zer0/Projects/Pointcept/pointcept/models/point_transformer_v3/point_transformer_v3m1_base.py", line 203, in forward
    attn = attn.float()
torch.OutOfMemoryError: CUDA out of memory. Tried to allocate 1.56 GiB. GPU 0 has a total capacity of 7.53 GiB of which 91.12 MiB is free. Including non-PyTorch memory, this process has 7.28 GiB memory in use. Of the allocated memory 6.71 GiB is allocated by PyTorch, and 457.70 MiB is reserved by PyTorch but unallocated. If reserved but unallocated memory is large try setting PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True to avoid fragmentation.  See documentation for Memory Management  (https://pytorch.org/docs/stable/notes/cuda.html#environment-variables)

wandb:
wandb: 🚀 View run exp/default at: https://wandb.ai/osom8979-blackhole/pointcept/runs/oibzqe5r

This is purely a GPU memory shortage. PTv3 base (46M params) + the attn.float() upcast make 8GB tight.

Three things you can try quickly (in order of strength)

① Minimal change

Change only 2 places in configs/weld/semseg-pt-v3m1-0-base.py:

# 메모리 주범: attention upcast 해제 + patch 축소
upcast_attention=False,   # True → False (line 38)
upcast_softmax=False,     # True → False (line 39)
enc_patch_size=(256, 256, 256, 256, 256),   # 1024 → 256
dec_patch_size=(256, 256, 256, 256),

...

# 그리고 transform에서:
dict(type="GridSample", grid_size=0.03, ...),   # 0.02 → 0.03
dict(type="SphereCrop", point_max=40960, ...),  # 102400 → 40960

Environment variables:

PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True \
  PYTHONPATH=$PWD:$PYTHONPATH ./.venv/bin/python tools/train.py \
  --config-file configs/weld/semseg-pt-v3m1-0-base.py --num-gpus 1

② Halve channels and depth (a proper slim PTv3)

enc_depths=(1, 1, 1, 3, 1),
enc_channels=(16, 32, 64, 128, 256),
enc_num_head=(1, 2, 4, 8, 16),
dec_depths=(1, 1, 1, 1),
dec_channels=(32, 32, 64, 128),
dec_num_head=(2, 2, 4, 8),
backbone_out_channels=32,

params reduced to about 11M.

③ Replace the model

SpUNet (~39M, but uses far less memory because it is sparse conv) or LitePT

Model slimming results

Training succeeded, but the results are hard to consider normal.

Still, the whole process is shared here: Pointcept:Examples:Weld:LVS:SetupAndOOM

See also

author Zer0 sitemap