Skip to content

Feat: Gaussian Splatting - #2017

Merged
pierotofy merged 2 commits into
WebODM:masterfrom
pierotofy:gsplats
Sep 4, 2026
Merged

Feat: Gaussian Splatting#2017
pierotofy merged 2 commits into
WebODM:masterfrom
pierotofy:gsplats

Conversation

@pierotofy

@pierotofy pierotofy commented Sep 2, 2026

Copy link
Copy Markdown
Member

Adds support for:

  • Downloading training data for gaussian splats, compatible with OpenSplat, Brush and plenty others.
  • Uploading splat files
  • Automatically tile splat files into .RAD via https://github.com/uav4geo/Splat-Tools
  • Display splat files in the 3D model view and make measurements, use the standard Potree tools.
image image
recording.webm

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The current implementation contains a confirmed chunked-upload corruption bug and multiple insecure temporary-file creation sites that should be addressed before approval.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds end-to-end Gaussian Splatting support to WebODM: exporting COLMAP-style training data, uploading/processing splat models into .rad, and rendering splats in the 3D viewer alongside existing Potree tooling.

Changes:

  • Added backend APIs + worker tasks for splats training export, model upload/delete, and .rad range-enabled delivery.
  • Added frontend UI (task dialog + model-view integration) to guide download/train/upload and to display splats in 3D.
  • Updated Potree shaders/renderer path to support WebGL2-focused rendering and added docs link plumbing.
File summaries
File Description
worker/tasks.py Adds Celery tasks for exporting training data and converting uploaded splats via splat-tools.
webodm/settings.py Introduces a dedicated docs link for Gaussian Splats.
package.json Bumps app version to 3.3.0.
Dockerfile Installs splat-tools into the runtime image.
app/views/app.py Passes model-type from query param to the model display template.
app/tests/test_splats.py Adds comprehensive API tests for splats export, upload/processing, delete, and range requests.
app/tests/test_api_task.py Extends task API tests for splats asset presence and new /rtc endpoint.
app/templatetags/settings.py Exposes SPLAT_DOCS_LINK as a template tag.
app/templates/app/base.html Injects window.__splatDocsLink for frontend usage.
app/static/app/js/vendor/potree/build/potree/potree.js Updates Potree to a WebGL2/GLSL300 shader path and related extension logic.
app/static/app/js/ModelView.jsx Adds splats model type handling and integrates THREEdgs-based renderer into the 3D view.
app/static/app/js/css/SplatsDialog.scss Adds styling for the new Splats dialog UI.
app/static/app/js/css/ManageMediaDialog.scss Minor style tweak (removes opacity rule).
app/static/app/js/components/tests/SplatsDialog.test.jsx Adds a smoke test for the Splats dialog component.
app/static/app/js/components/TaskListItem.jsx Adds Splats dialog entry point and improves clipboard copy fallback for non-secure contexts.
app/static/app/js/components/SplatsDialog.jsx Implements the full Splats UI flow (download training zip, upload model w/ progress, delete/view).
app/static/app/js/classes/AssetDownloads.js Adds splats.rad to available asset downloads.
app/static/app/css/main.scss Adds a .fa-splat icon mask definition.
app/splats.py Implements training export generation (COLMAP bin files, image resizing, optional PDAL sampling, zip streaming).
app/models/task.py Registers splats.rad in task assets map and tweaks image resize to use reducing_gap.
app/geoutils.py Adds get_rtc_offset() helper for RTC coordinate offsets.
app/api/urls.py Registers new /rtc and /splats/* API routes.
app/api/tasks.py Adds range support for .rad assets and introduces TaskRtc API view.
app/api/splats.py Adds splats download/upload/delete API endpoints and integrates worker tasks.
Review details

Suppressed comments (1)

app/api/splats.py:156

  • tmp_splats_file is created via tempfile.mktemp(), which is vulnerable to path-race attacks and can collide under concurrency. Use mkstemp/NamedTemporaryFile to reserve the filename securely before moving the uploaded file.
        else:
            tmp_splats_file = tempfile.mktemp('_splats' + ext, dir=settings.MEDIA_TMP)
            shutil.move(uploaded_file, tmp_splats_file)

            celery_task_id = process_splats.delay(task.id, tmp_splats_file).task_id
            return Response({'success': True, 'celery_task_id': celery_task_id}, status=status.HTTP_200_OK)
  • Files reviewed: 23/25 changed files
  • Comments generated: 4
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread Dockerfile
Comment on lines +155 to +156
# Install splat-tools
curl -fsSL "https://github.com/uav4geo/Splat-Tools/releases/download/v1.0.3/splat-tools-1.0.3-$(uname -m)-linux.tar.gz" | tar -xz -C /usr/bin --strip-components=1 "splat-tools-1.0.3-$(uname -m)-linux/splat-tools" && /usr/bin/splat-tools --version
Comment thread worker/tasks.py
Comment on lines +263 to +268
def process_splats(self, task_id, splats_file):
if not os.path.isfile(splats_file):
raise Exception("Splats file not found")

tmp_out = tempfile.mktemp('_model.rad', dir=settings.MEDIA_TMP)
try:
Comment thread app/api/splats.py
Comment on lines +109 to +120
if chunk_info is not None:
if os.path.isfile(chunk_info['tmp_upload_file']) and chunk_info['chunk_index'] == 0:
os.unlink(chunk_info['tmp_upload_file'])

with open(chunk_info['tmp_upload_file'], 'ab') as fd:
fd.seek(chunk_info['byte_offset'])
if isinstance(file, InMemoryUploadedFile):
for chunk in file.chunks():
fd.write(chunk)
else:
with open(file.temporary_file_path(), 'rb') as f:
shutil.copyfileobj(f, fd)
Comment thread app/api/tasks.py
Comment on lines +506 to +519
range_match = re.match(r'^bytes=(\d+)-(\d*)$', request.META.get('HTTP_RANGE', ''))
if range_match:
start = int(range_match.group(1))
end = int(range_match.group(2)) if range_match.group(2) else filesize - 1

if start >= filesize:
file.close()
response = HttpResponse(status=416)
response['Content-Range'] = 'bytes */{}'.format(filesize)
return response

end = min(end, filesize - 1)
length = end - start + 1
file.seek(start)
@pierotofy

Copy link
Copy Markdown
Member Author

We're good to go 👍

@pierotofy
pierotofy merged commit 86c4735 into WebODM:master Sep 4, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants