DJI OSMO, anybody tried or knows how to make work with Mapillary

Has anybody thought of using The DJI OSMO with Mapillary? I’d like to but can’t see, to figure out how.

I guess you should use the uploader on the website or an upload script @tryl knows all about upload scripts.
I don’t know if the osmo has gps, otherwise you could use a free app on your phone to record a gpx-track

1 Like

If you have the images you needs to geotag them. Either with your favorite GUI program or with the python scripts in https://github.com/mapillary/mapillary_tools/tree/master/python . Then you need to upload them, as @Harry described.

Basically it is the standard action cam workflow. Just ask any question here :slight_smile:

Hello! I made a tutorial but I can’t post it since I’m too new of a user.

I made a post on the OpenStreetMap community forum: Tutorial: How to make geotagged image sequences with a GPX track and a timelapse video - General talk - OpenStreetMap Community Forum

1 Like

Update February 2026:
I’m currently working on a pipeline to process video recordings from the DJI OSMO 360 for use with Mapillary. Here’s a draft:

Previous experiences and misconceptions: Unfortunately, a DJI OSMO 360 (firmware version November 2025) doesn’t record GPS information in time-shift mode. Therefore, for recordings suitable for Mapillary, you have to use video mode, which generates enormous amounts of data.

Operating a DJI Osmo 360 in video mode consumes a lot of power. Who wants to climb onto the car roof every 100 minutes during a long trip to change the battery? And since you’re usually not traveling alone, the constant worry about the camera’s battery level, as well as whether a sudden downpour might damage the USB-C port, can be nerve-wracking for fellow passengers. Fortunately, the Osmo 360 has hidden charging contacts on its underside. Third-party clamp mounts with these contacts are already available online. This allows for a waterproof, continuous power supply and uninterrupted recording. And now for the particularly welcome news: unlike the Insta360, DJI doesn’t impose an 8-second pause after 30 minutes. Uninterrupted long-distance recording – that’s a statement from DJI.

Dean Zwikel’s UL2GSV tool extracts a GPX file from the raw OSMO data. This tool also patches the MP4 file created with DJI Studio so that it contains the correct time. Unfortunately, UL2GSV currently doesn’t take video editing, for example using DJI Studio, into account. For Mapillary purposes, we therefore need to pay attention to the recording length during the recording process itself. Anyone who tries to upload an unprocessed MP4 file (without time information) to Mapillary will find that the video plays twice as fast as the GPX track..

The UL2GSV team certainly deserves our support in the form of a donation for their excellent preliminary work in making DJI GPS recordings accessible to us.

Step Description Result / Purpose
1. Recording Record a 360-degree video with the DJI OSMO 360 camera. Select video resolution: 8K 24 frames. To obtain a usable GPX file, I recommend using a universal Bluetooth GPX remote from STARTRC. Raw video + GPS data embedded in camera files
2. Software You will need the OpenStreetMap Editor JOSM, DJI Studio, and Excel installed on your computer, along with the Excel VBA macros I described here: No more GPS stress thanks to navigation using video frames - #6 by osmplus_org, and the tool OSV2GPX.exe by Dean Zwikel and the Mapillary Desktor Uploader I use the following computer setup: A MacBook Pro with additional cooling fans for DJI Studio and JOSM. A Windows PC for Excel macros and OSV2GPX.exe. An Ubuntu machine with 16GB of RAM for uploading with the desktop uploader. The result is performance and perfection.
3. Preparation of data Copy a maximum of 8 DJI Studio raw recording files (without a directory) to an external SSD. Also copy the OSV2GPX.exe tool and a shortcut to the Excel file to the SSD.
4. MP4 Generation Create MP4 files in 24 frames per second, 8K resolution using DJI Studio. The MP4 file size, independent of the actual video size, should not exceed 40 GB. Use a separate SSD as the storage location for the DJI project file. It’s best to save the exported MP4 files on the same external SSD drive as the DJI raw files. I name each MP4 export file with the same sequential number as the original OSV file. For example, CAM_20260214113010_0004_D.OSV becomes 4.MP4
5. MP4 Rename Rename the exported MP4 files so that they subsequently have the same name as the original OSV file. For example, CAM_20260214113010_0004_D.OSV and the exported 4.mp4 file are renamed to CAM_20260214113010_0004_D.mp4
6. GPX Generation Count the number of exported MP4 files and open a corresponding number of OSV2GPX windows. Select OSV raw data, analogous to the MP4 files, and extract a GPX file from each of these. Leave the OSV2GPX windows open for now. For each window, select the MP4 export that corresponds to the OSV raw data and transfer the date and time from the OSV file to the MP4 file. You now have GPX files and videos with the correct time for each recording.
7. GPX Reduktion To reduce GPX density, run my Excel VBA BatchReduceGPX_XM function. You will receive GPX files whose GPX node density has been reduced by a factor of 28. The macro creates a directory called “Reduced” for reduced GPX datasets.
8. Error Correction in JOSM Run my VBA macro OpenJOSM, which opens all GPX datasets in the JOSM editor. Convert individual GPX datasets into JOSM tracks and check their location using aerial photos or OpenStreetMap. You can now correct errors, add additional GPX nodes for tunnels, or, as I described, set frame points for terrain crossings where you know the location of the change in direction. Make sure you export the edited GPX track to the GPX_Reduced directory by overwriting the original GPX file. Clean GPX file without signal loss
9. GPX Filler Now, for GPX data records contained in the GPX_Reduced# directory, run my macro BatchProcessFüller. The macro calculates the appropriate time for each frame point and adds the appropriate time for newly added GPX points through interpolation. The processed GPX datasets are then exported to a directory called Export.
10. Merging data Move the MP4 files named #dated.mp4 to the Export directory. Preparation for upload completed
11. Upload Connect the SSD drive to an Ubuntu computer and upload the data using the Mapillary Desktop Uploader. complete

The concept is good, but why using a Macro? It’s more easy to use a powershell script with Exiftool (and ffprobe/ffmpeg). With Gemini or ChatGPT is a script easy to produce. You’re also more flexible when you’re using menu choices. But that’s my opion :wink:

My experience with AI so far is that it promises a lot but delivers little. I prefer to develop a workflow step by step, ignoring all the grandiose promises of AI solving everything at once. With Excel, every step is understandable. For Insta360, I’ve already created my own working batch processing workflow—a workflow for which I’ve applied AI, but whose code I can trace step by step and therefore doesn’t slip out of my control.
I’m very open to parallel suggestions for implementing these steps with other tools, which is precisely why I’m publishing this project here at a very early stage. May the best solution win.

1 Like

@osmplus_org - thank you for sharing your experience! Do you think you might be able to share an example of a raw video file you get from the DJI Osmo 360 and an example of a raw GPX trace you get from the DJI Remote (for the same video file).

We’d appreciate having a copy for our testing at Mapillary - again these are the raw unprocessed versions (before sending through any tool). Thank you so much in advance!

Thanks so much @osmplus_org !

Looks like exiftool also supports extracting .gpx from the .osv file.

`*Thank you, that’s very interesting information for me. I’m currently noticing that the GPX track extracted with the OSV2GPX tool shows extremely different speeds compared to the video. https://www.mapillary.com/app/user/osmplus_org?lat=47.457603236764015&lng=12.363177758953952&z=17&panos=true&username[]=osmplus_org&pKey=1551988939181586&x=0.30513986230430395&y=0.6091746677677109&zoom=0.42990654205607476&focus=photo Even a test reduction of the GPX points showed no improvement. I’m currently a bit stumped, and would next try changing the GPX times by recalculating them. *

OSV2GPX consists of two separate functions: one for extracting the GPX track, and the second function adds the video time, which GSV requires, to the video file. Since Mapillary can easily process the video file exported by DJI Studio even without timestamps, we can focus solely on the GPX track for Mapillary with the DJI Osmo 360. Your information that exiftool can also extract a GPX track from the camera’s raw file .osv is therefore very helpful. The question is whether the GPX track generated in this way remains synchronized with the video.

I have original .OSV files from the DJI Osmo 360 camera (example filename: E:\Sonntag\DCIM\CAM_001\CAM_20251116120149_0002_D.OSV).
Could you please tell me the exact exiftool command you use to successfully extract a working .gpx file from these?
I would really appreciate the precise command. Thank you!

Update:
My assumption in this post that recordings with 25 frames per second were required later proved to be incorrect. The frame rate chosen during OSMO recording has no influence on the subsequent GPX speed. The MP4 files exported by DJI Studio lack the time information. Therefore, in order to use OSMO 360 recordings with Mapillary, this information must be added to the MP4 file afterward. OSV2GPX.exe software by Dean Zwikel performs this task. After such processing of the MP4 files, the GPX speed and the GPX track run synchronously in Mapillary.

Sure thing, I used:

exiftool -p /usr/local/bin/gpx.fmt -ee3 -api largefilesupport=1 "FILENAMEHERE.OSV" > out.gpx

Note that this does require you to have the gpx.fmt file saved to /usr/local/bin/gpx.fmt

However, after uploading the capture to Mapillary I think I see the same issue you’re describing (gpx out of sync with video). I don’t have the time to troubleshoot at the moment, would be worth looking at the the fps of the video perhaps?

2 Likes

Apparently, the DJI OSMO features really nice 360° image quality. :+1: I bet the original image quality is even better than what we can see on Mapillary. And, although at closer look the imagery suffers from blurry edges due to that wide fixed aperture, the quality overall may be even a bit better than what MAX2 is able to deliver.

Could you please also share some 16 MP planar images that the OSMO, according to the specs, should be capable of?

I had something similar on my mind for what the root cause may be. Try recording at 25 fps because at this recording speed the frame time is a finite fraction of a second, not an infinite fraction. Perhaps this may help?

A new problem on the horizon for the productive use of the DJI Remote:

During my testing of the DJI Osmo 360 with the DJI GPS Remote (for the 360° setup), a new issue has appeared. Having been spoiled by the excellent reception of the Insta360 GPS Remote, I mounted the DJI remote in the same place inside the car – on the left side of the steering wheel, above the driver-side air vent.On mountain drives this mounting position now causes a clear problem: as soon as the GPS signal gets weaker, the GPX track suddenly jumps up to 20 meters off the actual road. Even after driving further and regaining better reception, the track often doesn’t recover for many kilometers and stays offset. I have never seen this behavior with the Insta360 remote. As soon as it picks up the signal again, it very quickly snaps back to the correct road with almost no deviation.Has anyone else noticed similar GPS tracking issues with the DJI remote when it’s placed inside the car (especially in the A-pillar/dashboard area)?

To determine the performance of the DJI GPX Remote with regard to its GPS reception, I will conduct tests and install the GPX Remote on the roof of my vehicle. The DJI Mimo smartphone app will then be responsible for controlling the camera.

Does anyone have experience with 3D-printed housings for GPX receivers on the vehicle roof? Such a dome would certainly look very professional and also provide optimal GPS reception.
Thanks!

Update February 2026

The solution here is to use an alternative GPX remote from StarTRC to generate usable GPX tracks. Even with the best intentions, I cannot currently recommend the original DJI remote for generating GPX tracks suitable for Mapillary.

@osmplus_org - we did not do any backend changes - so I think setting the camera to record 25 FPS did the trick.

2 Likes

First DJI OSMO 360 impressions in better lighting conditions.
8k 25ffs

Lens alignment sideways to the vehicle.

2 Likes

The quality is really stunning. :grinning_face_with_smiling_eyes: Maybe even better than the GoPro MAX2! I just do not know whether this is due to the Mapillary processing pipeline botching MAX2 imagery, MAX2 quality being slightly worse, or just the weather. :laughing: Anyhow, one can though clearly see that the wide fixed aperture really does not do the OSMO any good. DJI would have been well advised to tighten the aperture because this would have also de facto increased the OSMO’s effective scanning range. Imho digital sharpening cannot compensate for true physical sharpness. AI sharpening will not help here either. You just cannot argue with physics. :person_shrugging:

The timestamps on the sequence you shared clearly indicate that the video has been captured at 25 fps. Good job! :+1:
Now, since you have a tool for it and have been modifying point density in GPX tracks, why don’t you increase point density to match every frame? This should give best results on Mapillary.

*I made a mistake and simply tried to transfer my workflow that worked perfectly with the Insta360 X3 straight to the DJI Osmo 360. It didn’t work at all. I quickly realized that with the DJI I was losing every single microsecond in the timing data.I stayed up all night yesterday, coding until 2 a.m. The first upload result was disappointing, but my current workflow is now looking extremely promising. *
A new upload with microsecond-accurate GPX timestamps is running right now.

*I’m now getting far more usable GPX points by drastically reducing the number of nodes the camera outputs – currently by an astonishing factor of 28. Before, almost all nodes were identical duplicates. This reduction finally gives me a GPX track that’s actually editable, which is absolutely essential with this camera.The next challenge is figuring out how to deal with the sometimes massive outliers in the GPX track. If I can’t solve that, the whole investment in this camera will unfortunately have been for nothing. Yes, the image quality is fantastic – a real joy to watch. But if I can’t eliminate these GPX errors, the DJI Osmo will end up being just my secondary camera for quick shots during driving breaks. *

Right now, that means I have to remove the roof camera each time, take the quick shots, and then re-mount it on the rig before continuing. Instead, I’d then just put an Insta360 X5 on the car roof.I love driving through tunnels – and that’s exactly where the risk of a rollover is much higher when the camera restarts recording every 14 minutes (DJI) instead of only every 30 minutes like with the Insta360.On the plus side, the big advantage of the DJI is that it starts recording the moment I drive off and only stops after 5 hours – of course powered via USB-C the whole time. That would finally allow me to mount it permanently on the roof together with the remote.

I’m trying to streamline the rather tedious prep work needed to get images from this camera into Mapillary. Right now, the beautiful results are the only thing that makes it worth the hassle.

1 Like

A process to make DJI OSMO 360 recordings usable for Streetview purposes.
EXPERIMENTAL

Starting Point 1

The Excel VBA macro Sub C_BatchReduceGPX_28_XML() uses the UL2GSV tool OSV2GPX to reduce GPX data from raw recordings obtained with a DJI OSMO 360 camera by a factor of 28 (the optimal reduction value still needs to be determined). The macro requires a directory containing GPX data for processing. The reduced GPX data is then exported to a directory named “reduced”.

Intermediate Step Missing GPX points (for example, in tunnels) are checked and added using the JOSM Editor. Ensure an even distribution of added points; I use a consistent 30-meter spacing. Drive through tunnels at a constant speed.

Starting Point 2

The macro Sub D_BatchProcessFüller() imports GPX data from the “reduced” directory, including any data that has been processed or supplemented with JOSM. It then adds missing timestamps via interpolation and finally exports the GPX data to a directory named “Export”. The GPX datasets also receive the suffix “_dated” (since Mapillary requires identical .gpx and .mp4 file pairs).

:compass: GPX-Zeitverarbeitung in Excel: Ablauf und Logik

:small_blue_diamond: Spalte D: Rohzeit aus GPX

  • Enthält den originalen Zeitstempel aus der GPX-Datei, z. B. 2025-11-19T12:32:47.956376Z.
  • Format: ISO 8601 mit Mikrosekunden.

:small_blue_diamond: Spalte E: Excel-kompatibles Datum

  • Hier wird aus Spalte D ein Excel-Datum extrahiert, das für Berechnungen nutzbar ist.
  • Die Mikrosekunden werden separat gespeichert (z. B. in Spalte G), aber Spalte E enthält nur das Date-Objekt.
  • Wird mit ParseIsoTimeToDateAndUs erzeugt.

:small_blue_diamond: Spalte F: Zeit für Export (bereinigt/interpoliert)

  • Zielspalte für die bereinigte oder interpolierte Zeit.
  • Wenn in Spalte E ein gültiger Zeitwert steht → wird 1:1 übernommen.
  • Wenn Spalte E leer ist → wird interpoliert:
    • Suche rückwärts den letzten gültigen Zeitwert.
    • Suche vorwärts den nächsten gültigen Zeitwert.
  • Zähle die Anzahl der leeren Zeilen dazwischen.
  • Berechne gleichmäßige Zeitabstände (inkl. Mikrosekunden).
  • Fülle die Zwischenwerte in Spalte F ein.


Sub C_BatchReduceGPX_28_XML()
    Dim fso As Object, folderPath As String, file As Object
    Dim xmlDoc As Object, trkpts As Object, newDoc As Object
    Dim root As Object, trkNode As Object, trksegNode As Object
    Dim inputFile As String, outputFolder As String, outputPath As String
    Dim i As Long, hasTime As Boolean

    With Application.FileDialog(msoFileDialogFolderPicker)
        .Title = "GPX-Verzeichnis auswählen"
        If .Show <> -1 Then Exit Sub
        folderPath = .SelectedItems(1)
    End With

    Set fso = CreateObject("Scripting.FileSystemObject")
    Dim parentFolder As String
    parentFolder = fso.GetParentFolderName(folderPath)

    outputFolder = parentFolder & "\GPX_Reduced_" & Format(Now, "yyyymmdd_hhnnss")
    If Not fso.FolderExists(outputFolder) Then fso.CreateFolder outputFolder

    For Each file In fso.GetFolder(folderPath).Files
        If LCase(fso.GetExtensionName(file.Name)) = "gpx" Then
            inputFile = file.Path

            Set xmlDoc = CreateObject("MSXML2.DOMDocument.6.0")
            xmlDoc.async = False
            xmlDoc.Load inputFile
            If xmlDoc.ParseError.ErrorCode <> 0 Then GoTo NextFile

            xmlDoc.SetProperty "SelectionNamespaces", _
                "xmlns:gpx='http://www.topografix.com/GPX/1/1'"

            Set trkpts = xmlDoc.SelectNodes("//gpx:trkpt")
            If trkpts Is Nothing Then GoTo NextFile

            Set newDoc = CreateObject("MSXML2.DOMDocument.6.0")
            newDoc.appendChild newDoc.createProcessingInstruction("xml", "version='1.0' encoding='UTF-8'")

            Set root = newDoc.createElement("gpx")
            root.setAttribute "xmlns", "http://www.topografix.com/GPX/1/1"
            root.setAttribute "version", "1.1"
            root.setAttribute "creator", "GPX Reducer"
            ' Optional: SchemaLocation
            root.setAttribute "xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance"
            root.setAttribute "xsi:schemaLocation", "http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd"
            newDoc.appendChild root

            Set trkNode = newDoc.createElement("trk")
            root.appendChild trkNode

            Set trksegNode = newDoc.createElement("trkseg")
            trkNode.appendChild trksegNode

            For i = 0 To trkpts.Length - 1
                hasTime = Not trkpts.Item(i).SelectSingleNode("gpx:time") Is Nothing
                If hasTime Then
                    If i Mod 28 = 0 Then
                        trksegNode.appendChild trkpts.Item(i).CloneNode(True)
                    End If
                Else
                    trksegNode.appendChild trkpts.Item(i).CloneNode(True)
                End If
            Next i

            outputPath = outputFolder & "\" & file.Name
            newDoc.Save outputPath
        End If
NextFile:
    Next file

    MsgBox "Alle GPX-Dateien erfolgreich reduziert: " & vbCrLf & outputFolder, vbInformation
End Sub




Sub D_BatchProcessFüller()
    Dim fso As Object, folderPath As String, file As Object
    Dim ws As Worksheet
    Dim fileName As String, ext As String
    Dim Pfad_und_Datei As String

    With Application.FileDialog(msoFileDialogFolderPicker)
        .Title = "GPX-Verzeichnis auswählen"
        If .Show <> -1 Then Exit Sub
        folderPath = .SelectedItems(1)
    End With

    Set fso = CreateObject("Scripting.FileSystemObject")

    Sheets("GPX_Import").Select
    Set ws = Worksheets("GPX_Import")

    For Each file In fso.GetFolder(folderPath).Files
        fileName = file.Name
        ext = LCase(fso.GetExtensionName(fileName))

        ' DJI-Dateien beginnen mit "CAM_" – Bedingung darauf, plus .gpx
        If ext = "gpx" And Left$(fileName, 4) = "CAM_" Then
            Pfad_und_Datei = file.Path
            ws.Cells.Clear

            ws.Range("K5").value = fileName
            ws.Range("K6").value = Pfad_und_Datei

            ImportAndFixGPX Pfad_und_Datei
            ExportGPXWithBounds
        End If
    Next file

    Set fso = Nothing
    Set file = Nothing

    MsgBox "Alle GPX-Dateien wurden verarbeitet und exportiert.", vbInformation
End Sub



Sub ImportAndFixGPX(Pfad_und_Datei As String)
    Dim xmlDoc As Object, trkpts As Object, boundsNode As Object
    Dim i As Long
    Dim ws As Worksheet
    Dim lastRow As Long
    Dim ergänzteZeitpunkte As Long
    Dim boundsText As String

    Dim prevDt As Date, prevUs As Long
    Dim nextDt As Date, nextUs As Long
    Dim gapStart As Long, gapEnd As Long
    Dim gapCount As Long
    Dim prevSec As Double, nextSec As Double
    Dim stepSec As Double, newSec As Double
    Dim dtOut As Date, usOut As Long

    ergänzteZeitpunkte = 0

    Sheets("GPX_Import").Select
    Set ws = Worksheets("GPX_Import")
    ws.Cells.Clear

    ' Datei-Infos
    Dim fso As Object
    Set fso = CreateObject("Scripting.FileSystemObject")
    ws.Range("K5").value = fso.GetFileName(Pfad_und_Datei)
    ws.Range("K6").value = Pfad_und_Datei

    ' XML laden
    Set xmlDoc = CreateObject("MSXML2.DOMDocument.6.0")
    xmlDoc.async = False: xmlDoc.Load Pfad_und_Datei
    If xmlDoc.ParseError.ErrorCode <> 0 Then
        MsgBox "Fehler beim Laden der GPX-Datei: " & xmlDoc.ParseError.Reason, vbCritical
        Exit Sub
    End If
    xmlDoc.SetProperty "SelectionNamespaces", "xmlns:gpx='http://www.topografix.com/GPX/1/1'"

    ' Bounds
    Set boundsNode = xmlDoc.SelectSingleNode("//gpx:bounds")
    If Not boundsNode Is Nothing Then
        boundsText = "<bounds minlat=""" & boundsNode.getAttribute("minlat") & _
                     """ minlon=""" & boundsNode.getAttribute("minlon") & _
                     """ maxlon=""" & boundsNode.getAttribute("maxlon") & _
                     """ maxlat=""" & boundsNode.getAttribute("maxlat") & """ />"
        ws.Range("K2").value = boundsText
    Else
        ws.Range("K2").value = "Keine Bounds gefunden"
    End If

    ' Trackpunkte extrahieren
    Set trkpts = xmlDoc.SelectNodes("//gpx:trkpt")

    ws.Cells(1, 1).value = "Lat"
    ws.Cells(1, 2).value = "Lon"
    ws.Cells(1, 3).value = "Ele"
    ws.Cells(1, 4).value = "Time (raw)"
    ws.Cells(1, 5).value = "Time (Date)"
    ws.Cells(1, 6).value = "Time (ISO us)"
    ws.Cells(1, 7).value = "Microseconds"

    Dim timeFound As Boolean
    timeFound = False

    For i = 0 To trkpts.Length - 1
        ws.Cells(i + 2, 1).value = trkpts.Item(i).getAttribute("lat")
        ws.Cells(i + 2, 2).value = trkpts.Item(i).getAttribute("lon")

        If Not trkpts.Item(i).SelectSingleNode("gpx:ele") Is Nothing Then
            ws.Cells(i + 2, 3).value = trkpts.Item(i).SelectSingleNode("gpx:ele").Text
        End If

        If Not trkpts.Item(i).SelectSingleNode("gpx:time") Is Nothing Then
            Dim rawTime As String, dt As Date, us As Long
            rawTime = trkpts.Item(i).SelectSingleNode("gpx:time").Text
            ws.Cells(i + 2, 4).value = rawTime
            dt = ParseIsoTimeToDateAndUs(rawTime, us)
            ws.Cells(i + 2, 5).value = dt
            ws.Cells(i + 2, 5).NumberFormat = "yyyy-mm-dd hh:mm:ss"
            ws.Cells(i + 2, 7).value = us
            ws.Cells(i + 2, 6).value = FormatDateWithMicroseconds(dt, us)
            timeFound = True
        End If
    Next i

    If Not timeFound Then
        MsgBox "Keine Zeitinformationen gefunden, Import abgebrochen.", vbCritical
        Exit Sub
    End If

    ' Interpolation mit Mikrosekunden startet hier ...


       ' Interpolation mit Mikrosekunden
lastRow = ws.Cells(ws.Rows.Count, 1).End(xlUp).Row

prevDt = 0: prevUs = 0
For i = 2 To lastRow
    Dim hasDate As Boolean
    hasDate = IsDate(ws.Cells(i, 5).value)

    If hasDate Then
        ' Zeitwert vorhanden ? direkt übernehmen
        Dim curDt As Date, curUs As Long
        curDt = ws.Cells(i, 5).value
        curUs = CLng(Val(ws.Cells(i, 7).value))

        ' Ordnung sicherstellen: keine Rückwärtszeit
        If DateUsToSeconds(curDt, curUs) < DateUsToSeconds(prevDt, prevUs) Then
            Dim adjSec As Double, dtAdj As Date, usAdj As Long
            adjSec = DateUsToSeconds(prevDt, prevUs) + 0.000001 ' +1 µs
            SecondsToDateUs prevDt, adjSec, dtAdj, usAdj
            curDt = dtAdj: curUs = usAdj
            ws.Cells(i, 5).value = curDt
            ws.Cells(i, 7).value = curUs
        End If

        ' ISO-Zeit mit Mikrosekunden schreiben
        ws.Cells(i, 6).value = FormatDateWithMicroseconds(curDt, curUs)
        prevDt = curDt: prevUs = curUs

    Else
        ' Lücke erkennen
        gapStart = i - 1
        gapEnd = i
        Do While gapEnd <= lastRow And Not IsDate(ws.Cells(gapEnd, 5).value)
            gapEnd = gapEnd + 1
        Loop

        If gapEnd <= lastRow And IsDate(ws.Cells(gapEnd, 5).value) Then
            ' Zeitpunkte vor und nach der Lücke holen
            prevDt = ws.Cells(gapStart, 5).value
            prevUs = CLng(Val(ws.Cells(gapStart, 7).value))
            nextDt = ws.Cells(gapEnd, 5).value
            nextUs = CLng(Val(ws.Cells(gapEnd, 7).value))

            ' Sekundenbasis berechnen
            prevSec = DateUsToSeconds(prevDt, prevUs)
            nextSec = DateUsToSeconds(nextDt, nextUs)

            gapCount = gapEnd - gapStart - 1
            stepSec = (nextSec - prevSec) / (gapCount + 1)

            ' Interpolierte Werte einfügen
            For j = gapStart + 1 To gapEnd - 1
                newSec = prevSec + stepSec * (j - gapStart)
                SecondsToDateUs prevDt, newSec, dtOut, usOut

                ws.Cells(j, 5).value = dtOut
                ws.Cells(j, 5).NumberFormat = "yyyy-mm-dd hh:mm:ss"
                ws.Cells(j, 7).value = usOut
                ws.Cells(j, 6).value = FormatDateWithMicroseconds(dtOut, usOut)

                ergänzteZeitpunkte = ergänzteZeitpunkte + 1
            Next j

            ' Schleifenindex vorziehen und prev aktualisieren
            i = gapEnd - 1
            prevDt = ws.Cells(i, 5).value
            prevUs = CLng(Val(ws.Cells(i, 7).value))
        End If
    End If
Next i

' Anzahl der ergänzten Zeitpunkte ausgeben
ws.Range("K4").value = ergänzteZeitpunkte

' Spaltenbreite anpassen
ws.Columns(4).AutoFit
ws.Columns(5).AutoFit
ws.Columns(6).AutoFit
ws.Columns(7).AutoFit


End Sub

                
                
 '======================================================================================================
    Sub ExportGPXWithBounds()
    Dim ws As Worksheet
    Dim i As Long, lastRow As Long
    Dim minLat As Double, maxLat As Double, minLon As Double, maxLon As Double
    Dim boundsCalculated As String
    Dim gpxText As String, trkptText As String
    Dim boundsText As String, fileName As String
    Dim exportFolder As String, outputPath As String
    Dim TimeZoneOffset As Integer
    Dim currentTime As String
    Dim fso As Object
    Dim originalPath As String, originalFolder As String, parentFolder As String
    Dim exportBaseName As String
    Dim folderItem As Object
    Dim exportFound As Boolean

    Set ws = Worksheets("GPX_Import")
    lastRow = ws.Cells(ws.Rows.Count, 1).End(xlUp).Row
    

    ' === Schritt 6: Eigene Bounds berechnen ===
    minLat = 90: maxLat = -90: minLon = 180: maxLon = -180
    For i = 2 To lastRow
        If IsNumeric(ws.Cells(i, 1).value) And IsNumeric(ws.Cells(i, 2).value) Then
            If ws.Cells(i, 1).value < minLat Then minLat = ws.Cells(i, 1).value
            If ws.Cells(i, 1).value > maxLat Then maxLat = ws.Cells(i, 1).value
            If ws.Cells(i, 2).value < minLon Then minLon = ws.Cells(i, 2).value
            If ws.Cells(i, 2).value > maxLon Then maxLon = ws.Cells(i, 2).value
        End If
    Next i

    boundsCalculated = "<bounds minlat=""" & Format(minLat, "0.000000000000000") & _
                       """ minlon=""" & Format(minLon, "0.000000000000000") & _
                       """ maxlon=""" & Format(maxLon, "0.000000000000000") & _
                       """ maxlat=""" & Format(maxLat, "0.000000000000000") & """ />"

    ws.Range("K2").value = boundsCalculated

    ' === Zeitstempel für Header ===
    TimeZoneOffset = 2 ' UTC+2 für MESZ
    currentTime = Format(Now - (TimeZoneOffset / 24), "yyyy-mm-dd\THH:NN:SS") & "Z"

    ' === Bounds aus Zelle K2 übernehmen und systemabhängig korrigieren ===
    boundsText = Replace(ws.Range("K2").value, Application.International(xlDecimalSeparator), ".")

    ' === Dateiname aus Zelle K5 übernehmen ===
    fileName = ws.Range("K5").value
    If fileName = "" Then
        MsgBox "Kein Dateiname in Zelle K5 gefunden.", vbCritical
        Exit Sub
    End If

    ' === Pfad zur GPX-Datei aus Zelle K6 übernehmen ===
    originalPath = ws.Range("K6").value
    Set fso = CreateObject("Scripting.FileSystemObject")
    originalFolder = fso.GetParentFolderName(originalPath)

    ' === Übergeordnetes Verzeichnis ermitteln ===
    On Error Resume Next
    parentFolder = fso.GetParentFolderName(originalFolder)
    On Error GoTo 0
    If parentFolder = "" Or parentFolder = originalFolder Then parentFolder = originalFolder

    ' === Export#<Datum>-Verzeichnis suchen oder erzeugen ===
    exportBaseName = "Export#" & Format(Date, "yyyymmdd")
    exportFolder = parentFolder & "\" & exportBaseName
    exportFound = False
    For Each folderItem In fso.GetFolder(parentFolder).SubFolders
        If folderItem.Name = exportBaseName Then
            exportFolder = folderItem.Path
            exportFound = True
            Exit For
        End If
    Next folderItem
    If Not exportFound Then
        If Not fso.FolderExists(exportFolder) Then fso.CreateFolder exportFolder
    End If

    ' === Exportpfad festlegen ===
    ' outputPath = exportFolder & "\" & fileName
    
    ' === Exportpfad festlegen mit Appendix "_dated" ===
Dim baseName As String, extName As String

' Dateiname in Basis und Erweiterung zerlegen
baseName = fso.GetBaseName(fileName)      ' z.B. CAM_20251119151440_0016_D
extName = fso.GetExtensionName(fileName)  ' z.B. gpx

' neuen Namen zusammensetzen
fileName = baseName & "_dated." & extName

outputPath = exportFolder & "\" & fileName
    
    

    ' === GPX-Header aufbauen ===
    gpxText = "<?xml version=""1.0"" encoding=""UTF-8""?>" & vbCrLf & _
              "<gpx xmlns=""http://www.topografix.com/GPX/1/1"" creator=""Insta360 Studio"" version=""1.1"">" & vbCrLf & _
              "<metadata>" & vbCrLf & _
              "<link href=""https://www.insta360.com"">" & vbCrLf & _
              "<text>Insta360 GPS Dashboard</text>" & vbCrLf & _
              "</link>" & vbCrLf & _
              "<time>" & currentTime & "</time>" & vbCrLf & _
              boundsText & vbCrLf & _
              "</metadata>" & vbCrLf & _
              "<trk>" & vbCrLf & _
              "<name>Insta360 GPS Data</name>" & vbCrLf & _
              "<trkseg>" & vbCrLf

    ' === Trackpunkte erzeugen ===
    For i = 2 To lastRow
    If ws.Cells(i, 1).value <> "" And ws.Cells(i, 2).value <> "" Then
        trkptText = "<trkpt lat=""" & FormatDecimalForGPX(ws.Cells(i, 1).value) & _
                    """ lon=""" & FormatDecimalForGPX(ws.Cells(i, 2).value) & """>" & vbCrLf

        If ws.Cells(i, 3).value <> "" Then
            trkptText = trkptText & "<ele>" & FormatDecimalForGPX(ws.Cells(i, 3).value) & "</ele>" & vbCrLf
        End If

' Zeitstempel aus Spalte 5 (Date) + Spalte 7 (Microseconds)
If IsDate(ws.Cells(i, 5).value) Then
    Dim dtExp As Date, usExp As Long
    dtExp = ws.Cells(i, 5).value
    usExp = CLng(Val(ws.Cells(i, 7).value))
    trkptText = trkptText & "<time>" & FormatDateWithMicroseconds(dtExp, usExp) & "</time>" & vbCrLf
End If

        trkptText = trkptText & "</trkpt>" & vbCrLf
        gpxText = gpxText & trkptText
    End If
Next i


    ' === GPX-Ende anhängen ===
    gpxText = gpxText & "</trkseg>" & vbCrLf & "</trk>" & vbCrLf & "</gpx>"

    ' === Datei schreiben ===
    Dim stream As Object
    Set stream = CreateObject("ADODB.Stream")
    stream.Type = 2
    stream.Charset = "utf-8"
    stream.Open
    stream.WriteText gpxText
    stream.SaveToFile outputPath, 2
    stream.Close

    Sheets("Start").Select
End Sub


' === Funktion zum automatischen Anpassen des Trennzeichens ===
Function FormatDecimalForGPX(value As Variant) As String
    Dim systemSeparator As String
    systemSeparator = Application.International(xlDecimalSeparator)
    FormatDecimalForGPX = Replace(Format(CDbl(value), "0.000000"), systemSeparator, ".")
End Function



' Zeitstring parsen und Mikrosekunden extrahieren
Public Function ParseIsoTimeToDateAndUs(iso As String, ByRef usOut As Long) As Date
    Dim basePart As String, fracPart As String
    Dim pDot As Long, pZ As Long
    usOut = 0
    If Len(iso) = 0 Then Exit Function

    pZ = InStr(iso, "Z")
    If pZ = 0 Then pZ = Len(iso) + 1

    pDot = InStrRev(iso, ".")
    If pDot > 0 And pDot < pZ Then
        basePart = Left$(iso, pDot - 1)
        fracPart = Mid$(iso, pDot + 1, pZ - pDot - 1)
        ' Nur die ersten 6 Ziffern nehmen (Mikrosekunden)
        If Len(fracPart) > 6 Then fracPart = Left$(fracPart, 6)
        If IsNumeric(fracPart) Then
            usOut = CLng(fracPart)
        End If
    Else
        basePart = Left$(iso, pZ - 1)
    End If

    ParseIsoTimeToDateAndUs = CDate(Replace(basePart, "T", " "))
End Function




' Sekunden + Mikrosekunden innerhalb eines Tages
Public Function DateUsToSeconds(dt As Date, us As Long) As Double
    DateUsToSeconds = (Hour(dt) * 3600# + Minute(dt) * 60# + Second(dt)) + (us / 1000000#)
End Function

Public Sub SecondsToDateUs(baseDate As Date, secVal As Double, ByRef dtOut As Date, ByRef usOut As Long)
    Dim wholeSec As Long, frac As Double
    Dim h As Long, m As Long, s As Long

    ' Ganze Sekunden und Bruchteil trennen
    wholeSec = Fix(secVal)
    frac = secVal - wholeSec

    ' Zerlegen in Stunden, Minuten, Sekunden
    h = wholeSec \ 3600
    m = (wholeSec Mod 3600) \ 60
    s = wholeSec Mod 60

    ' Datum + Uhrzeit zusammensetzen
    dtOut = DateSerial(Year(baseDate), Month(baseDate), Day(baseDate)) + TimeSerial(h, m, s)

    ' Mikrosekunden berechnen
    usOut = CLng(frac * 1000000#)
    If usOut < 0 Then usOut = 0
    If usOut > 999999 Then usOut = 999999
End Sub


Public Function FormatDateWithMicroseconds(dt As Date, us As Long) As String
    If us < 0 Then us = 0
    If us > 999999 Then us = 999999
    FormatDateWithMicroseconds = Format(dt, "yyyy-mm-dd\THH:nn:ss") & "." & Format(us, "000000") & "Z"
End Function




Several test drives using the DJI Osmo 360° camera together with a DJI remote connected via Bluetooth have shown that the GPX data quality achieved with this setup still falls short of current technological standards. It must be acknowledged that DJI itself does not position its GPX Remote as a GPX Tracker, but rather that this accessory is intended as a remote control, and its GPX capabilities are intended for generating stats in videos.
Even mounting the DJI remote on the car roof to improve GPS reception did not result in any significant improvement.The 360° camera itself delivers excellent image quality and shows great promise, but to make it viable for productive Street View applications, a different approach to positioning is required. For this reason, I am pausing the current setup.

Update:
My order for an alternative GPX Bluetooth remote from STARTRC has arrived. This remote is not described as compatible with the DJI OSMO 360. https://www.startrc.com/startrc-gps-bluetooth-remote-controller-for-dji-action-5-pro-action-4.html Initial tests with my OSMO 360 show promising results regarding the GPX tracks generated. This remote can control the OSMO 360, but it doesn’t indicate an ongoing recording with a flashing red light. A successful recording start can be inferred by the remote’s status indicator changing from standby to active.

Since this GPS remote is also sold in slightly different versions as a third-party alternative for the GoPro and Insta360 ecosystems.

Operating the StarTRC GPX Remote (already paired):
Turn on the system:

  1. Insert the DJI Osmo 360 battery and press the power button.

  2. Press and hold the StarTRC power button for three seconds.

  3. Wait for Bluetooth pairing to complete (this takes a few seconds).

  4. Now put the camera into readiness mode. To do this, press and hold the red front button on the StarTRC for several seconds. A moon icon will appear on the remote’s display.

  5. Start recording. Briefly press the front button on the StarTRC.

  6. Stop recording. Briefly press the front button on the StarTRC; the moon icon will reappear.

  7. Start next recording. Briefly press the front button on the StarTRC.

  8. Stop recording. Briefly press the front button on the StarTRC; the moon icon will reappear.

  9. Leave the system in readiness mode for a longer period. The camera consumes very little power in this mode.

  10. After a few minutes, the remote’s display goes blank after a few minutes, and the remote enters standby mode.

    Reactivate the camera (for example, if you were away for an hour and return to your car).

  11. Press and hold the StarTRC front button for several seconds. After successful pairing, the remote display will be blank. To obtain a definitive system state, press and hold the front button on the TRC again for several seconds. A moon icon will now reappear on the StarTRC display. (The camera is now in a definitive state for the next recording and is ready for the next recording.)

  12. Turn off the system by pressing and holding the left On button on the StarTRC for several seconds. Press the DJI Osmo Power button and remove the camera battery. (If the StarTRC was turned off using the TRC Power button, remote reactivation of the camera using the TRC remote is no longer possible.) The camera must first be manually turned on to turn the system back on.

    Background: The bidirectional communication between the DJI Osmo 360 and the GPX Remote StarTRC does not transmit the current camera status to the remote. The remote can detect the following status states: readiness (moon symbol), paired, and disconnected (for example, due to a depleted camera battery).

    Switching between photo and video modes is possible by briefly pressing the remote’s power button, but the lack of a display indicator can be confusing regarding the camera’s current mode. Therefore, I advise against briefly pressing the remote’s power button.

    The remote moon symbol is particularly relevant here, as briefly pressing the red remote’s front button in this state directly switches the camera to recording mode.

Osmo 360 battery extension rod replacement

I’ve been running controlled lab tests on the external charging contacts of the DJI Osmo 360 to understand how they behave electrically and what is actually required to power the camera through them.

These four contacts are intended for the official DJI Battery Extension Tube. The rear‑right pin is the negative terminal, and the front‑left pin is the positive terminal. When applying 5 V across these two pins, the camera draws a small current of about 200 mA. At 6 V, the camera begins to charge, and once recording starts, it no longer uses the internal battery. As the voltage increases, the current draw gradually decreases. At 13 V, the current flow stops abruptly and the camera enters a protective safety mode, which resets once the power source is removed.

Based on these measurements, the magnetic clamp mounts (search term DJI 1/ “6327001336A”), which are supposed to power an Osmo 360 via the waterproof contacts, do not work with a standard USB-C power adapter. For power supply, a USB-C charging cable needs to be slightly modified. A 7.5V 1.5A power adapter connected Example: https://www.amazon.de/dp/B0DTG8DX8K to the positive (+) and negative (-) wires of a USB-C cable works perfectly. Data lines are not used in this quick-release mount, as it contains no electronics.

This mount has a practical problem: the USB-C connector attached to this magnetic mount protrudes into the 360° field of view, disrupting the “invisible rod” effect and rendering the mount unsuitable for clean mapillary or Street View shots. Therefore, I drilled two 0.3 mm holes on the left and right sides of the mount and connected the positive and negative terminals directly to the internal circuit board. I sealed the USB-C socket with waterproof adhesive and filled the aluminum housing containing the small circuit board, to which the two spring contacts are attached, with leak-proof ball bearing grease. This solution is now waterproof, and the cable no longer obstructs the 360-degree camera’s field of view.

When an OSMO 360 is placed on this magnetic mount, it wobbles slightly. Take a patch pad from a bicycle repair kit, the kind used to vulcanize a puncture in an inner tube. I cut a thin strip from one of these pads and stretched it across the center of the DJI magnetic mount. This now acts as a shock absorber, and the DJI Osmo no longer wobbles on the quick-release mount.

Ever since I solved the Osmo 360’s electrical quirks by routing power through its external housing contacts, life on the road has become noticeably calmer. The USB‑C connector is gone from the power line now — a small victory, but one that feels surprisingly liberating. I’ve already taken the camera through several drives on salt‑soaked winter roads, the kind that leave a thin white crust on everything they touch.


Because of that, I now travel with a water bag in the car. It’s a simple ritual: whenever the Osmo 360 comes back looking like it survived a minor ocean storm, I dip it into the bag and wash the salt film off its shell. Strange as it sounds, the moment has become part of the workflow — a quiet pause between stretches of road.

If you look closely at my recordings, you’ll recently notice an analog multimeter mounted directly in front of the steering wheel. This allows me to monitor the camera’s power consumption in real time. I can see exactly what the camera is doing, whether it’s charging or recording. This works much better than just a flashing red light. I recently experienced an unusual issue where recordings were suddenly interrupted. Thousands of kilometers of trouble-free recordings, and now this. The solution was to perform a one-time reset of the DJI Osmo 360. To detect such problems, including a full memory card, monitoring the camera is very helpful. An analog display showing the power flow from 0 to 1 amp is perfect.

Technically, I solved this by connecting a simple steel spring in series with the 7.5-volt power supply line on the roof of my vehicle. I chose the spring coils so that a current of 1 amp results in a voltage drop of 100 millivolts. I tap into this voltage, and a simple digital multimeter (DV) in the 100-millivolt range displays the instantaneous current flow to the camera on the steering wheel. Here’s what happens: The camera is off, the battery is empty, and therefore charging. The camera then draws almost exactly 1 amp at 7.5 volts. When the battery is fully charged, the current draw drops to zero. If I start a recording while it’s charging, the current draw drops from 100 Millivolt (1A) to approximately 40 millivolts. If the battery is fully charged and I start a recording, the current draw rises from 0 to approximately 40 millivolts. In every operating state, I expect the analog multimeter to read in the 40-millivolt range during a perfectly running recording. That’s perfect.

I’ve reached an interesting point in this project — several separate ideas and experiments have finally merged into one coherent, reliable workflow. And at this stage, it’s fair to say that what I’m doing is far removed from the simple approach Mapillary officially promotes: buy a GoPro Max 2, record a raw 360° video, and upload it untouched. Anyone who follows that path accepts a long list of compromises..

Right now, my M1 MacBook is busy rendering .mp4 files in DJI Studio. It takes its time — and without a PC cooling pad underneath, the machine would probably run hot enough to fry breakfast. At least this setup, along with the entire Mapillary upload workflow, has finally reached a point where everything runs reliably.

DJI Studio itself still feels like early‑stage software when it comes to processing speed. It gets the job done, but you can sense that there’s a lot of untapped potential waiting for future updates.

1 Like