From 2f5618ebb6fd0997e8f7d03e00a53f8fdb045330 Mon Sep 17 00:00:00 2001 From: "M. Samil Atesoglu" Date: Tue, 31 Mar 2026 18:37:49 +0300 Subject: [PATCH 1/9] Add Record Track (COLMAP) node to nosTrack plugin New node that records camera tracking data per frame and exports COLMAP-format files (cameras.txt + images.txt). - RecordTrackCOLMAP.cpp: Node implementation with Record/Stop/Save/Clear/ Open Folder functions. Captures position, rotation, FOV, sensor size, and lens distortion each frame. Exports OPENCV camera model intrinsics and world-to-camera extrinsics. - RecordTrackCOLMAP.nosdef: Node definition with Track input/output, output directory, image resolution, euler order, record toggle, and frame count pins. - Track.fbs: Added EulerOrder enum (ZYX, XYZ, YXZ, YZX, ZXY, XZY) for configurable euler angle rotation order in COLMAP export. - TrackMain.cpp: Registered RecordTrackCOLMAP in TrackNode enum and ExportNodeFunctions switch. - Track.noscfg: Bumped plugin version to 1.10.0, added nosdef entry. Review fixes applied: - Pin buffer size looked up by name instead of hardcoded index - Null checks on Track flatbuffer fields to prevent crashes - Euler convention matches MakeRotation (eulerAngleZYX with sign negation) - Float output precision set to 12 digits for camera parameters - macOS support added to Open Folder Co-Authored-By: Claude Opus 4.6 (1M context) # Conflicts: # Subsystems/nosTrackSubsystem/Config/Track.fbs --- Plugins/nosTrack/CHANGES.md | 57 +++ .../nosTrack/Config/RecordTrackCOLMAP.nosdef | 114 +++++ Plugins/nosTrack/Source/RecordTrackCOLMAP.cpp | 467 ++++++++++++++++++ Plugins/nosTrack/Source/TrackMain.cpp | 7 +- Plugins/nosTrack/Track.noscfg | 3 +- Subsystems/nosTrackSubsystem/Config/Track.fbs | 9 + 6 files changed, 655 insertions(+), 2 deletions(-) create mode 100644 Plugins/nosTrack/CHANGES.md create mode 100644 Plugins/nosTrack/Config/RecordTrackCOLMAP.nosdef create mode 100644 Plugins/nosTrack/Source/RecordTrackCOLMAP.cpp diff --git a/Plugins/nosTrack/CHANGES.md b/Plugins/nosTrack/CHANGES.md new file mode 100644 index 00000000..ec713c45 --- /dev/null +++ b/Plugins/nosTrack/CHANGES.md @@ -0,0 +1,57 @@ +# Record Track (COLMAP) Node + +## Summary + +A new node "Record Track (COLMAP)" added to the `nosTrack` plugin. It records incoming camera tracking data per frame and exports it in COLMAP's text format (`cameras.txt` + `images.txt`). + +## Files Changed + +### New files +- `Source/RecordTrackCOLMAP.cpp` — Node implementation +- `Config/RecordTrackCOLMAP.nosdef` — Node definition (pins, functions, metadata) + +### Modified files +- `Source/TrackMain.cpp` — Added `RecordTrackCOLMAP` to the `TrackNode` enum and `ExportNodeFunctions` switch +- `Track.noscfg` — Added `Config/RecordTrackCOLMAP.nosdef` to `node_definitions` + +## Node Design + +### Pins +| Pin | Type | Direction | Description | +|-----|------|-----------|-------------| +| Track | `nos.track.Track` | Input | Incoming tracking data | +| Track Out | `nos.track.Track` | Output (only) | Pass-through of input | +| Output Directory | `string` | Property | Folder picker for output | +| Image Resolution | `nos.fb.vec2u` | Property | Width/height (default 1920x1080) | +| Record | `bool` | Property | Mirrors Record/Stop functions | +| Frame Count | `uint` | Output (only) | Frames in buffer | + +### Functions +| Function | Behavior | +|----------|----------| +| Record | Validates folder is empty, clears buffer, starts recording. Orphaned while recording. | +| Stop | Stops recording (does NOT save). Orphaned while idle. | +| Save | Writes `cameras.txt` + `images.txt` to disk. Does not clear buffer. | +| Clear | Clears frame buffer and resets count. | +| Open Folder | Opens output directory in explorer (Windows) or xdg-open (Linux). | + +### State Management +- Record pin and functions are kept in sync bidirectionally. A `SyncingRecordPin` bool guard prevents re-entrant loops between pin changes and function calls. +- Function orphan states: Record/Stop toggle via `SetNodeOrphanState` using a `Name -> UUID` map built in constructor. +- Status messages show recording state + frame count, and persist error messages (e.g., "Target folder is not empty") via `LastError` until user changes the output directory. +- Non-empty folder check: Recording fails with a FAILURE status if the target folder already has files. + +### COLMAP Output Format +- `cameras.txt`: One OPENCV camera per frame — `fx, fy, cx, cy, k1, k2, p1, p2` derived from Track FOV, sensor size, pixel aspect ratio, lens distortion. +- `images.txt`: Per-frame pose — Euler angles converted to quaternion (world-to-camera), translation as `t = -R * C`. + +## Known Review Points +- Euler-to-quaternion convention: The Track's rotation fields (roll/tilt/pan) are passed through `glm::quat(eulerRadians)` then inverted for COLMAP's world-to-camera convention. May need validation against actual tracker output. +- One camera per frame: Each frame gets its own camera entry. This handles zoom/FOV changes but may be unusual for COLMAP workflows with constant intrinsics. +- No `points3D.txt`: COLMAP expects this file too (can be empty). Not currently written. +- `std::system()` for Open Folder: Works but is a simple shell call. Could be replaced with platform APIs if needed. + +## Build +``` +./nodos dev build -p Project13 --target nosTrack +``` diff --git a/Plugins/nosTrack/Config/RecordTrackCOLMAP.nosdef b/Plugins/nosTrack/Config/RecordTrackCOLMAP.nosdef new file mode 100644 index 00000000..ad4288b7 --- /dev/null +++ b/Plugins/nosTrack/Config/RecordTrackCOLMAP.nosdef @@ -0,0 +1,114 @@ +{ + "nodes": [ + { + "class_name": "RecordTrackCOLMAP", + "menu_info": { + "category": "nosTrack", + "display_name": "Record Track (COLMAP)", + "name_aliases": [ "colmap", "export camera", "record camera" ] + }, + "node": { + "class_name": "RecordTrackCOLMAP", + "display_name": "Record Track (COLMAP)", + "contents_type": "Job", + "description": "Records camera tracking data each frame while recording is enabled, then exports cameras.txt and images.txt in COLMAP format. Intrinsics (focal length, distortion) are derived from the Track's FOV, sensor size, and lens distortion. Extrinsics (rotation, translation) are stored per frame in world-to-camera convention.", + "pins": [ + { + "name": "Track", + "type_name": "nos.track.Track", + "show_as": "INPUT_PIN", + "can_show_as": "INPUT_PIN_OR_PROPERTY", + "description": "Incoming camera tracking data to record. Position, rotation, FOV, sensor size, and lens distortion are captured each frame." + }, + { + "name": "TrackOut", + "display_name": "Track Out", + "type_name": "nos.track.Track", + "show_as": "OUTPUT_PIN", + "can_show_as": "OUTPUT_PIN_ONLY", + "description": "Pass-through of the incoming Track data." + }, + { + "name": "OutputDirectory", + "display_name": "Output Directory", + "type_name": "string", + "show_as": "PROPERTY", + "can_show_as": "INPUT_PIN_OR_PROPERTY", + "visualizer": { "type": "FOLDER_PICKER" }, + "description": "Directory where cameras.txt and images.txt will be written when recording stops. Must be empty to start recording." + }, + { + "name": "ImageResolution", + "display_name": "Image Resolution", + "type_name": "nos.fb.vec2u", + "show_as": "PROPERTY", + "can_show_as": "INPUT_PIN_OR_PROPERTY", + "data": { + "x": 1920, + "y": 1080 + }, + "description": "Image resolution in pixels (width, height). Used to compute focal length and principal point for COLMAP camera model." + }, + { + "name": "EulerOrder", + "display_name": "Euler Order", + "type_name": "nos.track.EulerOrder", + "show_as": "PROPERTY", + "can_show_as": "INPUT_PIN_OR_PROPERTY", + "data": "ZYX", + "description": "Euler angle rotation order used when converting Track rotation to COLMAP extrinsics. Default ZYX matches the FreeD node convention." + }, + { + "name": "Record", + "type_name": "bool", + "show_as": "PROPERTY", + "can_show_as": "INPUT_PIN_OR_PROPERTY", + "data": false, + "description": "Toggle recording. Mirrors Record/Stop functions. Enabling clears previous frames and starts capturing. Will fail if the output directory is not empty." + }, + { + "name": "FrameCount", + "display_name": "Frame Count", + "type_name": "uint", + "show_as": "OUTPUT_PIN", + "can_show_as": "OUTPUT_PIN_ONLY", + "data": 0, + "description": "Number of frames recorded in the current session." + } + ], + "functions": [ + { + "class_name": "RecordTrackCOLMAP_Record", + "display_name": "Record", + "contents_type": "Job", + "pins": [] + }, + { + "class_name": "RecordTrackCOLMAP_Stop", + "display_name": "Stop", + "contents_type": "Job", + "pins": [] + }, + { + "class_name": "RecordTrackCOLMAP_Save", + "display_name": "Save", + "contents_type": "Job", + "pins": [] + }, + { + "class_name": "RecordTrackCOLMAP_Clear", + "display_name": "Clear", + "contents_type": "Job", + "pins": [] + }, + { + "class_name": "RecordTrackCOLMAP_OpenFolder", + "display_name": "Open Folder", + "contents_type": "Job", + "pins": [] + } + ] + } + } + ] +} diff --git a/Plugins/nosTrack/Source/RecordTrackCOLMAP.cpp b/Plugins/nosTrack/Source/RecordTrackCOLMAP.cpp new file mode 100644 index 00000000..9b2751cd --- /dev/null +++ b/Plugins/nosTrack/Source/RecordTrackCOLMAP.cpp @@ -0,0 +1,467 @@ +// Copyright MediaZ Teknoloji A.S. All Rights Reserved. + +#include +#include "Track_generated.h" + +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace nos::track +{ + +NOS_REGISTER_NAME(OutputDirectory); +NOS_REGISTER_NAME(ImageResolution); +NOS_REGISTER_NAME(EulerOrder); +NOS_REGISTER_NAME(Record); +NOS_REGISTER_NAME(FrameCount); + +NOS_REGISTER_NAME(RecordTrackCOLMAP_Record); +NOS_REGISTER_NAME(RecordTrackCOLMAP_Stop); +NOS_REGISTER_NAME(RecordTrackCOLMAP_Save); +NOS_REGISTER_NAME(RecordTrackCOLMAP_Clear); +NOS_REGISTER_NAME(RecordTrackCOLMAP_OpenFolder); + +struct RecordedFrame +{ + glm::vec3 Location; + glm::vec3 Rotation; // Euler degrees (roll, tilt, pan) + float FOV; + glm::vec2 SensorSize; + float FocusDistance; + float PixelAspectRatio; + float K1; + float K2; +}; + +struct RecordTrackCOLMAPContext : NodeContext +{ + std::string OutputDir; + nosVec2u ImageResolution = {1920, 1080}; + track::EulerOrder EulerOrd = track::EulerOrder::ZYX; + bool Recording = false; + bool SyncingRecordPin = false; + std::string LastError; + std::vector Frames; + std::unordered_map FunctionIds; + + RecordTrackCOLMAPContext(nosFbNodePtr node) : NodeContext(node) + { + if (node->functions()) + { + for (auto* func : *node->functions()) + FunctionIds[nos::Name(func->class_name()->c_str())] = *func->id(); + } + + if (node->pins()) + { + for (auto* pin : *node->pins()) + { + auto name = nos::Name(pin->name()->c_str()); + if (flatbuffers::IsFieldPresent(pin, fb::Pin::VT_DATA)) + { + nosBuffer value = {.Data = (void*)pin->data()->data(), .Size = pin->data()->size()}; + OnPinValueChanged(name, *pin->id(), value); + } + } + } + UpdateFunctionOrphanStates(); + UpdateStatus(); + } + + void SetFunctionOrphanState(nos::Name funcName, fb::NodeOrphanStateType type) + { + auto it = FunctionIds.find(funcName); + if (it != FunctionIds.end()) + NodeContext::SetNodeOrphanState(it->second, type); + } + + void UpdateFunctionOrphanStates() + { + if (Recording) + { + SetFunctionOrphanState(NSN_RecordTrackCOLMAP_Record, fb::NodeOrphanStateType::ORPHAN); + SetFunctionOrphanState(NSN_RecordTrackCOLMAP_Stop, fb::NodeOrphanStateType::ACTIVE); + } + else + { + SetFunctionOrphanState(NSN_RecordTrackCOLMAP_Record, fb::NodeOrphanStateType::ACTIVE); + SetFunctionOrphanState(NSN_RecordTrackCOLMAP_Stop, fb::NodeOrphanStateType::ORPHAN); + } + } + + void SyncRecordPin(bool value) + { + SyncingRecordPin = true; + nosEngine.SetPinValueByName(NodeId, NSN_Record, nosBuffer{.Data = &value, .Size = sizeof(value)}); + SyncingRecordPin = false; + } + + bool StartRecording() + { + std::string error; + if (!CanStartRecording(error)) + { + LastError = std::move(error); + UpdateStatus(); + return false; + } + LastError.clear(); + Frames.clear(); + Recording = true; + SyncRecordPin(true); + UpdateFrameCountPin(); + UpdateFunctionOrphanStates(); + UpdateStatus(); + nosEngine.LogI("RecordTrackCOLMAP: Recording started"); + return true; + } + + void StopRecording() + { + Recording = false; + SyncRecordPin(false); + UpdateFunctionOrphanStates(); + UpdateStatus(); + nosEngine.LogI("RecordTrackCOLMAP: Recording stopped (%zu frames in buffer)", Frames.size()); + } + + void OnPinValueChanged(nos::Name pinName, uuid const& pinId, nosBuffer val) override + { + if (pinName == NSN_OutputDirectory) + { + OutputDir = InterpretPinValue(val.Data); + LastError.clear(); + UpdateStatus(); + } + else if (pinName == NSN_ImageResolution) + ImageResolution = *(nosVec2u*)val.Data; + else if (pinName == NSN_EulerOrder) + EulerOrd = *(track::EulerOrder*)val.Data; + else if (pinName == NSN_Record) + { + if (SyncingRecordPin) + return; + bool newVal = *(bool*)val.Data; + if (newVal && !Recording) + StartRecording(); + else if (!newVal && Recording) + StopRecording(); + } + } + + bool CanStartRecording(std::string& outError) + { + if (OutputDir.empty()) + { + outError = "Set output directory"; + return false; + } + + std::filesystem::path outDir = nos::Utf8ToPath(OutputDir); + try + { + if (std::filesystem::exists(outDir) && !std::filesystem::is_empty(outDir)) + { + outError = "Target folder is not empty"; + return false; + } + } + catch (std::filesystem::filesystem_error& e) + { + nosEngine.LogE("RecordTrackCOLMAP: %s", e.what()); + outError = e.what(); + return false; + } + return true; + } + + void UpdateFrameCountPin() + { + uint32_t count = (uint32_t)Frames.size(); + nosEngine.SetPinValueByName(NodeId, NSN_FrameCount, nosBuffer{.Data = &count, .Size = sizeof(count)}); + } + + void UpdateStatus() + { + if (!LastError.empty()) + SetNodeStatusMessage(LastError, fb::NodeStatusMessageType::FAILURE); + else if (OutputDir.empty()) + SetNodeStatusMessage("Set output directory", fb::NodeStatusMessageType::WARNING); + else if (Recording) + SetNodeStatusMessage("Recording (" + std::to_string(Frames.size()) + " frames)", fb::NodeStatusMessageType::INFO); + else if (!Frames.empty()) + SetNodeStatusMessage("Idle (" + std::to_string(Frames.size()) + " frames in buffer)", fb::NodeStatusMessageType::INFO); + else + SetNodeStatusMessage("Idle", fb::NodeStatusMessageType::INFO); + } + + nosResult ExecuteNode(nosNodeExecuteParams* params) override + { + auto pins = GetPinValues(params); + auto ids = GetPinIds(params); + + // Pass through Track input to output + auto trackPinData = pins[NOS_NAME("Track")]; + size_t trackDataSize = 0; + for (size_t i = 0; i < params->PinCount; ++i) + { + if (params->Pins[i].Name == NOS_NAME("Track")) + { + trackDataSize = params->Pins[i].Data->Size; + break; + } + } + nosEngine.SetPinValue(ids[NOS_NAME("TrackOut")], {.Data = trackPinData, .Size = trackDataSize}); + + if (!Recording) + return NOS_RESULT_SUCCESS; + + auto* trackData = flatbuffers::GetRoot(trackPinData); + if (!trackData) + return NOS_RESULT_SUCCESS; + + RecordedFrame frame{}; + if (auto* loc = trackData->location()) + frame.Location = {loc->x(), loc->y(), loc->z()}; + if (auto* rot = trackData->rotation()) + frame.Rotation = {rot->x(), rot->y(), rot->z()}; + frame.FOV = trackData->fov(); + if (auto* ss = trackData->sensor_size()) + frame.SensorSize = {ss->x(), ss->y()}; + frame.FocusDistance = trackData->focus_distance(); + frame.PixelAspectRatio = trackData->pixel_aspect_ratio(); + if (auto* ld = trackData->lens_distortion()) + { + frame.K1 = ld->k1k2().x(); + frame.K2 = ld->k1k2().y(); + } + Frames.push_back(frame); + + UpdateFrameCountPin(); + UpdateStatus(); + + return NOS_RESULT_SUCCESS; + } + + void WriteFiles() + { + if (OutputDir.empty()) + { + nosEngine.LogE("RecordTrackCOLMAP: Output directory is empty"); + return; + } + if (Frames.empty()) + { + nosEngine.LogW("RecordTrackCOLMAP: No frames recorded"); + return; + } + + std::filesystem::path outDir = nos::Utf8ToPath(OutputDir); + try + { + if (!std::filesystem::exists(outDir)) + std::filesystem::create_directories(outDir); + } + catch (std::filesystem::filesystem_error& e) + { + nosEngine.LogE("RecordTrackCOLMAP: %s", e.what()); + return; + } + + WriteCamerasTxt(outDir); + WriteImagesTxt(outDir); + nosEngine.LogI("RecordTrackCOLMAP: Saved %zu frames to %s", Frames.size(), OutputDir.c_str()); + } + + float ComputeFocalLengthPixels(const RecordedFrame& frame) const + { + if (frame.FOV <= 0.0f) + return static_cast(ImageResolution.x); + float fovRad = glm::radians(frame.FOV); + return (ImageResolution.x * 0.5f) / std::tan(fovRad * 0.5f); + } + + void WriteCamerasTxt(const std::filesystem::path& outDir) + { + auto path = outDir / "cameras.txt"; + std::ofstream file(path); + if (!file.is_open()) + { + nosEngine.LogE("RecordTrackCOLMAP: Cannot open %s", nos::PathToUtf8(path).c_str()); + return; + } + + file << std::setprecision(12); + file << "# Camera list with one line of data per camera:\n"; + file << "# CAMERA_ID, MODEL, WIDTH, HEIGHT, PARAMS[]\n"; + file << "# Number of cameras: " << Frames.size() << "\n"; + + for (size_t i = 0; i < Frames.size(); ++i) + { + float fx = ComputeFocalLengthPixels(Frames[i]); + float fy = fx; + if (Frames[i].PixelAspectRatio > 0.0f) + fy = fx / Frames[i].PixelAspectRatio; + + float cx = ImageResolution.x * 0.5f; + float cy = ImageResolution.y * 0.5f; + + // OPENCV model: fx, fy, cx, cy, k1, k2, p1, p2 + float k1 = Frames[i].K1; + float k2 = Frames[i].K2; + + file << (i + 1) << " OPENCV " << ImageResolution.x << " " << ImageResolution.y << " " + << fx << " " << fy << " " << cx << " " << cy << " " + << k1 << " " << k2 << " 0 0\n"; + } + } + + static glm::mat3 EulerToRotationMatrix(glm::vec3 rot, track::EulerOrder order) + { + // rot is (roll, tilt, pan) = (x, y, z) in radians + // Sign convention matches MakeRotation: negate roll (x) and tilt (y) + float r = -rot.x, t = -rot.y, p = rot.z; + switch (order) + { + default: + case track::EulerOrder::ZYX: return glm::mat3(glm::eulerAngleZYX(p, t, r)); + case track::EulerOrder::XYZ: return glm::mat3(glm::eulerAngleXYZ(r, t, p)); + case track::EulerOrder::YXZ: return glm::mat3(glm::eulerAngleYXZ(t, r, p)); + case track::EulerOrder::YZX: return glm::mat3(glm::eulerAngleYZX(t, p, r)); + case track::EulerOrder::ZXY: return glm::mat3(glm::eulerAngleZXY(p, r, t)); + case track::EulerOrder::XZY: return glm::mat3(glm::eulerAngleXZY(r, p, t)); + } + } + + void WriteImagesTxt(const std::filesystem::path& outDir) + { + auto path = outDir / "images.txt"; + std::ofstream file(path); + if (!file.is_open()) + { + nosEngine.LogE("RecordTrackCOLMAP: Cannot open %s", nos::PathToUtf8(path).c_str()); + return; + } + + file << std::setprecision(12); + file << "# Image list with two lines of data per image:\n"; + file << "# IMAGE_ID, QW, QX, QY, QZ, TX, TY, TZ, CAMERA_ID, NAME\n"; + file << "# POINTS2D[] as (X, Y, POINT3D_ID)\n"; + file << "# Number of images: " << Frames.size() << "\n"; + + for (size_t i = 0; i < Frames.size(); ++i) + { + auto& frame = Frames[i]; + + // Convert Euler angles to rotation matrix + // Sign convention matches MakeRotation: negate roll (x) and tilt (y) + glm::vec3 rot = glm::radians(frame.Rotation); + glm::mat3 R_c2w = EulerToRotationMatrix(rot, EulerOrd); + + // COLMAP expects world-to-camera rotation + glm::mat3 R_w2c = glm::transpose(R_c2w); + glm::quat q_w2c = glm::quat_cast(R_w2c); + + // COLMAP translation: t = -R * C (camera center in world coords) + glm::vec3 t = -R_w2c * frame.Location; + + // IMAGE_ID, QW, QX, QY, QZ, TX, TY, TZ, CAMERA_ID, NAME + file << (i + 1) << " " + << q_w2c.w << " " << q_w2c.x << " " << q_w2c.y << " " << q_w2c.z << " " + << t.x << " " << t.y << " " << t.z << " " + << (i + 1) << " " + << "frame_" << std::setfill('0') << std::setw(6) << i << ".png\n"; + // Empty points line (required by COLMAP format) + file << "\n"; + } + } + + // TODO: Replace std::system with platform APIs (ShellExecuteW / posix_spawnp) to avoid shell injection via crafted paths + static void OpenFolderInExplorer(const std::filesystem::path& folder) + { +#if defined(_WIN32) + std::string cmd = "explorer \"" + nos::PathToUtf8(folder) + "\""; +#elif defined(__APPLE__) + std::string cmd = "open \"" + nos::PathToUtf8(folder) + "\""; +#else + std::string cmd = "xdg-open \"" + nos::PathToUtf8(folder) + "\""; +#endif + std::system(cmd.c_str()); + } + + static nosResult GetFunctions(size_t* count, nosName* names, nosPfnNodeFunctionExecute* fns) + { + *count = 5; + if (!names || !fns) + return NOS_RESULT_SUCCESS; + + names[0] = NOS_NAME_STATIC("RecordTrackCOLMAP_Record"); + fns[0] = [](void* ctx, nosFunctionExecuteParams*) { + auto* self = static_cast(ctx); + if (self->Recording) + return NOS_RESULT_SUCCESS; + self->StartRecording(); + return NOS_RESULT_SUCCESS; + }; + + names[1] = NOS_NAME_STATIC("RecordTrackCOLMAP_Stop"); + fns[1] = [](void* ctx, nosFunctionExecuteParams*) { + auto* self = static_cast(ctx); + if (!self->Recording) + return NOS_RESULT_SUCCESS; + self->StopRecording(); + return NOS_RESULT_SUCCESS; + }; + + names[2] = NOS_NAME_STATIC("RecordTrackCOLMAP_Save"); + fns[2] = [](void* ctx, nosFunctionExecuteParams*) { + auto* self = static_cast(ctx); + self->WriteFiles(); + return NOS_RESULT_SUCCESS; + }; + + names[3] = NOS_NAME_STATIC("RecordTrackCOLMAP_Clear"); + fns[3] = [](void* ctx, nosFunctionExecuteParams*) { + auto* self = static_cast(ctx); + self->Frames.clear(); + self->UpdateFrameCountPin(); + self->UpdateStatus(); + nosEngine.LogI("RecordTrackCOLMAP: Buffer cleared"); + return NOS_RESULT_SUCCESS; + }; + + names[4] = NOS_NAME_STATIC("RecordTrackCOLMAP_OpenFolder"); + fns[4] = [](void* ctx, nosFunctionExecuteParams*) { + auto* self = static_cast(ctx); + if (self->OutputDir.empty()) + { + nosEngine.LogW("RecordTrackCOLMAP: Output directory not set"); + return NOS_RESULT_FAILED; + } + std::filesystem::path outDir = nos::Utf8ToPath(self->OutputDir); + if (!std::filesystem::exists(outDir)) + { + nosEngine.LogW("RecordTrackCOLMAP: Directory does not exist: %s", self->OutputDir.c_str()); + return NOS_RESULT_FAILED; + } + OpenFolderInExplorer(outDir); + return NOS_RESULT_SUCCESS; + }; + + return NOS_RESULT_SUCCESS; + } +}; + +void RegisterRecordTrackCOLMAP(nosNodeFunctions* fn) +{ + NOS_BIND_NODE_CLASS(NOS_NAME("RecordTrackCOLMAP"), RecordTrackCOLMAPContext, fn); +} + +} // namespace nos::track diff --git a/Plugins/nosTrack/Source/TrackMain.cpp b/Plugins/nosTrack/Source/TrackMain.cpp index c330165d..628abd33 100644 --- a/Plugins/nosTrack/Source/TrackMain.cpp +++ b/Plugins/nosTrack/Source/TrackMain.cpp @@ -15,12 +15,14 @@ enum TrackNode : int FreeD, UserTrack, AddTrack, + RecordTrackCOLMAP, Count }; void RegisterFreeDNode(nosNodeFunctions* functions); void RegisterController(nosNodeFunctions* functions); void RegisterAddTrack(nosNodeFunctions*); +void RegisterRecordTrackCOLMAP(nosNodeFunctions*); nosResult NOSAPI_CALL ExportNodeFunctions(size_t* outSize, nosNodeFunctions** outList) { @@ -40,7 +42,10 @@ nosResult NOSAPI_CALL ExportNodeFunctions(size_t* outSize, nosNodeFunctions** ou RegisterController(node); break; case TrackNode::AddTrack: - RegisterAddTrack(node); + RegisterAddTrack(node); + break; + case TrackNode::RecordTrackCOLMAP: + RegisterRecordTrackCOLMAP(node); break; } } diff --git a/Plugins/nosTrack/Track.noscfg b/Plugins/nosTrack/Track.noscfg index 861423c0..d6310c82 100644 --- a/Plugins/nosTrack/Track.noscfg +++ b/Plugins/nosTrack/Track.noscfg @@ -16,7 +16,8 @@ "node_definitions": [ "Config/FreeD.nosdef", "Config/UserTrack.nosdef", - "Config/AddTrack.nosdef" + "Config/AddTrack.nosdef", + "Config/RecordTrackCOLMAP.nosdef" ], "defaults": [ "Config/Defaults.json" diff --git a/Subsystems/nosTrackSubsystem/Config/Track.fbs b/Subsystems/nosTrackSubsystem/Config/Track.fbs index e1dcce23..75e6e1ff 100644 --- a/Subsystems/nosTrackSubsystem/Config/Track.fbs +++ b/Subsystems/nosTrackSubsystem/Config/Track.fbs @@ -42,3 +42,12 @@ enum RotationSystem : uint { RPT = 4, PRT = 5, } + +enum EulerOrder : uint { + ZYX = 0, + XYZ = 1, + YXZ = 2, + YZX = 3, + ZXY = 4, + XZY = 5, +} From 803bab585622b3a9508e0e6b6c205e94ada16365 Mon Sep 17 00:00:00 2001 From: "M. Samil Atesoglu" Date: Wed, 1 Apr 2026 15:38:38 +0300 Subject: [PATCH 2/9] Add Playback Track (COLMAP) node, rename pins, use NodeContext helpers New PlaybackTrackCOLMAP node loads cameras.txt + images.txt and outputs Track data. Two modes via PlaybackMode enum: - Sequential: Play/Stop auto-advance frames each execution - Manual: frame index input pin controls which frame to output Pins and functions are orphaned based on mode. Also: - Added PlaybackMode enum to Track.fbs - Renamed Record node pins to InTrack/OutTrack with "Track" display name - Renamed Playback frame pins to InFrameIndex/OutFrameIndex - Replaced nosEngine.SetPinValueByName with NodeContext::SetPinValue Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Config/PlaybackTrackCOLMAP.nosdef | 109 ++++ .../nosTrack/Config/RecordTrackCOLMAP.nosdef | 7 +- .../nosTrack/Source/PlaybackTrackCOLMAP.cpp | 486 ++++++++++++++++++ Plugins/nosTrack/Source/RecordTrackCOLMAP.cpp | 16 +- Plugins/nosTrack/Source/TrackMain.cpp | 5 + Plugins/nosTrack/Track.noscfg | 3 +- Subsystems/nosTrackSubsystem/Config/Track.fbs | 5 + 7 files changed, 618 insertions(+), 13 deletions(-) create mode 100644 Plugins/nosTrack/Config/PlaybackTrackCOLMAP.nosdef create mode 100644 Plugins/nosTrack/Source/PlaybackTrackCOLMAP.cpp diff --git a/Plugins/nosTrack/Config/PlaybackTrackCOLMAP.nosdef b/Plugins/nosTrack/Config/PlaybackTrackCOLMAP.nosdef new file mode 100644 index 00000000..d0f260c5 --- /dev/null +++ b/Plugins/nosTrack/Config/PlaybackTrackCOLMAP.nosdef @@ -0,0 +1,109 @@ +{ + "nodes": [ + { + "class_name": "PlaybackTrackCOLMAP", + "menu_info": { + "category": "nosTrack", + "display_name": "Playback Track (COLMAP)", + "name_aliases": [ "colmap", "import camera", "playback camera" ] + }, + "node": { + "class_name": "PlaybackTrackCOLMAP", + "display_name": "Playback Track (COLMAP)", + "contents_type": "Job", + "always_execute": true, + "description": "Loads camera tracking data from COLMAP text format (cameras.txt + images.txt) and outputs Track data. Sequential mode auto-advances each frame with Play/Stop control. Manual mode outputs the frame at a given index.", + "pins": [ + { + "name": "InputDirectory", + "display_name": "Input Directory", + "type_name": "string", + "show_as": "PROPERTY", + "can_show_as": "INPUT_PIN_OR_PROPERTY", + "visualizer": { "type": "FOLDER_PICKER" }, + "description": "Directory containing cameras.txt and images.txt in COLMAP text format." + }, + { + "name": "EulerOrder", + "display_name": "Euler Order", + "type_name": "nos.track.EulerOrder", + "show_as": "PROPERTY", + "can_show_as": "INPUT_PIN_OR_PROPERTY", + "data": "ZYX", + "description": "Euler angle rotation order for converting COLMAP quaternion to Track rotation. Default ZYX matches the FreeD node convention." + }, + { + "name": "Mode", + "type_name": "nos.track.PlaybackMode", + "show_as": "PROPERTY", + "can_show_as": "INPUT_PIN_OR_PROPERTY", + "data": "Sequential", + "description": "Sequential: auto-advances one frame per execution with Play/Stop control. Manual: outputs the frame at the given Frame Input index." + }, + { + "name": "Loop", + "type_name": "bool", + "show_as": "PROPERTY", + "can_show_as": "INPUT_PIN_OR_PROPERTY", + "data": true, + "description": "Loop playback to the beginning when the last frame is reached." + }, + { + "name": "InFrameIndex", + "display_name": "Frame Index", + "type_name": "uint", + "show_as": "INPUT_PIN", + "can_show_as": "INPUT_PIN_OR_PROPERTY", + "data": 0, + "description": "Frame index to output (Manual mode only)." + }, + { + "name": "Track", + "type_name": "nos.track.Track", + "show_as": "OUTPUT_PIN", + "can_show_as": "OUTPUT_PIN_ONLY", + "description": "Track data for the current frame." + }, + { + "name": "OutFrameIndex", + "display_name": "Frame Index", + "type_name": "uint", + "show_as": "OUTPUT_PIN", + "can_show_as": "OUTPUT_PIN_ONLY", + "data": 0, + "description": "Current playback frame index." + }, + { + "name": "FrameCount", + "display_name": "Frame Count", + "type_name": "uint", + "show_as": "OUTPUT_PIN", + "can_show_as": "OUTPUT_PIN_ONLY", + "data": 0, + "description": "Total number of frames loaded." + } + ], + "functions": [ + { + "class_name": "PlaybackTrackCOLMAP_Play", + "display_name": "Play", + "contents_type": "Job", + "pins": [] + }, + { + "class_name": "PlaybackTrackCOLMAP_Stop", + "display_name": "Stop", + "contents_type": "Job", + "pins": [] + }, + { + "class_name": "PlaybackTrackCOLMAP_OpenFolder", + "display_name": "Open Folder", + "contents_type": "Job", + "pins": [] + } + ] + } + } + ] +} diff --git a/Plugins/nosTrack/Config/RecordTrackCOLMAP.nosdef b/Plugins/nosTrack/Config/RecordTrackCOLMAP.nosdef index ad4288b7..5a17b344 100644 --- a/Plugins/nosTrack/Config/RecordTrackCOLMAP.nosdef +++ b/Plugins/nosTrack/Config/RecordTrackCOLMAP.nosdef @@ -14,15 +14,16 @@ "description": "Records camera tracking data each frame while recording is enabled, then exports cameras.txt and images.txt in COLMAP format. Intrinsics (focal length, distortion) are derived from the Track's FOV, sensor size, and lens distortion. Extrinsics (rotation, translation) are stored per frame in world-to-camera convention.", "pins": [ { - "name": "Track", + "name": "InTrack", + "display_name": "Track", "type_name": "nos.track.Track", "show_as": "INPUT_PIN", "can_show_as": "INPUT_PIN_OR_PROPERTY", "description": "Incoming camera tracking data to record. Position, rotation, FOV, sensor size, and lens distortion are captured each frame." }, { - "name": "TrackOut", - "display_name": "Track Out", + "name": "OutTrack", + "display_name": "Track", "type_name": "nos.track.Track", "show_as": "OUTPUT_PIN", "can_show_as": "OUTPUT_PIN_ONLY", diff --git a/Plugins/nosTrack/Source/PlaybackTrackCOLMAP.cpp b/Plugins/nosTrack/Source/PlaybackTrackCOLMAP.cpp new file mode 100644 index 00000000..d57b5b0c --- /dev/null +++ b/Plugins/nosTrack/Source/PlaybackTrackCOLMAP.cpp @@ -0,0 +1,486 @@ +// Copyright MediaZ Teknoloji A.S. All Rights Reserved. + +#include +#include "Track_generated.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace nos::track +{ + +NOS_REGISTER_NAME_SPACED(Playback_InputDirectory, "InputDirectory"); +NOS_REGISTER_NAME_SPACED(Playback_EulerOrder, "EulerOrder"); +NOS_REGISTER_NAME_SPACED(Playback_Mode, "Mode"); +NOS_REGISTER_NAME_SPACED(Playback_Loop, "Loop"); +NOS_REGISTER_NAME_SPACED(Playback_InFrameIndex, "InFrameIndex"); +NOS_REGISTER_NAME_SPACED(Playback_OutFrameIndex, "OutFrameIndex"); +NOS_REGISTER_NAME_SPACED(Playback_FrameCount, "FrameCount"); + +NOS_REGISTER_NAME(PlaybackTrackCOLMAP_Play); +NOS_REGISTER_NAME(PlaybackTrackCOLMAP_Stop); +NOS_REGISTER_NAME(PlaybackTrackCOLMAP_OpenFolder); + +struct COLMAPCamera +{ + uint32_t Id = 0; + std::string Model; + uint32_t Width = 0; + uint32_t Height = 0; + float Fx = 0, Fy = 0, Cx = 0, Cy = 0; + float K1 = 0, K2 = 0, P1 = 0, P2 = 0; +}; + +struct COLMAPImage +{ + uint32_t Id = 0; + glm::quat Q{1, 0, 0, 0}; + glm::vec3 T{0}; + uint32_t CameraId = 0; +}; + +struct PlaybackTrackCOLMAPContext : NodeContext +{ + std::string InputDir; + track::EulerOrder EulerOrd = track::EulerOrder::ZYX; + track::PlaybackMode Mode = track::PlaybackMode::Sequential; + bool Loop = true; + bool Playing = false; + uint32_t ManualFrame = 0; + std::string LastError; + std::vector Frames; + uint32_t CurrentFrame = 0; + std::unordered_map FunctionIds; + std::unordered_map PinIds; + + PlaybackTrackCOLMAPContext(nosFbNodePtr node) : NodeContext(node) + { + if (node->functions()) + { + for (auto* func : *node->functions()) + FunctionIds[nos::Name(func->class_name()->c_str())] = *func->id(); + } + + if (node->pins()) + { + for (auto* pin : *node->pins()) + { + auto name = nos::Name(pin->name()->c_str()); + PinIds[name] = *pin->id(); + if (flatbuffers::IsFieldPresent(pin, fb::Pin::VT_DATA)) + { + nosBuffer value = {.Data = (void*)pin->data()->data(), .Size = pin->data()->size()}; + OnPinValueChanged(name, *pin->id(), value); + } + } + } + UpdateOrphanStates(); + UpdateStatus(); + } + + void SetFunctionOrphanState(nos::Name funcName, fb::NodeOrphanStateType type) + { + auto it = FunctionIds.find(funcName); + if (it != FunctionIds.end()) + NodeContext::SetNodeOrphanState(it->second, type); + } + + void SetPinOrphanState(nos::Name pinName, fb::PinOrphanStateType type) + { + auto it = PinIds.find(pinName); + if (it != PinIds.end()) + NodeContext::SetPinOrphanState(it->second, type); + } + + void UpdateOrphanStates() + { + bool sequential = Mode == track::PlaybackMode::Sequential; + + // Sequential: Play/Stop active, Load/FrameInput orphaned + // Manual: Load/FrameInput active, Play/Stop orphaned + if (sequential) + { + SetPinOrphanState(NSN_Playback_InFrameIndex, fb::PinOrphanStateType::ORPHAN); + + if (Playing) + { + SetFunctionOrphanState(NSN_PlaybackTrackCOLMAP_Play, fb::NodeOrphanStateType::ORPHAN); + SetFunctionOrphanState(NSN_PlaybackTrackCOLMAP_Stop, fb::NodeOrphanStateType::ACTIVE); + } + else + { + SetFunctionOrphanState(NSN_PlaybackTrackCOLMAP_Play, fb::NodeOrphanStateType::ACTIVE); + SetFunctionOrphanState(NSN_PlaybackTrackCOLMAP_Stop, fb::NodeOrphanStateType::ORPHAN); + } + } + else + { + SetFunctionOrphanState(NSN_PlaybackTrackCOLMAP_Play, fb::NodeOrphanStateType::ORPHAN); + SetFunctionOrphanState(NSN_PlaybackTrackCOLMAP_Stop, fb::NodeOrphanStateType::ORPHAN); + SetPinOrphanState(NSN_Playback_InFrameIndex, fb::PinOrphanStateType::ACTIVE); + } + } + + void OnPinValueChanged(nos::Name pinName, uuid const& pinId, nosBuffer val) override + { + if (pinName == NSN_Playback_InputDirectory) + { + InputDir = InterpretPinValue(val.Data); + LastError.clear(); + if (Mode == track::PlaybackMode::Manual && !InputDir.empty()) + LoadFromDirectory(); + else + UpdateStatus(); + } + else if (pinName == NSN_Playback_EulerOrder) + EulerOrd = *(track::EulerOrder*)val.Data; + else if (pinName == NSN_Playback_Mode) + { + auto newMode = *(track::PlaybackMode*)val.Data; + if (newMode != Mode) + { + Mode = newMode; + Playing = false; + UpdateOrphanStates(); + UpdateStatus(); + } + } + else if (pinName == NSN_Playback_Loop) + Loop = *(bool*)val.Data; + else if (pinName == NSN_Playback_InFrameIndex) + ManualFrame = *(uint32_t*)val.Data; + } + + void UpdateFrameCountPin() + { + uint32_t count = (uint32_t)Frames.size(); + SetPinValue(NSN_Playback_FrameCount, nosBuffer{.Data = &count, .Size = sizeof(count)}); + } + + void UpdateFrameIndexPin() + { + SetPinValue(NSN_Playback_OutFrameIndex, nosBuffer{.Data = &CurrentFrame, .Size = sizeof(CurrentFrame)}); + } + + void UpdateStatus() + { + if (!LastError.empty()) + SetNodeStatusMessage(LastError, fb::NodeStatusMessageType::FAILURE); + else if (InputDir.empty()) + SetNodeStatusMessage("Set input directory", fb::NodeStatusMessageType::WARNING); + else if (Frames.empty()) + SetNodeStatusMessage("No data loaded", fb::NodeStatusMessageType::WARNING); + else if (Mode == track::PlaybackMode::Sequential && Playing) + SetNodeStatusMessage("Playing (" + std::to_string(CurrentFrame + 1) + "/" + std::to_string(Frames.size()) + ")", fb::NodeStatusMessageType::INFO); + else + SetNodeStatusMessage("Loaded (" + std::to_string(Frames.size()) + " frames)", fb::NodeStatusMessageType::INFO); + } + + // --- Parsing --- + + bool LoadFromDirectory() + { + if (InputDir.empty()) + { + LastError = "Set input directory"; + UpdateStatus(); + return false; + } + + std::filesystem::path dir = nos::Utf8ToPath(InputDir); + auto camerasPath = dir / "cameras.txt"; + auto imagesPath = dir / "images.txt"; + + if (!std::filesystem::exists(camerasPath)) + { + LastError = "cameras.txt not found"; + UpdateStatus(); + return false; + } + if (!std::filesystem::exists(imagesPath)) + { + LastError = "images.txt not found"; + UpdateStatus(); + return false; + } + + std::unordered_map cameras; + if (!ParseCamerasTxt(camerasPath, cameras)) + return false; + + std::vector images; + if (!ParseImagesTxt(imagesPath, images)) + return false; + + if (images.empty()) + { + LastError = "No images found in images.txt"; + UpdateStatus(); + return false; + } + + Frames.clear(); + Frames.reserve(images.size()); + + for (auto& img : images) + { + track::TTrack trackData{}; + auto camIt = cameras.find(img.CameraId); + + // Convert COLMAP world-to-camera back to camera-to-world + glm::mat3 R_w2c = glm::mat3_cast(img.Q); + glm::mat3 R_c2w = glm::transpose(R_w2c); + glm::vec3 C = -R_c2w * img.T; + + glm::vec3 euler = RotationMatrixToEuler(R_c2w, EulerOrd); + trackData.location = reinterpret_cast(C); + trackData.rotation = reinterpret_cast(euler); + + if (camIt != cameras.end()) + { + auto& cam = camIt->second; + if (cam.Fx > 0) + trackData.fov = glm::degrees(2.0f * std::atan(cam.Width * 0.5f / cam.Fx)); + trackData.sensor_size = nos::fb::vec2(cam.Width, cam.Height); + if (cam.Fx > 0 && cam.Fy > 0) + trackData.pixel_aspect_ratio = cam.Fx / cam.Fy; + trackData.lens_distortion.mutable_k1k2() = nos::fb::vec2(cam.K1, cam.K2); + } + + Frames.push_back(std::move(trackData)); + } + + CurrentFrame = 0; + LastError.clear(); + UpdateFrameCountPin(); + UpdateFrameIndexPin(); + UpdateStatus(); + nosEngine.LogI("PlaybackTrackCOLMAP: Loaded %zu frames from %s", Frames.size(), InputDir.c_str()); + return true; + } + + bool ParseCamerasTxt(const std::filesystem::path& path, std::unordered_map& cameras) + { + std::ifstream file(path); + if (!file.is_open()) + { + LastError = "Cannot open cameras.txt"; + UpdateStatus(); + return false; + } + + std::string line; + while (std::getline(file, line)) + { + if (line.empty() || line[0] == '#') + continue; + std::istringstream ss(line); + COLMAPCamera cam; + ss >> cam.Id >> cam.Model >> cam.Width >> cam.Height; + if (cam.Model == "OPENCV") + ss >> cam.Fx >> cam.Fy >> cam.Cx >> cam.Cy >> cam.K1 >> cam.K2 >> cam.P1 >> cam.P2; + else if (cam.Model == "PINHOLE") + ss >> cam.Fx >> cam.Fy >> cam.Cx >> cam.Cy; + else if (cam.Model == "SIMPLE_PINHOLE") + { + float f; + ss >> f >> cam.Cx >> cam.Cy; + cam.Fx = cam.Fy = f; + } + else if (cam.Model == "SIMPLE_RADIAL") + { + float f; + ss >> f >> cam.Cx >> cam.Cy >> cam.K1; + cam.Fx = cam.Fy = f; + } + else if (cam.Model == "RADIAL") + { + float f; + ss >> f >> cam.Cx >> cam.Cy >> cam.K1 >> cam.K2; + cam.Fx = cam.Fy = f; + } + else + { + nosEngine.LogW("PlaybackTrackCOLMAP: Unsupported camera model '%s', treating as PINHOLE", cam.Model.c_str()); + ss >> cam.Fx >> cam.Fy >> cam.Cx >> cam.Cy; + } + cameras[cam.Id] = cam; + } + return true; + } + + bool ParseImagesTxt(const std::filesystem::path& path, std::vector& images) + { + std::ifstream file(path); + if (!file.is_open()) + { + LastError = "Cannot open images.txt"; + UpdateStatus(); + return false; + } + + std::string line; + while (std::getline(file, line)) + { + if (line.empty() || line[0] == '#') + continue; + std::istringstream ss(line); + COLMAPImage img; + float qw, qx, qy, qz; + std::string name; + ss >> img.Id >> qw >> qx >> qy >> qz + >> img.T.x >> img.T.y >> img.T.z + >> img.CameraId >> name; + img.Q = glm::quat(qw, qx, qy, qz); + images.push_back(img); + // Skip POINTS2D line + std::getline(file, line); + } + + std::sort(images.begin(), images.end(), [](auto& a, auto& b) { return a.Id < b.Id; }); + return true; + } + + // --- Euler extraction (inverse of EulerToRotationMatrix in RecordTrackCOLMAP) --- + + static glm::vec3 RotationMatrixToEuler(const glm::mat3& R_c2w, track::EulerOrder order) + { + float r, t, p; + switch (order) + { + default: + case track::EulerOrder::ZYX: glm::extractEulerAngleZYX(glm::mat4(R_c2w), p, t, r); break; + case track::EulerOrder::XYZ: glm::extractEulerAngleXYZ(glm::mat4(R_c2w), r, t, p); break; + case track::EulerOrder::YXZ: glm::extractEulerAngleYXZ(glm::mat4(R_c2w), t, r, p); break; + case track::EulerOrder::YZX: glm::extractEulerAngleYZX(glm::mat4(R_c2w), t, p, r); break; + case track::EulerOrder::ZXY: glm::extractEulerAngleZXY(glm::mat4(R_c2w), p, r, t); break; + case track::EulerOrder::XZY: glm::extractEulerAngleXZY(glm::mat4(R_c2w), r, p, t); break; + } + // Undo sign convention: r = -roll, t = -tilt, p = pan + return glm::degrees(glm::vec3(-r, -t, p)); + } + + // --- Execution --- + + nosResult ExecuteNode(nosNodeExecuteParams* params) override + { + if (Frames.empty()) + { + track::TTrack empty{}; + auto buf = nos::Buffer::From(empty); + SetPinValue(NOS_NAME("Track"), {.Data = buf.Data(), .Size = buf.Size()}); + return NOS_RESULT_SUCCESS; + } + + uint32_t frameIdx = 0; + if (Mode == track::PlaybackMode::Sequential) + { + if (!Playing) + { + frameIdx = CurrentFrame; + } + else + { + frameIdx = CurrentFrame; + uint32_t next = CurrentFrame + 1; + if (next >= (uint32_t)Frames.size()) + next = Loop ? 0 : (uint32_t)Frames.size() - 1; + CurrentFrame = next; + } + } + else + { + frameIdx = ManualFrame < (uint32_t)Frames.size() ? ManualFrame : (uint32_t)Frames.size() - 1; + CurrentFrame = frameIdx; + } + + auto buf = nos::Buffer::From(Frames[frameIdx]); + SetPinValue(NOS_NAME("Track"), {.Data = buf.Data(), .Size = buf.Size()}); + UpdateFrameIndexPin(); + + if (Mode == track::PlaybackMode::Sequential && Playing) + UpdateStatus(); + + return NOS_RESULT_SUCCESS; + } + + static nosResult GetFunctions(size_t* count, nosName* names, nosPfnNodeFunctionExecute* fns) + { + *count = 3; + if (!names || !fns) + return NOS_RESULT_SUCCESS; + + names[0] = NOS_NAME_STATIC("PlaybackTrackCOLMAP_Play"); + fns[0] = [](void* ctx, nosFunctionExecuteParams*) { + auto* self = static_cast(ctx); + if (self->Playing) + return NOS_RESULT_SUCCESS; + if (self->Frames.empty()) + self->LoadFromDirectory(); + if (self->Frames.empty()) + return NOS_RESULT_SUCCESS; + self->Playing = true; + self->CurrentFrame = 0; + self->UpdateOrphanStates(); + self->UpdateStatus(); + nosEngine.LogI("PlaybackTrackCOLMAP: Playing (%zu frames)", self->Frames.size()); + return NOS_RESULT_SUCCESS; + }; + + names[1] = NOS_NAME_STATIC("PlaybackTrackCOLMAP_Stop"); + fns[1] = [](void* ctx, nosFunctionExecuteParams*) { + auto* self = static_cast(ctx); + if (!self->Playing) + return NOS_RESULT_SUCCESS; + self->Playing = false; + self->UpdateOrphanStates(); + self->UpdateStatus(); + nosEngine.LogI("PlaybackTrackCOLMAP: Stopped at frame %u", self->CurrentFrame); + return NOS_RESULT_SUCCESS; + }; + + names[2] = NOS_NAME_STATIC("PlaybackTrackCOLMAP_OpenFolder"); + fns[3] = [](void* ctx, nosFunctionExecuteParams*) { + auto* self = static_cast(ctx); + if (self->InputDir.empty()) + { + nosEngine.LogW("PlaybackTrackCOLMAP: Input directory not set"); + return NOS_RESULT_FAILED; + } + std::filesystem::path dir = nos::Utf8ToPath(self->InputDir); + if (!std::filesystem::exists(dir)) + { + nosEngine.LogW("PlaybackTrackCOLMAP: Directory does not exist: %s", self->InputDir.c_str()); + return NOS_RESULT_FAILED; + } + // TODO: Replace std::system with platform APIs (ShellExecuteW / posix_spawnp) +#if defined(_WIN32) + std::string cmd = "explorer \"" + nos::PathToUtf8(dir) + "\""; +#elif defined(__APPLE__) + std::string cmd = "open \"" + nos::PathToUtf8(dir) + "\""; +#else + std::string cmd = "xdg-open \"" + nos::PathToUtf8(dir) + "\""; +#endif + std::system(cmd.c_str()); + return NOS_RESULT_SUCCESS; + }; + + return NOS_RESULT_SUCCESS; + } +}; + +void RegisterPlaybackTrackCOLMAP(nosNodeFunctions* fn) +{ + NOS_BIND_NODE_CLASS(NOS_NAME("PlaybackTrackCOLMAP"), PlaybackTrackCOLMAPContext, fn); +} + +} // namespace nos::track diff --git a/Plugins/nosTrack/Source/RecordTrackCOLMAP.cpp b/Plugins/nosTrack/Source/RecordTrackCOLMAP.cpp index 9b2751cd..e7e91b55 100644 --- a/Plugins/nosTrack/Source/RecordTrackCOLMAP.cpp +++ b/Plugins/nosTrack/Source/RecordTrackCOLMAP.cpp @@ -99,7 +99,7 @@ struct RecordTrackCOLMAPContext : NodeContext void SyncRecordPin(bool value) { SyncingRecordPin = true; - nosEngine.SetPinValueByName(NodeId, NSN_Record, nosBuffer{.Data = &value, .Size = sizeof(value)}); + SetPinValue(NSN_Record, nosBuffer{.Data = &value, .Size = sizeof(value)}); SyncingRecordPin = false; } @@ -185,7 +185,7 @@ struct RecordTrackCOLMAPContext : NodeContext void UpdateFrameCountPin() { uint32_t count = (uint32_t)Frames.size(); - nosEngine.SetPinValueByName(NodeId, NSN_FrameCount, nosBuffer{.Data = &count, .Size = sizeof(count)}); + SetPinValue(NSN_FrameCount, nosBuffer{.Data = &count, .Size = sizeof(count)}); } void UpdateStatus() @@ -205,25 +205,23 @@ struct RecordTrackCOLMAPContext : NodeContext nosResult ExecuteNode(nosNodeExecuteParams* params) override { auto pins = GetPinValues(params); - auto ids = GetPinIds(params); // Pass through Track input to output - auto trackPinData = pins[NOS_NAME("Track")]; - size_t trackDataSize = 0; + nosBuffer trackBuf{}; for (size_t i = 0; i < params->PinCount; ++i) { - if (params->Pins[i].Name == NOS_NAME("Track")) + if (params->Pins[i].Name == NOS_NAME("InTrack")) { - trackDataSize = params->Pins[i].Data->Size; + trackBuf = {.Data = (void*)params->Pins[i].Data->Data, .Size = params->Pins[i].Data->Size}; break; } } - nosEngine.SetPinValue(ids[NOS_NAME("TrackOut")], {.Data = trackPinData, .Size = trackDataSize}); + SetPinValue(NOS_NAME("OutTrack"), trackBuf); if (!Recording) return NOS_RESULT_SUCCESS; - auto* trackData = flatbuffers::GetRoot(trackPinData); + auto* trackData = flatbuffers::GetRoot(trackBuf.Data); if (!trackData) return NOS_RESULT_SUCCESS; diff --git a/Plugins/nosTrack/Source/TrackMain.cpp b/Plugins/nosTrack/Source/TrackMain.cpp index 628abd33..acca3be8 100644 --- a/Plugins/nosTrack/Source/TrackMain.cpp +++ b/Plugins/nosTrack/Source/TrackMain.cpp @@ -16,6 +16,7 @@ enum TrackNode : int UserTrack, AddTrack, RecordTrackCOLMAP, + PlaybackTrackCOLMAP, Count }; @@ -23,6 +24,7 @@ void RegisterFreeDNode(nosNodeFunctions* functions); void RegisterController(nosNodeFunctions* functions); void RegisterAddTrack(nosNodeFunctions*); void RegisterRecordTrackCOLMAP(nosNodeFunctions*); +void RegisterPlaybackTrackCOLMAP(nosNodeFunctions*); nosResult NOSAPI_CALL ExportNodeFunctions(size_t* outSize, nosNodeFunctions** outList) { @@ -47,6 +49,9 @@ nosResult NOSAPI_CALL ExportNodeFunctions(size_t* outSize, nosNodeFunctions** ou case TrackNode::RecordTrackCOLMAP: RegisterRecordTrackCOLMAP(node); break; + case TrackNode::PlaybackTrackCOLMAP: + RegisterPlaybackTrackCOLMAP(node); + break; } } return NOS_RESULT_SUCCESS; diff --git a/Plugins/nosTrack/Track.noscfg b/Plugins/nosTrack/Track.noscfg index d6310c82..c8a13052 100644 --- a/Plugins/nosTrack/Track.noscfg +++ b/Plugins/nosTrack/Track.noscfg @@ -17,7 +17,8 @@ "Config/FreeD.nosdef", "Config/UserTrack.nosdef", "Config/AddTrack.nosdef", - "Config/RecordTrackCOLMAP.nosdef" + "Config/RecordTrackCOLMAP.nosdef", + "Config/PlaybackTrackCOLMAP.nosdef" ], "defaults": [ "Config/Defaults.json" diff --git a/Subsystems/nosTrackSubsystem/Config/Track.fbs b/Subsystems/nosTrackSubsystem/Config/Track.fbs index 75e6e1ff..2da4998e 100644 --- a/Subsystems/nosTrackSubsystem/Config/Track.fbs +++ b/Subsystems/nosTrackSubsystem/Config/Track.fbs @@ -51,3 +51,8 @@ enum EulerOrder : uint { ZXY = 4, XZY = 5, } + +enum PlaybackMode : uint { + Sequential = 0, + Manual = 1, +} From e3af9029ed9502e1a48173018a93dbbb8e30b487 Mon Sep 17 00:00:00 2001 From: "M. Samil Atesoglu" Date: Fri, 3 Apr 2026 13:46:12 +0300 Subject: [PATCH 3/9] Backport MultiLiveOut node to 1.3 --- .../nosUtilities/Config/MultiLiveOut.nosdef | 30 +++ Plugins/nosUtilities/Source/MultiLiveOut.cpp | 189 ++++++++++++++++++ Plugins/nosUtilities/Source/UtilitiesMain.cpp | 3 + Plugins/nosUtilities/Utilities.noscfg | 5 +- 4 files changed, 225 insertions(+), 2 deletions(-) create mode 100644 Plugins/nosUtilities/Config/MultiLiveOut.nosdef create mode 100644 Plugins/nosUtilities/Source/MultiLiveOut.cpp diff --git a/Plugins/nosUtilities/Config/MultiLiveOut.nosdef b/Plugins/nosUtilities/Config/MultiLiveOut.nosdef new file mode 100644 index 00000000..36997973 --- /dev/null +++ b/Plugins/nosUtilities/Config/MultiLiveOut.nosdef @@ -0,0 +1,30 @@ +{ + "nodes": [ + { + "class_name": "MultiLiveOut", + "menu_info": { + "category": "Scheduling", + "display_name": "Multi Live Out" + }, + "node": { + "class_name": "MultiLiveOut", + "contents_type": "Job", + "pins": [ + { + "name": "Input_0", + "type_name": "nos.Generic", + "show_as": "INPUT_PIN", + "can_show_as": "INPUT_PIN_ONLY" + }, + { + "name": "Output_0", + "type_name": "nos.Generic", + "show_as": "OUTPUT_PIN", + "can_show_as": "OUTPUT_PIN_ONLY", + "live": true + } + ] + } + } + ] +} diff --git a/Plugins/nosUtilities/Source/MultiLiveOut.cpp b/Plugins/nosUtilities/Source/MultiLiveOut.cpp new file mode 100644 index 00000000..c4d08d88 --- /dev/null +++ b/Plugins/nosUtilities/Source/MultiLiveOut.cpp @@ -0,0 +1,189 @@ +// Copyright MediaZ Teknoloji A.S. All Rights Reserved. + +#include + +namespace nos::utilities +{ + +struct MultiLiveOutNode : NodeContext +{ + MultiLiveOutNode(nosFbNodePtr node) : NodeContext(node) + { + for (auto* pin : *node->pins()) + { + SetPinOrphanState(*pin->id(), nos::fb::PinOrphanStateType::ACTIVE); + auto index = GetPinIndex(pin->name()->string_view()); + if (!index) + { + nosEngine.LogE("Failed to parse index from pin name: %s", pin->name()->c_str()); + continue; + } + if (pin->show_as() == nosFbShowAs::OUTPUT_PIN) + IndexToPairs[*index].second = uuid(*pin->id()); + else + IndexToPairs[*index].first = uuid(*pin->id()); + } + } + + void OnNodeUpdated(nosNodeUpdate const* update) override + { + if (update->Type == NOS_NODE_UPDATE_PIN_CREATED) + { + auto* pin = update->PinCreated; + auto index = GetPinIndex(pin->name()->string_view()); + if (!index) + return; + if (pin->show_as() == nosFbShowAs::OUTPUT_PIN) + IndexToPairs[*index].second = uuid(*pin->id()); + else + IndexToPairs[*index].first = uuid(*pin->id()); + } + else if (update->Type == NOS_NODE_UPDATE_PIN_DELETED) + { + for (auto it = IndexToPairs.begin(); it != IndexToPairs.end(); ++it) + { + if (it->second.first == update->PinDeleted || it->second.second == update->PinDeleted) + { + IndexToPairs.erase(it); + break; + } + } + } + } + + void OnMenuRequested(nosContextMenuRequestPtr request) override + { + flatbuffers::FlatBufferBuilder fbb; + std::vector> items; + if (*request->item_id() == NodeId) + items.push_back(nos::CreateContextMenuItemDirect(fbb, "Add New Pair", 1)); + else + { + auto* pin = GetPin(*request->item_id()); + if (!pin) + return; + if (pin->Name == NOS_NAME("Input_0") || pin->Name == NOS_NAME("Output_0")) + return; + items.push_back(nos::CreateContextMenuItemDirect(fbb, "Remove Pair", 1)); + } + HandleEvent(CreateAppEvent( + fbb, CreateAppContextMenuUpdate( + fbb, request->item_id(), request->pos(), request->instigator(), fbb.CreateVector(items)))); + } + + void OnMenuCommand(uuid const& itemID, uint32_t cmd) override + { + flatbuffers::FlatBufferBuilder fbb; + if (itemID == NodeId) + { + int index = 0; + for (; index < (int)IndexToPairs.size(); index++) + { + if (!IndexToPairs.contains(index)) + break; + } + fb::TPin outPin; + outPin.id = uuid(nosEngine.GenerateID()); + outPin.name = "Output_" + std::to_string(index); + outPin.type_name = NOS_NAME("nos.Generic"); + outPin.live = true; + outPin.show_as = fb::ShowAs::OUTPUT_PIN; + outPin.can_show_as = fb::CanShowAs::OUTPUT_PIN_ONLY; + + fb::TPin inPin; + inPin.id = uuid(nosEngine.GenerateID()); + inPin.name = "Input_" + std::to_string(index); + inPin.type_name = NOS_NAME("nos.Generic"); + inPin.show_as = fb::ShowAs::INPUT_PIN; + inPin.can_show_as = fb::CanShowAs::INPUT_PIN_ONLY; + + nos::TPartialNodeUpdate update; + update.node_id = NodeId; + update.pins_to_add.emplace_back(std::make_unique(std::move(outPin))); + update.pins_to_add.emplace_back(std::make_unique(std::move(inPin))); + HandleEvent(CreateAppEvent(fbb, nos::CreatePartialNodeUpdate(fbb, &update))); + IndexToPairs[index] = {uuid(inPin.id), uuid(outPin.id)}; + } + else + { + auto* pin = GetPin(itemID); + if (!pin) + return; + auto index = GetPinIndex(pin->Name.AsString()); + if (!index) + { + nosEngine.LogE("Failed to parse index from pin name: %s", pin->Name.AsCStr()); + return; + } + nos::TPartialNodeUpdate update; + update.node_id = NodeId; + update.pins_to_delete = {IndexToPairs[*index].first, IndexToPairs[*index].second}; + HandleEvent(CreateAppEvent(fbb, nos::CreatePartialNodeUpdate(fbb, &update))); + IndexToPairs.erase(*index); + } + } + + nosResult OnResolvePinDataTypes(nosResolvePinDataTypesParams* params) override + { + auto pinName = nos::Name(params->InstigatorPinName).AsString(); + auto index = GetPinIndex(pinName); + if (!index.has_value()) + { + strcpy(params->OutErrorMessage, "Failed to parse pin index from pin name."); + return NOS_RESULT_FAILED; + } + auto const& [firstId, secondId] = IndexToPairs[*index]; + for (size_t i = 0; i < params->PinCount; i++) + { + auto& pin = params->Pins[i]; + if (pin.Id == firstId || pin.Id == secondId) + pin.OutResolvedTypeName = params->IncomingTypeName; + else + pin.OutResolvedTypeName = NOS_NAME("nos.Generic"); + } + return NOS_RESULT_SUCCESS; + } + + std::optional GetPinIndex(std::string_view pinName) const + { + auto indexPos = pinName.find_last_of('_'); + if (indexPos == std::string::npos) + return std::nullopt; + try + { + return std::stoi(std::string(pinName.substr(indexPos + 1))); + } + catch (...) + { + nosEngine.LogE("Failed to parse index from pin name: %s", std::string(pinName).c_str()); + return std::nullopt; + } + } + + nosResult ExecuteNode(nosNodeExecuteParams* params) override + { + for (auto const& [_, idPair] : IndexToPairs) + { + for (size_t i = 0; i < params->PinCount; ++i) + { + auto& pin = params->Pins[i]; + if (pin.Id == idPair.first && pin.Data) + { + nosEngine.SetPinValue(idPair.second, *pin.Data); + break; + } + } + } + return NOS_RESULT_SUCCESS; + } + + std::unordered_map> IndexToPairs; +}; + +nosResult RegisterMultiLiveOut(nosNodeFunctions* fn) +{ + NOS_BIND_NODE_CLASS(NOS_NAME("MultiLiveOut"), MultiLiveOutNode, fn) + return NOS_RESULT_SUCCESS; +} + +} // namespace nos::utilities diff --git a/Plugins/nosUtilities/Source/UtilitiesMain.cpp b/Plugins/nosUtilities/Source/UtilitiesMain.cpp index c3d3e24a..b3fcc186 100644 --- a/Plugins/nosUtilities/Source/UtilitiesMain.cpp +++ b/Plugins/nosUtilities/Source/UtilitiesMain.cpp @@ -57,6 +57,7 @@ enum Utilities : int GridOutputLayout, LoadCubeLUT, RepeatingJunction, + MultiLiveOut, Count }; @@ -93,6 +94,7 @@ nosResult RegisterFreeOutputLayout(nosNodeFunctions*); nosResult RegisterGridOutputLayout(nosNodeFunctions*); nosResult RegisterLoadCubeLUT(nosNodeFunctions*); nosResult RegisterRepeatingJunction(nosNodeFunctions*); +nosResult RegisterMultiLiveOut(nosNodeFunctions*); nosResult NOSAPI_CALL ExportNodeFunctions(size_t* outSize, nosNodeFunctions** outList) { @@ -145,6 +147,7 @@ nosResult NOSAPI_CALL ExportNodeFunctions(size_t* outSize, nosNodeFunctions** ou GEN_CASE_NODE(GridOutputLayout) GEN_CASE_NODE(LoadCubeLUT) GEN_CASE_NODE(RepeatingJunction) + GEN_CASE_NODE(MultiLiveOut) } } return NOS_RESULT_SUCCESS; diff --git a/Plugins/nosUtilities/Utilities.noscfg b/Plugins/nosUtilities/Utilities.noscfg index 79883ae9..0320cd0d 100644 --- a/Plugins/nosUtilities/Utilities.noscfg +++ b/Plugins/nosUtilities/Utilities.noscfg @@ -2,7 +2,7 @@ "info": { "id": { "name": "nos.utilities", - "version": "3.14.8" + "version": "3.15.0" }, "description": "Various utility nodes.", "display_name": "Utilities", @@ -63,7 +63,8 @@ "Config/CalculateDispatchSize.nosdef", "Config/YADIF.nosdef", "Config/YADIFWithAutoDispatchSize.nosdef", - "Config/RepeatingJunction.nosdef" + "Config/RepeatingJunction.nosdef", + "Config/MultiLiveOut.nosdef" ], "custom_types": [ "Config/Merge.fbs", From a797fb84ecc2b0bee3b66e40554033ae24695da2 Mon Sep 17 00:00:00 2001 From: "M. Samil Atesoglu" Date: Fri, 3 Apr 2026 13:46:58 +0300 Subject: [PATCH 4/9] Add RecordingFrame for track record node --- Plugins/nosTrack/Config/RecordTrackCOLMAP.nosdef | 11 ++++++++++- Plugins/nosTrack/Source/RecordTrackCOLMAP.cpp | 10 ++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/Plugins/nosTrack/Config/RecordTrackCOLMAP.nosdef b/Plugins/nosTrack/Config/RecordTrackCOLMAP.nosdef index 5a17b344..10e5c006 100644 --- a/Plugins/nosTrack/Config/RecordTrackCOLMAP.nosdef +++ b/Plugins/nosTrack/Config/RecordTrackCOLMAP.nosdef @@ -67,6 +67,15 @@ "data": false, "description": "Toggle recording. Mirrors Record/Stop functions. Enabling clears previous frames and starts capturing. Will fail if the output directory is not empty." }, + { + "name": "RecordingFrame", + "display_name": "Recording Frame", + "type_name": "uint", + "show_as": "OUTPUT_PIN", + "can_show_as": "OUTPUT_PIN_ONLY", + "data": 0, + "description": "Current recording frame index. Outputs 0 when not recording." + }, { "name": "FrameCount", "display_name": "Frame Count", @@ -74,7 +83,7 @@ "show_as": "OUTPUT_PIN", "can_show_as": "OUTPUT_PIN_ONLY", "data": 0, - "description": "Number of frames recorded in the current session." + "description": "Number of frames in the buffer." } ], "functions": [ diff --git a/Plugins/nosTrack/Source/RecordTrackCOLMAP.cpp b/Plugins/nosTrack/Source/RecordTrackCOLMAP.cpp index e7e91b55..8e269a0a 100644 --- a/Plugins/nosTrack/Source/RecordTrackCOLMAP.cpp +++ b/Plugins/nosTrack/Source/RecordTrackCOLMAP.cpp @@ -21,6 +21,7 @@ NOS_REGISTER_NAME(ImageResolution); NOS_REGISTER_NAME(EulerOrder); NOS_REGISTER_NAME(Record); NOS_REGISTER_NAME(FrameCount); +NOS_REGISTER_NAME(RecordingFrame); NOS_REGISTER_NAME(RecordTrackCOLMAP_Record); NOS_REGISTER_NAME(RecordTrackCOLMAP_Stop); @@ -117,6 +118,7 @@ struct RecordTrackCOLMAPContext : NodeContext Recording = true; SyncRecordPin(true); UpdateFrameCountPin(); + UpdateRecordingFramePin(); UpdateFunctionOrphanStates(); UpdateStatus(); nosEngine.LogI("RecordTrackCOLMAP: Recording started"); @@ -127,6 +129,7 @@ struct RecordTrackCOLMAPContext : NodeContext { Recording = false; SyncRecordPin(false); + UpdateRecordingFramePin(); UpdateFunctionOrphanStates(); UpdateStatus(); nosEngine.LogI("RecordTrackCOLMAP: Recording stopped (%zu frames in buffer)", Frames.size()); @@ -188,6 +191,12 @@ struct RecordTrackCOLMAPContext : NodeContext SetPinValue(NSN_FrameCount, nosBuffer{.Data = &count, .Size = sizeof(count)}); } + void UpdateRecordingFramePin() + { + uint32_t frame = Recording ? (uint32_t)Frames.size() : 0; + SetPinValue(NSN_RecordingFrame, nosBuffer{.Data = &frame, .Size = sizeof(frame)}); + } + void UpdateStatus() { if (!LastError.empty()) @@ -243,6 +252,7 @@ struct RecordTrackCOLMAPContext : NodeContext Frames.push_back(frame); UpdateFrameCountPin(); + UpdateRecordingFramePin(); UpdateStatus(); return NOS_RESULT_SUCCESS; From 8d19bd3029b8f96d4c498f5e2c365d319d0e99da Mon Sep 17 00:00:00 2001 From: "M. Samil Atesoglu" Date: Fri, 3 Apr 2026 18:58:38 +0300 Subject: [PATCH 5/9] Fix crash when invoking OpenFolder function in PlaybackTrackCOLMAP node --- Plugins/nosTrack/Source/PlaybackTrackCOLMAP.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Plugins/nosTrack/Source/PlaybackTrackCOLMAP.cpp b/Plugins/nosTrack/Source/PlaybackTrackCOLMAP.cpp index d57b5b0c..8a23b353 100644 --- a/Plugins/nosTrack/Source/PlaybackTrackCOLMAP.cpp +++ b/Plugins/nosTrack/Source/PlaybackTrackCOLMAP.cpp @@ -449,7 +449,7 @@ struct PlaybackTrackCOLMAPContext : NodeContext }; names[2] = NOS_NAME_STATIC("PlaybackTrackCOLMAP_OpenFolder"); - fns[3] = [](void* ctx, nosFunctionExecuteParams*) { + fns[2] = [](void* ctx, nosFunctionExecuteParams*) { auto* self = static_cast(ctx); if (self->InputDir.empty()) { From 6d1c7d2e19045916f6b4e0a306419b5281fe830e Mon Sep 17 00:00:00 2001 From: "M. Samil Atesoglu" Date: Wed, 8 Apr 2026 19:08:10 +0300 Subject: [PATCH 6/9] RecordTrackCOLMAP node should always execute --- Plugins/nosTrack/Config/RecordTrackCOLMAP.nosdef | 1 + Plugins/nosTrack/Track.noscfg | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Plugins/nosTrack/Config/RecordTrackCOLMAP.nosdef b/Plugins/nosTrack/Config/RecordTrackCOLMAP.nosdef index 10e5c006..94a4f8cf 100644 --- a/Plugins/nosTrack/Config/RecordTrackCOLMAP.nosdef +++ b/Plugins/nosTrack/Config/RecordTrackCOLMAP.nosdef @@ -11,6 +11,7 @@ "class_name": "RecordTrackCOLMAP", "display_name": "Record Track (COLMAP)", "contents_type": "Job", + "always_execute": true, "description": "Records camera tracking data each frame while recording is enabled, then exports cameras.txt and images.txt in COLMAP format. Intrinsics (focal length, distortion) are derived from the Track's FOV, sensor size, and lens distortion. Extrinsics (rotation, translation) are stored per frame in world-to-camera convention.", "pins": [ { diff --git a/Plugins/nosTrack/Track.noscfg b/Plugins/nosTrack/Track.noscfg index c8a13052..464b68c7 100644 --- a/Plugins/nosTrack/Track.noscfg +++ b/Plugins/nosTrack/Track.noscfg @@ -2,7 +2,7 @@ "info": { "id": { "name": "nos.track", - "version": "1.10.0" + "version": "1.11.0" }, "display_name": "Track", "category": "Virtual Studio", From e89c723b5fbdcf6f07cc5ba64e6f9c82b0e15412 Mon Sep 17 00:00:00 2001 From: "M. Samil Atesoglu" Date: Thu, 9 Apr 2026 13:30:04 +0300 Subject: [PATCH 7/9] Remove sequential playback mode from PlaybackTrackCOLMAP, keep manual frame index only Also fix frames not loading on node creation by always loading when InputDirectory or EulerOrder changes. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Config/PlaybackTrackCOLMAP.nosdef | 32 +--- .../nosTrack/Source/PlaybackTrackCOLMAP.cpp | 143 ++---------------- Subsystems/nosTrackSubsystem/Config/Track.fbs | 5 - 3 files changed, 12 insertions(+), 168 deletions(-) diff --git a/Plugins/nosTrack/Config/PlaybackTrackCOLMAP.nosdef b/Plugins/nosTrack/Config/PlaybackTrackCOLMAP.nosdef index d0f260c5..ead9359d 100644 --- a/Plugins/nosTrack/Config/PlaybackTrackCOLMAP.nosdef +++ b/Plugins/nosTrack/Config/PlaybackTrackCOLMAP.nosdef @@ -12,7 +12,7 @@ "display_name": "Playback Track (COLMAP)", "contents_type": "Job", "always_execute": true, - "description": "Loads camera tracking data from COLMAP text format (cameras.txt + images.txt) and outputs Track data. Sequential mode auto-advances each frame with Play/Stop control. Manual mode outputs the frame at a given index.", + "description": "Loads camera tracking data from COLMAP text format (cameras.txt + images.txt) and outputs Track data at a given frame index.", "pins": [ { "name": "InputDirectory", @@ -32,22 +32,6 @@ "data": "ZYX", "description": "Euler angle rotation order for converting COLMAP quaternion to Track rotation. Default ZYX matches the FreeD node convention." }, - { - "name": "Mode", - "type_name": "nos.track.PlaybackMode", - "show_as": "PROPERTY", - "can_show_as": "INPUT_PIN_OR_PROPERTY", - "data": "Sequential", - "description": "Sequential: auto-advances one frame per execution with Play/Stop control. Manual: outputs the frame at the given Frame Input index." - }, - { - "name": "Loop", - "type_name": "bool", - "show_as": "PROPERTY", - "can_show_as": "INPUT_PIN_OR_PROPERTY", - "data": true, - "description": "Loop playback to the beginning when the last frame is reached." - }, { "name": "InFrameIndex", "display_name": "Frame Index", @@ -55,7 +39,7 @@ "show_as": "INPUT_PIN", "can_show_as": "INPUT_PIN_OR_PROPERTY", "data": 0, - "description": "Frame index to output (Manual mode only)." + "description": "Frame index to output." }, { "name": "Track", @@ -84,18 +68,6 @@ } ], "functions": [ - { - "class_name": "PlaybackTrackCOLMAP_Play", - "display_name": "Play", - "contents_type": "Job", - "pins": [] - }, - { - "class_name": "PlaybackTrackCOLMAP_Stop", - "display_name": "Stop", - "contents_type": "Job", - "pins": [] - }, { "class_name": "PlaybackTrackCOLMAP_OpenFolder", "display_name": "Open Folder", diff --git a/Plugins/nosTrack/Source/PlaybackTrackCOLMAP.cpp b/Plugins/nosTrack/Source/PlaybackTrackCOLMAP.cpp index 8a23b353..3a3e5641 100644 --- a/Plugins/nosTrack/Source/PlaybackTrackCOLMAP.cpp +++ b/Plugins/nosTrack/Source/PlaybackTrackCOLMAP.cpp @@ -20,14 +20,10 @@ namespace nos::track NOS_REGISTER_NAME_SPACED(Playback_InputDirectory, "InputDirectory"); NOS_REGISTER_NAME_SPACED(Playback_EulerOrder, "EulerOrder"); -NOS_REGISTER_NAME_SPACED(Playback_Mode, "Mode"); -NOS_REGISTER_NAME_SPACED(Playback_Loop, "Loop"); NOS_REGISTER_NAME_SPACED(Playback_InFrameIndex, "InFrameIndex"); NOS_REGISTER_NAME_SPACED(Playback_OutFrameIndex, "OutFrameIndex"); NOS_REGISTER_NAME_SPACED(Playback_FrameCount, "FrameCount"); -NOS_REGISTER_NAME(PlaybackTrackCOLMAP_Play); -NOS_REGISTER_NAME(PlaybackTrackCOLMAP_Stop); NOS_REGISTER_NAME(PlaybackTrackCOLMAP_OpenFolder); struct COLMAPCamera @@ -52,30 +48,17 @@ struct PlaybackTrackCOLMAPContext : NodeContext { std::string InputDir; track::EulerOrder EulerOrd = track::EulerOrder::ZYX; - track::PlaybackMode Mode = track::PlaybackMode::Sequential; - bool Loop = true; - bool Playing = false; - uint32_t ManualFrame = 0; + uint32_t FrameIndex = 0; std::string LastError; std::vector Frames; uint32_t CurrentFrame = 0; - std::unordered_map FunctionIds; - std::unordered_map PinIds; - PlaybackTrackCOLMAPContext(nosFbNodePtr node) : NodeContext(node) { - if (node->functions()) - { - for (auto* func : *node->functions()) - FunctionIds[nos::Name(func->class_name()->c_str())] = *func->id(); - } - if (node->pins()) { for (auto* pin : *node->pins()) { auto name = nos::Name(pin->name()->c_str()); - PinIds[name] = *pin->id(); if (flatbuffers::IsFieldPresent(pin, fb::Pin::VT_DATA)) { nosBuffer value = {.Data = (void*)pin->data()->data(), .Size = pin->data()->size()}; @@ -83,81 +66,28 @@ struct PlaybackTrackCOLMAPContext : NodeContext } } } - UpdateOrphanStates(); UpdateStatus(); } - void SetFunctionOrphanState(nos::Name funcName, fb::NodeOrphanStateType type) - { - auto it = FunctionIds.find(funcName); - if (it != FunctionIds.end()) - NodeContext::SetNodeOrphanState(it->second, type); - } - - void SetPinOrphanState(nos::Name pinName, fb::PinOrphanStateType type) - { - auto it = PinIds.find(pinName); - if (it != PinIds.end()) - NodeContext::SetPinOrphanState(it->second, type); - } - - void UpdateOrphanStates() - { - bool sequential = Mode == track::PlaybackMode::Sequential; - - // Sequential: Play/Stop active, Load/FrameInput orphaned - // Manual: Load/FrameInput active, Play/Stop orphaned - if (sequential) - { - SetPinOrphanState(NSN_Playback_InFrameIndex, fb::PinOrphanStateType::ORPHAN); - - if (Playing) - { - SetFunctionOrphanState(NSN_PlaybackTrackCOLMAP_Play, fb::NodeOrphanStateType::ORPHAN); - SetFunctionOrphanState(NSN_PlaybackTrackCOLMAP_Stop, fb::NodeOrphanStateType::ACTIVE); - } - else - { - SetFunctionOrphanState(NSN_PlaybackTrackCOLMAP_Play, fb::NodeOrphanStateType::ACTIVE); - SetFunctionOrphanState(NSN_PlaybackTrackCOLMAP_Stop, fb::NodeOrphanStateType::ORPHAN); - } - } - else - { - SetFunctionOrphanState(NSN_PlaybackTrackCOLMAP_Play, fb::NodeOrphanStateType::ORPHAN); - SetFunctionOrphanState(NSN_PlaybackTrackCOLMAP_Stop, fb::NodeOrphanStateType::ORPHAN); - SetPinOrphanState(NSN_Playback_InFrameIndex, fb::PinOrphanStateType::ACTIVE); - } - } - void OnPinValueChanged(nos::Name pinName, uuid const& pinId, nosBuffer val) override { if (pinName == NSN_Playback_InputDirectory) { InputDir = InterpretPinValue(val.Data); LastError.clear(); - if (Mode == track::PlaybackMode::Manual && !InputDir.empty()) + if (!InputDir.empty()) LoadFromDirectory(); else UpdateStatus(); } else if (pinName == NSN_Playback_EulerOrder) - EulerOrd = *(track::EulerOrder*)val.Data; - else if (pinName == NSN_Playback_Mode) { - auto newMode = *(track::PlaybackMode*)val.Data; - if (newMode != Mode) - { - Mode = newMode; - Playing = false; - UpdateOrphanStates(); - UpdateStatus(); - } + EulerOrd = *(track::EulerOrder*)val.Data; + if (!InputDir.empty()) + LoadFromDirectory(); } - else if (pinName == NSN_Playback_Loop) - Loop = *(bool*)val.Data; else if (pinName == NSN_Playback_InFrameIndex) - ManualFrame = *(uint32_t*)val.Data; + FrameIndex = *(uint32_t*)val.Data; } void UpdateFrameCountPin() @@ -179,8 +109,6 @@ struct PlaybackTrackCOLMAPContext : NodeContext SetNodeStatusMessage("Set input directory", fb::NodeStatusMessageType::WARNING); else if (Frames.empty()) SetNodeStatusMessage("No data loaded", fb::NodeStatusMessageType::WARNING); - else if (Mode == track::PlaybackMode::Sequential && Playing) - SetNodeStatusMessage("Playing (" + std::to_string(CurrentFrame + 1) + "/" + std::to_string(Frames.size()) + ")", fb::NodeStatusMessageType::INFO); else SetNodeStatusMessage("Loaded (" + std::to_string(Frames.size()) + " frames)", fb::NodeStatusMessageType::INFO); } @@ -381,75 +309,24 @@ struct PlaybackTrackCOLMAPContext : NodeContext return NOS_RESULT_SUCCESS; } - uint32_t frameIdx = 0; - if (Mode == track::PlaybackMode::Sequential) - { - if (!Playing) - { - frameIdx = CurrentFrame; - } - else - { - frameIdx = CurrentFrame; - uint32_t next = CurrentFrame + 1; - if (next >= (uint32_t)Frames.size()) - next = Loop ? 0 : (uint32_t)Frames.size() - 1; - CurrentFrame = next; - } - } - else - { - frameIdx = ManualFrame < (uint32_t)Frames.size() ? ManualFrame : (uint32_t)Frames.size() - 1; - CurrentFrame = frameIdx; - } + uint32_t frameIdx = FrameIndex < (uint32_t)Frames.size() ? FrameIndex : (uint32_t)Frames.size() - 1; + CurrentFrame = frameIdx; auto buf = nos::Buffer::From(Frames[frameIdx]); SetPinValue(NOS_NAME("Track"), {.Data = buf.Data(), .Size = buf.Size()}); UpdateFrameIndexPin(); - if (Mode == track::PlaybackMode::Sequential && Playing) - UpdateStatus(); - return NOS_RESULT_SUCCESS; } static nosResult GetFunctions(size_t* count, nosName* names, nosPfnNodeFunctionExecute* fns) { - *count = 3; + *count = 1; if (!names || !fns) return NOS_RESULT_SUCCESS; - names[0] = NOS_NAME_STATIC("PlaybackTrackCOLMAP_Play"); + names[0] = NOS_NAME_STATIC("PlaybackTrackCOLMAP_OpenFolder"); fns[0] = [](void* ctx, nosFunctionExecuteParams*) { - auto* self = static_cast(ctx); - if (self->Playing) - return NOS_RESULT_SUCCESS; - if (self->Frames.empty()) - self->LoadFromDirectory(); - if (self->Frames.empty()) - return NOS_RESULT_SUCCESS; - self->Playing = true; - self->CurrentFrame = 0; - self->UpdateOrphanStates(); - self->UpdateStatus(); - nosEngine.LogI("PlaybackTrackCOLMAP: Playing (%zu frames)", self->Frames.size()); - return NOS_RESULT_SUCCESS; - }; - - names[1] = NOS_NAME_STATIC("PlaybackTrackCOLMAP_Stop"); - fns[1] = [](void* ctx, nosFunctionExecuteParams*) { - auto* self = static_cast(ctx); - if (!self->Playing) - return NOS_RESULT_SUCCESS; - self->Playing = false; - self->UpdateOrphanStates(); - self->UpdateStatus(); - nosEngine.LogI("PlaybackTrackCOLMAP: Stopped at frame %u", self->CurrentFrame); - return NOS_RESULT_SUCCESS; - }; - - names[2] = NOS_NAME_STATIC("PlaybackTrackCOLMAP_OpenFolder"); - fns[2] = [](void* ctx, nosFunctionExecuteParams*) { auto* self = static_cast(ctx); if (self->InputDir.empty()) { diff --git a/Subsystems/nosTrackSubsystem/Config/Track.fbs b/Subsystems/nosTrackSubsystem/Config/Track.fbs index 2da4998e..75e6e1ff 100644 --- a/Subsystems/nosTrackSubsystem/Config/Track.fbs +++ b/Subsystems/nosTrackSubsystem/Config/Track.fbs @@ -51,8 +51,3 @@ enum EulerOrder : uint { ZXY = 4, XZY = 5, } - -enum PlaybackMode : uint { - Sequential = 0, - Manual = 1, -} From 6f537b0266738c47e078c2cf230024d24e3b6afb Mon Sep 17 00:00:00 2001 From: "M. Samil Atesoglu" Date: Thu, 9 Apr 2026 15:44:28 +0300 Subject: [PATCH 8/9] Bump nos.sys.track version --- Plugins/nosTrack/Track.noscfg | 2 +- Subsystems/nosTrackSubsystem/nosTrackSubsystem.nossys | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Plugins/nosTrack/Track.noscfg b/Plugins/nosTrack/Track.noscfg index 464b68c7..97473fef 100644 --- a/Plugins/nosTrack/Track.noscfg +++ b/Plugins/nosTrack/Track.noscfg @@ -9,7 +9,7 @@ "dependencies": [ { "name": "nos.sys.track", - "version": "1.0" + "version": "1.1" } ] }, diff --git a/Subsystems/nosTrackSubsystem/nosTrackSubsystem.nossys b/Subsystems/nosTrackSubsystem/nosTrackSubsystem.nossys index d2f6b9cb..625fd3fe 100644 --- a/Subsystems/nosTrackSubsystem/nosTrackSubsystem.nossys +++ b/Subsystems/nosTrackSubsystem/nosTrackSubsystem.nossys @@ -2,7 +2,7 @@ "info": { "id": { "name": "nos.sys.track", - "version": "1.0.0" + "version": "1.1.0" }, "display_name": "Track Subsystem", "dependencies": [ From 8b13957c4b20a111efc4ef0e7e758ebfd6fd9800 Mon Sep 17 00:00:00 2001 From: "M. Samil Atesoglu" Date: Thu, 9 Apr 2026 18:00:47 +0300 Subject: [PATCH 9/9] Replace EulerOrder with CoordinateSystem from nos.sys.track Migrate COLMAP nodes from the removed plugin-local EulerOrder enum to the existing nos.sys.track.CoordinateSystem enum. Also fix nos.track namespace references to nos.sys.track after upstream merge. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Config/PlaybackTrackCOLMAP.nosdef | 8 ++--- .../nosTrack/Config/RecordTrackCOLMAP.nosdef | 10 +++--- .../nosTrack/Source/PlaybackTrackCOLMAP.cpp | 32 +++++++++---------- Plugins/nosTrack/Source/RecordTrackCOLMAP.cpp | 28 ++++++++-------- 4 files changed, 39 insertions(+), 39 deletions(-) diff --git a/Plugins/nosTrack/Config/PlaybackTrackCOLMAP.nosdef b/Plugins/nosTrack/Config/PlaybackTrackCOLMAP.nosdef index ead9359d..67b8f74e 100644 --- a/Plugins/nosTrack/Config/PlaybackTrackCOLMAP.nosdef +++ b/Plugins/nosTrack/Config/PlaybackTrackCOLMAP.nosdef @@ -24,9 +24,9 @@ "description": "Directory containing cameras.txt and images.txt in COLMAP text format." }, { - "name": "EulerOrder", - "display_name": "Euler Order", - "type_name": "nos.track.EulerOrder", + "name": "CoordinateSystem", + "display_name": "Coordinate System", + "type_name": "nos.sys.track.CoordinateSystem", "show_as": "PROPERTY", "can_show_as": "INPUT_PIN_OR_PROPERTY", "data": "ZYX", @@ -43,7 +43,7 @@ }, { "name": "Track", - "type_name": "nos.track.Track", + "type_name": "nos.sys.track.Track", "show_as": "OUTPUT_PIN", "can_show_as": "OUTPUT_PIN_ONLY", "description": "Track data for the current frame." diff --git a/Plugins/nosTrack/Config/RecordTrackCOLMAP.nosdef b/Plugins/nosTrack/Config/RecordTrackCOLMAP.nosdef index 94a4f8cf..2237b720 100644 --- a/Plugins/nosTrack/Config/RecordTrackCOLMAP.nosdef +++ b/Plugins/nosTrack/Config/RecordTrackCOLMAP.nosdef @@ -17,7 +17,7 @@ { "name": "InTrack", "display_name": "Track", - "type_name": "nos.track.Track", + "type_name": "nos.sys.track.Track", "show_as": "INPUT_PIN", "can_show_as": "INPUT_PIN_OR_PROPERTY", "description": "Incoming camera tracking data to record. Position, rotation, FOV, sensor size, and lens distortion are captured each frame." @@ -25,7 +25,7 @@ { "name": "OutTrack", "display_name": "Track", - "type_name": "nos.track.Track", + "type_name": "nos.sys.track.Track", "show_as": "OUTPUT_PIN", "can_show_as": "OUTPUT_PIN_ONLY", "description": "Pass-through of the incoming Track data." @@ -52,9 +52,9 @@ "description": "Image resolution in pixels (width, height). Used to compute focal length and principal point for COLMAP camera model." }, { - "name": "EulerOrder", - "display_name": "Euler Order", - "type_name": "nos.track.EulerOrder", + "name": "CoordinateSystem", + "display_name": "Coordinate System", + "type_name": "nos.sys.track.CoordinateSystem", "show_as": "PROPERTY", "can_show_as": "INPUT_PIN_OR_PROPERTY", "data": "ZYX", diff --git a/Plugins/nosTrack/Source/PlaybackTrackCOLMAP.cpp b/Plugins/nosTrack/Source/PlaybackTrackCOLMAP.cpp index 3a3e5641..567f929b 100644 --- a/Plugins/nosTrack/Source/PlaybackTrackCOLMAP.cpp +++ b/Plugins/nosTrack/Source/PlaybackTrackCOLMAP.cpp @@ -1,7 +1,7 @@ // Copyright MediaZ Teknoloji A.S. All Rights Reserved. #include -#include "Track_generated.h" +#include "nosSysTrack/Track_generated.h" #include #include @@ -19,7 +19,7 @@ namespace nos::track { NOS_REGISTER_NAME_SPACED(Playback_InputDirectory, "InputDirectory"); -NOS_REGISTER_NAME_SPACED(Playback_EulerOrder, "EulerOrder"); +NOS_REGISTER_NAME_SPACED(Playback_CoordinateSystem, "CoordinateSystem"); NOS_REGISTER_NAME_SPACED(Playback_InFrameIndex, "InFrameIndex"); NOS_REGISTER_NAME_SPACED(Playback_OutFrameIndex, "OutFrameIndex"); NOS_REGISTER_NAME_SPACED(Playback_FrameCount, "FrameCount"); @@ -47,10 +47,10 @@ struct COLMAPImage struct PlaybackTrackCOLMAPContext : NodeContext { std::string InputDir; - track::EulerOrder EulerOrd = track::EulerOrder::ZYX; + sys::track::CoordinateSystem CoordSys = sys::track::CoordinateSystem::ZYX; uint32_t FrameIndex = 0; std::string LastError; - std::vector Frames; + std::vector Frames; uint32_t CurrentFrame = 0; PlaybackTrackCOLMAPContext(nosFbNodePtr node) : NodeContext(node) { @@ -80,9 +80,9 @@ struct PlaybackTrackCOLMAPContext : NodeContext else UpdateStatus(); } - else if (pinName == NSN_Playback_EulerOrder) + else if (pinName == NSN_Playback_CoordinateSystem) { - EulerOrd = *(track::EulerOrder*)val.Data; + CoordSys = *(sys::track::CoordinateSystem*)val.Data; if (!InputDir.empty()) LoadFromDirectory(); } @@ -161,7 +161,7 @@ struct PlaybackTrackCOLMAPContext : NodeContext for (auto& img : images) { - track::TTrack trackData{}; + sys::track::TTrack trackData{}; auto camIt = cameras.find(img.CameraId); // Convert COLMAP world-to-camera back to camera-to-world @@ -169,7 +169,7 @@ struct PlaybackTrackCOLMAPContext : NodeContext glm::mat3 R_c2w = glm::transpose(R_w2c); glm::vec3 C = -R_c2w * img.T; - glm::vec3 euler = RotationMatrixToEuler(R_c2w, EulerOrd); + glm::vec3 euler = RotationMatrixToEuler(R_c2w, CoordSys); trackData.location = reinterpret_cast(C); trackData.rotation = reinterpret_cast(euler); @@ -280,18 +280,18 @@ struct PlaybackTrackCOLMAPContext : NodeContext // --- Euler extraction (inverse of EulerToRotationMatrix in RecordTrackCOLMAP) --- - static glm::vec3 RotationMatrixToEuler(const glm::mat3& R_c2w, track::EulerOrder order) + static glm::vec3 RotationMatrixToEuler(const glm::mat3& R_c2w, sys::track::CoordinateSystem order) { float r, t, p; switch (order) { default: - case track::EulerOrder::ZYX: glm::extractEulerAngleZYX(glm::mat4(R_c2w), p, t, r); break; - case track::EulerOrder::XYZ: glm::extractEulerAngleXYZ(glm::mat4(R_c2w), r, t, p); break; - case track::EulerOrder::YXZ: glm::extractEulerAngleYXZ(glm::mat4(R_c2w), t, r, p); break; - case track::EulerOrder::YZX: glm::extractEulerAngleYZX(glm::mat4(R_c2w), t, p, r); break; - case track::EulerOrder::ZXY: glm::extractEulerAngleZXY(glm::mat4(R_c2w), p, r, t); break; - case track::EulerOrder::XZY: glm::extractEulerAngleXZY(glm::mat4(R_c2w), r, p, t); break; + case sys::track::CoordinateSystem::ZYX: glm::extractEulerAngleZYX(glm::mat4(R_c2w), p, t, r); break; + case sys::track::CoordinateSystem::XYZ: glm::extractEulerAngleXYZ(glm::mat4(R_c2w), r, t, p); break; + case sys::track::CoordinateSystem::YXZ: glm::extractEulerAngleYXZ(glm::mat4(R_c2w), t, r, p); break; + case sys::track::CoordinateSystem::YZX: glm::extractEulerAngleYZX(glm::mat4(R_c2w), t, p, r); break; + case sys::track::CoordinateSystem::ZXY: glm::extractEulerAngleZXY(glm::mat4(R_c2w), p, r, t); break; + case sys::track::CoordinateSystem::XZY: glm::extractEulerAngleXZY(glm::mat4(R_c2w), r, p, t); break; } // Undo sign convention: r = -roll, t = -tilt, p = pan return glm::degrees(glm::vec3(-r, -t, p)); @@ -303,7 +303,7 @@ struct PlaybackTrackCOLMAPContext : NodeContext { if (Frames.empty()) { - track::TTrack empty{}; + sys::track::TTrack empty{}; auto buf = nos::Buffer::From(empty); SetPinValue(NOS_NAME("Track"), {.Data = buf.Data(), .Size = buf.Size()}); return NOS_RESULT_SUCCESS; diff --git a/Plugins/nosTrack/Source/RecordTrackCOLMAP.cpp b/Plugins/nosTrack/Source/RecordTrackCOLMAP.cpp index 8e269a0a..0284b2f6 100644 --- a/Plugins/nosTrack/Source/RecordTrackCOLMAP.cpp +++ b/Plugins/nosTrack/Source/RecordTrackCOLMAP.cpp @@ -1,7 +1,7 @@ // Copyright MediaZ Teknoloji A.S. All Rights Reserved. #include -#include "Track_generated.h" +#include "nosSysTrack/Track_generated.h" #include #include @@ -18,7 +18,7 @@ namespace nos::track NOS_REGISTER_NAME(OutputDirectory); NOS_REGISTER_NAME(ImageResolution); -NOS_REGISTER_NAME(EulerOrder); +NOS_REGISTER_NAME(CoordinateSystem); NOS_REGISTER_NAME(Record); NOS_REGISTER_NAME(FrameCount); NOS_REGISTER_NAME(RecordingFrame); @@ -45,7 +45,7 @@ struct RecordTrackCOLMAPContext : NodeContext { std::string OutputDir; nosVec2u ImageResolution = {1920, 1080}; - track::EulerOrder EulerOrd = track::EulerOrder::ZYX; + sys::track::CoordinateSystem CoordSys = sys::track::CoordinateSystem::ZYX; bool Recording = false; bool SyncingRecordPin = false; std::string LastError; @@ -145,8 +145,8 @@ struct RecordTrackCOLMAPContext : NodeContext } else if (pinName == NSN_ImageResolution) ImageResolution = *(nosVec2u*)val.Data; - else if (pinName == NSN_EulerOrder) - EulerOrd = *(track::EulerOrder*)val.Data; + else if (pinName == NSN_CoordinateSystem) + CoordSys = *(sys::track::CoordinateSystem*)val.Data; else if (pinName == NSN_Record) { if (SyncingRecordPin) @@ -230,7 +230,7 @@ struct RecordTrackCOLMAPContext : NodeContext if (!Recording) return NOS_RESULT_SUCCESS; - auto* trackData = flatbuffers::GetRoot(trackBuf.Data); + auto* trackData = flatbuffers::GetRoot(trackBuf.Data); if (!trackData) return NOS_RESULT_SUCCESS; @@ -331,7 +331,7 @@ struct RecordTrackCOLMAPContext : NodeContext } } - static glm::mat3 EulerToRotationMatrix(glm::vec3 rot, track::EulerOrder order) + static glm::mat3 EulerToRotationMatrix(glm::vec3 rot, sys::track::CoordinateSystem order) { // rot is (roll, tilt, pan) = (x, y, z) in radians // Sign convention matches MakeRotation: negate roll (x) and tilt (y) @@ -339,12 +339,12 @@ struct RecordTrackCOLMAPContext : NodeContext switch (order) { default: - case track::EulerOrder::ZYX: return glm::mat3(glm::eulerAngleZYX(p, t, r)); - case track::EulerOrder::XYZ: return glm::mat3(glm::eulerAngleXYZ(r, t, p)); - case track::EulerOrder::YXZ: return glm::mat3(glm::eulerAngleYXZ(t, r, p)); - case track::EulerOrder::YZX: return glm::mat3(glm::eulerAngleYZX(t, p, r)); - case track::EulerOrder::ZXY: return glm::mat3(glm::eulerAngleZXY(p, r, t)); - case track::EulerOrder::XZY: return glm::mat3(glm::eulerAngleXZY(r, p, t)); + case sys::track::CoordinateSystem::ZYX: return glm::mat3(glm::eulerAngleZYX(p, t, r)); + case sys::track::CoordinateSystem::XYZ: return glm::mat3(glm::eulerAngleXYZ(r, t, p)); + case sys::track::CoordinateSystem::YXZ: return glm::mat3(glm::eulerAngleYXZ(t, r, p)); + case sys::track::CoordinateSystem::YZX: return glm::mat3(glm::eulerAngleYZX(t, p, r)); + case sys::track::CoordinateSystem::ZXY: return glm::mat3(glm::eulerAngleZXY(p, r, t)); + case sys::track::CoordinateSystem::XZY: return glm::mat3(glm::eulerAngleXZY(r, p, t)); } } @@ -371,7 +371,7 @@ struct RecordTrackCOLMAPContext : NodeContext // Convert Euler angles to rotation matrix // Sign convention matches MakeRotation: negate roll (x) and tilt (y) glm::vec3 rot = glm::radians(frame.Rotation); - glm::mat3 R_c2w = EulerToRotationMatrix(rot, EulerOrd); + glm::mat3 R_c2w = EulerToRotationMatrix(rot, CoordSys); // COLMAP expects world-to-camera rotation glm::mat3 R_w2c = glm::transpose(R_c2w);