From 4cf6947af793cb61c26d074402b85fd2a8bdd52f Mon Sep 17 00:00:00 2001 From: Peter Johnson Date: Sat, 29 Aug 2020 11:30:45 -0700 Subject: [PATCH] [cscore] Add wpigui-based USB camera viewer --- CMakeLists.txt | 7 +- cscore/CMakeLists.txt | 10 ++- cscore/build.gradle | 53 ++++++++--- cscore/examples/usbviewer/usbviewer.cpp | 114 ++++++++++++++++++++++++ 4 files changed, 171 insertions(+), 13 deletions(-) create mode 100644 cscore/examples/usbviewer/usbviewer.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index e3361e087e..4033aea7e5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -120,6 +120,11 @@ endif() add_subdirectory(wpiutil) add_subdirectory(ntcore) +if (WITH_SIMULATION_MODULES AND NOT USE_EXTERNAL_HAL) + add_subdirectory(imgui) + add_subdirectory(wpigui) +endif() + if (NOT WITHOUT_CSCORE) set(CSCORE_DEP_REPLACE ${CSCORE_DEP_REPLACE_IMPL}) set(CAMERASERVER_DEP_REPLACE ${CAMERASERVER_DEP_REPLACE_IMPL}) @@ -137,8 +142,6 @@ if (NOT WITHOUT_CSCORE) endif() if (WITH_SIMULATION_MODULES AND NOT USE_EXTERNAL_HAL) - add_subdirectory(imgui) - add_subdirectory(wpigui) add_subdirectory(simulation) endif() diff --git a/cscore/CMakeLists.txt b/cscore/CMakeLists.txt index f5b4751d2f..92bef994b9 100644 --- a/cscore/CMakeLists.txt +++ b/cscore/CMakeLists.txt @@ -52,10 +52,18 @@ install(EXPORT cscore DESTINATION ${cscore_config_dir}) SUBDIR_LIST(cscore_examples "${CMAKE_CURRENT_SOURCE_DIR}/examples") foreach(example ${cscore_examples}) file(GLOB cscore_example_src examples/${example}/*.cpp) + unset(add_libs) + if(${example} STREQUAL "usbviewer") + if(TARGET wpigui) + set(add_libs wpigui) + else() + unset(cscore_example_src) + endif() + endif() if(cscore_example_src) add_executable(cscore_${example} ${cscore_example_src}) wpilib_target_warnings(cscore_${example}) - target_link_libraries(cscore_${example} cscore) + target_link_libraries(cscore_${example} cscore ${add_libs}) set_property(TARGET cscore_${example} PROPERTY FOLDER "examples") endif() endforeach() diff --git a/cscore/build.gradle b/cscore/build.gradle index 66c2be3616..90e5c20d51 100644 --- a/cscore/build.gradle +++ b/cscore/build.gradle @@ -170,17 +170,50 @@ nativeUtils.exportsConfigs { model { components { examplesMap.each { key, value -> - "${key}"(NativeExecutableSpec) { - targetBuildTypes 'debug' - binaries.all { - lib project: ':wpiutil', library: 'wpiutil', linkage: 'shared' - lib library: 'cscore', linkage: 'shared' + if (key == "usbviewer") { + if (!project.hasProperty('onlylinuxathena') && !project.hasProperty('onlylinuxraspbian') && !project.hasProperty('onlylinuxaarch64bionic')) { + "${key}"(NativeExecutableSpec) { + targetBuildTypes 'debug' + binaries.all { + lib project: ':wpiutil', library: 'wpiutil', linkage: 'shared' + lib project: ':wpigui', library: 'wpigui', linkage: 'static' + lib library: 'cscore', linkage: 'shared' + nativeUtils.useRequiredLibrary(it, 'imgui_static') + if (it.targetPlatform.name == nativeUtils.wpi.platforms.roborio || it.targetPlatform.name == nativeUtils.wpi.platforms.raspbian || it.targetPlatform.name == nativeUtils.wpi.platforms.aarch64bionic) { + it.buildable = false + return + } + if (it.targetPlatform.operatingSystem.isWindows()) { + it.linker.args << 'Gdi32.lib' << 'Shell32.lib' << 'd3d11.lib' << 'd3dcompiler.lib' + } else if (it.targetPlatform.operatingSystem.isMacOsX()) { + it.linker.args << '-framework' << 'Metal' << '-framework' << 'MetalKit' << '-framework' << 'Cocoa' << '-framework' << 'IOKit' << '-framework' << 'CoreFoundation' << '-framework' << 'CoreVideo' << '-framework' << 'QuartzCore' + } else { + it.linker.args << '-lX11' + } + } + sources { + cpp { + source { + srcDirs 'examples/' + "${key}" + include '**/*.cpp' + } + } + } + } } - sources { - cpp { - source { - srcDirs 'examples/' + "${key}" - include '**/*.cpp' + } else { + "${key}"(NativeExecutableSpec) { + targetBuildTypes 'debug' + binaries.all { + lib project: ':wpiutil', library: 'wpiutil', linkage: 'shared' + lib library: 'cscore', linkage: 'shared' + } + sources { + cpp { + source { + srcDirs 'examples/' + "${key}" + include '**/*.cpp' + } } } } diff --git a/cscore/examples/usbviewer/usbviewer.cpp b/cscore/examples/usbviewer/usbviewer.cpp new file mode 100644 index 0000000000..9692f4c023 --- /dev/null +++ b/cscore/examples/usbviewer/usbviewer.cpp @@ -0,0 +1,114 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include +#include +#include + +#define IMGUI_DEFINE_MATH_OPERATORS +#include +#include +#include +#include +#include +#include +#include + +#include "cscore.h" +#include "cscore_cv.h" + +namespace gui = wpi::gui; + +int main() { + std::atomic latestFrame; + std::vector sharedFreeList; + wpi::spinlock sharedFreeListMutex; + std::vector sourceFreeList; + std::atomic stopCamera{false}; + + cs::UsbCamera camera{"usbcam", 0}; + camera.SetVideoMode(cs::VideoMode::kMJPEG, 640, 480, 30); + cs::CvSink cvsink{"cvsink"}; + cvsink.SetSource(camera); + + std::thread thr([&] { + cv::Mat frame; + while (!stopCamera) { + // get frame from camera + uint64_t time = cvsink.GrabFrame(frame); + if (time == 0) { + wpi::outs() << "error: " << cvsink.GetError() << '\n'; + continue; + } + + // get or create a mat, prefer sourceFreeList over sharedFreeList + cv::Mat* out; + if (!sourceFreeList.empty()) { + out = sourceFreeList.back(); + sourceFreeList.pop_back(); + } else { + { + std::scoped_lock lock(sharedFreeListMutex); + for (auto mat : sharedFreeList) sourceFreeList.emplace_back(mat); + sharedFreeList.clear(); + } + if (!sourceFreeList.empty()) { + out = sourceFreeList.back(); + sourceFreeList.pop_back(); + } else { + out = new cv::Mat; + } + } + + // convert to RGBA + cv::cvtColor(frame, *out, cv::COLOR_BGR2RGBA); + + // make available + auto prev = latestFrame.exchange(out); + + // put prev on free list + if (prev) sourceFreeList.emplace_back(prev); + } + }); + + gui::CreateContext(); + gui::Initialize("Hello World", 1024, 768); + gui::Texture tex; + gui::AddEarlyExecute([&] { + auto frame = latestFrame.exchange(nullptr); + if (frame) { + // create or update texture + if (!tex || frame->cols != tex.GetWidth() || + frame->rows != tex.GetHeight()) { + tex = gui::Texture(gui::kPixelRGBA, frame->cols, frame->rows, + frame->data); + } else { + tex.Update(frame->data); + } + // put back on shared freelist + std::scoped_lock lock(sharedFreeListMutex); + sharedFreeList.emplace_back(frame); + } + + ImGui::SetNextWindowSize(ImVec2(640, 480), ImGuiCond_FirstUseEver); + if (ImGui::Begin("Video")) { + // render to window (best fit) + if (tex && tex.GetWidth() != 0 && tex.GetHeight() != 0) { + auto drawList = ImGui::GetWindowDrawList(); + ImVec2 windowPos = ImGui::GetWindowPos(); + ImVec2 imageMin = ImGui::GetWindowContentRegionMin(); + ImVec2 imageMax = ImGui::GetWindowContentRegionMax(); + gui::MaxFit(&imageMin, &imageMax, tex.GetWidth(), tex.GetHeight()); + drawList->AddImage(tex, windowPos + imageMin, windowPos + imageMax); + } + } + ImGui::End(); + }); + gui::Main(); + stopCamera = true; + thr.join(); +}