Vision¶
Dense-Armor sees a video the way it reads a robot's joints: one frame at a time, in fixed memory, with no pretrained model.
The pages, in order:
- Frames: every source gives the same
Framewith the image and the timestamp. - Features: the oriented gradients, the intensity moments, the colour.
- Flow: Lucas–Kanade in a cell, the pyramid, dense flow, the motion mask.
- Reduce: the random projection, the incremental PCA, the whole chain.
- Motion: the motion energy, the global shift, the scene changes, the sliding window.
- Tracking: the moving object tracker, the video to signal adapters, the joints to video fusion.
- Kinematics: landmarks, the cleaner, the joint angles, the registration.
- Quality: the frame quality, the event frame, the false colour, the overlay, the benchmark.
- Patches: patch memory and the RAD benchmark.
The ideas, and where they come from¶
Every technique on this page has been on someone's desk for decades, and each one answers a question that is easy to ask and surprisingly hard to answer well.
The histogram of oriented gradients was invented in 2005 by Dalal and Triggs, who wanted to find people walking in photographs. Their insight was that a person is recognisable not by the brightness of their clothes but by the directions of the edges on their body. It remains a standard way to describe an image cheaply.
The optical flow came from Lucas and Kanade in 1981. They wanted to know how much a patch of pixels moved from one frame to the next, and they realised that a single pixel is not enough to tell direction, but a small neighbourhood is. The global alternative, Horn and Schunck in the same year, adds a smoothness term over the whole image and solves one large system; Dense-Armor uses Lucas–Kanade because it is local and its cost per cell does not grow with the frame.
The Johnson and Lindenstrauss lemma is from 1984 and it is a statement about a question a pure mathematician would ask: how many dimensions do you really need to keep the distances between a set of points? The answer is surprisingly few, and a random projection is enough to keep them. This page uses that.
Principal component analysis is even older, from Pearson in 1901, and the incremental version used here is a form of Oja's rule from 1982 that learns the answer as data streams in. Krasulina, in 1969, arrived at almost the same rule from a different direction, and the modern analysis in Balsubramani, Dasgupta and Freund (2013) proves how quickly it converges. What all these ideas share is a willingness to throw information away: a frame becomes a hundred numbers, a hundred numbers become four, four become one anomaly score. The trick is doing it without losing what matters, and four decades of mathematics went into making that possible.
API reference¶
streams ¶
Frame streams for the vision module.
A stream is an iterator of :class:Frame objects, one image per sample.
A frame carries a numpy image (float in [0, 1] when grayscale) and a
timestamp t in seconds. Three streams cover the three sources a
robot or a developer has:
- :class:
CameraStreamreads from a camera through OpenCV. OpenCV is an optional dependency (extra[vision]) and imported lazily, so the rest of the vision pipeline works without it. - :class:
ImageFolderStreamreads a folder of image files in sorted order (Pillow). - :class:
ArrayStreamtakes an array of frames already in memory, used by tests and by anyone with the frames at hand.
Every stream yields the same object, :class:Frame, so the rest of the
pipeline is source-agnostic.
Frame
dataclass
¶
One image frame with its timestamp.
Attributes:
| Name | Type | Description |
|---|---|---|
array |
ndarray
|
H x W float array in |
t |
float
|
timestamp in seconds. |
dropped |
int
|
number of frames lost between the previous yielded frame
and this one. |
to_dict ¶
Return a dict with the image and the timestamp.
Returns:
| Type | Description |
|---|---|
dict
|
A dict |
ArrayStream ¶
ArrayStream(
frames: ndarray,
fps: float = 30.0,
size: tuple[int, int] | None = None,
gray: bool = True,
dtype: str = "float32",
)
Iterate over a numpy array of frames.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
frames
|
ndarray
|
array of shape |
required |
fps
|
float
|
frame rate used to set |
30.0
|
size
|
tuple[int, int] | None
|
|
None
|
gray
|
bool
|
convert color frames to grayscale. |
True
|
dtype
|
str
|
target dtype of the output array. |
'float32'
|
Raises:
| Type | Description |
|---|---|
ValueError
|
if |
Examples:
>>> import numpy as np
>>> frames = np.zeros((3, 4, 4), dtype=np.float32)
>>> s = ArrayStream(frames, fps=10.0)
>>> len(s)
3
>>> f = next(iter(s))
>>> f.array.shape, round(f.t, 3)
((4, 4), 0.0)
>>> frames16 = np.array([[[0, 1000], [60000, 65535]]], dtype=np.uint16)
>>> s16 = ArrayStream(frames16, dtype="uint16")
>>> next(iter(s16)).array.tolist()
[[0, 1000], [60000, 65535]]
Source code in dense_armor/utility/vision/streams.py
ImageFolderStream ¶
ImageFolderStream(
path: str | Path | Sequence[str | Path],
fps: float | None = None,
size: tuple[int, int] | None = None,
gray: bool = True,
)
Iterate over image files in a folder.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str | Path | Sequence[str | Path]
|
folder path, or a sequence of file paths. |
required |
fps
|
float | None
|
if given, |
None
|
size
|
tuple[int, int] | None
|
|
None
|
gray
|
bool
|
convert to grayscale. |
True
|
Raises:
| Type | Description |
|---|---|
ValueError
|
if the folder contains no image file. |
Examples:
>>> import tempfile, pathlib
>>> import numpy as np
>>> from PIL import Image
>>> d = tempfile.mkdtemp()
>>> for i in range(2):
... _ = Image.fromarray(
... (np.zeros((4, 4), dtype=np.uint8) + i)
... ).save(pathlib.Path(d) / f"{i:03d}.png")
>>> s = ImageFolderStream(d, fps=2.0)
>>> len(s)
2
>>> round(next(iter(s)).t, 3)
0.0
Source code in dense_armor/utility/vision/streams.py
CameraStream ¶
CameraStream(
device: int = 0,
fps: float = 30.0,
size: tuple[int, int] | None = (160, 120),
gray: bool = True,
max_frames: int | None = None,
)
Iterate over camera frames through OpenCV.
OpenCV is imported lazily inside __init__. If it is not
installed, the constructor raises :class:ImportError with a clear
message. OpenCV delivers color frames in BGR order; they are turned
to RGB before the grayscale conversion.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
device
|
int
|
camera index (default |
0
|
fps
|
float
|
requested frame rate, in Hz, passed to the camera. Must be positive. |
30.0
|
size
|
tuple[int, int] | None
|
|
(160, 120)
|
gray
|
bool
|
convert to grayscale. |
True
|
max_frames
|
int | None
|
stop after this many frames ( |
None
|
Raises:
| Type | Description |
|---|---|
ImportError
|
if OpenCV is not installed. |
ValueError
|
if |
Source code in dense_armor/utility/vision/streams.py
VideoStream ¶
VideoStream(
fps: float = 30.0,
size: tuple[int, int] | None = None,
gray: bool = True,
dtype: str = "float32",
start: int = 0,
stop: int | None = None,
stride: int = 1,
)
Common interface of the frame sources.
Every video source in the library is an iterator of :class:Frame
and exposes the same knobs: an optional resize, an optional grayscale
conversion, a target dtype, a start and stop index, a
stride to read one frame every k. The base class does not
read anything; it declares the interface that :class:ArrayStream,
:class:ImageFolderStream, :class:CameraStream,
:class:VideoFileStream and :class:CubeStream implement.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
fps
|
float
|
frame rate used when the source has no timestamp of its own. Must be positive. |
30.0
|
size
|
tuple[int, int] | None
|
|
None
|
gray
|
bool
|
convert color frames to grayscale. |
True
|
dtype
|
str
|
target dtype of the output array; see :class: |
'float32'
|
start
|
int
|
first frame index to yield. |
0
|
stop
|
int | None
|
last frame index to yield, exclusive, or |
None
|
stride
|
int
|
yield one frame every |
1
|
Raises:
| Type | Description |
|---|---|
ValueError
|
on a bad |
Source code in dense_armor/utility/vision/streams.py
VideoFileStream ¶
VideoFileStream(
source: str | Path | Sequence[str | Path],
fps: float = 30.0,
size: tuple[int, int] | None = None,
gray: bool = True,
dtype: str = "float32",
start: int = 0,
stop: int | None = None,
stride: int = 1,
max_frames: int | None = None,
rotate: str | int | float | bool = "auto",
follow: bool = False,
wait_s: float = 0.05,
max_waits: int | None = 200,
)
Bases: VideoStream
Read a video from a file, an URL, a glob of files or a network stream.
OpenCV is imported lazily inside __init__, so a session without it
can still import the module and use the numpy-only streams. The
timestamp of each frame is the container's own clock, read from the
decoder in milliseconds and converted to seconds; it is not
frame_index / fps, so a video whose frame rate varies with time
keeps the true time of every frame. A frame that the reader skips
because the analysis is slower than the source is counted in
:attr:Frame.dropped of the next yielded frame.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
str | Path | Sequence[str | Path]
|
a file path, an URL ( |
required |
fps
|
float
|
fallback rate used only if the container does not report
its own frame rate; it is never used to set |
30.0
|
size
|
tuple[int, int] | None
|
|
None
|
gray
|
bool
|
convert color frames to grayscale. |
True
|
dtype
|
str
|
target dtype; see :class: |
'float32'
|
start
|
int
|
first frame index. |
0
|
stop
|
int | None
|
last frame index, exclusive. |
None
|
stride
|
int
|
one frame every this many. |
1
|
max_frames
|
int | None
|
stop after this many yielded frames ( |
None
|
rotate
|
str | int | float | bool
|
|
'auto'
|
follow
|
bool
|
if |
False
|
wait_s
|
float
|
seconds to sleep between retries when |
0.05
|
max_waits
|
int | None
|
maximum number of consecutive retries before giving
up ( |
200
|
Raises:
| Type | Description |
|---|---|
ImportError
|
if OpenCV is not installed. |
FileNotFoundError
|
if |
ValueError
|
on bad parameters. |
Examples:
>>> import tempfile, pathlib, math
>>> import numpy as np
>>> import cv2
>>> d = pathlib.Path(tempfile.mkdtemp()) / "clip.mp4"
>>> w = cv2.VideoWriter(
... str(d), cv2.VideoWriter_fourcc(*"mp4v"), 10.0, (8, 8)
... )
>>> for i in range(5):
... frame = np.full((8, 8, 3), i * 50, dtype=np.uint8)
... _ = w.write(frame)
>>> w.release()
>>> s = VideoFileStream(str(d))
>>> frames = list(s)
>>> len(frames)
5
>>> round(frames[0].t, 3)
0.0
>>> round(frames[-1].t, 3)
0.4
Source code in dense_armor/utility/vision/streams.py
CubeStream ¶
CubeStream(
cube: ndarray,
fps: float = 30.0,
size: tuple[int, int] | None = None,
gray: bool = False,
dtype: str = "float32",
start: int = 0,
stop: int | None = None,
stride: int = 1,
max_frames: int | None = None,
)
Bases: VideoStream
Iterate over a multi-channel cube (T, H, W, C).
A cube is the format of a thermal sequence (one channel), a
multispectral or hyperspectral stack (many channels), a depth map
(one channel of floats) and, in general, of any video whose channel
count is not three. The time of the frame i is i / fps; the
cube carries no clock of its own.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cube
|
ndarray
|
array of shape |
required |
fps
|
float
|
frame rate; sets |
30.0
|
size
|
tuple[int, int] | None
|
|
None
|
gray
|
bool
|
reduce a 3-channel frame to grayscale; a cube with a channel count different from 3 is left untouched. |
False
|
dtype
|
str
|
target dtype; |
'float32'
|
start
|
int
|
first frame index. |
0
|
stop
|
int | None
|
last frame index, exclusive. |
None
|
stride
|
int
|
one frame every this many. |
1
|
max_frames
|
int | None
|
stop after this many yielded frames. |
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
on a cube whose first dimension is not the time, or on bad parameters. |
Examples:
>>> import numpy as np
>>> cube = np.arange(2 * 3 * 4, dtype=np.uint16).reshape(2, 3, 4)
>>> s = CubeStream(cube, fps=2.0, dtype="uint16", gray=False)
>>> frames = list(s)
>>> len(frames)
2
>>> frames[0].array.dtype
dtype('uint16')
>>> round(frames[1].t, 3)
0.5
Source code in dense_armor/utility/vision/streams.py
features ¶
Per-frame features: oriented gradients, moments, and short-range flow.
A single frame on its own carries little motion information, but three families of descriptors are cheap, local, and enough to tell a moving square from a still one:
- a histogram of oriented gradients (HOG) on a fixed grid of cells.
The image is divided into
cellsrectangles; inside each cell the gradient orientation of every pixel votes into one ofn_binsangular bins, weighted by the gradient magnitude. The cell histogram is normalised to unit L2 norm, so a change in brightness does not change the descriptor. The construction is the one in Huang and Huang (2017), section 2. - three global moments of the intensity: mean, standard deviation (the
contrast), and the centroid of the intensity, which is where the
"mass" of the image is. With
gray=Falsethe same two moments are computed for each colour channel separately, and three extra pairs of keys appear. - short-range optical flow against the previous frame, on a coarser
grid: in every flow cell the classic Lucas and Kanade (1981) least
squares is solved, as written in Ziani (2025), equations 4 and 5.
With
flow_levels > 1the flow is estimated coarse-to-fine on a pyramid (each level a 2x2 average of the one below; Ziani uses Gaussian pyramids) with bilinear interpolation between levels, the scheme of Ziani (2025), section 3.2, applied here to Lucas-Kanade.
The first frame has no previous, so its flow block is zero. A frame
that is empty, has a wrong shape, or contains a non-finite value is
skipped and counted in n_missing.
FrameFeatures ¶
FrameFeatures(
n_bins: int = 9,
cells: tuple[int, int] = (3, 4),
flow_cells: tuple[int, int] = (2, 3),
flow_levels: int = 1,
gray: bool = True,
eps: float = 1e-08,
)
Bases: Transformer
Turn one frame into a fixed-length dict of float features.
Keys of the returned dict (all float):
hog_{cy}_{cx}_{b}: the L2-normalised HOG histogram of cell(cy, cx), one value per angular binb. There arecells[0] * cells[1]cells andn_binsbins per cell.mean,contrast: the mean and the standard deviation of the luminance.centroid_x,centroid_y: the coordinates of the luminance centroid, normalised in[0, 1].flow_u_{fy}_{fx},flow_v_{fy}_{fx}: the mean optical flow components inside the coarse flow cell(fy, fx).flow_mag_{fy}_{fx}: the magnitude of the same mean flow.- if
gray=False, six extra keysmean_R,mean_G,mean_B,contrast_R,contrast_G,contrast_Bfor the per-channel moments.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_bins
|
int
|
number of angular bins in each cell. Default 9, the value used in Huang and Huang (2017). |
9
|
cells
|
tuple[int, int]
|
|
(3, 4)
|
flow_cells
|
tuple[int, int]
|
|
(2, 3)
|
flow_levels
|
int
|
number of pyramid levels for the optical flow.
Default |
1
|
gray
|
bool
|
|
True
|
eps
|
float
|
numerical guard for the normalisation and the least squares. |
1e-08
|
Raises:
| Type | Description |
|---|---|
ValueError
|
if |
Examples:
>>> import numpy as np
>>> from dense_armor.utility.vision.features import FrameFeatures
>>> ff = FrameFeatures()
>>> frame = np.zeros((6, 8), dtype=np.float32)
>>> out = ff.transform_one(frame)
>>> out["mean"], out["contrast"]
(0.0, 0.0)
Source code in dense_armor/utility/vision/features.py
learn_one ¶
Store the current frame so the next one can compute flow.
Source code in dense_armor/utility/vision/features.py
transform_one ¶
Return the feature dict of one frame.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
Any
|
a Frame, a dict with a |
required |
t
|
float | None
|
unused, kept for the interface. |
None
|
Returns:
| Type | Description |
|---|---|
dict[str, float]
|
A flat dict of float features. |
Source code in dense_armor/utility/vision/features.py
reduce ¶
Dimensionality reduction for vision features.
Two transformers, both in the spirit of the library: they read a dict of floats and return a smaller dict of floats, one sample at a time.
- :class:
RandomProjectiondraws a fixed random matrix once, in the constructor, and multiplies every incoming vector by it. The Johnson and Lindenstrauss (1984) lemma guarantees that the pairwise distances are preserved up to a factor(1 + eps)with high probability, provided the output dimensionkis large enough. The version used here follows Ghojogh et al. (2021), section 2.2: the elements of the matrix are i.i.d. Gaussian with mean zero and variance1 / k, so the expected squared norm of a projected vector equals the squared norm of the original one. The distortion bound is equation 21 of that paper, withdelta = 2 exp(-(eps^2 - eps^3) k / 4). -
:class:
IncrementalPCAlearns the topkdirections of the data as they stream in, with Oja's rule generalised tokcomponents. The update follows the one-component form in Balsubramani, Dasgupta, Freund (2013), equation (1),v_n = v_{n-1} + gamma_n (X_n X_n^T - (v_{n-1}^T X_n X_n^T v_{n-1}) I_d) v_{n-1},
For k > 1 the matrix V (d x k) is updated with the same
step, V + gamma_n z (V^T z)^T, and re-orthonormalised with a QR
decomposition after each sample: a stochastic subspace (power)
iteration. The QR makes the correction term of Oja's rule redundant,
so the multi-component case is this iteration, not a separate rule
from the paper. The running mean is removed from the input first. The step size is gamma_n = c / n with c
chosen by the user or by the default rule. The paper analyses the rate
of convergence: the potential 1 - (v . v*)^2 / |v|^2 goes to zero
as O(1 / n) when c is at least 1 / (2 (lambda_1 - lambda_2)).
RandomProjection ¶
Bases: Transformer
Fixed Gaussian random projection to k dimensions.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
k
|
int
|
output dimension. Must be positive. |
required |
seed
|
int | None
|
seed of the random generator, so a run is reproducible. |
None
|
input_dim
|
int | None
|
number of input features. If |
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
if |
Examples:
>>> import numpy as np
>>> from dense_armor.utility.vision.reduce import RandomProjection
>>> rp = RandomProjection(k=3, seed=0)
>>> x = {f"f{i}": float(i) for i in range(6)}
>>> out = rp.transform_one(x)
>>> len(out)
3
>>> sorted(out)
['p0', 'p1', 'p2']
Source code in dense_armor/utility/vision/reduce.py
learn_one ¶
No learning: the projection is fixed at construction.
Source code in dense_armor/utility/vision/reduce.py
transform_one ¶
Project one feature dict onto the random matrix.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
dict
|
dict of float features. |
required |
t
|
float | None
|
unused, kept for the interface. |
None
|
Returns:
| Type | Description |
|---|---|
dict[str, float]
|
A dict with keys |
Source code in dense_armor/utility/vision/reduce.py
IncrementalPCA ¶
Bases: Transformer
Online PCA: Oja's rule for one component, stochastic subspace iteration with QR for several.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
k
|
int
|
number of components. |
required |
lr
|
float | None
|
if given, the constant |
None
|
eps
|
float
|
numerical guard. |
1e-09
|
Raises:
| Type | Description |
|---|---|
ValueError
|
if |
Examples:
>>> import numpy as np
>>> from dense_armor.utility.vision.reduce import IncrementalPCA
>>> rng = np.random.default_rng(0)
>>> pca = IncrementalPCA(k=1)
>>> for _ in range(200):
... _ = pca.learn_one({"a": float(rng.normal(1, 0.1)),
... "b": float(rng.normal(0, 1))})
>>> out = pca.transform_one({"a": 1.0, "b": 0.0})
>>> len(out)
1
Source code in dense_armor/utility/vision/reduce.py
learn_one ¶
Update the components with one sample.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
dict
|
dict of float features. |
required |
y
|
Any
|
unused. |
None
|
t
|
float | None
|
unused. |
None
|
Returns:
| Type | Description |
|---|---|
IncrementalPCA
|
|
Source code in dense_armor/utility/vision/reduce.py
transform_one ¶
Project one feature dict onto the current components.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
dict
|
dict of float features. |
required |
t
|
float | None
|
unused, kept for the interface. |
None
|
Returns:
| Type | Description |
|---|---|
dict[str, float]
|
A dict with keys |
Source code in dense_armor/utility/vision/reduce.py
Details¶
- Everything on this page is computed by the library in numpy; the camera
driver (OpenCV) is the only optional dependency, extra
[vision], imported only byCameraStream. FrameFeatureskeys:hog_{cy}_{cx}_{b}(default 3×4 cells, 9 bins),mean,contrast,centroid_x,centroid_y,flow_u_*,flow_v_*,flow_mag_*(default 2×3 flow cells). The first frame has zero flow; an empty or non-finite frame is skipped and counted inn_missing.RandomProjectionuses a dense Gaussian matrix with variance1 / k(the matrix of equation 21). Sparse projections (Achlioptas 2003, Li et al. 2006) are named in the same survey but not defined there; they are not implemented yet.IncrementalPCAwith several components updates all of them with the same step and re-orthonormalises with a QR decomposition after each sample (a stochastic subspace iteration); the paper analyses the one-component rule.- Pipelines built with
|train every step: each intermediate step first transforms the sample, then learns from it, soFrameFeatureskeeps the previous frame for the flow. flow_levels: the pyramid is built by 2×2 averaging; Ziani (2025, section 3.2) builds Gaussian pyramids and uses the coarse-to-fine scheme for Horn–Schunck; the library applies the same scheme to Lucas–Kanade.flow_levels=1gives exactly the single-level flow.gray=False: keysmean_R,mean_G,mean_B,contrast_R,contrast_G,contrast_Bare always present; for a grey input they repeat the luminance moments.- Real robot images, RAD (Chang et al. 2024; MIT licence): 13 objects photographed by a Franka
arm from many viewpoints, normal and defective.
benchmarks/vision_rad.pylearnsFrameFeatures | IncrementalPCA(k=8) | OnlineRobustMahalanobison the normal training views and scores each test image (160×120, grey, same parameters for every category, no tuning). Image-level AUROC: binderclip 0.531, bowl 0.461, box 0.428, can 0.599, charger 0.508, cup1 0.443, cup2 0.488, gluebottle 0.492, phonecase 0.458, rubberduck 0.552, spoon 0.569, spraybottle 0.464, tennisball 0.473; mean 0.497, chance level. The paper reports 0.833 for PatchCore and 0.803 for EfficientAD (p. 10), both built on pretrained networks. Global features (gradients, moments) notice changes of the scene; they do not separate a small defect from a change of viewpoint. Each category has only 9–27 normal test images, so the per-category values are uncertain; the mean is the number to read. Latency 4.5 ms p50 per image (score only). - Sources:
- Chang, X. et al. (2024). RAD: A Realistic Multi-View Benchmark for Pose-Agnostic Anomaly Detection. arXiv:2410.00713. The robot images of the Details.
- Huang, C., Huang, J. (2017). A fast HOG descriptor using lookup table and integral image. arXiv:1703.06256. The HOG construction used in section 2.
- Dalal, N., Triggs, B. (2005). Histograms of oriented gradients for human detection. In IEEE Conference on Computer Vision and Pattern Recognition. The original HOG paper, cited in the opening.
- Ziani, H. (2025). Investigating optical flow computation: from local methods to a multiresolution Horn–Schunck implementation. arXiv:2511.16535. The Lucas–Kanade least squares of section 4, the bilinear interpolation of section 4b, and the coarse-to-fine scheme.
- Lucas, B. D., Kanade, T. (1981). An iterative image registration technique with an application to stereo vision. In Proceedings of the 7th International Joint Conference on Artificial Intelligence. The original Lucas–Kanade algorithm.
- Horn, B. K., Schunck, B. G. (1981). Determining optical flow. Artificial Intelligence 17(1-3), 185-203. The global method mentioned at the end of section 4.
- Johnson, W. B., Lindenstrauss, J. (1984). Extensions of Lipschitz mappings into a Hilbert space. Contemporary Mathematics 26. The distance-preservation lemma of section 5.
- Achlioptas, D. (2003). Database-friendly random projections: Johnson–Lindenstrauss with binary coins. Journal of Computer and System Sciences 66(4), 671-687. The sparse projection matrix named in the Details.
- Li, P., Hastie, T. J., Church, K. W. (2006). Very sparse random projections. In Proceedings of the 12th ACM SIGKDD International Conference on Knowledge Discovery and Data Mining. The sparser alternative named in the Details.
- Ghojogh, B., Ghodsi, A., Karray, F., Crowley, M. (2021). Johnson–Lindenstrauss lemma, linear and nonlinear random projections, random Fourier features, and random kitchen sinks: tutorial and survey. arXiv:2108.04172. The bound of section 5, equation 21.
- Pearson, K. (1901). On lines and planes of closest fit to systems of points in space. Philosophical Magazine 2(11), 559-572. The origin of principal component analysis.
- Krasulina, T. P. (1969). A method of stochastic approximation for the determination of the least eigenvalue of a symmetrical matrix. USSR Computational Mathematics and Mathematical Physics 9(6), 189-195. One of the two classical one-component rules.
- Oja, E. (1982). Simplified neuron model as a principal component analyzer. Journal of Mathematical Biology 15(3), 267-273. The other classical one-component rule.
- Balsubramani, A., Dasgupta, S., Freund, Y. (2013). The fast convergence of incremental PCA. In Advances in Neural Information Processing Systems (NIPS), arXiv:1501.03796. The convergence rate of section 6, equation 1.
- Guillot, A. et al. (2026). Online robust covariance and outlier detection. arXiv:2601.03957. The robust Mahalanobis detector used in the last step of section 7.
- Cardot, H., Cénac, P., Zitt, P.-A. (2013). Efficient and fast estimation of the geometric median in Hilbert spaces with an averaged stochastic gradient algorithm. Bernoulli 19(1), 18-43. The geometric median update of section 7.
- Cardot, H., Godichon-Baggioni, A. (2017). Fast estimation of the median covariation matrix with application to online robust principal components analysis. TEST 26(3), 461-480. The median covariation matrix update of section 7.