Filters
This page demonstrates practical filtering using the high-level classes from
pchandler.filters. These filters produce boolean masks and provide the
convenience methods sample, reduce, and extract via the
pchandler.core.PointCloudFilter base class.
Quick reference
f.mask(pcd)→ boolean mask (NumPy array of shape(N,))f.sample(pcd)→ returns a newpchandler.core.PointCloudDatawith points where mask is Truef.reduce(pcd)→ in-place reduction ofpcdto points where mask is Truef.extract(pcd)→ returns selected points; the originalpcdis reduced to the complement
Axis-aligned box filtering (Cartesian)
Use pchandler.filters.BoxFilter to keep points inside a 3D axis-aligned box.
The box can be evaluated in the point cloud’s local or global frame.
import numpy as np
from pchandler.core import PointCloudData
from pchandler.filters import BoxFilter
# Synthetic point cloud in [0, 1]^3
pcd = PointCloudData(np.random.rand(50_000, 3), numerical_optimization_shift=None)
# Define a box: [0.2, 0.8] along each axis
minimum = np.array([0.2, 0.2, 0.2], dtype=float)
maximum = np.array([0.8, 0.8, 0.8], dtype=float)
box = BoxFilter(minimum=minimum, maximum=maximum)
# 1) Get a sampled copy containing only inside points
cropped = box.sample(pcd)
# 2) In-place reduction (pcd is modified)
box.reduce(pcd)
# 3) Extract inside points into a new object; 'pcd' becomes the outside points
# Note: after this call, 'pcd' is reduced to points outside the box.
pcd = PointCloudData(np.random.rand(50_000, 3), numerical_optimization_shift=None)
inside = box.extract(pcd)
Range filtering (spherical radius)
Use pchandler.filters.RangeFilter to keep points by radial distance.
For spherical quantities (radius, horizontal, vertical angles) make sure to
define a scan origin (SOCS) so spherical coordinates are meaningful.
import numpy as np
from pchandler.core import PointCloudData
from pchandler.filters import RangeFilter
# Place points roughly within a sphere around the origin
rng = np.random.default_rng(0)
xyz = rng.normal(size=(80_000, 3)).astype(float)
# Provide an SOCS origin so spherical coordinates (r, hz, v) are available
pcd = PointCloudData(xyz, socs_origin=np.zeros(3), numerical_optimization_shift=None)
# Keep points with 0.5 m <= radius <= 1.5 m
rf = RangeFilter(low=0.5, high=1.5)
# Sample a copy using the filter
shell = rf.sample(pcd)
# You can also work with the raw mask if you need to combine filters
mask_r = rf.mask(pcd) # boolean array, shape (N,)
Field-of-view (FoV) clipping (spherical angles)
Use pchandler.filters.FoVFilter to select points by sensor field of view.
This filter evaluates horizontal and vertical angles from spherical coordinates.
import numpy as np
from pchandler.core import PointCloudData
from pchandler.filters import FoVFilter
from pchandler.geometry import FoV # FoV(top, bottom, left, right) in radians
# Example cloud with defined SOCS for spherical coordinates
pcd = PointCloudData(np.random.rand(60_000, 3) - 0.5,
socs_origin=np.zeros(3),
numerical_optimization_shift=None)
# Define a rectangular FoV in spherical angles:
# vertical in [-30°, +10°], horizontal in [-60°, +60°]
fov = FoV(
top=np.deg2rad(10.0),
bottom=np.deg2rad(120.0),
left=np.deg2rad(-60.0),
right=np.deg2rad(60.0),
)
fov_filter = FoVFilter(fov=fov)
in_fov = fov_filter.sample(pcd) # copy containing only points within the FoV
Combining multiple filters
You can intersect, union, or subtract filters by composing their masks before sampling or reducing.
import numpy as np
from pchandler.core import PointCloudData
from pchandler.filters import BoxFilter, RangeFilter
pcd = PointCloudData(np.random.rand(100_000, 3), socs_origin=np.zeros(3), numerical_optimization_shift=None)
# Box in Cartesian space
box = BoxFilter(minimum=np.array([0.1, 0.1, 0.1]), maximum=np.array([0.9, 0.9, 0.9]))
# Radial shell in spherical space
shell = RangeFilter(low=0.4, high=1.0)
# Intersection: inside box AND within radial shell
m = box.mask(pcd) & shell.mask(pcd)
subset = pcd.sample(m)
# Difference: inside box but NOT within the shell
outside_shell = pcd.sample(box.mask(pcd) & ~shell.mask(pcd))
Custom filters
You can implement a custom filter by subclassing
pchandler.core.PointCloudFilter and overriding mask(pcd).
The resulting filter will automatically support sample, reduce, and extract.
import numpy as np
from typing import Any
from pchandler.core import PointCloudData
from pchandler.filters.core import PointCloudFilter
class ZScoreOutlierFilter(PointCloudFilter):
def __init__(self, thresh: float = 3.0):
self.thresh = float(thresh)
def mask(self, pcd: PointCloudData) -> np.ndarray:
# keep points whose per-axis z-score is within +/- thresh
mean = pcd.xyz.mean(axis=0)
std = pcd.xyz.std(axis=0) + 1e-12
z = (pcd.xyz - mean) / std
return np.all(np.abs(z) < self.thresh, axis=1)
# Usage
pcd = PointCloudData(np.random.randn(30_000, 3), numerical_optimization_shift=None)
zf = ZScoreOutlierFilter(thresh=3.0)
filtered = zf.sample(pcd)