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

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 20. 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




Project Status Update – Temporary Halt

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. STARTRC GPS Bluetooth Remote Controller for DJI Action 5 Pro/ Action 4 - StartRC 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.

Extensive GPX recording tests will now follow. Since this GPS remote is also sold in slightly different versions as a third-party alternative for the GoPro and Insta360 ecosystems, I’m hoping the GPS reception quality lives up to the standard those brands have set. As mentioned, the first test rides already look very promising.

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 commonly available magnetic clamp mounts (Search term DJI 1/ “6327001336A”) that claim to power the Osmo 360 via the waterproof contacts do not work. They lack a USB‑C Power Delivery (PD) controller, meaning they cannot negotiate or boost the voltage to the levels the camera expects.

I was unable to initiate a fast charging function for the internal battery via the external contacts; the maximum current flow via the Osmo 360 housing contacts is approximately 1,5 A, which suggests that choosing an external voltage source for the housing contacts with 7,5 volts and 3 A should be sufficient. Example: Amazon.de

There’s also a practical issue: the USB‑C plug attached to this magnetic mount protrudes into the 360° field of view, which breaks the “invisible stick” effect and makes it unsuitable for clean Mapillary or Street View captures.

When an OSMO 360 is placed on this magnetic mount, it wobbles slightly. This problem can be fixed by sticking a thin rubber sticker underneath it.

In summary, this Chinese accessory can currently only be used productively with extensive technical modifications. Such work requires electrical expertise, and you risk voiding the DJI warranty if you don’t know exactly what you’re doing.

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 footage, you might notice an old analog multimeter sitting just in front of the steering wheel. It’s not there for decoration. I use it to watch the camera’s current draw in real time. Every now and then — not often, but often enough to matter — the camera stops recording without warning. The needle is my early‑warning system; the slightest drop tells me instantly that something has gone wrong.

The technical fix behind all this is almost comically simple: a steel spring integrated into the DC power line. That, combined with the multimeter I dug out of my workshop — a relic from another era — gives me exactly the feedback I need. I run it on the 100‑millivolt DC range, and it performs its quiet duty with the kind of reliability only old analog instruments seem to have.

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.

My old Insta360 X3 never suffered from random crashes; Insta360 really knows how to build stable recording systems. The GoPro universe, on the other hand, lost me years ago after too many frustrating failures with the Hero 8. And now, as much as I enjoy working with the DJI Osmo 360, it’s clear that this camera isn’t fully mature either.

But that’s where the fun begins. With the help of a 1975 analog multimeter — a relic I rediscovered in my workshop — I’ve solved the issue. It now has a new purpose in life, quietly monitoring the Osmo’s current draw and giving me instant feedback whenever the camera misbehaves. It’s a strangely satisfying blend of old‑school instrumentation and modern 360° mapping technology, and it works far better than I ever expected.

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