<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>3D Vision on Boris Dupoux</title><link>https://borisdupoux.com/categories/3d-vision/</link><description>Recent content in 3D Vision on Boris Dupoux</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Thu, 01 Oct 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://borisdupoux.com/categories/3d-vision/index.xml" rel="self" type="application/rss+xml"/><item><title>Handheld Video to Photogrammetry and Gaussian Splatting</title><link>https://borisdupoux.com/research/photogrammetry-splatting/</link><pubDate>Thu, 01 Oct 2026 00:00:00 +0000</pubDate><guid>https://borisdupoux.com/research/photogrammetry-splatting/</guid><description>&lt;h2 id="context"&gt;&lt;a href="#context" class="header-anchor"&gt;&lt;/a&gt;Context
&lt;/h2&gt;&lt;p&gt;Indoor sites are often documented with a handheld gimbal camera rather than a
survey-grade scanner. The goal was to turn such video into usable 3D deliverables
(dense cloud, textured mesh, plan and elevation orthoviews, a Gaussian Splatting
scene and lightweight web exports) with no manual steps between capture and output.
The pipeline runs natively on Windows on an RTX 5090 (Blackwell, sm_120) that is
&lt;strong&gt;shared with other long-running GPU workloads&lt;/strong&gt;, so it must never pre-empt them.&lt;/p&gt;
&lt;h2 id="approach"&gt;&lt;a href="#approach" class="header-anchor"&gt;&lt;/a&gt;Approach
&lt;/h2&gt;&lt;pre class="mermaid" style="visibility:hidden"&gt;flowchart TB
 A["inbox/project/videos + GO file"] --&gt; B["Keyframe extraction&lt;br/&gt;optical-flow overlap + sharpness"]
 B --&gt; C["COLMAP 4.2.1 SfM&lt;br/&gt;SIFT, global mapper"]
 C --&gt; D{"QC: registration, reprojection,&lt;br/&gt;trajectory continuity"}
 D -- fail --&gt; C2["Fallback: incremental mapper"] --&gt; D
 D -- pass --&gt; E["OpenMVS 2.4 (sm_120 build)&lt;br/&gt;densify, mesh, refine, texture"]
 E --&gt; F["Ortho alignment&lt;br/&gt;gravity, floor RANSAC, Manhattan yaw"]
 F --&gt; G["Blender renders&lt;br/&gt;orthos, axonometries, orbit"]
 E --&gt; H["gsplat training&lt;br/&gt;MCMC or default strategy"]
 G --&gt; I["Web exports&lt;br/&gt;Potree, GLB, SOG"]
 H --&gt; I
 I --&gt; J["outbox: deliverables, report.json, STATUS"]&lt;/pre&gt;&lt;h2 id="methods"&gt;&lt;a href="#methods" class="header-anchor"&gt;&lt;/a&gt;Methods
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Overlap-driven keyframe selection.&lt;/strong&gt; KLT tracking with a RANSAC similarity
transform estimates overlap with the last keyframe. A frame is emitted when overlap
drops below 0.80, tracking is lost or a time limit passes. The sharpest candidate
(variance of the Laplacian) is the one kept.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SfM with explicit QC.&lt;/strong&gt; COLMAP&amp;rsquo;s global mapper is fast but non-deterministic.
In testing it produced reconstructions that met registration and reprojection
criteria but were geometrically incoherent. A trajectory-continuity check (jumps
above 25× the median camera step) triggers an automatic fallback to the
incremental mapper. Below 20 points per image the step fails.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MVS hardening.&lt;/strong&gt; The official OpenMVS Windows binary has only sm_89 kernels, so
it was rebuilt for sm_120. The object ROI is off for rooms because it cropped the
cloud. Refinement is dropped when it diverges, and seam leveling (which blackened
faces) is off.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Metric-ready orthoviews.&lt;/strong&gt; The up axis comes from camera orientations. A RANSAC
floor plane sets z = 0 and a histogram of wall-normal azimuths (mod 90°) sets
Manhattan yaw. An optional two-point reference fixes scale. The 4×4 transform is
stored so it can be applied again to the mesh and the splat.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Gaussian Splatting.&lt;/strong&gt; gsplat 1.6.0 was compiled for sm_120 with CUDA 13 under
MSVC, which required local patches. Training uses the MCMC strategy with a
Gaussian budget and falls back to the default 3DGS strategy when MCMC diverges.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Operations.&lt;/strong&gt; Projects start in watch-folder mode when a GO file appears.
Stages can be resumed or redone. A GPU guard waits until enough VRAM has been
free for a stable period, serialises GPU stages through a lock file and records
peak VRAM per stage in &lt;code&gt;report.json&lt;/code&gt;. Degraded QC produces a &lt;code&gt;DONE_WITH_ALERTS&lt;/code&gt;
status instead of a silent success.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="results"&gt;&lt;a href="#results" class="header-anchor"&gt;&lt;/a&gt;Results
&lt;/h2&gt;&lt;p&gt;First real deployment: five locations inside a commercial showroom, one 4K
handheld video per location, processed unattended in about 30 minutes per location.&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;Location&lt;/th&gt;
					&lt;th&gt;Images&lt;/th&gt;
					&lt;th&gt;SfM registered / points / reproj.&lt;/th&gt;
					&lt;th&gt;Dense cloud&lt;/th&gt;
					&lt;th&gt;Splat PSNR / SSIM&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;1&lt;/td&gt;
					&lt;td&gt;116&lt;/td&gt;
					&lt;td&gt;116/116 · 21,458 · 1.49 px&lt;/td&gt;
					&lt;td&gt;1.40 M&lt;/td&gt;
					&lt;td&gt;23.2 / 0.86&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;2&lt;/td&gt;
					&lt;td&gt;103&lt;/td&gt;
					&lt;td&gt;103/103 · 18,972 · 1.75 px&lt;/td&gt;
					&lt;td&gt;1.23 M&lt;/td&gt;
					&lt;td&gt;20.6 / 0.81&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;3&lt;/td&gt;
					&lt;td&gt;47&lt;/td&gt;
					&lt;td&gt;47/47 · 13,167 · 1.17 px&lt;/td&gt;
					&lt;td&gt;1.26 M&lt;/td&gt;
					&lt;td&gt;23.1 / 0.86&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;4&lt;/td&gt;
					&lt;td&gt;121&lt;/td&gt;
					&lt;td&gt;121/121 · 11,970 · 1.34 px&lt;/td&gt;
					&lt;td&gt;1.99 M&lt;/td&gt;
					&lt;td&gt;21.9 / 0.85 (default strategy)&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;5&lt;/td&gt;
					&lt;td&gt;64&lt;/td&gt;
					&lt;td&gt;64/64 · 12,106 · 1.48 px&lt;/td&gt;
					&lt;td&gt;0.91 M&lt;/td&gt;
					&lt;td&gt;21.8 / 0.83&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;All frames were registered in every location. Reprojection error was 1.17–1.75 px.&lt;/li&gt;
&lt;li&gt;Measured VRAM peaks: SIFT +1.3 GB, matching +1.6 GB, texturing +0.5 GB,
Blender +5.4 GB, and splatting with 3 M Gaussians at 1920 px +25.2 GB.&lt;/li&gt;
&lt;li&gt;The sm_120 OpenMVS build densified the synthetic smoke test in 17 s. The CPU
fallback took about 40 min.&lt;/li&gt;
&lt;li&gt;Metadata analysis showed shutter speeds of 1/50–1/100 s (about 14 px of motion
blur at 4K) and automatic ISO and white balance. Exposure compensation in gsplat
(&lt;code&gt;ppisp&lt;/code&gt;) gave no gain (PSNR 20.5 vs 20.6). The quality limit is in the capture,
not the solver.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="limitations--next-steps"&gt;&lt;a href="#limitations--next-steps" class="header-anchor"&gt;&lt;/a&gt;Limitations &amp;amp; next steps
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;PSNR in the low 20s reflects motion blur and auto-exposure. The capture protocol
now requires a 1/250–1/500 s shutter, locked exposure and white balance, and a
dedicated floor pass.&lt;/li&gt;
&lt;li&gt;Merging videos from different locations caused false matches and an incoherent
model, so each location is processed separately.&lt;/li&gt;
&lt;li&gt;Dark glossy tiled floors reconstruct poorly. Manhattan alignment is not yet
reliable with furniture and glazing.&lt;/li&gt;
&lt;li&gt;CPU stages are the bottleneck on a 4-vCPU virtual machine.&lt;/li&gt;
&lt;li&gt;Planned: comparison with a commercial photogrammetry suite on the same frames, and
retesting mesh refinement with the sm_120 build.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="stack"&gt;&lt;a href="#stack" class="header-anchor"&gt;&lt;/a&gt;Stack
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;Python 3.11&lt;/code&gt; · &lt;code&gt;OpenCV&lt;/code&gt; · &lt;code&gt;Open3D&lt;/code&gt; · &lt;code&gt;NumPy&lt;/code&gt; · &lt;code&gt;COLMAP 4.2.1&lt;/code&gt; · &lt;code&gt;OpenMVS 2.4&lt;/code&gt; · &lt;code&gt;gsplat 1.6.0&lt;/code&gt; · &lt;code&gt;PyTorch 2.14 (CUDA 13)&lt;/code&gt; · &lt;code&gt;Blender 5.2&lt;/code&gt; · &lt;code&gt;FFmpeg&lt;/code&gt; · &lt;code&gt;PotreeConverter&lt;/code&gt; · &lt;code&gt;glTF-Transform&lt;/code&gt; · &lt;code&gt;splat-transform&lt;/code&gt;&lt;/p&gt;</description></item><item><title>Geolocated Video Analysis and Inter-Object Distance Measurement</title><link>https://borisdupoux.com/research/video-geolocation/</link><pubDate>Thu, 01 Jan 2026 00:00:00 +0000</pubDate><guid>https://borisdupoux.com/research/video-geolocation/</guid><description>&lt;h2 id="context"&gt;&lt;a href="#context" class="header-anchor"&gt;&lt;/a&gt;Context
&lt;/h2&gt;&lt;p&gt;Industrial sites are often covered by fixed cameras whose footage is only reviewed after an incident and yields no usable data. The aim was to turn existing cameras, and occasional drone footage, into a measuring instrument: for every detected object, a latitude/longitude position and metric distances rather than a pixel box.&lt;/p&gt;
&lt;p&gt;This is hard for three reasons. A single camera gives no depth, so scene scale must be inferred. The usual georeferencing method, a ground-plane homography, assumes a flat floor and fails silently for raised objects or uneven terrain. And high-resolution video is expensive, while the common shortcut of downscaling to 640 px discards the geometric detail ground positions depend on.&lt;/p&gt;
&lt;h2 id="approach"&gt;&lt;a href="#approach" class="header-anchor"&gt;&lt;/a&gt;Approach
&lt;/h2&gt;&lt;pre class="mermaid" style="visibility:hidden"&gt;flowchart TB
 A["Video source&lt;br/&gt;file · RTSP · drone clip (up to 3840×2160)"] --&gt; B["Object detector at native resolution&lt;br/&gt;detect · segment · pose · OBB"]
 A --&gt; C["Metric monocular depth model&lt;br/&gt;depth map"]
 B --&gt; D["Multi-object tracker&lt;br/&gt;motion model + association"]
 B --&gt; E["Per-object depth&lt;br/&gt;mask pixels, outlier rejection"]
 C --&gt; E
 E --&gt; F["Ray casting&lt;br/&gt;pixel ray + metric depth + camera pose"]
 F --&gt; G["WGS84 position&lt;br/&gt;optional DEM for ground elevation"]
 E --&gt; H["3D distances&lt;br/&gt;between masks · along trajectories"]
 D --&gt; H
 G --&gt; I["SQLite store&lt;br/&gt;detections · tracks · GPS trajectories"]
 H --&gt; I
 I --&gt; J["Analytics&lt;br/&gt;zones · flows · orthophoto overlay"]&lt;/pre&gt;&lt;h2 id="methods"&gt;&lt;a href="#methods" class="header-anchor"&gt;&lt;/a&gt;Methods
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Detection.&lt;/strong&gt; A YOLO-family detector, at several model sizes, in four modes (detect, segment, pose, oriented boxes). Inference runs at native resolution with no forced &lt;code&gt;imgsz&lt;/code&gt;. Segmentation is the default because masks give cleaner depth samples than boxes. PyTorch, ONNX, TensorRT and CoreML weights are accepted (CUDA and Apple MPS).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Monocular depth.&lt;/strong&gt; A metric monocular depth model predicts a depth map, which is resampled to the frame size. Each object&amp;rsquo;s depth is a robust estimate over its mask pixels, after outlier rejection. A validation step compares predicted depth with reference zones whose distance is known, to detect calibration drift.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Geolocation by ray casting.&lt;/strong&gt; A ray is cast from the camera centre through the object&amp;rsquo;s pixel and stopped at the object&amp;rsquo;s metric depth. The resulting point is converted from the camera frame to a local North-East-Down frame, then to WGS84. Unlike a homography, this does not assume a flat ground. An optional DEM supplies ground elevation. Positions can be overlaid on a georeferenced orthophoto for visual checks.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tracking.&lt;/strong&gt; A custom multi-object tracker with a motion model, confidence-aware association and a track-fusion step that links fragments using gap, heading and speed limits. When depth is available, each track stores a 3D trajectory, from which the distance travelled and the speed in m/s are derived.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Inter-object distances.&lt;/strong&gt; Later consolidated into a reusable vision library. Two segmented objects are back-projected into 3D, and the minimum point-to-point distance is reported. Overlapping masks are treated as contact. A Monte Carlo simulation perturbs depth, focal length and pixel positions to give a standard deviation and a 95 % interval. Zone membership uses the share of the mask inside the zone, with hysteresis, rather than a centre-point test.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Engineering.&lt;/strong&gt; The configuration supports a two-GPU split (detection on one device, depth estimation on the other), RTSP discovery and recording, and processing of several cameras in parallel.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="results"&gt;&lt;a href="#results" class="header-anchor"&gt;&lt;/a&gt;Results
&lt;/h2&gt;&lt;p&gt;Measured on a single NVIDIA RTX 5090. The detection benchmark covers detection and tracking only, with depth disabled.&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;Setting&lt;/th&gt;
					&lt;th&gt;Video&lt;/th&gt;
					&lt;th&gt;Throughput&lt;/th&gt;
					&lt;th&gt;Output&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;Largest detector, batch 1&lt;/td&gt;
					&lt;td&gt;3840×2160, 50 fps, 3,301 frames&lt;/td&gt;
					&lt;td&gt;5.1 fps&lt;/td&gt;
					&lt;td&gt;320,217 detections, 1,941 tracks&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Smallest detector, batch 1&lt;/td&gt;
					&lt;td&gt;3840×2160, 50 fps&lt;/td&gt;
					&lt;td&gt;13.0 fps&lt;/td&gt;
					&lt;td&gt;158,989 detections, 908 tracks&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Largest detector, batch 1&lt;/td&gt;
					&lt;td&gt;2304×1296, 15 fps, 5,000 frames&lt;/td&gt;
					&lt;td&gt;17.9 fps&lt;/td&gt;
					&lt;td&gt;73,502 detections, 2,706 tracks&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Smallest detector, batch 1&lt;/td&gt;
					&lt;td&gt;2304×1296, 15 fps&lt;/td&gt;
					&lt;td&gt;39.0 fps&lt;/td&gt;
					&lt;td&gt;51,890 detections, 1,499 tracks&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Full chain (detection + depth + tracking + storage)&lt;/td&gt;
					&lt;td&gt;2304×1296, 300 frames&lt;/td&gt;
					&lt;td&gt;2.02 fps&lt;/td&gt;
					&lt;td&gt;12.3 detections per frame&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;In the full-chain run, depth estimation took 117.6 s of the 148.3 s total (about 80 %), and detection took 20.9 s. Depth estimation, not the detector, is the bottleneck. On 4K footage the largest detector runs about ten times slower than real time. That suits the intended use, which is analysis of recorded archives, not live alerting.&lt;/p&gt;
&lt;h2 id="limitations--next-steps"&gt;&lt;a href="#limitations--next-steps" class="header-anchor"&gt;&lt;/a&gt;Limitations &amp;amp; next steps
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;No geolocation accuracy has been measured against surveyed ground truth yet. The ray-casting stage was added after the timing runs above, so its cost is not included in them.&lt;/li&gt;
&lt;li&gt;The metric scale depends on the depth model and on the camera pose and intrinsics. Errors in tilt or focal length propagate directly into ground positions.&lt;/li&gt;
&lt;li&gt;The depth model predicts at its own internal resolution, so fine depth detail is limited even when detection runs in 4K.&lt;/li&gt;
&lt;li&gt;Next steps: an accuracy study with GNSS-surveyed targets, profiling the two-GPU split, and measuring how accurate Monte Carlo distance intervals are on objects at known separations.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="stack"&gt;&lt;a href="#stack" class="header-anchor"&gt;&lt;/a&gt;Stack
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;Python&lt;/code&gt; · &lt;code&gt;PyTorch&lt;/code&gt; · &lt;code&gt;OpenCV&lt;/code&gt; · &lt;code&gt;NumPy&lt;/code&gt; · &lt;code&gt;SciPy&lt;/code&gt; · &lt;code&gt;pyproj&lt;/code&gt; · &lt;code&gt;rasterio&lt;/code&gt; · &lt;code&gt;SQLite&lt;/code&gt; · &lt;code&gt;CUDA&lt;/code&gt; · &lt;code&gt;Apple MPS&lt;/code&gt;&lt;/p&gt;</description></item></channel></rss>