Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision

Target

Select target project
  • nicolaspope/ftl
1 result
Select Git revision
Show changes
Showing
with 3703 additions and 0 deletions
#include "statistics.hpp"
#include "../screen.hpp"
#include "../views/statistics.hpp"
#include <ftl/streams/builder.hpp>
#include <ftl/streams/netstream.hpp>
#include <nanogui/entypo.h>
#include <loguru.hpp>
#include <nvml.h>
#ifdef WIN32
#pragma comment(lib, "nvml")
#endif
using ftl::gui2::Statistics;
using ftl::gui2::StatisticsPanel;
template <typename T>
std::string to_string_with_precision(const T a_value, const int n = 6) {
std::ostringstream out;
out.precision(n);
out << std::fixed << a_value;
return out.str();
}
Statistics::~Statistics() {
nvmlShutdown();
}
void Statistics::update(double delta) {
time_count_ += delta;
if (time_count_ > 1.0) {
float bitrate = ftl::stream::Net::getRequiredBitrate();
if (bitrate > 0.0f) {
getJSON(StatisticsPanel::PERFORMANCE_INFO)["Bitrate"] = to_string_with_precision(bitrate, 1) + std::string("Mbit/s");
}
time_count_ = 0.0;
size_t gpu_free_mem;
size_t gpu_total_mem;
cudaSafeCall(cudaMemGetInfo(&gpu_free_mem, &gpu_total_mem));
float gpu_mem = 1.0f - (float(gpu_free_mem) / float(gpu_total_mem));
getJSON(StatisticsPanel::PERFORMANCE_INFO)["GPU Memory"] = to_string_with_precision(gpu_mem*100.0f, 1) + std::string("%");
nvmlDevice_t device;
auto result = nvmlDeviceGetHandleByIndex(0, &device);
nvmlUtilization_st device_utilization;
result = nvmlDeviceGetUtilizationRates(device, &device_utilization);
getJSON(StatisticsPanel::PERFORMANCE_INFO)["GPU Usage"] = std::to_string(device_utilization.gpu) + std::string("%");
unsigned int decode_util;
unsigned int decode_period;
result = nvmlDeviceGetDecoderUtilization(device, &decode_util, &decode_period);
getJSON(StatisticsPanel::PERFORMANCE_INFO)["GPU Decoder"] = std::to_string(decode_util) + std::string("%");
// Doesn't seem to work
unsigned int encoder_sessions=0;
unsigned int encoder_fps;
unsigned int encoder_latency;
result = nvmlDeviceGetEncoderStats(device, &encoder_sessions, &encoder_fps, &encoder_latency);
unsigned int encoder_util;
unsigned int encoder_period;
result = nvmlDeviceGetEncoderUtilization(device, &encoder_util, &encoder_period);
getJSON(StatisticsPanel::PERFORMANCE_INFO)["GPU Encoder"] = std::to_string(encoder_util) + std::string("% (") + std::to_string(encoder_sessions) + std::string(")");
}
}
void Statistics::init() {
auto result = nvmlInit();
if (result != NVML_SUCCESS) throw FTL_Error("No NVML");
/**
* TODO: store all values in hash table and allow other modules to
* add/remove items/groups.
*/
widget = new ftl::gui2::StatisticsWidget(screen, this);
widget->setVisible(value("visible", false));
auto button = screen->addButton(ENTYPO_ICON_INFO);
button->setTooltip("Show Information");
button->setCallback([this, button](){
button->setPushed(false);
widget->setVisible(!widget->visible());
});
button->setVisible(true);
}
void Statistics::setCursor(nanogui::Cursor c) {
widget->setCursor(c);
}
/*void Statistics::set(const std::string &key, const std::string& value) {
text_[key] = value;
}
void Statistics::set(const std::string &key, float value, const std::string& unit) {
text_[key] = to_string_with_precision(value, 3) + unit;
}
std::vector<std::string> Statistics::get() {
std::vector<std::string> res;
res.reserve(text_.size());
for (auto& [k, v] : text_) {
res.push_back(k + ": " +v );
}
return res;
}*/
nlohmann::json &Statistics::getJSON(StatisticsPanel p) {
return groups_[p].json;
}
void Statistics::show(StatisticsPanel p, bool visible) {
groups_[p].visible = visible;
}
void Statistics::hide(StatisticsPanel p) {
groups_[p].visible = false;
}
bool Statistics::isVisible(StatisticsPanel p) {
return groups_[p].visible;
}
std::vector<std::pair<StatisticsPanel, const nlohmann::json &>> Statistics::get() const {
std::vector<std::pair<StatisticsPanel, const nlohmann::json &>> results;
for (const auto &i : groups_) {
results.emplace_back(i.first, i.second.json);
}
return results;
}
#pragma once
#include "../module.hpp"
#include <nlohmann/json.hpp>
namespace ftl
{
namespace gui2
{
enum class StatisticsPanel {
MEDIA_STATUS=0, // Live or not?
PERFORMANCE_INFO, // Bitrate, fps etc
STREAM_DATA, // Channel info
MEDIA_META, // Name, device, capabilities
CAMERA_DETAILS, // Calibration info
LOGGING // Stream error and log messages
// Chat, media name, ...
};
class Statistics : public Module {
public:
using Module::Module;
//Statistics();
~Statistics();
virtual void init() override;
virtual void update(double delta) override;
// not thread safe! (use only from gui thread or add lock)
/*void set(const std::string &key, const std::string& value);
void set(const std::string &key, int value);
void set(const std::string &key, float value, const std::string& unit = "");*/
nlohmann::json &getJSON(StatisticsPanel);
void show(StatisticsPanel, bool visible=true);
void hide(StatisticsPanel);
bool isVisible(StatisticsPanel);
void setCursor(nanogui::Cursor);
//void remove(const std::string &key) { text_.erase(key); }
std::vector<std::pair<StatisticsPanel, const nlohmann::json &>> get() const;
private:
struct StatsGroup {
// TODO: Other properties...
nlohmann::json json; // = nlohmann::json::object_t();
bool visible=true;
};
nanogui::Widget* widget;
std::map<StatisticsPanel, StatsGroup> groups_;
double time_count_=0.0;
};
}
}
#include "themes.hpp"
#include "nanogui/theme.h"
#include "../screen.hpp"
using ftl::gui2::Themes;
using nanogui::Theme;
void Themes::init() {
auto* toolbuttheme = screen->getTheme("toolbutton");
toolbuttheme->mBorderDark = nanogui::Color(0,0);
toolbuttheme->mBorderLight = nanogui::Color(0,0);
toolbuttheme->mButtonGradientBotFocused = nanogui::Color(60,255);
toolbuttheme->mButtonGradientBotUnfocused = nanogui::Color(0,0);
toolbuttheme->mButtonGradientTopFocused = nanogui::Color(60,255);
toolbuttheme->mButtonGradientTopUnfocused = nanogui::Color(0,0);
toolbuttheme->mButtonGradientTopPushed = nanogui::Color(60,180);
toolbuttheme->mButtonGradientBotPushed = nanogui::Color(60,180);
toolbuttheme->mTextColor = nanogui::Color(0.9f,0.9f,0.9f,0.9f);
toolbuttheme->mWindowDropShadowSize = 0;
toolbuttheme->mDropShadow = nanogui::Color(0,0);
auto* windowtheme = screen->getTheme("window_light");
windowtheme->mWindowFillFocused = nanogui::Color(220, 200);
windowtheme->mWindowFillUnfocused = nanogui::Color(220, 200);
windowtheme->mWindowHeaderGradientBot = nanogui::Color(60,230);
windowtheme->mWindowHeaderGradientTop = nanogui::Color(60,230);
windowtheme->mWindowHeaderSepBot = nanogui::Color(60, 230);
windowtheme->mTextColor = nanogui::Color(20,255);
windowtheme->mDisabledTextColor = nanogui::Color(140, 255);
windowtheme->mWindowCornerRadius = 2;
windowtheme->mButtonGradientBotFocused = nanogui::Color(210,255);
windowtheme->mButtonGradientBotUnfocused = nanogui::Color(190,255);
windowtheme->mButtonGradientTopFocused = nanogui::Color(230,255);
windowtheme->mButtonGradientTopUnfocused = nanogui::Color(230,255);
windowtheme->mButtonGradientTopPushed = nanogui::Color(170,255);
windowtheme->mButtonGradientBotPushed = nanogui::Color(210,255);
windowtheme->mBorderDark = nanogui::Color(150,255);
windowtheme->mBorderMedium = nanogui::Color(165,255);
windowtheme->mBorderLight = nanogui::Color(230,255);
windowtheme->mButtonFontSize = 16;
windowtheme->mTextColorShadow = nanogui::Color(0,0);
windowtheme->mWindowTitleUnfocused = windowtheme->mWindowTitleFocused;
windowtheme->mWindowTitleFocused = nanogui::Color(240,255);
windowtheme->mIconScale = 0.85f;
auto* viewtheme = screen->getTheme("view");
viewtheme->mWindowFillFocused = nanogui::Color(0, 0);
viewtheme->mWindowFillUnfocused = nanogui::Color(0, 0);
viewtheme->mWindowCornerRadius = 0;
viewtheme->mBorderDark = nanogui::Color(0 ,0);
viewtheme->mBorderMedium = nanogui::Color(0 ,0);
viewtheme->mBorderLight = nanogui::Color(0 ,0);
viewtheme->mWindowHeaderGradientBot = nanogui::Color(0, 0);
viewtheme->mWindowHeaderGradientTop = nanogui::Color(0, 0);
viewtheme->mWindowHeaderSepBot = nanogui::Color(0, 0);
viewtheme->mTextColorShadow = nanogui::Color(0, 0);
viewtheme->mWindowDropShadowSize = 0;
auto* windowtheme_dark = screen->getTheme("window_dark");
windowtheme_dark->mWindowCornerRadius = 5;
/*windowtheme_dark->mButtonGradientBotFocused = nanogui::Color(90,255);
windowtheme_dark->mButtonGradientBotUnfocused = nanogui::Color(70,255);
windowtheme_dark->mButtonGradientTopFocused = nanogui::Color(110,255);
windowtheme_dark->mButtonGradientTopUnfocused = nanogui::Color(110,255);
windowtheme_dark->mButtonGradientTopPushed = nanogui::Color(50,255);
windowtheme_dark->mButtonGradientBotPushed = nanogui::Color(90,255);*/
windowtheme_dark->mButtonGradientBotFocused = nanogui::Color(60,255);
windowtheme_dark->mButtonGradientBotUnfocused = nanogui::Color(35,35,40,180);
windowtheme_dark->mButtonGradientTopFocused = nanogui::Color(60,255);
windowtheme_dark->mButtonGradientTopUnfocused = nanogui::Color(35,35,40,180);
windowtheme_dark->mButtonGradientTopPushed = nanogui::Color(90,180);
windowtheme_dark->mButtonGradientBotPushed = nanogui::Color(90,180);
windowtheme_dark->mButtonFontSize = 16;
windowtheme_dark->mIconScale = 0.85f;
windowtheme_dark->mBorderDark = nanogui::Color(20,0);
windowtheme_dark->mBorderMedium = nanogui::Color(20,0);
windowtheme_dark->mBorderLight = nanogui::Color(20,0);
auto* mediatheme = screen->getTheme("media");
mediatheme->mIconScale = 1.2f;
mediatheme->mWindowDropShadowSize = 0;
mediatheme->mWindowFillFocused = nanogui::Color(45, 150);
mediatheme->mWindowFillUnfocused = nanogui::Color(45, 80);
mediatheme->mButtonGradientTopUnfocused = nanogui::Color(0,0);
mediatheme->mButtonGradientBotUnfocused = nanogui::Color(0,0);
mediatheme->mButtonGradientTopFocused = nanogui::Color(80,230);
mediatheme->mButtonGradientBotFocused = nanogui::Color(80,230);
mediatheme->mIconColor = nanogui::Color(255,255);
mediatheme->mTextColor = nanogui::Color(1.0f,1.0f,1.0f,1.0f);
mediatheme->mBorderDark = nanogui::Color(0,0);
mediatheme->mBorderMedium = nanogui::Color(0,0);
mediatheme->mBorderLight = nanogui::Color(0,0);
mediatheme->mDropShadow = nanogui::Color(0,0);
mediatheme->mButtonFontSize = 30;
mediatheme->mStandardFontSize = 20;
auto* mediatheme2 = screen->getTheme("media_small");
mediatheme2->mIconScale = 1.2f;
mediatheme2->mWindowDropShadowSize = 0;
mediatheme2->mWindowFillFocused = nanogui::Color(45, 150);
mediatheme2->mWindowFillUnfocused = nanogui::Color(45, 80);
mediatheme2->mButtonGradientTopUnfocused = nanogui::Color(0,0);
mediatheme2->mButtonGradientBotUnfocused = nanogui::Color(0,0);
mediatheme2->mButtonGradientTopFocused = nanogui::Color(80,230);
mediatheme2->mButtonGradientBotFocused = nanogui::Color(80,230);
mediatheme2->mIconColor = nanogui::Color(255,255);
mediatheme2->mTextColor = nanogui::Color(1.0f,1.0f,1.0f,1.0f);
mediatheme2->mBorderDark = nanogui::Color(0,0);
mediatheme2->mBorderMedium = nanogui::Color(0,0);
mediatheme2->mBorderLight = nanogui::Color(0,0);
mediatheme2->mDropShadow = nanogui::Color(0,0);
mediatheme2->mButtonFontSize = 16;
mediatheme2->mStandardFontSize = 14;
// https://flatuicolors.com/palette/defo
screen->setColor("highlight1", nanogui::Color(231, 76, 60, 255)); // red
screen->setColor("highlight2", nanogui::Color(52, 152, 219, 255)); // blue
screen->setColor("highlight1_disabled", nanogui::Color(166, 166, 166, 255));
screen->setColor("highlight2_disabled", nanogui::Color(166, 166, 166, 255));
}
#pragma once
#include "../module.hpp"
namespace ftl
{
namespace gui2
{
class Themes : public Module {
public:
using Module::Module;
virtual void init() override;
};
}
}
#include "thumbnails.hpp"
#include "../views/thumbnails.hpp"
#include "camera.hpp"
#include <ftl/codecs/channels.hpp>
#include <nanogui/entypo.h>
using ftl::codecs::Channel;
using ftl::gui2::ThumbnailsController;
void ThumbnailsController::init() {
auto button = screen->addButton(ENTYPO_ICON_HOME);
button->setTooltip("Home");
button->setCallback([this, button](){
button->setPushed(false);
activate();
});
button->setVisible(true);
}
void ThumbnailsController::activate() {
show_thumbnails();
}
ThumbnailsController::~ThumbnailsController() {
}
void ThumbnailsController::removeFrameset(uint32_t id) {
{
std::unique_lock<std::mutex> lk(mtx_);
framesets_.erase(id);
}
io->feed()->remove(id);
}
void ThumbnailsController::show_thumbnails() {
auto thumb_view = new ftl::gui2::Thumbnails(screen, this);
auto* filter = io->feed()->filter({Channel::Colour});
filter->on(
[this, thumb_view](const ftl::data::FrameSetPtr& fs){
{
std::unique_lock<std::mutex> lk(mtx_);
framesets_[fs->frameset()] = fs;
}
screen->redraw();
return true;
});
thumb_view->onClose([filter](){
filter->remove();
});
screen->setView(thumb_view);
}
std::vector<ftl::data::FrameSetPtr> ThumbnailsController::getFrameSets() {
std::unique_lock<std::mutex> lk(mtx_);
std::vector<ftl::data::FrameSetPtr> framesets;
framesets.reserve(framesets_.size());
for (auto& [k, v] : framesets_) {
std::ignore = k;
framesets.push_back(v);
}
return framesets;
}
void ThumbnailsController::show_camera(ftl::data::FrameID id) {
auto* camera = screen->getModule<ftl::gui2::Camera>();
camera->activate(id);
}
#pragma once
#include "../module.hpp"
#include "../screen.hpp"
namespace ftl {
namespace gui2 {
/**
* Controller for thumbnail view.
*/
class ThumbnailsController : public Module {
public:
using Module::Module;
virtual ~ThumbnailsController();
virtual void init() override;
virtual void activate();
void show_thumbnails();
void show_camera(ftl::data::FrameID id);
std::vector<ftl::data::FrameSetPtr> getFrameSets();
void removeFrameset(uint32_t id);
private:
std::mutex mtx_;
std::map<unsigned int, ftl::data::FrameSetPtr> framesets_;
};
}
}
#include <nanogui/opengl.h>
#include <nanogui/glutil.h>
#include <nanogui/screen.h>
#include <nanogui/window.h>
#include <nanogui/layout.h>
#include <nanogui/imageview.h>
#include <nanogui/label.h>
#include <nanogui/toolbutton.h>
#include <nanogui/popupbutton.h>
#include <Eigen/Eigen>
#include "screen.hpp"
#include "widgets/window.hpp"
#include <nanogui/messagedialog.h>
#include <loguru.hpp>
using std::min;
using std::max;
using Eigen::Vector2i;
using ftl::gui2::Screen;
static const int toolbar_w = 50;
static const Vector2i wsize(1280,720);
Screen::Screen() :
nanogui::Screen(wsize, "FT-Lab Remote Presence"),
toolbar_(nullptr),
active_view_(nullptr), msgerror_(nullptr) {
using namespace nanogui;
setSize(wsize);
toolbar_ = new FixedWindow(this);
toolbar_->setPosition({0, 0});
toolbar_->setWidth(toolbar_w);
toolbar_->setHeight(height());
toolbar_->setTheme(getTheme("media"));
setResizeCallback([this](const Vector2i &s) {
toolbar_->setFixedSize({toolbar_->width(), s[1]});
toolbar_->setPosition({0, 0});
if (active_view_) {
active_view_->setSize(viewSize(s));
}
performLayout();
});
tools_ = new Widget(toolbar_);
tools_->setLayout(new BoxLayout( Orientation::Vertical,
Alignment::Middle, 0, 10));
tools_->setPosition(Vector2i(5,10));
setVisible(true);
performLayout();
}
Screen::~Screen() {
// removes view; onClose() callback can depend on module
if (active_view_) {
this->removeChild(active_view_);
active_view_ = nullptr;
}
for (auto [name, ptr] : modules_) {
std::ignore = name;
delete ptr;
}
}
nanogui::Theme* Screen::getTheme(const std::string &name) {
if (themes_.count(name) == 0) {
themes_[name] = new nanogui::Theme(*theme());
}
return themes_[name];
}
nanogui::Color Screen::getColor(const std::string &name) {
if (colors_.count(name) == 0) {
return nanogui::Color(0, 0, 0, 0);
}
return colors_[name];
}
void Screen::setColor(const std::string &name, const nanogui::Color &c) {
colors_[name] = c;
}
void Screen::redraw() {
// glfwPostEmptyEvent() is safe to call from any thread
// https://www.glfw.org/docs/3.3/intro_guide.html#thread_safety
glfwPostEmptyEvent();
}
nanogui::Vector2i Screen::viewSize(const nanogui::Vector2i &ws) {
return {ws.x(), ws.y()};
}
nanogui::Vector2i Screen::viewSize() {
return viewSize(size());
}
void Screen::showError(const std::string&title, const std::string& msg) {
// FIXME: This isn't thread safe?
if (msgerror_) { return; }
msgerror_ = new nanogui::MessageDialog
(screen(), nanogui::MessageDialog::Type::Warning, title, msg);
msgerror_->setModal(false);
msgerror_->setCallback([this](int){
msgerror_ = nullptr;
});
}
void Screen::setView(ftl::gui2::View *view) {
view->setPosition({0, 0});
view->setTheme(getTheme("view"));
view->setVisible(true);
if (childIndex(view) == -1) {
addChild(view);
}
if (active_view_) {
active_view_->setVisible(false);
// View requires same cleanup as Window (see screen.cpp) before removed.
if (std::find(mFocusPath.begin(), mFocusPath.end(), active_view_) != mFocusPath.end()) {
mFocusPath.clear();
}
if (mDragWidget == active_view_) {
mDragWidget = nullptr;
}
removeChild(active_view_);
}
// all windows should be in front of new view
mChildren.erase(std::remove(mChildren.begin(), mChildren.end(), view), mChildren.end());
mChildren.insert(mChildren.begin(), view);
active_view_ = view;
LOG(INFO) << "number of children (Screen): "<< mChildren.size();
// hide all popups (TODO: only works on toolbar at the moment)
for (nanogui::Widget* widget : tools_->children()) {
if (auto button = dynamic_cast<nanogui::PopupButton*>(widget)) {
button->setPushed(false);
}
}
performLayout();
}
void Screen::render() {
if (active_view_) {
active_view_->render();
}
}
ftl::gui2::Module* Screen::addModule_(const std::string &name, ftl::gui2::Module* ptr) {
ptr->init();
if (modules_.find(name) != modules_.end()) {
LOG(WARNING) << "Module " << name << " already loaded. Removing old module";
delete modules_[name];
}
modules_[name] = ptr;
return ptr;
}
bool Screen::keyboardEvent(int key, int scancode, int action, int modifiers) {
if (nanogui::Screen::keyboardEvent(key, scancode, action, modifiers)) {
return true;
}
if (active_view_) {
// event not processed in any focused widget
return active_view_->keyboardEvent(key, scancode, action, modifiers);
}
return false;
}
bool Screen::keyboardCharacterEvent(unsigned int codepoint) {
if (nanogui::Screen::keyboardCharacterEvent(codepoint)) {
return true;
}
if (active_view_) {
// event not processed in any focused widget
return active_view_->keyboardCharacterEvent(codepoint);
}
return false;
}
void Screen::drawAll() {
double now = glfwGetTime();
double delta = now - last_draw_time_;
for (const auto& [name, mod] : modules_) {
mod->update(delta);
}
last_draw_time_ = now;
nanogui::Screen::drawAll();
}
#pragma once
#include <nanogui/screen.h>
#include <nanogui/glutil.h>
#include <nanogui/toolbutton.h>
#include <map>
#include <memory>
#include <typeinfo>
#include "view.hpp"
#include "module.hpp"
namespace ftl {
namespace gui2 {
/**
* FTL GUI main screen. Methods may only be called from main (GUI) threads
* unless otherwise documented.
*/
class Screen : public nanogui::Screen {
public:
explicit Screen();
virtual ~Screen();
virtual void drawAll() override;
virtual bool keyboardEvent(int key, int scancode, int action, int modifiers) override;
virtual bool keyboardCharacterEvent(unsigned int codepoint) override;
void render(); // necessary?
/** Redraw the screen (triggers an empty event). Thread safe. */
void redraw();
void activate(Module *ptr);
/** set active view (existing object */
void setView(ftl::gui2::View* view);
/** set active view (create new object)*/
template<typename T, typename ... Args>
void setView(Args ... args);
bool isActiveView(View* ptr) { return active_view_ == ptr; }
/** Add a module.*/
template<typename T, typename ... Args>
T* addModule(const std::string &name, ftl::Configurable *config, Args ... args);
/** Get a pointer to module. Module identified by name, exception thrown if not found */
template<typename T>
T* getModule(const std::string &name);
/** Get a pointer to module. Module indentified by dynamic type from template parameter.
* Throws an exception if not found. If more than one possible match (same module
* loaded multiple times), return value can be any.
*/
template<typename T>
T* getModule();
// prever above template (explicit who manages delete)
// template<typename T>
// T* addModule(T* ptr) { return addModule_(ptr); }
// TODO removeModule() as well?
/** add a button to toolbar */
template<typename T=nanogui::ToolButton, typename ... Args>
T* addButton(Args ... args);
/** themes/colors */
nanogui::Theme* getTheme(const std::string &name);
nanogui::Color getColor(const std::string &name);
void setColor(const std::string &name, const nanogui::Color &c);
// Implement in View or Screen? Add ID (address of creating instance)
// to each error to prevent spam?
/** Show error message popup */
void showError(const std::string& title, const std::string &msg);
nanogui::Vector2i viewSize(const nanogui::Vector2i &ws);
nanogui::Vector2i viewSize();
private:
Module* addModule_(const std::string &name, Module* ptr);
//std::mutex mtx_; // not used: do not modify gui outside gui (main) thread
std::map<std::string, ftl::gui2::Module*> modules_;
std::map<std::string, nanogui::ref<nanogui::Theme>> themes_;
std::map<std::string, nanogui::Color> colors_;
nanogui::Widget *toolbar_;
nanogui::Widget *tools_;
ftl::gui2::View *active_view_;
nanogui::MessageDialog* msgerror_;
double last_draw_time_=0.0f;
public:
EIGEN_MAKE_ALIGNED_OPERATOR_NEW
};
template<typename T, typename ... Args>
void Screen::setView(Args ... args) {
setView(new T(this, args ...));
}
template<typename T, typename ... Args>
T* Screen::addModule(const std::string &name, ftl::Configurable *config, Args ... args) {
static_assert(std::is_base_of<Module, T>::value);
return dynamic_cast<T*>(
addModule_(
name,
ftl::config::create<T>(config, name, args ...)
)
);
}
template<typename T>
T* Screen::getModule(const std::string &name) {
static_assert(std::is_base_of<Module, T>::value);
if (modules_.find(name) == modules_.end()) {
throw ftl::exception("module: " + name + " not found");
}
auto* ptr = dynamic_cast<T*>(modules_[name]);
if (ptr == nullptr) {
throw ftl::exception("bad cast, module requested with wrong type");
}
return ptr;
}
template<typename T>
T* Screen::getModule() {
static_assert(std::is_base_of<Module, T>::value);
for (auto& [name, ptr] : modules_) {
std::ignore = name;
if (typeid(*ptr) == typeid(T)) {
return dynamic_cast<T*>(ptr);
}
}
throw ftl::exception("module not found");
}
template<typename T, typename ... Args>
T* Screen::addButton(Args ... args) {
static_assert(std::is_base_of<nanogui::Button, T>::value);
T* button = new T(tools_, args ...);
button->setIconExtraScale(1.5f);
button->setTheme(themes_["toolbutton"]);
button->setFixedSize(nanogui::Vector2i(40, 40));
performLayout();
return button;
}
}
}
#include <nanogui/widget.h>
#include "view.hpp"
#include "screen.hpp"
using ftl::gui2::View;
View::View(Screen* screen) : nanogui::Widget(screen), screen_(screen) {
setSize(screen_->viewSize());
}
#pragma once
#include <nanogui/widget.h>
#include "inputoutput.hpp"
namespace ftl {
namespace gui2 {
class Screen;
class View : public nanogui::Widget {
public:
View(Screen* parent);
virtual ~View() {
if(cb_close_) {
cb_close_();
}
}
/** onClose callback; view closed (destroyed) */
void onClose(const std::function<void()> &cb) { cb_close_ = cb; }
virtual void render() {}// TODO remove if VR works?
inline Screen *gui() const { return screen_; }
private:
std::function<void()> cb_close_;
Screen *screen_;
public:
EIGEN_MAKE_ALIGNED_OPERATOR_NEW
};
};
};
#include "addsource.hpp"
#include "../modules/addsource.hpp"
#include "../widgets/combobox.hpp"
#include <nanogui/layout.h>
#include <nanogui/label.h>
#include <nanogui/button.h>
#include <nanogui/vscrollpanel.h>
#include <nanogui/tabwidget.h>
#include <nanogui/formhelper.h>
#include <loguru.hpp>
using ftl::gui2::AddSourceWindow;
AddSourceWindow::AddSourceWindow(nanogui::Widget* parent, AddCtrl *ctrl) :
nanogui::Window(parent, ""), ctrl_(ctrl) {
using namespace nanogui;
auto t = dynamic_cast<ftl::gui2::Screen*>(screen())->getTheme("window_dark");
setTheme(t);
//setFixedWidth(500);
setFixedSize(Vector2i(500,300));
setLayout(new nanogui::BoxLayout
(nanogui::Orientation::Vertical, nanogui::Alignment::Fill, 20, 10));
setPosition(Vector2i(parent->width()/2.0f - fixedWidth()/2.0f, parent->height()/2.0f - fixedHeight()/2.0f));
auto close = new nanogui::Button(buttonPanel(), "", ENTYPO_ICON_CROSS);
close->setTheme(dynamic_cast<ftl::gui2::Screen*>(screen())->getTheme("window_dark"));
close->setBackgroundColor(theme()->mWindowHeaderGradientBot);
close->setCallback([this](){ this->close();});
auto *title = new Label(this, "Add Source", "sans-bold");
title->setFontSize(28);
tabs_ = new TabWidget(this);
auto *recent_tab = tabs_->createTab("Recent");
recent_tab->setLayout(new nanogui::BoxLayout
(nanogui::Orientation::Vertical, nanogui::Alignment::Fill, 0, 0));
VScrollPanel *vscroll = new VScrollPanel(recent_tab);
vscroll->setFixedHeight(200);
Widget *recentscroll = new Widget(vscroll);
recentscroll->setLayout(new BoxLayout(Orientation::Vertical, Alignment::Fill, 10, 4));
auto *group_tab = tabs_->createTab("Groups");
group_tab->setLayout(new nanogui::BoxLayout
(nanogui::Orientation::Vertical, nanogui::Alignment::Fill, 0, 0));
vscroll = new VScrollPanel(group_tab);
vscroll->setFixedHeight(200);
Widget *groupscroll = new Widget(vscroll);
groupscroll->setLayout(new BoxLayout(Orientation::Vertical, Alignment::Fill, 10, 4));
auto *dev_tab = tabs_->createTab("Devices");
dev_tab->setLayout(new nanogui::BoxLayout
(nanogui::Orientation::Vertical, nanogui::Alignment::Fill, 0, 0));
vscroll = new VScrollPanel(dev_tab);
vscroll->setFixedHeight(200);
Widget *devscroll = new Widget(vscroll);
devscroll->setLayout(new BoxLayout(Orientation::Vertical, Alignment::Fill, 10, 4));
auto *host_tab = tabs_->createTab("Hosts");
host_tab->setLayout(new nanogui::BoxLayout
(nanogui::Orientation::Vertical, nanogui::Alignment::Fill, 0, 0));
vscroll = new VScrollPanel(host_tab);
vscroll->setFixedHeight(200);
Widget *hostscroll = new Widget(vscroll);
hostscroll->setLayout(new BoxLayout(Orientation::Vertical, Alignment::Fill, 10, 4));
auto *stream_tab = tabs_->createTab("Streams");
stream_tab->setLayout(new nanogui::BoxLayout
(nanogui::Orientation::Vertical, nanogui::Alignment::Fill, 0, 0));
vscroll = new VScrollPanel(stream_tab);
vscroll->setFixedHeight(200);
Widget *streamscroll = new Widget(vscroll);
streamscroll->setLayout(new BoxLayout(Orientation::Vertical, Alignment::Fill, 10, 4));
auto *file_tab = tabs_->createTab("Files");
file_tab->setLayout(new nanogui::BoxLayout
(nanogui::Orientation::Vertical, nanogui::Alignment::Fill, 0, 0));
vscroll = new VScrollPanel(file_tab);
vscroll->setFixedHeight(200);
Widget *filescroll = new Widget(vscroll);
filescroll->setLayout(new BoxLayout(Orientation::Vertical, Alignment::Fill, 10, 4));
tab_items_.resize(6);
tab_items_[0] = recentscroll;
tab_items_[1] = groupscroll;
tab_items_[2] = devscroll;
tab_items_[3] = hostscroll;
tab_items_[4] = streamscroll;
tab_items_[5] = filescroll;
uptodate_.test_and_set();
rebuild();
tabs_->setActiveTab(0);
new_source_handle_ = ctrl_->feed()->onNewSources([this](const std::vector<std::string> &srcs) {
UNIQUE_LOCK(mutex_, lk);
uptodate_.clear();
return true;
});
}
AddSourceWindow::~AddSourceWindow() {
}
nanogui::Button *AddSourceWindow::_addButton(const std::string &s, nanogui::Widget *parent, bool hide) {
using namespace nanogui;
ftl::URI uri(s);
int icon = 0;
switch (uri.getScheme()) {
case ftl::URI::SCHEME_DEVICE : icon = ENTYPO_ICON_CAMERA; break;
case ftl::URI::SCHEME_FILE : icon = ENTYPO_ICON_FOLDER_VIDEO; break;
case ftl::URI::SCHEME_FTL : icon = ENTYPO_ICON_CLOUD; break;
case ftl::URI::SCHEME_WS :
case ftl::URI::SCHEME_TCP : icon = ENTYPO_ICON_CLASSIC_COMPUTER; break;
case ftl::URI::SCHEME_GROUP : icon = ENTYPO_ICON_MERGE; break;
default: break;
}
auto *button = new Button(parent, ctrl_->getSourceName(s), icon);
if (ctrl_->isSourceActive(s)) {
button->setBackgroundColor(Color(0, 255, 0, 25));
}
button->setIconPosition(Button::IconPosition::Left);
button->setIconExtraScale(1.2);
button->setFontSize(18);
button->setTooltip(s);
button->setCallback([this, uri = s, hide]() {
//if (hide) close();
ctrl_->add(uri);
});
return button;
}
void AddSourceWindow::rebuild() {
using namespace nanogui;
for (auto *w : tab_items_) {
while (w->childCount() > 0) w->removeChild(w->childCount()-1);
}
Button *button;
auto srcs = ctrl_->getRecent();
for (auto &s : srcs) {
_addButton(s.uri, tab_items_[0]);
}
auto groups = ctrl_->getGroups();
for (auto &s : groups) {
_addButton(s, tab_items_[1]);
}
auto devsrcs = ctrl_->getDeviceSources();
for (auto &s : devsrcs) {
_addButton(s, tab_items_[2]);
}
auto *host_menu = new Widget(tab_items_[3]);
host_menu->setLayout(new BoxLayout(nanogui::Orientation::Horizontal, nanogui::Alignment::Maximum, 5,4));
button = new Button(host_menu, "Add", ENTYPO_ICON_PLUS);
button->setFontSize(18);
button->setTooltip("Connect to a new machine");
button->setCallback([this]() {
FormHelper *fh = new FormHelper(screen());
auto *win = fh->addWindow(Vector2i(10,10), "Add Host");
win->center();
win->setTheme(dynamic_cast<ftl::gui2::Screen*>(win->screen())->getTheme("window_dark"));
//win->setWidth(200);
fh->addVariable<std::string>("URI", [this,win](const std::string &v) {
try {
ctrl_->add(v);
} catch (const ftl::exception &e) {
LOG(ERROR) << "Add failed: " << e.what();
}
win->dispose();
}, [this]() {
return "";
})->setFixedWidth(150);
win->screen()->performLayout();
delete fh;
});
button = new Button(host_menu, "Clear", ENTYPO_ICON_CYCLE);
button->setFontSize(18);
button->setTooltip("Clear host history");
button->setCallback([this]() {
ctrl_->feed()->clearHostHistory();
uptodate_.clear();
});
auto hostsrcs = ctrl_->getHosts();
for (auto &s : hostsrcs) {
_addButton(s, tab_items_[3], false);
}
auto streamsrcs = ctrl_->getNetSources();
for (auto &s : streamsrcs) {
_addButton(s, tab_items_[4]);
}
auto *file_menu = new Widget(tab_items_[5]);
file_menu->setLayout(new BoxLayout(nanogui::Orientation::Horizontal, nanogui::Alignment::Maximum, 5,4));
button = new Button(file_menu, "Open", ENTYPO_ICON_PLUS);
button->setFontSize(18);
button->setTooltip("Open FTL File");
button->setCallback([this]() {
try {
std::string filename = file_dialog({ {"ftl", "FTL Captures"} }, false);
if (filename.size() > 0 && filename[0] == '/') {
filename = std::string("file://") + filename;
} else {
filename = std::string("file:///") + filename;
}
#ifdef WIN32
auto p = filename.find_first_of('\\');
while (p != std::string::npos) {
filename[p] = '/';
p = filename.find_first_of('\\');
}
#endif
ctrl_->add(filename);
} catch (const std::exception &e) {
LOG(ERROR) << "File load exception: " << e.what();
}
close();
});
button = new Button(file_menu, "Clear", ENTYPO_ICON_CYCLE);
button->setFontSize(18);
button->setTooltip("Clear file history");
button->setCallback([this]() {
ctrl_->feed()->clearFileHistory();
uptodate_.clear();
});
auto filesrcs = ctrl_->getFileSources();
for (auto &s : filesrcs) {
_addButton(s, tab_items_[5]);
}
}
void AddSourceWindow::close() {
setVisible(false);
//dispose();
ctrl_->disposeWindow();
}
void AddSourceWindow::draw(NVGcontext *ctx) {
{
UNIQUE_LOCK(mutex_, lk);
if (!uptodate_.test_and_set()) {
tabs_->requestFocus(); // Must ensure focus item not deleted
rebuild();
screen()->performLayout();
}
}
nanogui::Window::draw(ctx);
}
#pragma once
#include <nanogui/window.h>
#include <ftl/handle.hpp>
#include <ftl/threads.hpp>
namespace ftl {
namespace gui2 {
class AddCtrl;
/**
* Add source dialog
*/
class AddSourceWindow : public nanogui::Window {
public:
AddSourceWindow(nanogui::Widget *parent, AddCtrl *ctrl);
virtual ~AddSourceWindow();
virtual void draw(NVGcontext *ctx);
private:
AddCtrl *ctrl_;
void close();
void rebuild();
nanogui::Button *_addButton(const std::string &s, nanogui::Widget *parent, bool hide=true);
ftl::Handle new_source_handle_;
MUTEX mutex_;
std::atomic_flag uptodate_;
std::vector<nanogui::Widget*> tab_items_;
nanogui::TabWidget *tabs_;
public:
EIGEN_MAKE_ALIGNED_OPERATOR_NEW
};
}
}
#include "extrinsicview.hpp"
#include "visualization.hpp"
#include "widgets.hpp"
#include "../../screen.hpp"
#include "../../widgets/window.hpp"
#include <nanogui/common.h>
#include <nanogui/window.h>
#include <nanogui/layout.h>
#include <nanogui/button.h>
#include <nanogui/checkbox.h>
#include <nanogui/label.h>
#include <nanogui/formhelper.h>
#include <nanogui/tabwidget.h>
using ftl::gui2::ExtrinsicCalibrationStart;
using ftl::gui2::ExtrinsicCalibrationView;
using ftl::gui2::FixedWindow;
using ftl::data::FrameID;
using ftl::codecs::Channel;
ExtrinsicCalibrationStart::ExtrinsicCalibrationStart(Screen* widget, ExtrinsicCalibration* ctrl) :
ftl::gui2::View(widget), ctrl_(ctrl), fsid_(-1), sources_(0), show_all_(false) {
show_all_ = false;
window_ = new nanogui::Window(screen(), std::string("Extrinsic Calibration"));
window_->setLayout(new nanogui::BoxLayout(nanogui::Orientation::Vertical,
nanogui::Alignment::Fill, 6, 12));
auto* button_refresh = new nanogui::Button(window_->buttonPanel(), "", ENTYPO_ICON_CCW);
button_refresh->setCallback([this](){
update();
updateSources();
screen()->performLayout();
});
lsframesets_ = new nanogui::Widget(window_);
lsframesets_->setLayout(new nanogui::BoxLayout(nanogui::Orientation::Vertical,
nanogui::Alignment::Fill, 0, 8));
lselect_ = new nanogui::Label(window_, "Select Cameras");
lselect_->setVisible(false);
lssources_ = new nanogui::Widget(window_);
lssources_->setLayout(new nanogui::BoxLayout(nanogui::Orientation::Vertical,
nanogui::Alignment::Fill, 0, 8));
cball_ = new nanogui::CheckBox(window_, "Show all sources",
[this](bool v){
show_all_ = v;
updateSources();
screen()->performLayout();
});
cball_->setChecked(show_all_);
cball_->setVisible(false);
bcontinue_ = new nanogui::Button(window_, "Continue");
bcontinue_->setEnabled(false);
bcontinue_->setVisible(false);
bcontinue_->setCallback([this](){
ctrl_->start(fsid_, getSources());
});
window_->setFixedWidth(400);
window_->setVisible(true);
update();
}
ExtrinsicCalibrationStart::~ExtrinsicCalibrationStart() {
window_->setVisible(false);
if (parent()->getRefCount() > 0) {
window_->dispose();
}
}
void ExtrinsicCalibrationStart::draw(NVGcontext* ctx) {
window_->center();
bcontinue_->setEnabled((lssources_->childCount() != 0));
ftl::gui2::View::draw(ctx);
}
void ExtrinsicCalibrationStart::resetSources() {
sources_ = ~uint64_t(0);
}
bool ExtrinsicCalibrationStart::sourceSelected(unsigned int idx) {
return (sources_ & (uint64_t(1) << idx));
}
void ExtrinsicCalibrationStart::addSource(unsigned int idx) {
sources_ |= (uint64_t(1) << idx);
}
void ExtrinsicCalibrationStart::removeSource(unsigned int idx) {
sources_ &= ~(uint64_t(1) << idx);
}
std::vector<FrameID> ExtrinsicCalibrationStart::getSources() {
std::vector<FrameID> sources;
unsigned int nmax = ctrl_->listSources(fsid_, show_all_).size();
CHECK(nmax < 64);
for (unsigned int i = 0; i < nmax; i++) {
if (sourceSelected(i)) {
sources.push_back(FrameID(fsid_, i));
}
}
return sources;
}
void ExtrinsicCalibrationStart::updateSources() {
while (lssources_->childCount() > 0) {
lssources_->removeChild(lssources_->childCount() - 1);
}
if (fsid_ == (unsigned int)(-1)) {
return;
}
for (const auto& [name, id] : ctrl_->listSources(fsid_, show_all_)) {
auto* button = new nanogui::Button(lssources_, name);
button->setFlags(nanogui::Button::Flags::ToggleButton);
button->setChangeCallback([this, button, id = id.source()](bool value){
if (value) { addSource(id); }
else { removeSource(id); }
});
if (sourceSelected(id.source())) {
button->setPushed(true);
}
}
}
void ExtrinsicCalibrationStart::update() {
while (lsframesets_->childCount() > 0) {
lsframesets_->removeChild(lsframesets_->childCount() - 1);
}
for (const auto& [uri, fsid] : ctrl_->listFrameSets()) {
auto* button = new nanogui::Button(lsframesets_, uri, ENTYPO_ICON_IMAGES);
button->setFlags(nanogui::Button::Flags::RadioButton);
if (fsid == fsid_) { button->setPushed(true); }
button->setCallback([button, fsid, this](){
fsid_ = fsid;
lselect_->setVisible(true);
cball_->setVisible(true);
bcontinue_->setVisible(true);
resetSources();
updateSources();
screen()->performLayout();
});
}
}
////////////////////////////////////////////////////////////////////////////////
class ExtrinsicCalibrationView::ControlWindow : public FixedWindow {
public:
ControlWindow(nanogui::Widget* parent, ExtrinsicCalibrationView* view);
virtual void draw(NVGcontext* ctx) override;
private:
ExtrinsicCalibrationView* view_;
ExtrinsicCalibration* ctrl_;
nanogui::Button* bsave_;
nanogui::Button* bupload_;
nanogui::Button* bapply_;
nanogui::Button* bfreeze_;
nanogui::Button* bcalibrate_;
nanogui::Button* bpause_;
nanogui::Button* bresults_;
public:
EIGEN_MAKE_ALIGNED_OPERATOR_NEW
};
ExtrinsicCalibrationView::ControlWindow::ControlWindow(nanogui::Widget* parent, ExtrinsicCalibrationView* view) :
FixedWindow(parent, ""), view_(view), ctrl_(view->ctrl_) {
setLayout(new nanogui::BoxLayout
(nanogui::Orientation::Vertical, nanogui::Alignment::Fill, 6, 6));
auto* buttons = new nanogui::Widget(this);
buttons->setLayout(new nanogui::BoxLayout
(nanogui::Orientation::Horizontal, nanogui::Alignment::Middle, 0, 0));
bsave_ = new nanogui::Button(buttons, "", ENTYPO_ICON_SAVE);
bsave_->setTooltip("Save input to file (Debug)");
bsave_->setCallback([this](){
std::string fname = nanogui::file_dialog({{"bin", "Binary file"}}, true);
ctrl_->saveInput(fname);
});
bsave_ = new nanogui::Button(buttons, "", ENTYPO_ICON_FOLDER);
bsave_->setTooltip("Load input from file (Debug)");
bsave_->setCallback([this](){
std::string fname = nanogui::file_dialog({{"bin", "Binary file"}}, true);
ctrl_->loadInput(fname);
});
bupload_ = new nanogui::Button(buttons, "", ENTYPO_ICON_UPLOAD);
bupload_->setTooltip("Save input to sources");
bupload_->setCallback([this](){
ctrl_->updateCalibration();
bupload_->setTextColor(nanogui::Color(32, 192, 32, 255));
});
bapply_ = new nanogui::Button(buttons, "");
bapply_->setFixedWidth(40);
bapply_->setTooltip("Rectify stereo images");
bapply_->setFlags(nanogui::Button::Flags::ToggleButton);
bapply_->setPushed(view_->rectify());
bapply_->setChangeCallback([button = bapply_, view = view_](bool v){
view->setMode(Mode::VIDEO); // stop capture
view->setRectify(v);
});
bfreeze_ = new nanogui::Button(buttons, "", ENTYPO_ICON_CONTROLLER_PLAY);
bfreeze_->setFixedWidth(40);
bfreeze_->setTooltip("Freeze view");
bfreeze_->setCallback([button=bapply_, view=view_, ctrl=ctrl_](){
ctrl->setCapture(view->paused());
view->pause(!view->paused());
});
bresults_ = new nanogui::Button(buttons, "Show Calibration");
//bresults_->setEnabled(ctrl_->calib().calibrated());
bresults_->setCallback([view = view_, button = bresults_]{
view->setMode(Mode::RESULTS);
});
bpause_ = new nanogui::Button(buttons, "");
bpause_->setFixedWidth(140);
bpause_->setCallback([&ctrl = ctrl_, button = bpause_](){
ctrl->setCapture(!ctrl->capturing());
});
bcalibrate_ = new nanogui::Button(buttons, "Calibrate");
bcalibrate_->setFixedWidth(140);
bcalibrate_->setCallback([view = view_, button = bcalibrate_](){
view->setMode(Mode::CALIBRATION);
});
}
void ExtrinsicCalibrationView::ControlWindow::draw(NVGcontext* ctx) {
if (ctrl_->capturing()) {
bpause_->setCaption("Pause");
view_->setRectify(false);
}
else {
bpause_->setCaption("Continue");
}
bapply_->setIcon(view_->rectify() ? ENTYPO_ICON_EYE : ENTYPO_ICON_EYE_WITH_LINE);
bapply_->setPushed(view_->rectify());
bfreeze_->setIcon(view_->paused() ? ENTYPO_ICON_CONTROLLER_PLAY : ENTYPO_ICON_CONTROLLER_PAUS);
//bcalibrate_->setEnabled(ctrl_->calib().nFrames() > 0);
//bresults_->setEnabled(ctrl_->calib().calibrated());
FixedWindow::draw(ctx);
}
////////////////////////////////////////////////////////////////////////////////
class ExtrinsicCalibrationView::CalibrationWindow : public FixedWindow {
public:
CalibrationWindow(nanogui::Widget* parent, ExtrinsicCalibrationView* view);
virtual void draw(NVGcontext* ctx) override;
private:
void build();
ExtrinsicCalibrationView* view_;
ExtrinsicCalibration* ctrl_;
nanogui::Widget* cameras_;
nanogui::Label* status_;
nanogui::Button* brun_;
bool running_; // run button clicked
int flags_;
public:
EIGEN_MAKE_ALIGNED_OPERATOR_NEW
};
ExtrinsicCalibrationView::CalibrationWindow::CalibrationWindow(nanogui::Widget* parent, ExtrinsicCalibrationView* view) :
FixedWindow(parent, "Settings"), view_(view), ctrl_(view->ctrl_) {
running_ = false;
(new nanogui::Button(buttonPanel(), "", ENTYPO_ICON_CROSS))->setCallback(
[view = view_]() {
view->setMode(Mode::VIDEO);
});
setLayout(new nanogui::BoxLayout
(nanogui::Orientation::Vertical, nanogui::Alignment::Fill, 10 , 10));
build();
}
void ExtrinsicCalibrationView::CalibrationWindow::build() {
flags_ = ctrl_->flags();
auto* wfreeze = new nanogui::Widget(this);
wfreeze->setLayout(new nanogui::BoxLayout
(nanogui::Orientation::Vertical, nanogui::Alignment::Fill, 0 , 5));
auto* floss = new nanogui::CheckBox(wfreeze, "Cauchy loss");
floss->setChecked(flags_ & ExtrinsicCalibration::Flags::LOSS_CAUCHY);
floss->setCallback([&flags = flags_](bool v) {
if (v) { flags |= ExtrinsicCalibration::Flags::LOSS_CAUCHY; }
else { flags &= ~ExtrinsicCalibration::Flags::LOSS_CAUCHY; }
});
auto* nstep = new nanogui::CheckBox(wfreeze, "Non-monotonic step");
nstep->setChecked(flags_ & ExtrinsicCalibration::Flags::NONMONOTONIC_STEP);
nstep->setCallback([&flags = flags_](bool v) {
if (v) { flags |= ExtrinsicCalibration::Flags::NONMONOTONIC_STEP; }
else { flags &= ~ExtrinsicCalibration::Flags::NONMONOTONIC_STEP; }
});
auto* fall = new nanogui::CheckBox(wfreeze, "Freeze all intrinsic paramters");
fall->setChecked(flags_ & ExtrinsicCalibration::Flags::FIX_INTRINSIC);
fall->setCallback([&flags = flags_, wfreeze](bool v) {
for (int i = 3; i < wfreeze->childCount(); i++) {
wfreeze->childAt(i)->setEnabled(!v);
}
if (v) { flags |= ExtrinsicCalibration::Flags::FIX_INTRINSIC; }
else { flags &= ~ExtrinsicCalibration::Flags::FIX_INTRINSIC; }
});
auto* ff = new nanogui::CheckBox(wfreeze, "Fix focal length");
ff->setChecked(flags_ & ExtrinsicCalibration::Flags::FIX_FOCAL);
ff->setCallback([&flags = flags_](bool v) {
if (v) { flags |= ExtrinsicCalibration::Flags::FIX_FOCAL; }
else { flags &= ~ExtrinsicCalibration::Flags::FIX_FOCAL; }
});
auto* fpp = new nanogui::CheckBox(wfreeze, "Fix principal point");
fpp->setChecked(flags_ & ExtrinsicCalibration::Flags::FIX_PRINCIPAL_POINT);
fpp->setCallback([&flags = flags_](bool v) {
if (v) { flags |= ExtrinsicCalibration::Flags::FIX_PRINCIPAL_POINT; }
else { flags &= ~ExtrinsicCalibration::Flags::FIX_PRINCIPAL_POINT; }
});
auto* fdist = new nanogui::CheckBox(wfreeze, "Fix distortion coefficients");
fdist->setChecked(flags_ & ExtrinsicCalibration::Flags::FIX_DISTORTION);
fdist->setCallback([&flags = flags_](bool v) {
if (v) { flags |= ExtrinsicCalibration::Flags::FIX_DISTORTION; }
else { flags &= ~ExtrinsicCalibration::Flags::FIX_DISTORTION; }
});
auto* zdist = new nanogui::CheckBox(wfreeze, "Assume zero distortion");
zdist->setChecked(flags_ & ExtrinsicCalibration::Flags::ZERO_DISTORTION);
zdist->setCallback([&flags = flags_](bool v) {
if (v) { flags |= ExtrinsicCalibration::Flags::ZERO_DISTORTION; }
else { flags &= ~ExtrinsicCalibration::Flags::ZERO_DISTORTION; }
});
auto* rdist = new nanogui::CheckBox(wfreeze, "Rational distortion model");
rdist->setChecked(flags_ & ExtrinsicCalibration::Flags::RATIONAL_MODEL);
rdist->setCallback([&flags = flags_](bool v) {
if (v) { flags |= ExtrinsicCalibration::Flags::RATIONAL_MODEL; }
else { flags &= ~ExtrinsicCalibration::Flags::RATIONAL_MODEL; }
});
////////////////////////////////////////////////////////////////////////////
new nanogui::Label(wfreeze, "Use available (calibrated) extrinsics for cameras: ");
auto* use_extrinsics = new nanogui::Widget(wfreeze);
use_extrinsics->setLayout(new nanogui::BoxLayout
(nanogui::Orientation::Horizontal, nanogui::Alignment::Minimum));
for (int n = 0; n < ctrl_->cameraCount(); n++) {
auto* b = new nanogui::Button(use_extrinsics, std::to_string(n));
b->setFlags(nanogui::Button::Flags::ToggleButton);
b->setPushed(ctrl_->calib().useExtrinsic(n));
b->setEnabled(ctrl_->calib().calibration(n).extrinsic.valid());
b->setChangeCallback([this, n](bool v) {
ctrl_->calib().setUseExtrinsic(n, v);
});
}
{
auto* b = new nanogui::Button(use_extrinsics, "All");
b->setCallback([this, use_extrinsics](){
for (int i = 0; i < use_extrinsics->childCount() - 2; i ++) {
auto* b = dynamic_cast<nanogui::Button*>(use_extrinsics->childAt(i));
b->setPushed(true);
b->changeCallback()(true);
}
});
}
{
auto* b = new nanogui::Button(use_extrinsics, "None");
b->setCallback([this, use_extrinsics](){
for (int i = 0; i < use_extrinsics->childCount() - 2; i ++) {
auto* b = dynamic_cast<nanogui::Button*>(use_extrinsics->childAt(i));
b->setPushed(false);
b->changeCallback()(false);
}
});
}
////////////////////////////////////////////////////////////////////////////
// TODO: selecting camera should also enable use existing above for same c
new nanogui::Label(wfreeze, "Fix extrinsics for cameras: ");
auto* fix_extrinsics = new nanogui::Widget(wfreeze);
fix_extrinsics->setLayout(new nanogui::BoxLayout
(nanogui::Orientation::Horizontal, nanogui::Alignment::Minimum));
for (int n = 0; n < ctrl_->cameraCount(); n++) {
auto* b = new nanogui::Button(fix_extrinsics, std::to_string(n));
b->setFlags(nanogui::Button::Flags::ToggleButton);
b->setEnabled(ctrl_->calib().useExtrinsic(n));
b->setPushed(ctrl_->calib().options().fix_camera_extrinsic.count(n));
b->setChangeCallback([this, n](bool v){
if (v) {
ctrl_->calib().options().fix_camera_extrinsic.insert(n);
}
else {
ctrl_->calib().options().fix_camera_extrinsic.erase(n);
}
});
}
{
auto* b = new nanogui::Button(fix_extrinsics, "All");
b->setCallback([this, fix_extrinsics](){
for (int i = 0; i < fix_extrinsics->childCount() - 2; i ++) {
auto* b = dynamic_cast<nanogui::Button*>(fix_extrinsics->childAt(i));
b->setPushed(true);
b->changeCallback()(true);
}
});
}
{
auto* b = new nanogui::Button(fix_extrinsics, "None");
b->setCallback([this, fix_extrinsics](){
for (int i = 0; i < fix_extrinsics->childCount() - 2; i ++) {
auto* b = dynamic_cast<nanogui::Button*>(fix_extrinsics->childAt(i));
b->setPushed(false);
b->changeCallback()(false);
}
});
}
/* Needs thinking: visualize visibility graph? Use earlier alignment (if
* some of the cameras already calibrated), do elsewhere?
*/
status_ = new nanogui::Label(this, "Ready");
brun_ = new nanogui::Button(this, "Run");
brun_->setCallback([this](){
ctrl_->setFlags(flags_);
ctrl_->run();
running_ = true;
});
}
void ExtrinsicCalibrationView::CalibrationWindow::draw(NVGcontext* ctx) {
brun_->setEnabled(!ctrl_->isBusy());
if (ctrl_->isBusy()) {
if (running_) {
auto dots = std::string(int(round(glfwGetTime())) % 4, '.');
status_->setCaption(ctrl_->status() + dots);
}
else {
status_->setCaption("Busy");
}
}
else {
status_->setCaption("Ready");
}
if (running_ && !ctrl_->isBusy()) {
running_ = false;
view_->setMode(Mode::RESULTS);
}
FixedWindow::draw(ctx);
}
////////////////////////////////////////////////////////////////////////////////
class ExtrinsicCalibrationView::ResultsWindow : public FixedWindow {
public:
ResultsWindow(nanogui::Widget* parent, ExtrinsicCalibrationView* view);
virtual void draw(NVGcontext* ctx) override;
virtual void performLayout(NVGcontext* ctx);
//virtual nanogui::Vector2i preferredSize(NVGcontext* ctx) const override;
void update();
private:
ExtrinsicCalibrationView* view_;
ExtrinsicCalibration* ctrl_;
std::vector<ftl::calibration::CalibrationData::Calibration> calib_;
std::vector<std::string> names_;
nanogui::TabWidget* tabs_ = nullptr;
public:
EIGEN_MAKE_ALIGNED_OPERATOR_NEW
};
ExtrinsicCalibrationView::ResultsWindow::ResultsWindow(nanogui::Widget* parent, ExtrinsicCalibrationView* view) :
FixedWindow(parent, "Results"), view_(view), ctrl_(view->ctrl_) {
setLayout(new nanogui::BoxLayout
(nanogui::Orientation::Vertical, nanogui::Alignment::Maximum));
(new nanogui::Button(buttonPanel(), "", ENTYPO_ICON_CROSS))->setCallback(
[view = view_]() {
view->setMode(Mode::VIDEO);
});
tabs_ = new nanogui::TabWidget(this);
tabs_->createTab("Extrinsic");
}
/*nanogui::Vector2i ExtrinsicCalibrationView::ResultsWindow::preferredSize(NVGcontext* ctx) const {
return {600, 400};
}*/
void ExtrinsicCalibrationView::ResultsWindow::ResultsWindow::performLayout(NVGcontext* ctx) {
setFixedSize({600, 400});
tabs_->setFixedWidth(width());
FixedWindow::performLayout(ctx);
}
void ExtrinsicCalibrationView::ResultsWindow::ResultsWindow::update() {
calib_.resize(ctrl_->cameraCount());
while (tabs_->tabCount() > 1) {
// bug in nanogui: incorrect assert in removeTab(int).
// workaround: use tabLabelAt()
tabs_->removeTab(tabs_->tabLabelAt(tabs_->tabCount() - 1));
}
for (int i = 0; i < ctrl_->cameraCount(); i++) {
calib_[i] = ctrl_->calibration(i);
// nanogui issue: too many tabs/long names header goes outside of widget
// use just idx for now
auto* tab = tabs_->createTab(std::to_string(i));
new nanogui::Label(tab, ctrl_->cameraName(i), "sans-bold", 18);
tab->setLayout(new nanogui::BoxLayout
(nanogui::Orientation::Vertical, nanogui::Alignment::Middle, 0, 8));
auto* display = new IntrinsicDetails(tab);
display->update(calib_[i].intrinsic);
}
}
void ExtrinsicCalibrationView::ResultsWindow::draw(NVGcontext* ctx) {
FixedWindow::draw(ctx);
if (tabs_->activeTab() == 0) { // create a widget and move there
drawFloorPlan(ctx, tabs_->tab(0), calib_);
}
}
////////////////////////////////////////////////////////////////////////////////
static void drawText(NVGcontext* ctx, const nanogui::Vector2f &pos, const std::string& text,
float size=12.0f, int align=NVG_ALIGN_MIDDLE|NVG_ALIGN_CENTER) {
nvgFontSize(ctx, size);
nvgFontFace(ctx, "sans-bold");
nvgTextAlign(ctx, align);
nvgFillColor(ctx, nanogui::Color(8, 8, 8, 255)); // shadow
nvgText(ctx, pos.x(), pos.y(), text.c_str(), nullptr);
nvgFillColor(ctx, nanogui::Color(244, 244, 244, 255));
nvgText(ctx, pos.x() + 1, pos.y() + 1, text.c_str(), nullptr);
}
////////////////////////////////////////////////////////////////////////////////
class StereoCalibrationImageView : public ftl::gui2::StereoImageView {
public:
using ftl::gui2::StereoImageView::StereoImageView;
virtual bool keyboardCharacterEvent(unsigned int codepoint) override;
virtual bool mouseButtonEvent(const nanogui::Vector2i &p, int button, bool down, int modifiers) override;
virtual void draw(NVGcontext* ctx) override;
void reset();
private:
std::set<int> rows_;
std::map<int, nanogui::Color> colors_;
int n_colors_ = 8;
float alpha_threshold_ = 2.0f;
public:
EIGEN_MAKE_ALIGNED_OPERATOR_NEW;
};
void StereoCalibrationImageView::reset() {
rows_.clear();
}
bool StereoCalibrationImageView::keyboardCharacterEvent(unsigned int codepoint) {
if (codepoint == 'r') {
reset();
return true;
}
return StereoImageView::keyboardCharacterEvent(codepoint);
}
bool StereoCalibrationImageView::mouseButtonEvent(const nanogui::Vector2i &p, int button, bool down, int modifiers) {
nanogui::Widget::mouseButtonEvent(p, button, down, modifiers);
if (button == GLFW_MOUSE_BUTTON_1 && !down) {
// half a pixel offset to match "square pixel" visualization
nanogui::Vector2f offset{left()->scale()/2.0f, left()->scale()/2.0f};
float row = round(imageCoordinateAt(p.cast<float>() + offset).y());
if (rows_.count(row)) { rows_.erase(row); }
else { rows_.insert(row); }
}
return true;
}
void StereoCalibrationImageView::draw(NVGcontext* ctx) {
StereoImageView::draw(ctx);
// assumes vertical alignment (horizontal not implemented)
CHECK(orientation() == nanogui::Orientation::Vertical);
int x = position().x();
int y = position().y();
int w = width();
int h = left()->height();
float swidth = std::max(1.0f, left()->scale());
int c = 0; // color
for (int row : rows_) {
int y_im = y;
nanogui::Vector2f l = left()->positionForCoordinate({0.0f, row}) + left()->position().cast<float>();
nanogui::Vector2f r = right()->positionForCoordinate({0.0f, row}) + right()->position().cast<float>();
auto color = nvgHSLA(float(c%n_colors_)/float(n_colors_), 0.9, 0.5, (swidth < alpha_threshold_) ? 255 : 96);
for (auto& p : {l, r}) {
nvgScissor(ctx, x, y_im, w, h);
nvgBeginPath(ctx);
nvgMoveTo(ctx, x, p.y() - swidth*0.5f);
nvgLineTo(ctx, x + w, p.y() - swidth*0.5f);
nvgStrokeColor(ctx, color);
nvgStrokeWidth(ctx, swidth);
nvgStroke(ctx);
/*if (swidth*0.5f > alpha_threshold_) {
nvgBeginPath(ctx);
nvgMoveTo(ctx, x, p.y() - swidth*0.5f);
nvgLineTo(ctx, x + w, p.y() - swidth*0.5f);
nvgStrokeColor(ctx, nvgRGBA(0, 0, 0, 196));
nvgStrokeWidth(ctx, 1.0f);
nvgStroke(ctx);
}*/
nvgResetScissor(ctx);
y_im += h;
}
c++;
}
}
////////////////////////////////////////////////////////////////////////////////
ExtrinsicCalibrationView::ExtrinsicCalibrationView(Screen* widget, ExtrinsicCalibration* ctrl) :
ftl::gui2::View(widget), ctrl_(ctrl), rows_(0) {
frames_ = new nanogui::Widget(this);
draw_number_ = false;
rectify_ = false;
frames_->setLayout(new nanogui::BoxLayout
(nanogui::Orientation::Horizontal, nanogui::Alignment::Maximum, 0, 0));
// assumes all cameras stereo cameras, indexed in order
for (int i = 0; i < ctrl_->cameraCount(); i += 2) {
new StereoCalibrationImageView(frames_, nanogui::Orientation::Vertical);
}
paused_ = false;
wcontrol_ = new ControlWindow(screen(), this);
wcalibration_ = new CalibrationWindow(screen(), this);
wresults_ = new ResultsWindow(screen(), this);
setMode(Mode::CAPTURE_IMAGES);
}
void ExtrinsicCalibrationView::performLayout(NVGcontext* ctx) {
auto sz = wcontrol_->size();
wcontrol_->setPosition(
nanogui::Vector2i(width() / 2 - sz[0]/2, height() - 30 - sz[1]));
wcalibration_->center();
wresults_->center();
frames_->setSize(size());
nanogui::Vector2i fsize = { width()/(frames_->childCount()), height() };
for (int i = 0; i < frames_->childCount(); i++) {
auto* stereo = dynamic_cast<StereoCalibrationImageView*>(frames_->childAt(i));
stereo->setFixedSize(fsize);
stereo->fit();
}
View::performLayout(ctx);
}
void ExtrinsicCalibrationView::draw(NVGcontext* ctx) {
if (ctrl_->next() && !paused_) {
for (int i = 0; i < ctrl_->cameraCount(); i += 2) {
auto* imview = dynamic_cast<StereoImageView*>(frames_->childAt(i/2));
int l = i;
int r = i + 1;
if (ctrl_->hasFrame(l)) {
if (!rectify_) { imview->left()->copyFrom(ctrl_->getFrame(l)); }
else { imview->left()->copyFrom(ctrl_->getFrameRectified(l)); }
imview->left()->setVisible(true);
}
else { imview->left()->setVisible(false); }
if (ctrl_->hasFrame(r)) {
if (!rectify_) { imview->right()->copyFrom(ctrl_->getFrame(r)); }
else { imview->right()->copyFrom(ctrl_->getFrameRectified(r)); }
imview->right()->setVisible(true);
}
else { imview->right()->setVisible(false); }
}
}
Widget::draw(ctx);
// draw corner labels
for (int i = 0; i < ctrl_->cameraCount(); i++) {
FTLImageView* imview;
if (i%2 == 0) {
imview = dynamic_cast<StereoImageView*>(frames_->childAt(i/2))->left();
}
else {
imview = dynamic_cast<StereoImageView*>(frames_->childAt(i/2))->right();
}
auto points = ctrl_->previousPoints(i);
std::vector<Eigen::Vector2f, Eigen::aligned_allocator<Eigen::Vector2f>>
paths;
nanogui::Vector2f wpos = imview->absolutePosition().cast<float>();
nanogui::Vector2f wsize = imview->sizeF();
for (unsigned int p = 0; p < points.size(); p++) {
auto pos = imview->positionForCoordinate({points[p].x, points[p].y});
nanogui::Vector2f apos = pos + wpos;
paths.push_back(apos);
}
nvgScissor(ctx, wpos.x(), wpos.y(), wsize.x(), wsize.y());
// draw border
for (unsigned int p = 0; p < paths.size(); p += 4) {
nvgBeginPath(ctx);
nvgMoveTo(ctx, paths[p + 0].x(), paths[p + 0].y());
nvgLineTo(ctx, paths[p + 1].x(), paths[p + 1].y());
nvgLineTo(ctx, paths[p + 2].x(), paths[p + 2].y());
nvgLineTo(ctx, paths[p + 3].x(), paths[p + 3].y());
nvgLineTo(ctx, paths[p + 0].x(), paths[p + 0].y());
if (p == 0) nvgStrokeColor(ctx, nvgRGBA(255, 32, 32, 255));
if (p == 4) nvgStrokeColor(ctx, nvgRGBA(32, 255, 32, 255));
nvgStrokeWidth(ctx, 1.5f);
nvgStroke(ctx);
}
// draw number
/*if (draw_number_ ) {
for (unsigned int p = 0; p < paths.size(); p += 1) {
auto str = std::to_string(p);
drawText(ctx, paths[p], std::to_string(p), 14.0f);
}
}*/
// TODO: move to stereocalibrateimageview
nanogui::Vector2f tpos = wpos + nanogui::Vector2f{10.0f, 10.0f};
drawText(ctx, tpos, std::to_string(ctrl_->getFrameCount(i)), 20.0f, NVG_ALIGN_TOP|NVG_ALIGN_LEFT);
tpos = wpos + nanogui::Vector2f{10.0f, wsize.y() - 30.0f};
drawText(ctx, tpos, ctrl_->cameraName(i), 20.0f, NVG_ALIGN_TOP|NVG_ALIGN_LEFT);
nvgResetScissor(ctx);
}
{
float h = 14.0f;
for (const auto& text : {"Left click: draw line",
"Right click: pan",
"Scroll: zoom",
"C center",
"F fit",
"R clear lines"
}) {
drawText(ctx, {float(width()) - 60.0, h}, text, 14, NVGalign::NVG_ALIGN_BOTTOM | NVG_ALIGN_MIDDLE);
h += 20.0;
}
}
}
ExtrinsicCalibrationView::~ExtrinsicCalibrationView() {
wcontrol_->dispose();
wcalibration_->dispose();
wresults_->dispose();
}
void ExtrinsicCalibrationView::setMode(Mode mode) {
switch(mode) {
case Mode::CAPTURE_IMAGES:
ctrl_->setCapture(true);
wcontrol_->setVisible(true);
wcalibration_->setVisible(false);
wresults_->setVisible(false);
break;
case Mode::VIDEO:
ctrl_->setCapture(false);
wcontrol_->setVisible(true);
wcalibration_->setVisible(false);
wresults_->setVisible(false);
break;
case Mode::CALIBRATION:
ctrl_->setCapture(false);
wcontrol_->setVisible(false);
wcalibration_->setVisible(true);
wresults_->setVisible(false);
break;
case Mode::RESULTS:
ctrl_->setCapture(false);
wcontrol_->setVisible(false);
wcalibration_->setVisible(false);
wresults_->setVisible(true);
wresults_->update();
break;
}
screen()->performLayout();
}
#pragma once
#include <unordered_set>
#include "../../modules/calibration/calibration.hpp"
#include "../../view.hpp"
#include <ftl/utility/gltexture.hpp>
#include "../../widgets/imageview.hpp"
namespace ftl
{
namespace gui2
{
class ExtrinsicCalibrationStart : public View {
public:
ExtrinsicCalibrationStart(Screen* widget, ExtrinsicCalibration* ctrl);
virtual ~ExtrinsicCalibrationStart();
virtual void draw(NVGcontext *ctx) override;
/** query about current state */
void addSource(unsigned int);
void removeSource(unsigned int);
void resetSources();
bool sourceSelected(unsigned int source);
std::vector<ftl::data::FrameID> getSources();
/** update widgets */
void update();
void updateSources();
private:
ExtrinsicCalibration* ctrl_;
nanogui::Window* window_;
nanogui::Label* lselect_;
nanogui::CheckBox* cball_;
nanogui::Widget* lsframesets_;
nanogui::Widget* lssources_;
nanogui::Button* bcontinue_;
unsigned int fsid_;
uint64_t sources_;
bool show_all_;
public:
EIGEN_MAKE_ALIGNED_OPERATOR_NEW
};
class ExtrinsicCalibrationView : public View {
public:
class ControlWindow;
class CalibrationWindow;
class ResultsWindow;
enum Mode {
CAPTURE_IMAGES, // capture images
CALIBRATION, // calibration options
RESULTS, // calibration results
VIDEO // same as capture images but paused
};
ExtrinsicCalibrationView(Screen* widget, ExtrinsicCalibration* ctrl);
virtual ~ExtrinsicCalibrationView();
virtual void draw(NVGcontext *ctx) override;
virtual void performLayout(NVGcontext *ctx) override;
bool rectify() { return rectify_; };
void setRectify(bool v) { rectify_ = v; };
void setMode(Mode m);
bool paused() { return paused_; }
void pause(bool v) { paused_ = v; }
protected:
int rows(); // calculate optimum number of rows;
void setRows(int rows);
private:
ExtrinsicCalibration* ctrl_;
nanogui::Widget* frames_;
ControlWindow* wcontrol_;
CalibrationWindow* wcalibration_;
ResultsWindow* wresults_;
int rows_;
bool draw_number_;
bool rectify_;
bool paused_;
public:
EIGEN_MAKE_ALIGNED_OPERATOR_NEW
};
}
}
#include <sstream>
#include "visualization.hpp"
#include "widgets.hpp"
#include "intrinsicview.hpp"
#include "../../screen.hpp"
#include "../../widgets/window.hpp"
#include <opencv2/calib3d.hpp>
#include <nanogui/messagedialog.h>
#include <nanogui/window.h>
#include <nanogui/layout.h>
#include <nanogui/button.h>
#include <nanogui/checkbox.h>
#include <nanogui/textbox.h>
#include <nanogui/label.h>
using ftl::codecs::Channel;
using ftl::gui2::Screen;
using ftl::gui2::View;
using ftl::gui2::FixedWindow;
using ftl::gui2::IntrinsicCalibrationStart;
using ftl::gui2::IntrinsicCalibration;
using ftl::gui2::IntrinsicCalibrationView;
using Mode = ftl::gui2::IntrinsicCalibrationView::Mode;
////////////////////////////////////////////////////////////////////////////////
template<typename T>
std::string to_string(T v, int precision = 2) {
std::stringstream stream;
stream << std::fixed << std::setprecision(precision) << v;
return stream.str();
}
////////////////////////////////////////////////////////////////////////////////
class IntrinsicCalibrationView::CaptureWindow : public FixedWindow {
public:
CaptureWindow(nanogui::Widget* parent, IntrinsicCalibrationView* view);
virtual void draw(NVGcontext* ctx) override;
private:
void update();
IntrinsicCalibrationView* view_;
IntrinsicCalibration* ctrl_;
nanogui::Widget* channels_;
int width_;
int height_;
double square_size_;
public:
EIGEN_MAKE_ALIGNED_OPERATOR_NEW
};
class IntrinsicCalibrationView::ControlWindow : public FixedWindow {
public:
ControlWindow(nanogui::Widget* parent, IntrinsicCalibrationView* view);
virtual void draw(NVGcontext* ctx) override;
private:
void updateCount();
IntrinsicCalibrationView* view_;
IntrinsicCalibration* ctrl_;
nanogui::Label* txtnframes_;
nanogui::Button* bcalibrate_;
nanogui::Button* bsave_;
nanogui::Button* bapply_;
nanogui::Button* bresults_;
nanogui::Button* bpause_;
public:
EIGEN_MAKE_ALIGNED_OPERATOR_NEW
};
class IntrinsicCalibrationView::CalibrationWindow : public FixedWindow {
public:
CalibrationWindow(nanogui::Widget* parent, IntrinsicCalibrationView* view);
void update();
virtual void draw(NVGcontext* ctx) override;
private:
IntrinsicCalibrationView* view_;
IntrinsicCalibration* ctrl_;
nanogui::Label* status_;
nanogui::Button* bcalibrate_;
nanogui::FloatBox<double>* sensor_width_;
nanogui::FloatBox<double>* sensor_height_;
nanogui::FloatBox<double>* focal_length_;
nanogui::CheckBox* reset_dist_;
nanogui::CheckBox* reset_pp_;
bool calibrating_;
public:
EIGEN_MAKE_ALIGNED_OPERATOR_NEW
};
class IntrinsicCalibrationView::ResultWindow : public FixedWindow {
public:
ResultWindow(nanogui::Widget* parent, IntrinsicCalibrationView* view);
virtual void draw(NVGcontext* ctx) override;
void update();
private:
IntrinsicCalibrationView* view_;
IntrinsicCalibration* ctrl_;
nanogui::Button* bsave_;
nanogui::Label* rms_;
ftl::gui2::IntrinsicDetails* info_;
public:
EIGEN_MAKE_ALIGNED_OPERATOR_NEW
};
////////////////////////////////////////////////////////////////////////////////
//
IntrinsicCalibrationStart::IntrinsicCalibrationStart(ftl::gui2::Screen *parent, IntrinsicCalibration *ctrl) :
ftl::gui2::View(parent), ctrl_(ctrl) {
show_all_ = false;
window_ = new FixedWindow(parent, std::string("Intrinsic Calibration"));
window_->setLayout(new nanogui::BoxLayout(nanogui::Orientation::Vertical,
nanogui::Alignment::Fill, 6, 12));
auto* button_refresh = new nanogui::Button(window_->buttonPanel(), "", ENTYPO_ICON_CCW);
button_refresh->setCallback([this](){ update(); });
buttons_ = new nanogui::Widget(window_);
buttons_->setLayout(new nanogui::BoxLayout(nanogui::Orientation::Vertical,
nanogui::Alignment::Fill, 0, 8));
auto bshow_all = new nanogui::CheckBox(window_, "Show all sources",
[this](bool v){
show_all_ = v;
update();
});
bshow_all->setChecked(show_all_);
window_->setFixedWidth(400);
window_->setVisible(true);
update();
}
IntrinsicCalibrationStart::~IntrinsicCalibrationStart() {
window_->setVisible(false);
if (parent()->getRefCount() > 0) {
window_->dispose();
}
}
void IntrinsicCalibrationStart::update() {
while (buttons_->childCount() > 0) {
buttons_->removeChild(buttons_->childCount() - 1);
}
for (const auto& [name, id] : ctrl_->listSources(show_all_)) {
auto* button = new nanogui::Button(buttons_, name, ENTYPO_ICON_CAMERA);
button->setCallback([ctrl = this->ctrl_, id](){
ctrl->start(id);
});
}
screen()->performLayout();
}
void IntrinsicCalibrationStart::draw(NVGcontext* ctx) {
window_->center();
View::draw(ctx);
}
////////////////////////////////////////////////////////////////////////////////
// Capture Window
void IntrinsicCalibrationView::CaptureWindow::update() {
ctrl_->setChessboard({width_, height_}, square_size_);
}
IntrinsicCalibrationView::CaptureWindow::CaptureWindow(nanogui::Widget* parent, IntrinsicCalibrationView* view) :
FixedWindow(parent, "Capture Options"), view_(view), ctrl_(view->ctrl_) {
width_ = ctrl_->chessboardSize().width;
height_ = ctrl_->chessboardSize().height;
square_size_ = ctrl_->squareSize();
setLayout(new nanogui::BoxLayout
(nanogui::Orientation::Vertical, nanogui::Alignment::Fill, 6, 6));
(new nanogui::Button(buttonPanel(), "", ENTYPO_ICON_CROSS))->setCallback(
[view = view_]() {
view->setMode(Mode::VIDEO);
});
// Capture parameters
new nanogui::Label(this, "Select Camera");
channels_ = new nanogui::Widget(this);
channels_->setLayout(new nanogui::GridLayout
(nanogui::Orientation::Horizontal, 2, nanogui::Alignment::Fill, 0, 0));
auto* button_left = new nanogui::Button(channels_, "Left");
button_left->setPushed(ctrl_->channel() == Channel::Left);
button_left->setFlags(nanogui::Button::RadioButton);
button_left->setCallback([ctrl = ctrl_, view=view_](){
if (ctrl->channel() != Channel::Left) {
ctrl->setChannel(Channel::Left);
view->setUndistort(false);
}
});
auto* button_right = new nanogui::Button(channels_, "Right");
button_right->setFlags(nanogui::Button::RadioButton);
button_right->setPushed(ctrl_->channel() == Channel::Right);
button_right->setCallback([ctrl = ctrl_, view=view_](){
if (ctrl->channel() != Channel::Right) {
ctrl->setChannel(Channel::Right);
view->setUndistort(false);
}
});
button_right->setEnabled(ctrl_->hasChannel(Channel::Right));
new nanogui::Label(this, "Capture interval");
auto* interval = new nanogui::FloatBox<float>(this, ctrl_->frequency());
interval->setEditable(true);
interval->setFormat("[0-9]*\\.?[0-9]+");
interval->setUnits("s");
interval->setCallback([ctrl = this->ctrl_](float v){
ctrl->setFrequency(v);
});
// Chessboard parameters
auto* chessboard = new nanogui::Widget(this);
chessboard->setLayout(new nanogui::BoxLayout
(nanogui::Orientation::Vertical, nanogui::Alignment::Fill, 0, 4));
// width
new nanogui::Label(chessboard, "Chessboard width");
auto* chessboard_size_x = new nanogui::IntBox<int>(chessboard, width_);
chessboard_size_x->setEditable(true);
chessboard_size_x->setFormat("[1-9][0-9]*");
chessboard_size_x->setCallback([this](int v){
width_ = max(0, v);
});
// height
new nanogui::Label(chessboard, "Chessboard height");
auto* chessboard_size_y = new nanogui::IntBox<int>(chessboard, height_);
chessboard_size_y->setEditable(true);
chessboard_size_y->setFormat("[1-9][0-9]*");
chessboard_size_y->setCallback([this](int v){
height_ = max(0, v);
});
// square size
new nanogui::Label(chessboard, "Chessboard square size");
auto* square_size = new nanogui::FloatBox<float>(chessboard, square_size_*1000.0);
square_size->setEditable(true);
square_size->setFormat("[0-9]*\\.?[0-9]+");
square_size->setUnits("mm");
square_size->setCallback([this](float v){
square_size_ = v/1000.0;
});
auto* button_start = new nanogui::Button(this, "Start");
button_start->setCallback([this]() {
update();
view_->setMode(Mode::CAPTURE_IMAGES);
});
}
void IntrinsicCalibrationView::CaptureWindow::draw(NVGcontext* ctx) {
channels_->childAt(1)->setEnabled(ctrl_->hasChannel(Channel::Right));
FixedWindow::draw(ctx);
}
////////////////////////////////////////////////////////////////////////////////
// Control Window
IntrinsicCalibrationView::ControlWindow::ControlWindow(nanogui::Widget* parent, IntrinsicCalibrationView* view) :
FixedWindow(parent, ""), view_(view), ctrl_(view->ctrl_) {
setLayout(new nanogui::BoxLayout
(nanogui::Orientation::Vertical, nanogui::Alignment::Fill, 6, 6));
txtnframes_ = new nanogui::Label(this, "");
updateCount();
auto* buttons = new nanogui::Widget(this);
buttons->setLayout(new nanogui::BoxLayout
(nanogui::Orientation::Horizontal, nanogui::Alignment::Middle, 0, 0));
auto* bback_ = new nanogui::Button(buttons, "", ENTYPO_ICON_ARROW_LEFT);
bback_->setFixedWidth(40);
bback_->setTooltip("Back to capture options");
bback_->setCallback([this, button = bback_](){
view_->setMode(Mode::CAPTURE_INIT);
});
bsave_ = new nanogui::Button(buttons, "", ENTYPO_ICON_SAVE);
bsave_->setFixedWidth(40);
bsave_->setTooltip("Save calibration");
bsave_->setEnabled(ctrl_->calibrated());
bsave_->setCallback([ctrl = ctrl_, view = view_](){
ctrl->save();
new nanogui::MessageDialog
(view->screen(), nanogui::MessageDialog::Type::Information, "Calibration", "Calibration sent");
});
bapply_ = new nanogui::Button(buttons, "");
bapply_->setFixedWidth(40);
bapply_->setTooltip("Apply distortion correction");
bapply_->setEnabled(ctrl_->calibrated());
bapply_->setFlags(nanogui::Button::Flags::ToggleButton);
bapply_->setPushed(view_->undistort());
bapply_->setChangeCallback([button = bapply_, view = view_](bool v){
view->setUndistort(v);
});
bresults_ = new nanogui::Button(buttons, "Details");
bresults_->setFixedWidth(120);
bresults_->setEnabled(ctrl_->calibrated());
bresults_->setCallback([view = view_, button = bresults_]{
view->setMode(Mode::RESULTS);
});
bpause_ = new nanogui::Button(buttons, "");
bpause_->setFixedWidth(120);
bpause_->setCallback([&ctrl = ctrl_](){
// TODO: add buttons to browse captured images and allow deleting
// images
ctrl->setCapture(!ctrl->capturing());
});
bcalibrate_ = new nanogui::Button(buttons, "Calibrate");
bcalibrate_->setFixedWidth(120);
bcalibrate_->setCallback([view = view_, button = bcalibrate_](){
view->setMode(Mode::CALIBRATION);
});
}
void IntrinsicCalibrationView::ControlWindow::draw(NVGcontext* ctx) {
if (ctrl_->capturing()) { bpause_->setCaption("Pause"); }
else { bpause_->setCaption("Continue"); }
//bcalibrate_->setEnabled(ctrl_->count() > 0);
bresults_->setEnabled(ctrl_->calibrated());
bsave_->setEnabled(ctrl_->calibrated());
bapply_->setEnabled(ctrl_->calibrated());
bapply_->setIcon(view_->undistort() ? ENTYPO_ICON_EYE : ENTYPO_ICON_EYE_WITH_LINE);
bapply_->setPushed(view_->undistort());
updateCount();
FixedWindow::draw(ctx);
}
void IntrinsicCalibrationView::ControlWindow::updateCount() {
txtnframes_->setCaption("Detected patterns: " +
std::to_string(ctrl_->count()));
}
////////////////////////////////////////////////////////////////////////////////
// Calibration Window
IntrinsicCalibrationView::CalibrationWindow::CalibrationWindow(nanogui::Widget* parent, IntrinsicCalibrationView* view) :
FixedWindow(parent, "Calibration"), view_(view), ctrl_(view->ctrl_) {
calibrating_ = false;
setLayout(new nanogui::BoxLayout
(nanogui::Orientation::Vertical, nanogui::Alignment::Fill, 6, 6));
(new nanogui::Button(buttonPanel(), "", ENTYPO_ICON_CROSS))->setCallback(
[view = view_]() {
view->setMode(Mode::VIDEO);
});
// sensor size
new nanogui::Label(this, "Initial values");
nanogui::GridLayout *grid_layout = new nanogui::GridLayout
(nanogui::Orientation::Horizontal, 2, nanogui::Alignment::Fill, 0, 5);
grid_layout->setColAlignment
({nanogui::Alignment::Maximum, nanogui::Alignment::Fill});
grid_layout->setSpacing(0, 10);
auto* initial_values = new nanogui::Widget(this);
initial_values->setLayout(grid_layout);
new nanogui::Label(initial_values, "Sensor width");
sensor_width_ = new nanogui::FloatBox<double>(initial_values, ctrl_->sensorSize().width);
sensor_width_->setEditable(true);
sensor_width_->setFormat("[0-9]*\\.?[0-9]+");
sensor_width_->setUnits("mm");
new nanogui::Label(initial_values, "Sensor height");
sensor_height_ = new nanogui::FloatBox<double>(initial_values, ctrl_->sensorSize().height);
sensor_height_->setEditable(true);
sensor_height_->setFormat("[0-9]*\\.?[0-9]+");
sensor_height_->setUnits("mm");
new nanogui::Label(initial_values, "Focal length");
focal_length_ = new nanogui::FloatBox<double>(initial_values, ctrl_->focalLength());
focal_length_->setEditable(true);
focal_length_->setFormat("[0-9]*\\.?[0-9]+");
focal_length_->setUnits("mm");
new nanogui::Label(initial_values, "Reset principal point");
reset_pp_ = new nanogui::CheckBox(initial_values, "");
reset_pp_->setChecked(false);
new nanogui::Label(initial_values, "Reset distortion coefficients");
reset_dist_ = new nanogui::CheckBox(initial_values, "");
reset_dist_->setChecked(false);
// flags
new nanogui::Label(this, "Flags");
new ftl::gui2::OpenCVFlagWidget(this, &(ctrl_->flags()), ctrl_->defaultFlags());
status_ = new nanogui::Label(this, " ");
bcalibrate_ = new nanogui::Button(this, "Run");
bcalibrate_->setEnabled(false);
bcalibrate_->setCallback([this](){
if (!ctrl_->isBusy()) {
ctrl_->setSensorSize({sensor_width_->value(), sensor_height_->value()});
ctrl_->setFocalLength(focal_length_->value(), ctrl_->sensorSize());
if (reset_pp_->checked()) { ctrl_->resetPrincipalPoint(); }
if (reset_dist_->checked()) { ctrl_->resetDistortion(); }
ctrl_->run();
calibrating_ = true;
}
});
}
void IntrinsicCalibrationView::CalibrationWindow::update() {
focal_length_->setValue(ctrl_->focalLength());
}
void IntrinsicCalibrationView::CalibrationWindow::draw(NVGcontext* ctx) {
bool use_guess = ctrl_->flags().has(cv::CALIB_USE_INTRINSIC_GUESS);
focal_length_->setEnabled(use_guess);
reset_pp_->setEnabled(use_guess);
reset_dist_->setEnabled(use_guess);
if (ctrl_->isBusy()) {
if (calibrating_) {
auto dots = std::string(int(round(glfwGetTime())) % 4, '.');
status_->setCaption("Calibrating " + dots);
}
else {
status_->setCaption("Busy");
}
}
else {
status_->setCaption(" ");
}
bcalibrate_->setEnabled(!ctrl_->isBusy() && (ctrl_->count() > 0));
if (calibrating_ && !ctrl_->isBusy()) {
calibrating_ = false;
view_->setUndistort(true);
view_->setMode(Mode::RESULTS);
}
FixedWindow::draw(ctx);
}
////////////////////////////////////////////////////////////////////////////////
// Result window
IntrinsicCalibrationView::ResultWindow::ResultWindow(nanogui::Widget* parent, IntrinsicCalibrationView* view) :
FixedWindow(parent, "Results"), view_(view), ctrl_(view->ctrl_) {
setLayout(new nanogui::BoxLayout
(nanogui::Orientation::Vertical, nanogui::Alignment::Fill, 8 , 8));
(new nanogui::Button(buttonPanel(), "", ENTYPO_ICON_CROSS))->setCallback(
[view = view_]() {
view->setMode(Mode::VIDEO);
});
rms_ = new nanogui::Label(this, "");
info_ = new ftl::gui2::IntrinsicDetails(this);
bsave_ = new nanogui::Button(this, "Save");
bsave_->setCallback([button = bsave_, ctrl = ctrl_](){
ctrl->saveCalibration();
button->setCaption("Saved");
button->setEnabled(false);
});
}
void IntrinsicCalibrationView::ResultWindow::draw(NVGcontext* ctx) {
nanogui::Window::draw(ctx);
}
void IntrinsicCalibrationView::ResultWindow::update() {
if (!isnan(ctrl_->reprojectionError())) {
rms_->setCaption("Reprojection error (RMS): " + to_string(ctrl_->reprojectionError()));
rms_->setVisible(true);
}
else {
rms_->setVisible(false);
}
info_->update(ctrl_->calibration());
bsave_->setEnabled(true);
bsave_->setCaption("Save");
}
////////////////////////////////////////////////////////////////////////////////
IntrinsicCalibrationView::IntrinsicCalibrationView(Screen* parent,
IntrinsicCalibration* ctrl) : View(parent), ctrl_(ctrl) {
undistort_ = false;
imview_ = new ftl::gui2::FTLImageView(this);
int w = 300;
wcapture_ = new CaptureWindow(screen(), this);
wcapture_->setFixedWidth(w);
wcontrol_ = new ControlWindow(screen(), this);
wcalibration_ = new CalibrationWindow(screen(), this);
wcalibration_->setFixedWidth(w);
wresults_ = new ResultWindow(screen(), this);
wresults_->update();
screen()->performLayout();
setMode(Mode::CAPTURE_INIT);
}
IntrinsicCalibrationView::~IntrinsicCalibrationView() {
wcapture_->setVisible(false);
wcapture_->dispose();
wcontrol_->setVisible(false);
wcontrol_->dispose();
wcalibration_->setVisible(false);
wcalibration_->dispose();
wresults_->setVisible(false);
wresults_->dispose();
}
void IntrinsicCalibrationView::performLayout(NVGcontext *ctx) {
auto sz = wcontrol_->size();
wcontrol_->setPosition(
nanogui::Vector2i(width() / 2 - sz[0]/2, height() - 30 - sz[1]));
wcapture_->center();
wcalibration_->center();
wresults_->center();
imview_->setSize(size());
View::performLayout(ctx);
}
void IntrinsicCalibrationView::draw(NVGcontext *ctx) {
if (ctrl_->hasFrame()) {
bool was_valid = imview_->texture().isValid();
if (undistort_) {
auto frame = ctrl_->getFrameUndistort();
imview_->copyFrom(frame);
}
else {
auto frame = ctrl_->getFrame();
imview_->copyFrom(frame);
}
if (!was_valid) {
imview_->fit();
}
}
View::draw(ctx);
if (ctrl_->capturing()) {
drawChessboardCorners(ctx, imview_, ctrl_->previousPoints());
}
}
void IntrinsicCalibrationView::setMode(Mode m) {
switch(m) {
case Mode::CAPTURE_INIT:
ctrl_->setCapture(false);
wcapture_->setVisible(true);
wcontrol_->setVisible(false);
wcalibration_->setVisible(false);
wresults_->setVisible(false);
break;
case Mode::CAPTURE_IMAGES:
ctrl_->setCapture(true);
wcapture_->setVisible(false);
wcontrol_->setVisible(true);
wcalibration_->setVisible(false);
wresults_->setVisible(false);
break;
case Mode::VIDEO:
ctrl_->setCapture(false);
wcapture_->setVisible(false);
wcontrol_->setVisible(true);
wcalibration_->setVisible(false);
wresults_->setVisible(false);
break;
case Mode::CALIBRATION:
ctrl_->setCapture(false);
wcapture_->setVisible(false);
wcontrol_->setVisible(false);
wcalibration_->update();
wcalibration_->setVisible(true);
wresults_->setVisible(false);
break;
case Mode::RESULTS:
ctrl_->setCapture(false);
wcapture_->setVisible(false);
wcontrol_->setVisible(false);
wcalibration_->setVisible(false);
wresults_->setVisible(true);
wresults_->update();
break;
}
screen()->performLayout();
}
#pragma once
#include "../../modules/calibration/calibration.hpp"
#include "../../view.hpp"
#include "../../widgets/imageview.hpp"
#include <ftl/utility/gltexture.hpp>
namespace ftl
{
namespace gui2
{
class IntrinsicCalibrationStart : public View {
public:
IntrinsicCalibrationStart(Screen* widget, IntrinsicCalibration* ctrl);
virtual ~IntrinsicCalibrationStart();
virtual void draw(NVGcontext *ctx) override;
void update();
private:
nanogui::Window* window_;
nanogui::Widget* buttons_;
IntrinsicCalibration* ctrl_;
bool show_all_;
public:
EIGEN_MAKE_ALIGNED_OPERATOR_NEW
};
class IntrinsicCalibrationView : public View {
class CaptureWindow;
class ControlWindow;
class CalibrationWindow;
class ResultWindow;
public:
IntrinsicCalibrationView(Screen* screen, IntrinsicCalibration* ctrl);
virtual ~IntrinsicCalibrationView();
enum Mode {
CAPTURE_INIT, // set capture parameters
CAPTURE_IMAGES, // capture images
CALIBRATION, // calibration options
RESULTS, // calibration results
VIDEO // same as capture images but paused
};
void setMode(Mode m);
virtual void performLayout(NVGcontext* ctx) override;
virtual void draw(NVGcontext* ctx) override;
void setUndistort(bool v) { undistort_ = v; }
bool undistort() { return undistort_; }
private:
IntrinsicCalibration* ctrl_;
FTLImageView* imview_;
CaptureWindow* wcapture_;
ControlWindow* wcontrol_;
CalibrationWindow* wcalibration_;
ResultWindow* wresults_;
bool undistort_;
public:
EIGEN_MAKE_ALIGNED_OPERATOR_NEW
};
} // namespace gui2
} // namespace ftl
#include <sstream>
#include "visualization.hpp"
#include "widgets.hpp"
#include "stereoview.hpp"
#include "../../screen.hpp"
#include "../../widgets/window.hpp"
#include <nanogui/window.h>
#include <nanogui/layout.h>
#include <nanogui/button.h>
#include <nanogui/checkbox.h>
#include <nanogui/textbox.h>
#include <nanogui/label.h>
#include <nanogui/tabwidget.h>
using ftl::codecs::Channel;
using ftl::gui2::Screen;
using ftl::gui2::View;
using ftl::gui2::FixedWindow;
using ftl::gui2::StereoCalibrationStart;
using ftl::gui2::StereoCalibration;
using ftl::gui2::StereoCalibrationView;
using Mode = ftl::gui2::StereoCalibrationView::Mode;
////////////////////////////////////////////////////////////////////////////////
template<typename T>
std::string to_string(T v, int precision = 2) {
std::stringstream stream;
stream << std::fixed << std::setprecision(precision) << v;
return stream.str();
}
////////////////////////////////////////////////////////////////////////////////
class StereoCalibrationView::CaptureWindow : public FixedWindow {
public:
CaptureWindow(nanogui::Widget* parent, StereoCalibrationView* view);
virtual void draw(NVGcontext* ctx) override;
private:
void update();
StereoCalibrationView* view_;
StereoCalibration* ctrl_;
int width_;
int height_;
double square_size_;
public:
EIGEN_MAKE_ALIGNED_OPERATOR_NEW
};
class StereoCalibrationView::ControlWindow : public FixedWindow {
public:
ControlWindow(nanogui::Widget* parent, StereoCalibrationView* view);
virtual void draw(NVGcontext* ctx) override;
private:
void updateCount();
StereoCalibrationView* view_;
StereoCalibration* ctrl_;
nanogui::Label* txtnframes_;
nanogui::Button* bcalibrate_;
nanogui::Button* bresults_;
nanogui::Button* bpause_;
public:
EIGEN_MAKE_ALIGNED_OPERATOR_NEW
};
class StereoCalibrationView::CalibrationWindow : public FixedWindow {
public:
CalibrationWindow(nanogui::Widget* parent, StereoCalibrationView* view);
virtual void draw(NVGcontext* ctx) override;
double sensorWidth() { return sensor_width_->value(); }
double sensorHeight() { return sensor_width_->value(); }
private:
StereoCalibrationView* view_;
StereoCalibration* ctrl_;
nanogui::Button* bcalibrate_;
nanogui::FloatBox<double>* sensor_width_;
nanogui::FloatBox<double>* sensor_height_;
bool calibrating_;
public:
EIGEN_MAKE_ALIGNED_OPERATOR_NEW
};
class StereoCalibrationView::ResultWindow : public FixedWindow {
public:
ResultWindow(nanogui::Widget* parent, StereoCalibrationView* view);
virtual void performLayout(NVGcontext* ctx) override;
virtual void draw(NVGcontext* ctx) override;
void update();
private:
StereoCalibrationView* view_;
StereoCalibration* ctrl_;
nanogui::TabWidget* tabs_;
nanogui::Button* bsave_;
ftl::gui2::IntrinsicDetails* infol_;
ftl::gui2::IntrinsicDetails* infor_;
public:
EIGEN_MAKE_ALIGNED_OPERATOR_NEW
};
////////////////////////////////////////////////////////////////////////////////
//
StereoCalibrationStart::StereoCalibrationStart(ftl::gui2::Screen *parent, StereoCalibration *ctrl) :
ftl::gui2::View(parent), ctrl_(ctrl) {
show_all_ = false;
window_ = new FixedWindow(parent, std::string("Stereo Calibration"));
window_->setLayout(new nanogui::BoxLayout(nanogui::Orientation::Vertical,
nanogui::Alignment::Fill, 6, 12));
auto* button_refresh = new nanogui::Button(window_->buttonPanel(), "", ENTYPO_ICON_CCW);
button_refresh->setCallback([this](){ update(); });
buttons_ = new nanogui::Widget(window_);
buttons_->setLayout(new nanogui::BoxLayout(nanogui::Orientation::Vertical,
nanogui::Alignment::Fill, 0, 8));
auto bshow_all = new nanogui::CheckBox(window_, "Show all sources",
[this](bool v){
show_all_ = v;
update();
});
bshow_all->setChecked(show_all_);
window_->setFixedWidth(400);
window_->setVisible(true);
update();
}
StereoCalibrationStart::~StereoCalibrationStart() {
window_->setVisible(false);
if (parent()->getRefCount() > 0) {
window_->dispose();
}
}
void StereoCalibrationStart::update() {
while (buttons_->childCount() > 0) {
buttons_->removeChild(buttons_->childCount() - 1);
}
for (const auto& [name, id] : ctrl_->listSources(show_all_)) {
auto* button = new nanogui::Button(buttons_, name, ENTYPO_ICON_CAMERA);
button->setCallback([ctrl = this->ctrl_, id](){
ctrl->start(id);
});
}
screen()->performLayout();
}
void StereoCalibrationStart::draw(NVGcontext* ctx) {
window_->center();
View::draw(ctx);
}
////////////////////////////////////////////////////////////////////////////////
// Capture Window
void StereoCalibrationView::CaptureWindow::update() {
ctrl_->setChessboard({width_, height_}, square_size_);
}
StereoCalibrationView::CaptureWindow::CaptureWindow(nanogui::Widget* parent, StereoCalibrationView* view) :
FixedWindow(parent, "Capture Options"), view_(view), ctrl_(view->ctrl_) {
width_ = ctrl_->chessboardSize().width;
height_ = ctrl_->chessboardSize().height;
square_size_ = ctrl_->squareSize();
setLayout(new nanogui::BoxLayout
(nanogui::Orientation::Vertical, nanogui::Alignment::Fill, 6, 6));
(new nanogui::Button(buttonPanel(), "", ENTYPO_ICON_CROSS))->setCallback(
[this]() {
update();
view_->setMode(Mode::VIDEO);
});
new nanogui::Label(this, "Capture interval");
auto* interval = new nanogui::FloatBox<float>(this, ctrl_->frequency());
interval->setEditable(true);
interval->setFormat("[0-9]*\\.?[0-9]+");
interval->setUnits("s");
interval->setCallback([ctrl = this->ctrl_](float v){
ctrl->setFrequency(v);
});
// Chessboard parameters
auto* chessboard = new nanogui::Widget(this);
chessboard->setLayout(new nanogui::BoxLayout
(nanogui::Orientation::Vertical, nanogui::Alignment::Fill, 0, 4));
// width
new nanogui::Label(chessboard, "Chessboard width");
auto* chessboard_size_x = new nanogui::IntBox<int>(chessboard, width_);
chessboard_size_x->setEditable(true);
chessboard_size_x->setFormat("[1-9][0-9]*");
chessboard_size_x->setCallback([this](int v){
width_ = max(0, v);
});
// height
new nanogui::Label(chessboard, "Chessboard height");
auto* chessboard_size_y = new nanogui::IntBox<int>(chessboard, height_);
chessboard_size_y->setEditable(true);
chessboard_size_y->setFormat("[1-9][0-9]*");
chessboard_size_y->setCallback([this](int v){
height_ = max(0, v);
});
// square size
new nanogui::Label(chessboard, "Chessboard square size");
auto* square_size = new nanogui::FloatBox<float>(chessboard, square_size_*1000.0);
square_size->setEditable(true);
square_size->setFormat("[0-9]*\\.?[0-9]+");
square_size->setUnits("mm");
square_size->setCallback([this](float v){
square_size_ = v/1000.0;
});
auto* button_start = new nanogui::Button(this, "Start");
button_start->setCallback([this]() {
update();
view_->setMode(Mode::CAPTURE_IMAGES);
});
}
void StereoCalibrationView::CaptureWindow::draw(NVGcontext* ctx) {
FixedWindow::draw(ctx);
}
////////////////////////////////////////////////////////////////////////////////
// Control Window
StereoCalibrationView::ControlWindow::ControlWindow(nanogui::Widget* parent, StereoCalibrationView* view) :
FixedWindow(parent, ""), view_(view), ctrl_(view->ctrl_) {
setLayout(new nanogui::BoxLayout
(nanogui::Orientation::Vertical, nanogui::Alignment::Fill, 6, 6));
txtnframes_ = new nanogui::Label(this, "");
updateCount();
auto* buttons = new nanogui::Widget(this);
buttons->setLayout(new nanogui::BoxLayout
(nanogui::Orientation::Horizontal, nanogui::Alignment::Middle, 0, 0));
auto* button_back = new nanogui::Button(buttons, "", ENTYPO_ICON_ARROW_LEFT);
button_back->setCallback([this, button = button_back](){
view_->setMode(Mode::CAPTURE_INIT);
});
bresults_ = new nanogui::Button(buttons, "Details");
bresults_->setFixedWidth(120);
//bresults_->setEnabled(ctrl_->calib().calibrated());
bresults_->setCallback([view = view_, button = bresults_]{
view->setMode(Mode::RESULTS);
});
bpause_ = new nanogui::Button(buttons, "");
bpause_->setFixedWidth(120);
bpause_->setCallback([&ctrl = ctrl_](){
ctrl->setCapture(!ctrl->capturing());
});
bcalibrate_ = new nanogui::Button(buttons, "Calibrate");
bcalibrate_->setFixedWidth(120);
bcalibrate_->setCallback([view = view_, button = bcalibrate_](){
view->setMode(Mode::CALIBRATION);
});
}
void StereoCalibrationView::ControlWindow::draw(NVGcontext* ctx) {
if (ctrl_->capturing()) { bpause_->setCaption("Pause"); }
else { bpause_->setCaption("Continue"); }
//bcalibrate_->setEnabled(ctrl_->calib().count() > 0);
//bresults_->setEnabled(ctrl_->calib().calibrated());
updateCount();
FixedWindow::draw(ctx);
}
void StereoCalibrationView::ControlWindow::updateCount() {
txtnframes_->setCaption("Detected patterns: " +
std::to_string(ctrl_->count()));
}
////////////////////////////////////////////////////////////////////////////////
// Calibration Window
StereoCalibrationView::CalibrationWindow::CalibrationWindow(nanogui::Widget* parent, StereoCalibrationView* view) :
FixedWindow(parent, "Calibration"), view_(view), ctrl_(view->ctrl_) {
calibrating_ = false;
setLayout(new nanogui::BoxLayout
(nanogui::Orientation::Vertical, nanogui::Alignment::Fill, 6, 6));
(new nanogui::Button(buttonPanel(), "", ENTYPO_ICON_CROSS))->setCallback(
[view = view_]() {
view->setMode(Mode::VIDEO);
});
nanogui::GridLayout *grid_layout = new nanogui::GridLayout
(nanogui::Orientation::Horizontal, 2, nanogui::Alignment::Fill, 0, 5);
grid_layout->setColAlignment
({nanogui::Alignment::Maximum, nanogui::Alignment::Fill});
grid_layout->setSpacing(0, 10);
auto* sensor = new nanogui::Widget(this);
sensor->setLayout(grid_layout);
// flags
new nanogui::Label(this, "Flags");
new ftl::gui2::OpenCVFlagWidget(this, &(ctrl_->flags()));
bcalibrate_ = new nanogui::Button(this, "Run");
bcalibrate_->setEnabled(false);
bcalibrate_->setCallback([&ctrl = ctrl_, &running = calibrating_](){
if (!ctrl->isBusy()) {
ctrl->run();
running = true;
}
});
}
void StereoCalibrationView::CalibrationWindow::draw(NVGcontext* ctx) {
bcalibrate_->setEnabled(!ctrl_->isBusy() && (ctrl_->count() > 0));
if (calibrating_ && !ctrl_->isBusy()) {
calibrating_ = false;
view_->setMode(Mode::RESULTS);
}
FixedWindow::draw(ctx);
}
////////////////////////////////////////////////////////////////////////////////
// Result window
StereoCalibrationView::ResultWindow::ResultWindow(nanogui::Widget* parent, StereoCalibrationView* view) :
FixedWindow(parent, "Results"), view_(view), ctrl_(view->ctrl_) {
setLayout(new nanogui::BoxLayout
(nanogui::Orientation::Vertical, nanogui::Alignment::Fill, 8 , 0));
tabs_ = new nanogui::TabWidget(this);
auto* tabl = tabs_->createTab("Left (intrinsic)");
auto* tabr = tabs_->createTab("Right (intrinsic)");
infol_ = new ftl::gui2::IntrinsicDetails(tabl);
infor_ = new ftl::gui2::IntrinsicDetails(tabr);
(new nanogui::Button(buttonPanel(), "", ENTYPO_ICON_CROSS))->setCallback(
[view = view_]() {
view->setMode(Mode::VIDEO);
});
bsave_ = new nanogui::Button(this, "Save");
bsave_->setCallback([button = bsave_, ctrl = ctrl_](){
ctrl->saveCalibration();
button->setCaption("Saved");
button->setEnabled(false);
});
}
void StereoCalibrationView::ResultWindow::draw(NVGcontext* ctx) {
nanogui::Window::draw(ctx);
}
void StereoCalibrationView::ResultWindow::performLayout(NVGcontext* ctx) {
nanogui::Window::performLayout(ctx);
auto sz = infor_->preferredSize(ctx);
infol_->parent()->setSize(sz);
infor_->parent()->setSize(sz);
center();
}
void StereoCalibrationView::ResultWindow::update() {
infol_->update(ctrl_->calibrationLeft().intrinsic);
infor_->update(ctrl_->calibrationRight().intrinsic);
bsave_->setEnabled(true);
bsave_->setCaption("Save");
screen()->performLayout();
}
////////////////////////////////////////////////////////////////////////////////
StereoCalibrationView::StereoCalibrationView(Screen* parent,
StereoCalibration* ctrl) : View(parent), ctrl_(ctrl) {
imview_ = new ftl::gui2::StereoImageView(this);
int w = 300;
wcapture_ = new CaptureWindow(screen(), this);
wcapture_->setFixedWidth(w);
wcontrol_ = new ControlWindow(screen(), this);
wcalibration_ = new CalibrationWindow(screen(), this);
wcalibration_->setFixedWidth(w);
wresults_ = new ResultWindow(screen(), this);
screen()->performLayout();
setMode(Mode::CAPTURE_INIT);
}
StereoCalibrationView::~StereoCalibrationView() {
wcapture_->setVisible(false);
wcapture_->dispose();
wcontrol_->setVisible(false);
wcontrol_->dispose();
wcalibration_->setVisible(false);
wcalibration_->dispose();
wresults_->setVisible(false);
wresults_->dispose();
}
void StereoCalibrationView::performLayout(NVGcontext *ctx) {
auto sz = wcontrol_->size();
wcontrol_->setPosition(
nanogui::Vector2i(width() / 2 - sz[0]/2, height() - 30 - sz[1]));
wcapture_->center();
wcalibration_->center();
wresults_->center();
imview_->setFixedSize(size());
View::performLayout(ctx);
}
void StereoCalibrationView::draw(NVGcontext *ctx) {
if (ctrl_->hasFrame()) {
auto l = ctrl_->getLeft();
auto r = ctrl_->getRight();
if (l.size() != cv::Size(0, 0) && r.size() != cv::Size(0, 0)) {
imview_->left()->copyFrom(l);
imview_->right()->copyFrom(r);
}
}
View::draw(ctx);
auto points = ctrl_->previousPoints();
if (points.size() == 2) {
drawChessboardCorners(ctx, imview_->left(), points[0]);
drawChessboardCorners(ctx, imview_->right(), points[1]);
}
}
void StereoCalibrationView::setMode(Mode m) {
switch(m) {
case Mode::CAPTURE_INIT:
ctrl_->setCapture(false);
wcapture_->setVisible(true);
wcontrol_->setVisible(false);
wcalibration_->setVisible(false);
wresults_->setVisible(false);
break;
case Mode::CAPTURE_IMAGES:
ctrl_->setCapture(true);
wcapture_->setVisible(false);
wcontrol_->setVisible(true);
wcalibration_->setVisible(false);
wresults_->setVisible(false);
break;
case Mode::VIDEO:
ctrl_->setCapture(false);
wcapture_->setVisible(false);
wcontrol_->setVisible(true);
wcalibration_->setVisible(false);
wresults_->setVisible(false);
break;
case Mode::CALIBRATION:
ctrl_->setCapture(false);
wcapture_->setVisible(false);
wcontrol_->setVisible(false);
wcalibration_->setVisible(true);
wresults_->setVisible(false);
break;
case Mode::RESULTS:
ctrl_->setCapture(false);
wcapture_->setVisible(false);
wcontrol_->setVisible(false);
wcalibration_->setVisible(false);
wresults_->setVisible(true);
wresults_->update();
break;
}
}
#pragma once
#include "../../modules/calibration/calibration.hpp"
#include "../../view.hpp"
#include "../../widgets/imageview.hpp"
#include <ftl/utility/gltexture.hpp>
namespace ftl
{
namespace gui2
{
class StereoCalibrationStart : public View {
public:
StereoCalibrationStart(Screen* widget, StereoCalibration* ctrl);
virtual ~StereoCalibrationStart();
virtual void draw(NVGcontext *ctx) override;
void update();
private:
nanogui::Window* window_;
nanogui::Widget* buttons_;
StereoCalibration* ctrl_;
bool show_all_;
public:
EIGEN_MAKE_ALIGNED_OPERATOR_NEW
};
class StereoCalibrationView : public View {
class CaptureWindow;
class ControlWindow;
class CalibrationWindow;
class ResultWindow;
public:
StereoCalibrationView(Screen* screen, StereoCalibration* ctrl);
virtual ~StereoCalibrationView();
enum Mode {
CAPTURE_INIT, // set capture parameters
CAPTURE_IMAGES, // capture images
CALIBRATION, // calibration options
RESULTS, // calibration results
VIDEO // same as capture images but paused
};
void setMode(Mode m);
virtual void performLayout(NVGcontext* ctx) override;
virtual void draw(NVGcontext* ctx) override;
private:
StereoCalibration* ctrl_;
StereoImageView* imview_;
CaptureWindow* wcapture_;
ControlWindow* wcontrol_;
CalibrationWindow* wcalibration_;
ResultWindow* wresults_;
public:
EIGEN_MAKE_ALIGNED_OPERATOR_NEW
};
} // namespace gui2
} // namespace ftl
#pragma once
#include "../../widgets/imageview.hpp"
#include <ftl/calibration/structures.hpp>
/** Draw Chessboard Corners with OpenGL to ImageView widget. */
template<typename T>
static void drawChessboardCorners(NVGcontext* ctx, ftl::gui2::ImageView* imview, const std::vector<T>& points) {
if (points.size() == 0) { return; }
nanogui::Vector2f wpos = imview->absolutePosition().cast<float>();
nanogui::Vector2f wsize = imview->sizeF();
nanogui::Vector2f apos = imview->positionForCoordinate({points[0].x, points[0].y}) + wpos;
nvgShapeAntiAlias(ctx, 1);
nvgScissor(ctx, wpos.x(), wpos.y(), wsize.x(), wsize.y());
nvgBeginPath(ctx);
nvgMoveTo(ctx, apos.x(), apos.y());
for (unsigned int i = 1; i < points.size(); i++) {
apos = imview->positionForCoordinate({points[i].x, points[i].y}) + wpos;
nvgLineTo(ctx, apos.x(), apos.y());
}
nvgStrokeColor(ctx, nvgRGBA(255, 32, 32, 192));
nvgStrokeWidth(ctx, 1.0f);
nvgStroke(ctx);
for (unsigned int i = 0; i < points.size(); i++) {
apos = imview->positionForCoordinate({points[i].x, points[i].y}) + wpos;
nvgBeginPath(ctx);
nvgCircle(ctx, apos.x(), apos.y(), 2.5);
nvgStrokeColor(ctx, nvgRGBA(0, 0, 0, 255));
nvgStrokeWidth(ctx, 1.5f);
nvgStroke(ctx);
nvgBeginPath(ctx);
nvgCircle(ctx, apos.x(), apos.y(), 2.5);
nvgStrokeColor(ctx, nvgRGBA(255, 255, 255, 255));
nvgStrokeWidth(ctx, 1.0f);
nvgStroke(ctx);
}
nvgResetScissor(ctx);
}
static void drawTriangle(NVGcontext* ctx, const ftl::calibration::CalibrationData::Extrinsic &calib,
const nanogui::Vector2f &pos, const nanogui::Vector2f offset, float scale, float sz=1.0f) {
const int idx_x = 0;
const int idx_y = 2;
cv::Mat T = calib.matrix();
cv::Vec4f p1(cv::Mat(T * cv::Vec4d{sz/2.0f, 0.0f, 0.0f, 1.0f}));
cv::Vec4f p2(cv::Mat(T * cv::Vec4d{-sz/2.0f, 0.0f, 0.0f, 1.0f}));
cv::Vec4f p3(cv::Mat(T * cv::Vec4d{0.0f, 0.0f, -sz*sqrtf(3.0f)/2.0f, 1.0f}));
p1[idx_x] -= offset.x();
p2[idx_x] -= offset.x();
p3[idx_x] -= offset.x();
p1[idx_y] -= offset.y();
p2[idx_y] -= offset.y();
p3[idx_y] -= offset.y();
p1 *= scale;
p2 *= scale;
p3 *= scale;
nvgBeginPath(ctx);
// NOTE: flip x
nvgMoveTo(ctx, pos.x() + p1[idx_x], pos.y() + p1[idx_y]);
nvgLineTo(ctx, pos.x() + p2[idx_x], pos.y() + p2[idx_y]);
nvgLineTo(ctx, pos.x() + p3[idx_x], pos.y() + p3[idx_y]);
nvgLineTo(ctx, pos.x() + p1[idx_x], pos.y() + p1[idx_y]);
if (calib.tvec == cv::Vec3d{0.0, 0.0, 0.0}) {
nvgStrokeColor(ctx, nvgRGBA(255, 64, 64, 255));
}
else {
nvgStrokeColor(ctx, nvgRGBA(255, 255, 255, 255));
}
nvgStrokeWidth(ctx, 1.0f);
nvgStroke(ctx);
}
static void drawFloorPlan(NVGcontext* ctx, nanogui::Widget* parent,
const std::vector<ftl::calibration::CalibrationData::Calibration>& calib,
const std::vector<std::string>& names = {},
int origin=0) {
float minx = INFINITY;
float miny = INFINITY;
float maxx = -INFINITY;
float maxy = -INFINITY;
cv::Vec3f center = {0.0f, 0.0f};
std::vector<cv::Point2f> points(calib.size());
for (unsigned int i = 0; i < points.size(); i++) {
const auto& extrinsic = calib[i].extrinsic;
// xz, assume floor on y-plane y = 0
float x = extrinsic.tvec[0];
float y = extrinsic.tvec[2];
points[i] = {x, y};
minx = std::min(minx, x);
miny = std::min(miny, y);
maxx = std::max(maxx, x);
maxy = std::max(maxy, y);
center += extrinsic.tvec;
}
center /= float(points.size());
float w = parent->width();
float dx = maxx - minx;
float h = parent->height();
float dy = maxy - miny;
float s = min(w/dx, h/dy) * 0.8; // scale
nanogui::Vector2f apos = parent->absolutePosition().cast<float>() + nanogui::Vector2f{w/2.0f, h/2.0f};
nanogui::Vector2f off{center[0], center[2]};
for (unsigned int i = 0; i < points.size(); i++) {
drawTriangle(ctx, calib[i].extrinsic, apos, off, s, 0.3);
}
}
#include "widgets.hpp"
#include <nanogui/label.h>
#include <nanogui/layout.h>
#include <nanogui/checkbox.h>
#include <opencv2/calib3d.hpp>
using ftl::gui2::OpenCVFlagWidget;
using ftl::gui2::OpenCVCalibrateFlags;
template<typename T>
std::string to_string(T v, int precision = 2) {
std::stringstream stream;
stream << std::fixed << std::setprecision(precision) << v;
return stream.str();
}
OpenCVFlagWidget::OpenCVFlagWidget(nanogui::Widget* parent, OpenCVCalibrateFlags* flags, int defaultv) :
nanogui::Widget(parent), flags_(flags), defaults_(defaultv) {
if (defaultv == -1) {
defaults_ = flags_->defaultFlags();
}
setLayout(new nanogui::BoxLayout
(nanogui::Orientation::Vertical, nanogui::Alignment::Fill, 0, 4));
reset();
}
void OpenCVFlagWidget::reset() {
while(childCount() > 0) {
removeChild(childCount() - 1);
}
for(int flag : flags_->list()) {
auto* checkbox = new nanogui::CheckBox(this, flags_->name(flag),
[flag, this](bool state){
if (state) { flags_->set(flag); }
else { flags_->unset(flag); }
});
checkbox->setChecked(flags_->has(flag));
checkbox->setTooltip(flags_->explain(flag));
}
// reset button
auto* reset = new nanogui::Button(this, "Reset flags");
reset->setCallback([this](){
// update widget
auto all_flags = flags_->list();
for(size_t i = 0; i < all_flags.size(); i++) {
auto* checkbox = dynamic_cast<nanogui::CheckBox*>(childAt(i));
checkbox->setChecked(all_flags[i] & defaults_);
}
});
}
////////////////////////////////////////////////////////////////////////////////
using ftl::gui2::IntrinsicDetails;
IntrinsicDetails::IntrinsicDetails(nanogui::Widget* parent) :
nanogui::Widget(parent), padding_(8) {
setLayout(new nanogui::BoxLayout
(nanogui::Orientation::Vertical, nanogui::Alignment::Fill, 0 , padding_));
params_ = new nanogui::Widget(this);
dist_ = new nanogui::Widget(this);
dist_->setLayout(new nanogui::BoxLayout
(nanogui::Orientation::Vertical, nanogui::Alignment::Fill, 0, padding_));
}
void IntrinsicDetails::update(const ftl::calibration::CalibrationData::Intrinsic &values) {
while (params_->childCount() > 0) {
params_->removeChild(params_->childCount() - 1);
}
while (dist_->childCount() > 0) {
dist_->removeChild(dist_->childCount() - 1);
}
bool use_physical = values.sensorSize != cv::Size2d{0.0, 0.0};
nanogui::GridLayout* grid_layout;
if (use_physical) {
grid_layout = new nanogui::GridLayout
(nanogui::Orientation::Horizontal, 3, nanogui::Alignment::Fill, 0, padding_);
}
else {
grid_layout = new nanogui::GridLayout
(nanogui::Orientation::Horizontal, 2, nanogui::Alignment::Fill, 0, padding_);
}
grid_layout->setColAlignment
({nanogui::Alignment::Maximum, nanogui::Alignment::Fill});
params_->setLayout(grid_layout);
auto sw = values.sensorSize.width;
auto sh = values.sensorSize.height;
auto K = values.matrix();
auto imsize = values.resolution;
double fovx;
double fovy;
double f;
cv::Point2d pp;
double ar;
cv::calibrationMatrixValues(K, imsize, sw, sh, fovx, fovy, f, pp, ar);
new nanogui::Label(params_, "Size (sensor/image):");
if (use_physical) new nanogui::Label(params_, to_string(sw, 1) + std::string("x") + to_string(sh, 1));
new nanogui::Label(params_, std::to_string(imsize.width) + std::string("x") + std::to_string(imsize.height));
new nanogui::Label(params_, "Focal length:");
if (use_physical) new nanogui::Label(params_, to_string(f) + " mm");
new nanogui::Label(params_,
((values.fx == values.fy) ? to_string(values.fx) + " px": (
"(" + to_string(values.fx) + ", "
+ to_string(values.fy) + ")")));
new nanogui::Label(params_, "Principal point:");
if (use_physical) new nanogui::Label(params_,
"(" + to_string(pp.x) + ", " +
to_string(pp.y) + ")");
new nanogui::Label(params_,
"(" + to_string(values.cx) + ", " +
to_string(values.cy) + ")");
new nanogui::Widget(params_);
new nanogui::Label(params_,
"(" + to_string(100.0*(2.0*values.cx/double(imsize.width) - 1.0)) + "% , " +
to_string(100.0*(2.0*values.cy/double(imsize.height) - 1.0)) + "%)");
if (use_physical) new nanogui::Widget(params_);
new nanogui::Label(params_, "Field of View (x):");
new nanogui::Label(params_, to_string(fovx) + "°");
if (use_physical) new nanogui::Widget(params_);
new nanogui::Label(params_, "Field of View (y):");
new nanogui::Label(params_, to_string(fovy)+ "°");
if (use_physical) new nanogui::Widget(params_);
new nanogui::Label(params_, "Aspect ratio:");
new nanogui::Label(params_, to_string(ar));
if (use_physical) new nanogui::Widget(params_);
std::string pK;
std::string pP;
std::string pS;
auto& D = values.distCoeffs;
pK += "K1: " + to_string(D[0] ,3);
pK += ", K2: " + to_string(D[1] ,3);
pP += "P1: " + to_string(D[2], 3);
pP += ", P2: " + to_string(D[3], 3);
pK += ", K3: " + to_string(D[4], 3);
pK += ", K4: " + to_string(D[5] ,3);
pK += ", K5: " + to_string(D[6] ,3);
pK += ", K6: " + to_string(D[7] ,3);
pS += "S1: " + to_string(D[8] ,3);
pS += ", S2: " + to_string(D[9] ,3);
pS += ", S3: " + to_string(D[10] ,3);
pS += ", S4: " + to_string(D[11] ,3);
if (!pK.empty()) new nanogui::Label(dist_, pK);
if (!pP.empty()) new nanogui::Label(dist_, pP);
if (!pS.empty()) new nanogui::Label(dist_, pS);
}