diff --git a/.gitignore b/.gitignore index b216e575..dd93b251 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ !/plugins /plugins/* !/plugins/example_plugin +!/plugins/gesture_plugin doc/*.html diff --git a/.gitmodules b/.gitmodules index e69de29b..5216d5ed 100644 --- a/.gitmodules +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "plugins/gesture_plugin/async-sockets-cpp"] + path = plugins/gesture_plugin/async-sockets-cpp + url = https://github.com/JungWhoNam/async-sockets-cpp.git diff --git a/plugins/gesture_plugin/CMakeLists.txt b/plugins/gesture_plugin/CMakeLists.txt new file mode 100644 index 00000000..0d5be65d --- /dev/null +++ b/plugins/gesture_plugin/CMakeLists.txt @@ -0,0 +1,39 @@ +option(BUILD_PLUGIN_GESTURE "Gesture plugin" OFF) + +if (BUILD_PLUGIN_GESTURE) + set(pluginName "ospray_studio_plugin_gesture") + + add_library(${pluginName} SHARED + plugin_gesture.cpp + PanelGesture.cpp + tracker/TrackingManager.cpp + ) + + target_link_libraries(${pluginName} ospray_sg) + + # Only link against imgui if needed (ie, pure file importers don't) + target_link_libraries(${pluginName} ospray_ui) + + # There can be other plugins using async_sockets library + if (NOT TARGET async_sockets) + message(STATUS "Adding async_sockets library...") + add_library(async_sockets INTERFACE) + target_include_directories(async_sockets + INTERFACE $) + endif() + + target_link_libraries(${pluginName} async_sockets) + + target_include_directories(${pluginName} + PRIVATE ${CMAKE_SOURCE_DIR} + ) + + install(TARGETS ${pluginName} + DESTINATION ${CMAKE_INSTALL_LIBDIR} + COMPONENT lib + # on Windows put the dlls into bin + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + COMPONENT lib + ) + +endif() diff --git a/plugins/gesture_plugin/PanelGesture.cpp b/plugins/gesture_plugin/PanelGesture.cpp new file mode 100644 index 00000000..6b415f12 --- /dev/null +++ b/plugins/gesture_plugin/PanelGesture.cpp @@ -0,0 +1,107 @@ +#include + +#include "PanelGesture.h" + +#include "app/widgets/GenerateImGuiWidgets.h" +#include "app/MainWindow.h" + +#include "imgui.h" + + +namespace ospray { +namespace gesture_plugin { + +PanelGesture::PanelGesture(std::shared_ptr _context, std::string _panelName, std::string _configFilePath) + : Panel(_panelName.c_str(), _context) + , panelName(_panelName) + , configFilePath(_configFilePath) +{ + trackingManager.reset(new TrackingManager(configFilePath)); +} + +void PanelGesture::processRequests() { + if (!trackingManager->isRunning()) + return; + + TrackingState state = trackingManager->pollState(); + if (state.mode == INTERACTION_FLYING) { + MainWindow* pMW = reinterpret_cast(context->getMainWindow()); + + vec3f pos = pMW->arcballCamera->center(); + vec3f center = state.leaningDir + pos; + pMW->arcballCamera->setCenter(center); + context->updateCamera(); + } +} + +void PanelGesture::buildUI(void *ImGuiCtx) +{ + processRequests(); + + // Allows plugin to still do other work if the UI isn't shown. + if (!isShown()) + return; + + // Need to set ImGuiContext in *this* address space + ImGui::SetCurrentContext((ImGuiContext *)ImGuiCtx); + ImGui::OpenPopup(panelName.c_str()); + + if (!ImGui::BeginPopupModal(panelName.c_str(), nullptr, ImGuiWindowFlags_None)) return; + + if (trackingManager->isRunning()) { + ImGui::Text("%s", "Currently connected to the server..."); + + if (ImGui::Button("Disconnect")) { + trackingManager->close(); + } + } + else { + ImGui::Text("%s", "Currently NOT connected to the server..."); + std::string str = "- Connect to " + trackingManager->ipAddress + ":" + std::to_string(trackingManager->portNumber); + ImGui::Text("%s", str.c_str()); + + if (ImGui::Button("Connect")) { + trackingManager->start(); + } + } + ImGui::Separator(); + + // Close button + if (ImGui::Button("Close")) { + setShown(false); + ImGui::CloseCurrentPopup(); + } + ImGui::Separator(); + + if (ImGui::CollapsingHeader("Configuration", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Text("%s", "Offset(s)"); + ImGui::DragFloat3("Scale", trackingManager->scaleOffset, 0.001, -100, 100, "%.3f"); + ImGui::DragFloat3("Translate", trackingManager->translationOffset, 0, -100, 100, "%.1f"); + + ImGui::Text("%s", "Gestures(s)"); + ImGui::DragInt("Confidence Level Threshold", &trackingManager->confidenceLevelThreshold, K4ABT_JOINT_CONFIDENCE_LOW, K4ABT_JOINT_CONFIDENCE_NONE, K4ABT_JOINT_CONFIDENCE_LEVELS_COUNT); + ImGui::DragFloat("Leaning Angle Threshold", &trackingManager->leaningAngleThreshold, 1, 0, 180); + ImGui::DragFloat3("Leaning Dir Scale", trackingManager->leaningDirScaleFactor, 0, -100, 100, "%.1f"); + + ImGui::Separator(); + if (ImGui::Button("Save")) { + trackingManager->saveConfig(this->configFilePath); + } + } + ImGui::Separator(); + + // Display statuses in a scrolling region + if (ImGui::CollapsingHeader("Status", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::BeginChild("Scrolling", ImVec2(0, 0), false, ImGuiWindowFlags_AlwaysAutoResize); + for (std::string status : trackingManager->statuses) { + ImGui::Text("%s", status.c_str()); + } + ImGui::EndChild(); + } + ImGui::Separator(); + + ImGui::EndPopup(); +} + +} // namespace gesture_plugin +} // namespace ospray diff --git a/plugins/gesture_plugin/PanelGesture.h b/plugins/gesture_plugin/PanelGesture.h new file mode 100644 index 00000000..f801f56f --- /dev/null +++ b/plugins/gesture_plugin/PanelGesture.h @@ -0,0 +1,26 @@ +#pragma once + +#include "app/widgets/Panel.h" +#include "app/ospStudio.h" + +#include "tracker/TrackingManager.h" + +namespace ospray { +namespace gesture_plugin { + +struct PanelGesture : public Panel +{ + PanelGesture(std::shared_ptr _context, std::string _panelName, std::string _configFilePath); + + void buildUI(void *ImGuiCtx) override; + + void processRequests(); + +private: + std::string panelName; + std::string configFilePath; + std::unique_ptr trackingManager; +}; + +} // namespace gesture_plugin +} // namespace ospray diff --git a/plugins/gesture_plugin/README.md b/plugins/gesture_plugin/README.md new file mode 100644 index 00000000..a5ab629a --- /dev/null +++ b/plugins/gesture_plugin/README.md @@ -0,0 +1,100 @@ +# Gesture interaction plugin for OSPRay Studio +> This project is part of a larger project called [Immersive OSPray Studio](https://github.com/jungwhonam/ImmersiveOSPRay). + +## Overview +
+ + + +
+ + + +
+
+ +We created a plugin for [OSPRay Studio v1.0.0](https://github.com/RenderKit/ospray-studio/releases/tag/v1.0.0) allowing users to navigate a 3D virtual environment using gestures. Lifting both hands above a belly button triggers a flying mode. Once in the mode, the camera moves into a body-leaning direction. + +
+ + + +
+ + + +
+
+ +This plugin recieves the body tracking data from [Gesture Tracking Server](https://github.com/jungwhonam/GestureTrackingServer). After processing user inputs, such as key-pressed events, OSPRay Studio calls a method in the plugin to update GUIs. At this stage, the plugin retrieves the latest tracking result and use it to update relevant 3D objects, such as modifying camera positions. + +## Prerequisites +Before running `ospStudio` with the plugin, you need to start [Gesture Tracking Server](https://github.com/jungwhonam/GestureTrackingServer). + + +## Setup +```shell +# clone this branch +git clone -b jungwho.nam-feature-plugin-gesture https://github.com/JungWhoNam/ospray_studio.git +cd ospray_studio + +mkdir build +cd build +mkdir release +``` + + +## CMake configuration and build +OSPRay Studio needs to be built with `-DBUILD_PLUGINS=ON` and `-DBUILD_PLUGIN_GESTURE=ON` in CMake. + +```shell +cmake -S .. \ +-B release \ +-DCMAKE_BUILD_TYPE=Release \ +-DBUILD_PLUGINS=ON \ +-DBUILD_PLUGIN_GESTURE=ON + +cmake --build release + +cmake --install release +``` + + +## Run `ospStudio` with the gesture plugin + +1. First, start the server by following the steps written in [the server repo](https://github.com/jungwhonam/GestureTrackingServer). +2. Start `ospStudio` with the plugin. +```shell +./release/ospStudio \ +--plugin gesture \ +--plugin:gesture:config gesture_settings.json +``` +1. Go to `Plugins` > `Gesture Panel` in the menu. +2. Click "Connect" button. + +If connected, you will see "Connected to the server successfully" displayed on the Status sub-panel. + + +## Plugin configuration JSON file +When running `ospStudio`, you must specify the location of this JSON file using `--plugin:gesture:config` flag. This file contains information about the gesture tracking server and instructions on handling the tracking data. + +```json +{ + "ipAddress": "127.0.0.1", + "portNumber": 8888, + + "scaleOffset": [0.001, -0.001, -0.001], + "translationOffset": [0.0, -0.1, 1.19], + "confidenceLevelThreshold": 1, + "leaningAngleThreshold": 1.0, + "leaningDirScaleFactor": [1.0, 1.0, 1.0] +} +``` +* `ipAddress` and `portNumber` are used for connecting the gesture tracking server. +* `scaleOffset` is multiplied to position values of joints to adjust for differences between Kinect and OSPRay Studio coordinate systems. +* `translationOffset` offsets the sensor's center to calibrate it with displays. +* `confidenceLevelThreshold` specifies the joint confidence level considered for processing, with different levels detailed in [Microsoft's documentation](https://microsoft.github.io/Azure-Kinect-Body-Tracking/release/1.1.x/namespace_microsoft_1_1_azure_1_1_kinect_1_1_body_tracking_adfff503ebc1491373c89e96887cad226.html#adfff503ebc1491373c89e96887cad226) for different levels. +* `leaningAngleThreshold` is a threshhold for activating the flying mode when a user's body leans beyond this angle. +* `leaningDirScaleFactor` sets the speed of camera movement based on body leaning direction. + +> Here is [an example JSON file](./gesture_settings.json). \ No newline at end of file diff --git a/plugins/gesture_plugin/async-sockets-cpp b/plugins/gesture_plugin/async-sockets-cpp new file mode 160000 index 00000000..78641cfd --- /dev/null +++ b/plugins/gesture_plugin/async-sockets-cpp @@ -0,0 +1 @@ +Subproject commit 78641cfde398d2cd71649f6911ee1bf4953498c0 diff --git a/plugins/gesture_plugin/gesture_plugin_demo0.png b/plugins/gesture_plugin/gesture_plugin_demo0.png new file mode 100644 index 00000000..048b8e04 Binary files /dev/null and b/plugins/gesture_plugin/gesture_plugin_demo0.png differ diff --git a/plugins/gesture_plugin/gesture_plugin_demo1.png b/plugins/gesture_plugin/gesture_plugin_demo1.png new file mode 100644 index 00000000..1d0d711a Binary files /dev/null and b/plugins/gesture_plugin/gesture_plugin_demo1.png differ diff --git a/plugins/gesture_plugin/gesture_plugin_gui.png b/plugins/gesture_plugin/gesture_plugin_gui.png new file mode 100644 index 00000000..a1ba284b Binary files /dev/null and b/plugins/gesture_plugin/gesture_plugin_gui.png differ diff --git a/plugins/gesture_plugin/gesture_plugin_system.png b/plugins/gesture_plugin/gesture_plugin_system.png new file mode 100644 index 00000000..b15e0168 Binary files /dev/null and b/plugins/gesture_plugin/gesture_plugin_system.png differ diff --git a/plugins/gesture_plugin/gesture_settings.json b/plugins/gesture_plugin/gesture_settings.json new file mode 100644 index 00000000..64e14795 --- /dev/null +++ b/plugins/gesture_plugin/gesture_settings.json @@ -0,0 +1,21 @@ +{ + "ipAddress": "127.0.0.1", + "portNumber": 8888, + "scaleOffset": [ + -0.0010000000474974513, + -0.0010000000474974513, + 0.0010000000474974513 + ], + "translationOffset": [ + 0.0, + 0.0, + 1.190000057220459 + ], + "confidenceLevelThreshold": 1, + "leaningAngleThreshold": 1.0, + "leaningDirScaleFactor": [ + 1.0, + 1.0, + 1.0 + ] +} \ No newline at end of file diff --git a/plugins/gesture_plugin/plugin_gesture.cpp b/plugins/gesture_plugin/plugin_gesture.cpp new file mode 100644 index 00000000..85656802 --- /dev/null +++ b/plugins/gesture_plugin/plugin_gesture.cpp @@ -0,0 +1,52 @@ +#include + +#include "PanelGesture.h" + +#include "app/ospStudio.h" +#include "app/Plugin.h" + +namespace ospray { +namespace gesture_plugin { + +struct PluginGesture : public Plugin +{ + PluginGesture() : Plugin("Gesture") {} + + void mainMethod(std::shared_ptr ctx) override + { + if (ctx->mode == StudioMode::GUI) { + auto &studioCommon = ctx->studioCommon; + int ac = studioCommon.plugin_argc; + const char **av = studioCommon.plugin_argv; + + std::string optPanelName = "Gesture Panel"; + std::string configFilePath = "config/tracking_settings.json"; + + for (int i=0; i + +namespace ospray { +namespace gesture_plugin { + +TrackingManager::TrackingManager(std::string configFilePath) { + tcpSocket = nullptr; + updated = false; + + JSON config = nullptr; + try { + std::ifstream configFile(configFilePath); + if (configFile) + configFile >> config; + else + std::cerr << "The gesture config file does not exist." << std::endl; + } catch (nlohmann::json::exception &e) { + std::cerr << "Failed to parse the gesture config file: " << e.what() << std::endl; + } + + if (config == nullptr) + return; + + if (config != nullptr && config.contains("ipAddress")) + ipAddress = config["ipAddress"]; + if (config != nullptr && config.contains("portNumber")) + portNumber = config["portNumber"]; + if (config != nullptr && config.contains("scaleOffset")) + scaleOffset = config["scaleOffset"].get(); + // if (config != nullptr && config.contains("rotationOffset")) + // rotationOffset = config["rotationOffset"].get(); + if (config != nullptr && config.contains("translationOffset")) + translationOffset = config["translationOffset"].get(); + if (config != nullptr && config.contains("confidenceLevelThreshold")) + confidenceLevelThreshold = config["confidenceLevelThreshold"].get(); + if (config != nullptr && config.contains("leaningAngleThreshold")) + leaningAngleThreshold = config["leaningAngleThreshold"]; + if (config != nullptr && config.contains("leaningDirScaleFactor")) + leaningDirScaleFactor = config["leaningDirScaleFactor"].get(); +} + +TrackingManager::~TrackingManager() { + this->close(); +} + +void TrackingManager::saveConfig(std::string configFilePath) { + std::ofstream config(configFilePath); + + JSON j; + j["ipAddress"] = ipAddress; + j["portNumber"] = portNumber; + j["scaleOffset"] = scaleOffset; + // j["rotationOffset"] = rotationOffset; + j["translationOffset"] = translationOffset; + j["confidenceLevelThreshold"] = confidenceLevelThreshold; + j["leaningAngleThreshold"] = leaningAngleThreshold; + j["leaningDirScaleFactor"] = leaningDirScaleFactor; + + config << std::setw(4) << j << std::endl; + addStatus("Saved the configuration to " + configFilePath); +} + +void TrackingManager::start() { + if (tcpSocket != nullptr) { + std::cout << "Connection has already been set." << std::endl; + return; + } + + // Initialize socket. + tcpSocket = new TCPSocket<>([&](int errorCode, std::string errorMessage){ + addStatus("Socket creation error: " + std::to_string(errorCode) + " : " + errorMessage); + }); + + // Start receiving from the host. + tcpSocket->onMessageReceived = [&](std::string message) { + std::lock_guard guard(mtx); + + // std::cout << "Message from the Server: " << message << std::endl << std::flush; + updateState(message); + updated = true; + }; + + // On socket closed: + tcpSocket->onSocketClosed = [&](int errorCode){ + addStatus("Connection closed: " + std::to_string(errorCode)); + delete tcpSocket; + tcpSocket = nullptr; + updateState("{}"); + updated = true; + }; + + // Connect to the host. + tcpSocket->Connect(ipAddress, portNumber, [&] { + addStatus("Connected to the server successfully."); + }, + [&](int errorCode, std::string errorMessage){ // Connection failed + addStatus(std::to_string(errorCode) + " : " + errorMessage); + delete tcpSocket; + tcpSocket = nullptr; + }); +} + +void TrackingManager::close() { + if (tcpSocket == nullptr) + return; + + tcpSocket->Close(); +} + +bool TrackingManager::isRunning() { + return tcpSocket != nullptr; +} + +bool TrackingManager::isUpdated() { + return updated; +} + +TrackingState TrackingManager::pollState() { + std::lock_guard guard(mtx); + + updated = false; + return state; +} + +void TrackingManager::updateState(std::string message) { + // set the default state + for (int i = 0; i < K4ABT_JOINT_COUNT; i++) { + state.positions[i] = vec3f(0.f); + state.confidences[i] = K4ABT_JOINT_CONFIDENCE_NONE; + } + state.mode = INTERACTION_NONE; + state.leaningAngle = 0.0f; + state.leaningDir = vec3f(0.0f); + + // parse the message (which is supposed to be in a JSON format) + nlohmann::ordered_json j; + try { + j = nlohmann::ordered_json::parse(message); + } catch (nlohmann::json::exception& e) { + std::cout << "Parse exception: " << e.what() << std::endl; + j = nullptr; + } + + // check if the tracking data is reliable. + if (j == nullptr || j.size() != K4ABT_JOINT_COUNT) { + return; + } + + // update positions and confidence levels + for (int i = 0; i < K4ABT_JOINT_COUNT; i++) { + if (j[i].contains("pos")) { + state.positions[i] = j[i]["pos"].get() * scaleOffset + translationOffset; + } + if (j[i].contains("conf")) { + state.confidences[i] = j[i]["conf"]; + } + } + + // check if the tracking data is reliable for further detections. + if (state.confidences[K4ABT_JOINT_SPINE_NAVEL] < confidenceLevelThreshold || + state.confidences[K4ABT_JOINT_WRIST_LEFT] < confidenceLevelThreshold || + state.confidences[K4ABT_JOINT_WRIST_RIGHT] < confidenceLevelThreshold || + state.confidences[K4ABT_JOINT_NECK] < confidenceLevelThreshold) { + return; + } + + // compute additional states (angle and direction) + vec3f spine = state.positions[K4ABT_JOINT_NECK] - state.positions[K4ABT_JOINT_SPINE_NAVEL]; + vec3f normal (0.0f, 1.0f, 0.0f); + state.leaningAngle = acos(dot(spine, normal) / (length(spine) * length(normal))) / M_PI * 180.f; + state.leaningDir = spine - dot(spine, normal) / length(normal) * normal; + state.leaningDir *= leaningDirScaleFactor; + + // compute additional states (mode) + bool leftHandUp = state.positions[K4ABT_JOINT_SPINE_NAVEL].y < state.positions[K4ABT_JOINT_WRIST_LEFT].y; + bool rightHandUp = state.positions[K4ABT_JOINT_SPINE_NAVEL].y < state.positions[K4ABT_JOINT_WRIST_RIGHT].y; + state.mode = (leftHandUp && rightHandUp && state.leaningAngle > leaningAngleThreshold) ? INTERACTION_FLYING : INTERACTION_IDLE; +} + +void TrackingManager::addStatus(std::string status) { + // write time before status + time_t now = time(0); + tm *ltm = localtime(&now); + status.insert(0, "(" + std::to_string(ltm->tm_hour) + ":" + std::to_string(ltm->tm_min) + ":" + std::to_string(ltm->tm_sec) + ") "); + + statuses.push_back(status); +} + +// show "important" tracking information in a readable format +std::string TrackingManager::getResultsInReadableForm() { + std::string result; + + result += "headPos.x: " + std::to_string(state.positions[K4ABT_JOINT_HEAD].x) + "\n"; + result += "headPos.y: " + std::to_string(state.positions[K4ABT_JOINT_HEAD].y) + "\n"; + result += "headPos.z: " + std::to_string(state.positions[K4ABT_JOINT_HEAD].z) + "\n"; + + if (state.mode == INTERACTION_NONE) result += "mode: NONE\n"; + else if (state.mode == INTERACTION_IDLE) result += "mode: IDLE\n"; + else if (state.mode == INTERACTION_FLYING) result += "mode: FLYING\n"; + + result += "leaningAngle: " + std::to_string(state.leaningAngle) + " °\n"; + + result += "leaningDir.x: " + std::to_string(state.leaningDir.x) + "\n"; + result += "leaningDir.y: " + std::to_string(state.leaningDir.y) + " (proj. to x-z plane)\n"; + result += "leaningDir.z: " + std::to_string(state.leaningDir.z); + + return result; +} + +} // namespace gesture_plugin +} // namespace ospray \ No newline at end of file diff --git a/plugins/gesture_plugin/tracker/TrackingManager.h b/plugins/gesture_plugin/tracker/TrackingManager.h new file mode 100644 index 00000000..25763938 --- /dev/null +++ b/plugins/gesture_plugin/tracker/TrackingManager.h @@ -0,0 +1,55 @@ +#pragma once + +#include "TrackingData.h" +#include "tcpsocket.hpp" + +#include +#include +#include + +namespace ospray { +namespace gesture_plugin { + +using namespace rkcommon::math; + +class TrackingManager +{ +public: + TrackingManager(std::string configFilePath); + ~TrackingManager(); + + void saveConfig(std::string configFilePath); + + void start(); + void close(); + bool isRunning(); + bool isUpdated(); + + TrackingState pollState(); + std::string getResultsInReadableForm(); + + std::string ipAddress { "localhost" }; + uint portNumber { 8888 }; + // Kinect - right-hand, y-down, z-forward, in milli-meters + // OSPRay - right-hand, y-up, z-forward, in meters + vec3f scaleOffset { -0.001f, -0.001f, +0.001f}; + // vec3f rotationOffset { 0.0f, 0.0f, 0.0f}; + vec3f translationOffset { 0.0f, 0.0f, 0.0f}; + int confidenceLevelThreshold { K4ABT_JOINT_CONFIDENCE_LOW }; + float leaningAngleThreshold { 8.0f }; // in degrees + vec3f leaningDirScaleFactor { 1.0f, 1.0f, 1.0f }; + + std::list statuses; +private: + void updateState(std::string message); + void addStatus(std::string status); + + TCPSocket<> *tcpSocket; + TrackingState state; + bool updated; + + std::mutex mtx; +}; + +} // namespace gesture_plugin +} // namespace ospray \ No newline at end of file