summaryrefslogtreecommitdiffhomepage
path: root/tracker-trackhat
diff options
context:
space:
mode:
authorStanislaw Halik <sthalik@misaki.pl>2022-05-11 20:42:25 +0200
committerStanislaw Halik <sthalik@misaki.pl>2022-05-11 20:45:05 +0200
commit2b19c6c532c37c2eeb2547cf92f719197957ab72 (patch)
tree26fb69b756d8ae49fb87a18956eca26dd2c55005 /tracker-trackhat
parentbe42a20f03e3ccb1eaa390c072ac72cbe2d9f13b (diff)
tracker/trackhat: new tracker
Sponsored by: TrackHat
Diffstat (limited to 'tracker-trackhat')
-rw-r--r--tracker-trackhat/CMakeLists.txt16
-rw-r--r--tracker-trackhat/camera.cpp126
-rw-r--r--tracker-trackhat/dialog.cpp144
-rw-r--r--tracker-trackhat/dialog.hpp36
-rw-r--r--tracker-trackhat/dialog.ui522
-rw-r--r--tracker-trackhat/extractor.cpp21
-rw-r--r--tracker-trackhat/frame.cpp127
-rw-r--r--tracker-trackhat/handle.cpp72
-rw-r--r--tracker-trackhat/images/trackhat-64x64.pngbin0 -> 1430 bytes
-rw-r--r--tracker-trackhat/images/trackhat.icobin0 -> 3758 bytes
-rw-r--r--tracker-trackhat/images/trackhat.pngbin0 -> 14581 bytes
-rw-r--r--tracker-trackhat/lang/nl_NL.ts134
-rw-r--r--tracker-trackhat/lang/ru_RU.ts134
-rw-r--r--tracker-trackhat/lang/stub.ts134
-rw-r--r--tracker-trackhat/lang/zh_CN.ts134
-rw-r--r--tracker-trackhat/metadata.cpp39
-rw-r--r--tracker-trackhat/metadata.hpp21
-rw-r--r--tracker-trackhat/settings.cpp147
-rw-r--r--tracker-trackhat/tracker_trackhat.qrc5
-rw-r--r--tracker-trackhat/trackhat-res.qrc5
-rw-r--r--tracker-trackhat/trackhat.hpp180
21 files changed, 1997 insertions, 0 deletions
diff --git a/tracker-trackhat/CMakeLists.txt b/tracker-trackhat/CMakeLists.txt
new file mode 100644
index 00000000..483bf4c9
--- /dev/null
+++ b/tracker-trackhat/CMakeLists.txt
@@ -0,0 +1,16 @@
+if(WIN32)
+ include(opentrack-opencv)
+ find_package(OpenCV QUIET)
+ if(OpenCV_FOUND)
+ foreach(k core imgproc)
+ otr_install_lib("opencv_${k}" "${opentrack-libexec}")
+ endforeach()
+ set(SDK_TRACKHAT_SENSOR CACHE PATH "")
+ if(SDK_TRACKHAT_SENSOR)
+ include_directories("${SDK_TRACKHAT_SENSOR}/include" ${OpenCV_INCLUDE_DIRS})
+ link_directories("${SDK_TRACKHAT_SENSOR}/lib")
+ link_libraries(opencv_imgproc opencv_core opentrack-tracker-pt-base track-hat)
+ otr_module(tracker-trackhat)
+ endif()
+ endif()
+endif()
diff --git a/tracker-trackhat/camera.cpp b/tracker-trackhat/camera.cpp
new file mode 100644
index 00000000..dd1f3d3a
--- /dev/null
+++ b/tracker-trackhat/camera.cpp
@@ -0,0 +1,126 @@
+#include "trackhat.hpp"
+#include "compat/sleep.hpp"
+#include <cstdio>
+
+namespace trackhat_impl {
+
+TH_ErrorCode log_error(TH_ErrorCode error, const char* source,
+ const char* file, int line, const char* function)
+{
+ if (error == TH_ERROR_DEVICE_ALREADY_OPEN)
+ error = TH_SUCCESS;
+ if (error)
+ {
+ auto logger = QMessageLogger(file, line, function).warning();
+ logger << "tracker/trackhat: error" << (void*)-error << "in" << source;
+ }
+ return error;
+}
+
+} // ns trackhat_impl
+
+pt_camera::result trackhat_camera::get_info() const
+{
+ return {true, get_desired() };
+}
+
+pt_camera_info trackhat_camera::get_desired() const
+{
+ pt_camera_info ret = {};
+
+ ret.fov = sensor_fov;
+ ret.fps = 250;
+ ret.res_x = sensor_size;
+ ret.res_y = sensor_size;
+
+ return ret;
+}
+
+QString trackhat_camera::get_desired_name() const
+{
+ return QStringLiteral("TrackHat sensor");
+}
+
+QString trackhat_camera::get_active_name() const
+{
+ return get_desired_name();
+}
+
+void trackhat_camera::set_fov(pt_camera::f) {}
+void trackhat_camera::show_camera_settings() {}
+
+trackhat_camera::trackhat_camera()
+{
+ s.set_raii_dtor_state(false);
+ t.set_raii_dtor_state(false);
+
+ for (auto* slider : { &t.exposure, /*&t.threshold,*/ })
+ {
+ QObject::connect(slider, options::value_::value_changed<options::slider_value>(),
+ &sig, &trackhat_impl::setting_receiver::settings_changed,
+ Qt::DirectConnection);
+ }
+}
+
+trackhat_camera::~trackhat_camera()
+{
+ stop();
+}
+
+pt_camera::result trackhat_camera::get_frame(pt_frame& frame_)
+{
+ if (!device.ensure_connected())
+ goto error;
+
+ if (sig.test_and_clear() && !init_regs())
+ goto error;
+
+ set_pt_options();
+
+ {
+ trackHat_ExtendedPoints_t points;
+ if (!!th_check(trackHat_GetDetectedPointsExtended(&*device, &points)))
+ goto error;
+ auto& frame = *frame_.as<trackhat_frame>();
+ frame.init_points(points, t.min_pt_size, t.max_pt_size);
+ }
+
+ return {true, get_desired()};
+
+error:
+ stop();
+ return {false, {}};
+}
+
+static void log_handler(const char* file, int line, const char* function, char level, const char* str, size_t len)
+{
+ if (level != 'E')
+ return;
+ char file_[128];
+ snprintf(file_, std::size(file_), "trackhat/%s", file);
+ auto logger = QMessageLogger(file_, line, function).debug();
+ logger << "tracker/trackhat:";
+ logger.noquote() << QLatin1String(str, (int)len);
+}
+
+bool trackhat_camera::start(const pt_settings&)
+{
+ trackHat_SetDebugHandler(log_handler);
+
+ if constexpr(debug_mode)
+ trackHat_EnableDebugMode();
+ else
+ trackHat_DisableDebugMode();
+
+ if (!device.ensure_device_exists())
+ return false;
+
+ set_pt_options();
+
+ return true;
+}
+
+void trackhat_camera::stop()
+{
+ device.disconnect();
+}
diff --git a/tracker-trackhat/dialog.cpp b/tracker-trackhat/dialog.cpp
new file mode 100644
index 00000000..cfff03ba
--- /dev/null
+++ b/tracker-trackhat/dialog.cpp
@@ -0,0 +1,144 @@
+#include "dialog.hpp"
+
+using namespace options;
+
+trackhat_dialog::trackhat_dialog()
+{
+ ui.setupUi(this);
+ poll_tracker_info();
+ poll_timer.setInterval(100);
+
+ const std::tuple<QString, model_type> model_types[] = {
+ { tr("Cap"), model_cap },
+ { tr("Clip (left)"), model_clip_left },
+ { tr("Clip (right)"), model_clip_right },
+ { tr("Mini Clip (left)"), model_mini_clip_left },
+ { tr("Mini Clip (right)"), model_mini_clip_right },
+ { tr("Custom"), model_mystery_meat },
+ };
+
+ ui.model_type->clear();
+
+ for (const auto& [name, type] : model_types)
+ ui.model_type->addItem(QIcon{}, name, (QVariant)(int)type);
+
+ // model
+
+ tie_setting(t.model, ui.model_type);
+ tie_setting(t.min_pt_size, ui.min_point_size);
+ tie_setting(t.max_pt_size, ui.max_point_size);
+ tie_setting(t.point_filter_limit, ui.point_filter_limit);
+
+ // exposure
+
+ ui.exposure_slider->setMinimum((int)t.exposure->min());
+ ui.exposure_slider->setMaximum((int)t.exposure->max());
+
+ tie_setting(t.exposure, ui.exposure_slider);
+ ui.exposure_label->setValue((int)*t.exposure);
+ ui.point_filter_limit_label->setValue(*t.point_filter_limit);
+
+ connect(&t.exposure, value_::value_changed<slider_value>(), ui.exposure_label,
+ [this] { ui.exposure_label->setValue((int)*t.exposure); }, Qt::QueuedConnection);
+ connect(&t.point_filter_limit, value_::value_changed<slider_value>(), ui.point_filter_limit_label,
+ [this] { ui.point_filter_limit_label->setValue(*t.point_filter_limit); }, Qt::QueuedConnection);
+
+ // threshold
+
+#if 0
+ tie_setting(t.threshold, ui.threshold_slider);
+
+ ui.threshold_label->setValue((int)*t.threshold);
+
+ connect(&t.threshold, value_::value_changed<slider_value>(), ui.threshold_label, [=] {
+ ui.threshold_label->setValue((int)*t.threshold);
+ }, Qt::QueuedConnection);
+#endif
+
+ // point filter
+
+ tie_setting(t.enable_point_filter, ui.enable_point_filter);
+ tie_setting(t.point_filter_coefficient, ui.point_filter_slider);
+ ui.point_filter_label->setValue(*t.point_filter_coefficient);
+
+ connect(&t.point_filter_coefficient, value_::value_changed<slider_value>(), ui.point_filter_label,
+ [this] { ui.point_filter_label->setValue(*t.point_filter_coefficient); }, Qt::QueuedConnection);
+
+ tie_setting(t.point_filter_deadzone, ui.point_filter_deadzone);
+ ui.point_filter_deadzone_label->setValue(*t.point_filter_deadzone);
+
+ connect(&t.point_filter_deadzone, value_::value_changed<slider_value>(), ui.point_filter_deadzone_label,
+ [this] { ui.point_filter_deadzone_label->setValue(*t.point_filter_deadzone); }, Qt::QueuedConnection);
+
+ // stuff
+
+ connect(&poll_timer, &QTimer::timeout, this, &trackhat_dialog::poll_tracker_info);
+ connect(ui.buttonBox, &QDialogButtonBox::accepted, this, &trackhat_dialog::doOK);
+ connect(ui.buttonBox, &QDialogButtonBox::rejected, this, &trackhat_dialog::doCancel);
+}
+
+void trackhat_dialog::register_tracker(ITracker* tracker_)
+{
+ tracker = static_cast<Tracker_PT*>(tracker_);
+ poll_tracker_info();
+ poll_timer.start();
+}
+
+void trackhat_dialog::unregister_tracker()
+{
+ tracker = nullptr;
+ poll_tracker_info();
+ poll_timer.stop();
+ update_raw_data();
+}
+
+void trackhat_dialog::save()
+{
+ s.b->save();
+ t.b->save();
+}
+
+void trackhat_dialog::reload()
+{
+ s.b->reload();
+ t.b->reload();
+}
+
+void trackhat_dialog::doCancel() { reload(); close(); }
+void trackhat_dialog::doOK() { save(); close(); }
+
+trackhat_dialog::~trackhat_dialog()
+{
+}
+
+void trackhat_dialog::poll_tracker_info()
+{
+ if (!tracker)
+ ui.status_label->setText(tr("Status: Tracking stopped."));
+ else if (tracker->get_n_points() == 3)
+ ui.status_label->setText(tr("Status: %1 points detected. Good!").arg(tracker->get_n_points()));
+ else
+ ui.status_label->setText(tr("Status: %1 points detected. BAD!").arg(tracker->get_n_points()));
+ update_raw_data();
+}
+
+void trackhat_dialog::set_buttons_visible(bool x)
+{
+ ui.buttonBox->setVisible(x);
+ adjustSize();
+}
+void trackhat_dialog::update_raw_data()
+{
+ QLabel* labels[] = { ui.label_x, ui.label_y, ui.label_z, ui.label_yaw, ui.label_pitch, ui.label_roll };
+ if (tracker)
+ {
+ QString str; str.reserve(16);
+ double data[6] {};
+ tracker->data(data);
+ for (unsigned i = 0; i < std::size(labels); i++)
+ labels[i]->setText(str.sprintf("%.2f%s", data[i], i >= 3 ? "°" : " mm"));
+ }
+ else
+ for (QLabel* x : labels)
+ x->setText(QStringLiteral("-"));
+}
diff --git a/tracker-trackhat/dialog.hpp b/tracker-trackhat/dialog.hpp
new file mode 100644
index 00000000..35ca866b
--- /dev/null
+++ b/tracker-trackhat/dialog.hpp
@@ -0,0 +1,36 @@
+#pragma once
+#include "trackhat.hpp"
+#include "ui_dialog.h"
+#include "tracker-pt/ftnoir_tracker_pt.h"
+#include "api/plugin-api.hpp"
+#include <QTimer>
+
+class trackhat_dialog final : public ITrackerDialog
+{
+ Q_OBJECT
+
+protected:
+ Ui_trackhat_dialog ui;
+ Tracker_PT* tracker = nullptr;
+ QTimer poll_timer{this};
+
+ pt_settings s{trackhat_metadata::module_name};
+ trackhat_settings t;
+
+ void set_buttons_visible(bool x) override;
+ void update_raw_data();
+
+public:
+ trackhat_dialog();
+ ~trackhat_dialog() override;
+ void register_tracker(ITracker *tracker) override;
+ void unregister_tracker() override;
+ bool embeddable() noexcept override { return true; }
+ void save() override;
+ void reload() override;
+
+public slots:
+ void doOK();
+ void doCancel();
+ void poll_tracker_info();
+};
diff --git a/tracker-trackhat/dialog.ui b/tracker-trackhat/dialog.ui
new file mode 100644
index 00000000..30cf78db
--- /dev/null
+++ b/tracker-trackhat/dialog.ui
@@ -0,0 +1,522 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>trackhat_dialog</class>
+ <widget class="QDialog" name="trackhat_dialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>365</width>
+ <height>429</height>
+ </rect>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>365</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="windowTitle">
+ <string>TrackHat</string>
+ </property>
+ <property name="windowIcon">
+ <iconset resource="trackhat-res.qrc">
+ <normaloff>:/images/trackhat-64x64.png</normaloff>:/images/trackhat-64x64.png</iconset>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QGroupBox" name="groupBox">
+ <property name="title">
+ <string>Camera</string>
+ </property>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="0" column="2">
+ <widget class="QSpinBox" name="exposure_label">
+ <property name="focusPolicy">
+ <enum>Qt::NoFocus</enum>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ <property name="readOnly">
+ <bool>true</bool>
+ </property>
+ <property name="buttonSymbols">
+ <enum>QAbstractSpinBox::NoButtons</enum>
+ </property>
+ <property name="maximum">
+ <number>999</number>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QSlider" name="exposure_slider">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Maximum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximum">
+ <number>239</number>
+ </property>
+ <property name="pageStep">
+ <number>10</number>
+ </property>
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0">
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string>Exposure</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="groupBox_2">
+ <property name="title">
+ <string>Model</string>
+ </property>
+ <layout class="QGridLayout" name="gridLayout_2">
+ <item row="0" column="0">
+ <widget class="QLabel" name="label_6">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+ <horstretch>10</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Type</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QComboBox" name="model_type">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Maximum">
+ <horstretch>9</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="label_4">
+ <property name="text">
+ <string>Min point size</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="label_5">
+ <property name="text">
+ <string>Max point size</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1" alignment="Qt::AlignRight">
+ <widget class="QDoubleSpinBox" name="max_point_size">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Maximum" vsizetype="Maximum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ <property name="suffix">
+ <string> px</string>
+ </property>
+ <property name="decimals">
+ <number>1</number>
+ </property>
+ <property name="minimum">
+ <double>1.000000000000000</double>
+ </property>
+ <property name="maximum">
+ <double>999.000000000000000</double>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1" alignment="Qt::AlignRight">
+ <widget class="QDoubleSpinBox" name="min_point_size">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Maximum" vsizetype="Maximum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ <property name="suffix">
+ <string> px</string>
+ </property>
+ <property name="decimals">
+ <number>1</number>
+ </property>
+ <property name="minimum">
+ <double>1.000000000000000</double>
+ </property>
+ <property name="maximum">
+ <double>999.000000000000000</double>
+ </property>
+ <property name="value">
+ <double>1.000000000000000</double>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="groupBox_3">
+ <property name="title">
+ <string>Tracking</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <widget class="QWidget" name="widget" native="true">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Maximum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <layout class="QGridLayout" name="gridLayout_3">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item row="0" column="2">
+ <widget class="QDoubleSpinBox" name="point_filter_label">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Maximum" vsizetype="Maximum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="focusPolicy">
+ <enum>Qt::NoFocus</enum>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ <property name="readOnly">
+ <bool>true</bool>
+ </property>
+ <property name="buttonSymbols">
+ <enum>QAbstractSpinBox::NoButtons</enum>
+ </property>
+ <property name="suffix">
+ <string> px</string>
+ </property>
+ <property name="decimals">
+ <number>2</number>
+ </property>
+ <property name="maximum">
+ <double>300.000000000000000</double>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QSlider" name="point_filter_limit">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Maximum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximum">
+ <number>99</number>
+ </property>
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="label_8">
+ <property name="text">
+ <string>Limit</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="label_10">
+ <property name="text">
+ <string>Deadzone</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0">
+ <widget class="QCheckBox" name="enable_point_filter">
+ <property name="text">
+ <string>Point filter</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="2">
+ <widget class="QDoubleSpinBox" name="point_filter_limit_label">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Maximum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="focusPolicy">
+ <enum>Qt::NoFocus</enum>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ <property name="readOnly">
+ <bool>true</bool>
+ </property>
+ <property name="buttonSymbols">
+ <enum>QAbstractSpinBox::NoButtons</enum>
+ </property>
+ <property name="prefix">
+ <string/>
+ </property>
+ <property name="suffix">
+ <string/>
+ </property>
+ <property name="decimals">
+ <number>2</number>
+ </property>
+ <property name="maximum">
+ <double>1.000000000000000</double>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QSlider" name="point_filter_slider">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Maximum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximum">
+ <number>300</number>
+ </property>
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <widget class="QSlider" name="point_filter_deadzone">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Maximum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximum">
+ <number>100</number>
+ </property>
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="2">
+ <widget class="QDoubleSpinBox" name="point_filter_deadzone_label">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Maximum" vsizetype="Maximum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="focusPolicy">
+ <enum>Qt::NoFocus</enum>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ <property name="readOnly">
+ <bool>true</bool>
+ </property>
+ <property name="buttonSymbols">
+ <enum>QAbstractSpinBox::NoButtons</enum>
+ </property>
+ <property name="suffix">
+ <string> px</string>
+ </property>
+ <property name="decimals">
+ <number>2</number>
+ </property>
+ <property name="maximum">
+ <double>300.000000000000000</double>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="status_label">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Maximum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Status</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="groupBox_4">
+ <property name="title">
+ <string>Raw data</string>
+ </property>
+ <layout class="QGridLayout" name="gridLayout_4">
+ <item row="0" column="0">
+ <widget class="QLabel" name="label_9">
+ <property name="text">
+ <string>X</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QLabel" name="label_y">
+ <property name="text">
+ <string>TextLabel</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <widget class="QLabel" name="label_z">
+ <property name="text">
+ <string>TextLabel</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="2">
+ <widget class="QLabel" name="label_13">
+ <property name="text">
+ <string>Yaw</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="2">
+ <widget class="QLabel" name="label_15">
+ <property name="text">
+ <string>Roll</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="label_11">
+ <property name="text">
+ <string>Y</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLabel" name="label_x">
+ <property name="text">
+ <string>TextLabel</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="label_12">
+ <property name="text">
+ <string>Z</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="2">
+ <widget class="QLabel" name="label_14">
+ <property name="text">
+ <string>Pitch</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="3">
+ <widget class="QLabel" name="label_yaw">
+ <property name="text">
+ <string>TextLabel</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="3">
+ <widget class="QLabel" name="label_pitch">
+ <property name="text">
+ <string>TextLabel</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="3">
+ <widget class="QLabel" name="label_roll">
+ <property name="text">
+ <string>TextLabel</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Expanding</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>10</width>
+ <height>1</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <tabstops>
+ <tabstop>exposure_slider</tabstop>
+ <tabstop>model_type</tabstop>
+ <tabstop>min_point_size</tabstop>
+ <tabstop>max_point_size</tabstop>
+ <tabstop>enable_point_filter</tabstop>
+ <tabstop>point_filter_slider</tabstop>
+ <tabstop>point_filter_limit</tabstop>
+ </tabstops>
+ <resources>
+ <include location="trackhat-res.qrc"/>
+ </resources>
+ <connections/>
+</ui>
diff --git a/tracker-trackhat/extractor.cpp b/tracker-trackhat/extractor.cpp
new file mode 100644
index 00000000..58b9fd05
--- /dev/null
+++ b/tracker-trackhat/extractor.cpp
@@ -0,0 +1,21 @@
+#include "trackhat.hpp"
+#include <algorithm>
+#include <iterator>
+
+void trackhat_extractor::extract_points(const pt_frame& data,
+ pt_preview&, bool,
+ std::vector<vec2>& points)
+{
+ points.clear();
+ points.reserve(trackhat_camera::point_count);
+ const auto& copy = data.as_const<trackhat_frame>()->points;
+
+ for (const auto& pt : copy)
+ {
+ if (!pt.ok)
+ continue;
+ constexpr int sz = trackhat_camera::sensor_size;
+ auto [ x, y ] = to_screen_pos(pt.x, pt.y, sz, sz);
+ points.push_back({x, y});
+ }
+}
diff --git a/tracker-trackhat/frame.cpp b/tracker-trackhat/frame.cpp
new file mode 100644
index 00000000..d8fae002
--- /dev/null
+++ b/tracker-trackhat/frame.cpp
@@ -0,0 +1,127 @@
+#include "trackhat.hpp"
+#include <opencv2/imgproc.hpp>
+#include "compat/math.hpp"
+
+trackhat_preview::trackhat_preview(int w, int h)
+{
+ frame_bgr.create(h, w, CV_8UC3);
+ frame_bgra.create(h, w, CV_8UC4);
+}
+
+void trackhat_preview::set_last_frame(const pt_frame& frame_)
+{
+ center = {-1, -1};
+ points = frame_.as_const<trackhat_frame>()->points;
+}
+
+void trackhat_preview::draw_head_center(pt_pixel_pos_mixin::f x, pt_pixel_pos_mixin::f y)
+{
+ center = {x, y};
+}
+
+QImage trackhat_preview::get_bitmap()
+{
+ frame_bgr.setTo({0});
+
+ draw_points();
+ draw_center();
+
+ cv::cvtColor(frame_bgr, frame_bgra, cv::COLOR_BGR2BGRA);
+
+ return QImage((const unsigned char*) frame_bgra.data,
+ frame_bgra.cols, frame_bgra.rows,
+ (int)frame_bgra.step.p[0],
+ QImage::Format_ARGB32);
+}
+
+void trackhat_preview::draw_center()
+{
+ if (center == numeric_types::vec2(-1, -1))
+ return;
+
+ auto [px_, py_] = to_pixel_pos(center[0], center[1], frame_bgr.cols, frame_bgr.rows);
+ int px = iround(px_), py = iround(py_);
+
+ const f dpi = (f)frame_bgr.cols / f(320);
+ constexpr int len_ = 9;
+ int len = iround(len_ * dpi);
+
+ static const cv::Scalar color(0, 255, 255);
+ cv::line(frame_bgr,
+ cv::Point(px - len, py),
+ cv::Point(px + len, py),
+ color, 1);
+ cv::line(frame_bgr,
+ cv::Point(px, py - len),
+ cv::Point(px, py + len),
+ color, 1);
+}
+
+void trackhat_preview::draw_points()
+{
+ for (const auto& pt : points)
+ {
+ if (pt.brightness == 0)
+ continue;
+
+ constexpr int sz = trackhat_camera::sensor_size;
+ constexpr f scaling_factor = 10;
+ const int x = pt.x * frame_bgr.cols / sz, y = pt.y * frame_bgr.rows / sz;
+ const f dpi = (f)frame_bgr.cols / f(320);
+ const int W = std::max(1, iround(pt.W * frame_bgr.cols * scaling_factor / sz)),
+ H = std::max(1, iround(pt.H * frame_bgr.rows * scaling_factor / sz));
+ const auto point_color = progn(double c = pt.brightness; return cv::Scalar{c, c, c};);
+ const auto outline_color = pt.ok
+ ? cv::Scalar{255, 255, 0}
+ : cv::Scalar{192, 192, 192};
+
+ cv::ellipse(frame_bgr, {x, y}, {W, H},
+ 0, 0, 360, point_color, -1, cv::LINE_AA);
+ cv::ellipse(frame_bgr, {x, y}, {iround(W + 2*dpi), iround(H + 2*dpi)},
+ 0, 0, 360, outline_color, iround(dpi), cv::LINE_AA);
+
+ char buf[16];
+ std::snprintf(buf, sizeof(buf), "%dpx", pt.area);
+ auto text_color = pt.ok
+ ? cv::Scalar(0, 0, 255)
+ : cv::Scalar(160, 160, 160);
+ const int offx = iround(W + 9*dpi), offy = H*3/2;
+
+ cv::putText(frame_bgr, buf, {x+offx, y+offy},
+ cv::FONT_HERSHEY_PLAIN, iround(dpi), text_color,
+ 1);
+ }
+}
+
+void trackhat_frame::init_points(const trackHat_ExtendedPoints_t& points_, double min_size, double max_size)
+{
+ trackHat_ExtendedPoints_t copy = points_;
+ points = {};
+
+ std::sort(std::begin(copy.m_point), std::end(copy.m_point),
+ [](trackHat_ExtendedPoint_t p1, trackHat_ExtendedPoint_t p2) {
+ return p1.m_averageBrightness > p2.m_averageBrightness;
+ });
+
+ unsigned i = 0;
+
+ for (const trackHat_ExtendedPoint_t& pt : copy.m_point)
+ {
+ if (pt.m_averageBrightness == 0)
+ continue;
+
+ point p = {};
+
+ if (pt.m_area >= min_size && pt.m_area <= max_size)
+ p.ok = true;
+
+ p.brightness = pt.m_averageBrightness;
+ p.area = pt.m_area;
+ p.W = std::max(1, pt.m_boundryRigth - pt.m_boundryLeft);
+ p.H = std::max(1, pt.m_boundryDown - pt.m_boundryUp);
+ p.x = trackhat_camera::sensor_size-1-pt.m_coordinateX;
+ p.y = pt.m_coordinateY;
+
+ points[i++] = p;
+ }
+}
diff --git a/tracker-trackhat/handle.cpp b/tracker-trackhat/handle.cpp
new file mode 100644
index 00000000..42902e76
--- /dev/null
+++ b/tracker-trackhat/handle.cpp
@@ -0,0 +1,72 @@
+#undef NDEBUG
+#include "trackhat.hpp"
+#include "compat/sleep.hpp"
+#include "compat/timer.hpp"
+#include <cassert>
+
+bool camera_handle::ensure_connected()
+{
+ if (state_ >= st_streaming)
+ return true;
+ else if (state_ == st_stopped)
+ return false;
+
+ Timer t;
+
+ constexpr int max_attempts = 5;
+
+ if (!ensure_device_exists())
+ goto error;
+
+ for (int i = 0; i < max_attempts; i++)
+ {
+ if (!th_check(trackHat_Connect(&device_, TH_FRAME_EXTENDED)))
+ {
+ state_ = st_streaming;
+ if (int ms = (int)t.elapsed_ms(); ms > 1000)
+ qDebug() << "tracker/trackhat: connecting took" << ms << "ms";
+ return true;
+ }
+
+ auto dbg = qDebug();
+ dbg << "tracker/trackhat: connect failed, retry";
+ dbg.space(); dbg.nospace();
+ dbg << (i+1) << "/" << max_attempts;
+ portable::sleep(50);
+ }
+
+error:
+ disconnect();
+ return false;
+}
+
+bool camera_handle::ensure_device_exists()
+{
+ switch (state_)
+ {
+ case st_streaming:
+ return true;
+ case st_detected:
+ disconnect();
+ [[fallthrough]];
+ case st_stopped:
+ assert(!th_check(trackHat_Initialize(&device_)) && device_.m_pInternal);
+ if (auto error = th_check(trackHat_DetectDevice(&device_)); error)
+ {
+ disconnect();
+ return false;
+ }
+ state_ = st_detected;
+ return true;
+ }
+}
+
+void camera_handle::disconnect()
+{
+ state_ = st_stopped;
+ if (device_.m_pInternal)
+ {
+ (void)!th_check(trackHat_Disconnect(&device_));
+ trackHat_Deinitialize(&device_);
+ }
+}
diff --git a/tracker-trackhat/images/trackhat-64x64.png b/tracker-trackhat/images/trackhat-64x64.png
new file mode 100644
index 00000000..9e856c23
--- /dev/null
+++ b/tracker-trackhat/images/trackhat-64x64.png
Binary files differ
diff --git a/tracker-trackhat/images/trackhat.ico b/tracker-trackhat/images/trackhat.ico
new file mode 100644
index 00000000..b5f34db3
--- /dev/null
+++ b/tracker-trackhat/images/trackhat.ico
Binary files differ
diff --git a/tracker-trackhat/images/trackhat.png b/tracker-trackhat/images/trackhat.png
new file mode 100644
index 00000000..4f17de81
--- /dev/null
+++ b/tracker-trackhat/images/trackhat.png
Binary files differ
diff --git a/tracker-trackhat/lang/nl_NL.ts b/tracker-trackhat/lang/nl_NL.ts
new file mode 100644
index 00000000..73e375fc
--- /dev/null
+++ b/tracker-trackhat/lang/nl_NL.ts
@@ -0,0 +1,134 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="nl_NL">
+<context>
+ <name>trackhat_dialog</name>
+ <message>
+ <source>Cap</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Clip (left)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Clip (right)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Mini Clip (left)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Mini Clip (right)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Custom</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Status: Tracking stopped.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Status: %1 points detected. Good!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Status: %1 points detected. BAD!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>TrackHat</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Camera</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Exposure</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Model</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Type</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Min point size</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Max point size</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source> px</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Tracking</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Point filter</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Limit</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Status</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Raw data</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>X</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>TextLabel</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Yaw</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Roll</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Y</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Z</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Pitch</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Deadzone</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>trackhat_module</name>
+ <message>
+ <source>TrackHat Point Tracker</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+</TS>
diff --git a/tracker-trackhat/lang/ru_RU.ts b/tracker-trackhat/lang/ru_RU.ts
new file mode 100644
index 00000000..091c2101
--- /dev/null
+++ b/tracker-trackhat/lang/ru_RU.ts
@@ -0,0 +1,134 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="ru_RU">
+<context>
+ <name>trackhat_dialog</name>
+ <message>
+ <source>Cap</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Clip (left)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Clip (right)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Mini Clip (left)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Mini Clip (right)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Custom</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Status: Tracking stopped.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Status: %1 points detected. Good!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Status: %1 points detected. BAD!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>TrackHat</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Camera</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Exposure</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Model</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Type</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Min point size</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Max point size</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source> px</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Tracking</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Point filter</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Limit</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Status</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Raw data</source>
+ <translation>Исходные данные</translation>
+ </message>
+ <message>
+ <source>X</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>TextLabel</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Yaw</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Roll</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Y</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Z</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Pitch</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Deadzone</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>trackhat_module</name>
+ <message>
+ <source>TrackHat Point Tracker</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+</TS>
diff --git a/tracker-trackhat/lang/stub.ts b/tracker-trackhat/lang/stub.ts
new file mode 100644
index 00000000..548138a3
--- /dev/null
+++ b/tracker-trackhat/lang/stub.ts
@@ -0,0 +1,134 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="stub">
+<context>
+ <name>trackhat_dialog</name>
+ <message>
+ <source>TrackHat</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Camera</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Exposure</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Model</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Type</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Min point size</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Max point size</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source> px</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Tracking</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Limit</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Deadzone</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Point filter</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Status</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Raw data</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>X</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>TextLabel</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Yaw</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Roll</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Y</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Z</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Pitch</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Cap</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Clip (left)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Clip (right)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Mini Clip (left)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Mini Clip (right)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Custom</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Status: Tracking stopped.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Status: %1 points detected. Good!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Status: %1 points detected. BAD!</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>trackhat_module</name>
+ <message>
+ <source>TrackHat Point Tracker</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+</TS>
diff --git a/tracker-trackhat/lang/zh_CN.ts b/tracker-trackhat/lang/zh_CN.ts
new file mode 100644
index 00000000..cb881fa8
--- /dev/null
+++ b/tracker-trackhat/lang/zh_CN.ts
@@ -0,0 +1,134 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="zh_CN">
+<context>
+ <name>trackhat_dialog</name>
+ <message>
+ <source>Cap</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Clip (left)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Clip (right)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Mini Clip (left)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Mini Clip (right)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Custom</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Status: Tracking stopped.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Status: %1 points detected. Good!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Status: %1 points detected. BAD!</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>TrackHat</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Camera</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Exposure</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Model</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Type</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Min point size</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Max point size</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source> px</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Tracking</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Point filter</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Limit</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Status</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Raw data</source>
+ <translation>跟踪器原始数据</translation>
+ </message>
+ <message>
+ <source>X</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>TextLabel</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Yaw</source>
+ <translation>偏航</translation>
+ </message>
+ <message>
+ <source>Roll</source>
+ <translation>横滚</translation>
+ </message>
+ <message>
+ <source>Y</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Z</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Pitch</source>
+ <translation>仰俯</translation>
+ </message>
+ <message>
+ <source>Deadzone</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>trackhat_module</name>
+ <message>
+ <source>TrackHat Point Tracker</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+</TS>
diff --git a/tracker-trackhat/metadata.cpp b/tracker-trackhat/metadata.cpp
new file mode 100644
index 00000000..8c6f4de0
--- /dev/null
+++ b/tracker-trackhat/metadata.cpp
@@ -0,0 +1,39 @@
+#include "metadata.hpp"
+#include "api/plugin-api.hpp"
+#include "trackhat.hpp"
+#include "dialog.hpp"
+
+// XXX TODO
+const QString trackhat_metadata::module_name = QStringLiteral("tracker-trackhat/pt");
+
+pt_runtime_traits::pointer<pt_camera> trackhat_metadata::make_camera() const
+{
+ return std::make_shared<trackhat_camera>();
+}
+
+pt_runtime_traits::pointer<pt_point_extractor> trackhat_metadata::make_point_extractor() const
+{
+ return std::make_shared<trackhat_extractor>();
+}
+
+pt_runtime_traits::pointer<pt_frame> trackhat_metadata::make_frame() const
+{
+ return std::make_shared<trackhat_frame>();
+}
+
+pt_runtime_traits::pointer<pt_preview> trackhat_metadata::make_preview(int w, int h) const
+{
+ return std::make_shared<trackhat_preview>(w, h);
+}
+
+QString trackhat_metadata::get_module_name() const
+{
+ return trackhat_metadata::module_name;
+}
+
+trackhat_pt::trackhat_pt() :
+ Tracker_PT(pt_runtime_traits::pointer<pt_runtime_traits>(new trackhat_metadata))
+{
+}
+
+OPENTRACK_DECLARE_TRACKER(trackhat_pt, trackhat_dialog, trackhat_module)
diff --git a/tracker-trackhat/metadata.hpp b/tracker-trackhat/metadata.hpp
new file mode 100644
index 00000000..c0f7567c
--- /dev/null
+++ b/tracker-trackhat/metadata.hpp
@@ -0,0 +1,21 @@
+#pragma once
+#include "trackhat.hpp"
+#include "../tracker-pt/ftnoir_tracker_pt.h"
+
+class trackhat_pt final : public Tracker_PT
+{
+ Q_OBJECT
+
+public:
+ trackhat_pt();
+};
+
+class trackhat_module final : public Metadata
+{
+ Q_OBJECT
+
+public:
+ QString name() override { return tr("TrackHat Point Tracker"); }
+ QIcon icon() override { return QIcon(":/images/trackhat-64x64.png"); }
+ static const QString module_name;
+};
diff --git a/tracker-trackhat/settings.cpp b/tracker-trackhat/settings.cpp
new file mode 100644
index 00000000..ec46b1e9
--- /dev/null
+++ b/tracker-trackhat/settings.cpp
@@ -0,0 +1,147 @@
+#include "trackhat.hpp"
+#include "compat/sleep.hpp"
+#include "compat/timer.hpp"
+
+namespace trackhat_impl {
+
+trackhat_settings::trackhat_settings() : opts{"tracker-trackhat"}
+{
+}
+int trackhat_settings::effective_exposure() const
+{
+ return std::clamp((int)*exposure, min_exposure, max_exposure);
+}
+int trackhat_settings::effective_gain() const
+{
+ return min_gain + std::clamp((int)*exposure - max_exposure, 0, max_gain - min_gain);
+}
+
+void setting_receiver::settings_changed()
+{
+ changed = true;
+}
+
+bool setting_receiver::test_and_clear()
+{
+ bool x = true;
+ return changed.compare_exchange_strong(x, false);
+}
+
+setting_receiver::setting_receiver(bool value) : changed{value}
+{
+}
+
+} // ns trackhat_impl
+
+void trackhat_camera::set_pt_options()
+{
+ s.min_point_size = t.min_pt_size;
+ s.max_point_size = t.max_pt_size;
+
+ switch (t.model)
+ {
+ default:
+ case model_cap:
+ s.t_MH_x = 0; s.t_MH_y = 0; s.t_MH_z = 0;
+ break;
+ case model_mystery_meat:
+ case model_clip_left:
+ case model_mini_clip_left:
+ s.t_MH_x = -135; s.t_MH_y = 0; s.t_MH_z = 0;
+ break;
+ case model_clip_right:
+ case model_mini_clip_right:
+ s.t_MH_x = 135; s.t_MH_y = 0; s.t_MH_z = 0;
+ break;
+ }
+
+ switch (t.model)
+ {
+ default:
+ eval_once(qDebug() << "tracker/trackhat: unknown model");
+ [[fallthrough]];
+ case model_clip_left:
+ case model_clip_right:
+ s.clip_tz = 27; s.clip_ty = 43; s.clip_by = 62; s.clip_bz = 74;
+ break;
+ case model_mini_clip_left:
+ case model_mini_clip_right:
+ s.clip_tz = 13; s.clip_ty = 42; s.clip_by = 60; s.clip_bz = 38;
+ break;
+ case model_cap:
+ s.cap_x = 60; s.cap_y = 90; s.cap_z = 95;
+ break;
+ case model_mystery_meat:
+ break;
+ }
+
+ s.dynamic_pose = t.model == model_cap;
+ s.init_phase_timeout = 500;
+
+ s.camera_name = QStringLiteral("TrackHat Sensor (WIP)");
+
+ s.active_model_panel = t.model == model_cap ? 1 : 0;
+ s.enable_point_filter = t.enable_point_filter;
+ s.point_filter_coefficient = *t.point_filter_coefficient;
+ s.point_filter_limit = *t.point_filter_limit;
+ s.point_filter_deadzone = *t.point_filter_deadzone;
+}
+
+bool trackhat_camera::init_regs()
+{
+ auto exp = (uint8_t)t.effective_exposure();
+ auto exp2 = (uint8_t)(exp == 0xff ? 0xf0 : 0xff);
+ auto thres = (uint8_t)0xfe;
+ auto thres2 = (uint8_t)3;
+
+ auto gain = (uint8_t)t.effective_gain();
+ auto gain_c = (uint8_t)(gain/0x10);
+ gain %= 0x10; gain_c %= 4;
+
+ trackHat_SetRegisterGroup_t regs = {
+ {
+ { 0x0c, 0x0f, exp2 }, // exposure lo
+ { 0x0c, 0x10, exp }, // exposure hi
+ { 0x00, 0x0b, 0xff }, // blob area max size
+ { 0x00, 0x0c, 0x03 }, // blob area min size
+ { 0x0c, 0x08, gain }, // gain
+ { 0x0c, 0x0c, gain_c }, // gain multiplier
+ { 0x0c, 0x47, thres }, // min brightness
+ { 0x00, 0x0f, thres2 }, // brightness margin, formula is `thres >= px > thres - fuzz'
+ { 0x00, 0x01, 0x01 }, // bank0 sync
+ { 0x01, 0x01, 0x01 }, // bank1 sync
+ },
+ 10
+ };
+
+ Timer t;
+
+ constexpr int max = 5;
+ int i = 0;
+ for (i = 0; i < max; i++)
+ {
+ TH_ErrorCode status = TH_SUCCESS;
+ status = th_check(trackHat_SetRegisterGroupValue(&*device, &regs));
+ if (status == TH_SUCCESS)
+ break;
+ else if (status != TH_FAILED_TO_SET_REGISTER &&
+ status != TH_ERROR_DEVICE_COMMUNICATION_TIMEOUT)
+ return false;
+ else
+ {
+ auto dbg = qDebug();
+ dbg << "tracker/trackhat: set register retry attempt";
+ dbg.space(); dbg.nospace();
+ dbg << i << "/" << max;
+ portable::sleep(50);
+ }
+ }
+
+ if (i == max)
+ return false;
+
+ if (int elapsed = (int)t.elapsed_ms(); elapsed > 100)
+ qDebug() << "tracker/trackhat: setting registers took" << elapsed << "ms";
+
+ return true;
+}
diff --git a/tracker-trackhat/tracker_trackhat.qrc b/tracker-trackhat/tracker_trackhat.qrc
new file mode 100644
index 00000000..d54010a0
--- /dev/null
+++ b/tracker-trackhat/tracker_trackhat.qrc
@@ -0,0 +1,5 @@
+<RCC>
+ <qresource prefix="/">
+ <file>images/trackhat-64x64.png</file>
+ </qresource>
+</RCC>
diff --git a/tracker-trackhat/trackhat-res.qrc b/tracker-trackhat/trackhat-res.qrc
new file mode 100644
index 00000000..9aeb8879
--- /dev/null
+++ b/tracker-trackhat/trackhat-res.qrc
@@ -0,0 +1,5 @@
+<RCC>
+ <qresource>
+ <file>images/trackhat-64x64.png</file>
+ </qresource>
+</RCC>
diff --git a/tracker-trackhat/trackhat.hpp b/tracker-trackhat/trackhat.hpp
new file mode 100644
index 00000000..d94841d2
--- /dev/null
+++ b/tracker-trackhat/trackhat.hpp
@@ -0,0 +1,180 @@
+#pragma once
+
+#include "../tracker-pt/pt-api.hpp"
+#include "compat/macros.hpp"
+#include "options/options.hpp"
+
+#include <track_hat_driver.h>
+
+#include <array>
+#include <atomic>
+#include <opencv2/core/mat.hpp>
+
+enum model_type : int
+{
+ model_cap = 1,
+ model_clip_left,
+ model_clip_right,
+ model_mini_clip_left,
+ model_mini_clip_right,
+ model_mystery_meat,
+};
+
+namespace trackhat_impl
+{
+using namespace options;
+
+TH_ErrorCode log_error(TH_ErrorCode error, const char* source, const char* file, int line, const char* function);
+#define th_check_(expr, expr2) ::trackhat_impl::log_error((expr), expr2)
+#define th_check(expr) ::trackhat_impl::log_error((expr), #expr, __FILE__, __LINE__, function_name)
+
+struct trackhat_settings : opts
+{
+ static constexpr int min_gain = 16, max_gain = 47,
+ min_exposure = 0x10, max_exposure = 0xff;
+ static constexpr int num_exposure_steps = max_gain + max_exposure - min_gain - min_exposure;
+ int effective_exposure() const;
+ int effective_gain() const;
+ trackhat_settings();
+ value<slider_value> exposure{b, "exposure", {min_exposure, min_exposure, max_exposure + max_gain - min_gain}};
+ //value<slider_value> threshold{b, "threshold", {0x97, 64, 0xfe}};
+ value<model_type> model{b, "model", model_mini_clip_left};
+ value<double> min_pt_size{b, "min-point-size", 10};
+ value<double> max_pt_size{b, "max-point-size", 50};
+ value<bool> enable_point_filter{b, "enable-point-filter", true };
+ value<slider_value> point_filter_coefficient{b, "point-filter-coefficient", { 1.5, 1, 4 }};
+ value<slider_value> point_filter_limit { b, "point-filter-limit", { 0.1, 0.01, 1 }};
+ value<slider_value> point_filter_deadzone { b, "point-filter-deadzone", {0, 0, 1}};
+};
+
+class setting_receiver : public QObject
+{
+ Q_OBJECT
+
+public:
+ explicit setting_receiver(bool value);
+ bool test_and_clear();
+public slots:
+ void settings_changed();
+private:
+ std::atomic<bool> changed{false};
+};
+
+} // ns trackhat_impl
+
+using typename trackhat_impl::trackhat_settings;
+
+struct trackhat_metadata final : pt_runtime_traits
+{
+ pt_runtime_traits::pointer<pt_camera> make_camera() const override;
+ pt_runtime_traits::pointer<pt_point_extractor> make_point_extractor() const override;
+ pt_runtime_traits::pointer<pt_frame> make_frame() const override;
+ pt_runtime_traits::pointer<pt_preview> make_preview(int w, int h) const override;
+ QString get_module_name() const override;
+
+ OTR_DISABLE_MOVE_COPY(trackhat_metadata);
+
+ trackhat_metadata() = default;
+ ~trackhat_metadata() override = default;
+
+ static const QString module_name;
+};
+
+struct point
+{
+ int brightness = 0, area, x, y, W, H;
+ bool ok = false;
+};
+
+struct camera_handle final
+{
+ OTR_DISABLE_MOVE_COPY(camera_handle);
+ trackHat_Device_t* operator->() { return &device_; }
+ trackHat_Device_t& operator*() { return device_; }
+
+ camera_handle() = default;
+ ~camera_handle() = default;
+
+ [[nodiscard]] bool ensure_connected();
+ [[nodiscard]] bool ensure_device_exists();
+ void disconnect();
+private:
+ trackHat_Device_t device_ = {};
+ enum state { st_stopped, st_detected, st_streaming, };
+ state state_ = st_stopped;
+};
+
+struct trackhat_camera final : pt_camera
+{
+ trackhat_camera();
+ ~trackhat_camera() override;
+
+ OTR_DISABLE_MOVE_COPY(trackhat_camera);
+
+ bool start(const pt_settings& s) override;
+ void stop() override;
+
+ pt_camera::result get_frame(pt_frame& frame) override;
+ pt_camera::result get_info() const override;
+ pt_camera_info get_desired() const override;
+
+ QString get_desired_name() const override;
+ QString get_active_name() const override;
+
+ void set_fov(f value) override;
+ void show_camera_settings() override;
+
+ f deadzone_amount() const override { return 10; }
+
+ static constexpr int sensor_size = 2940;
+ static constexpr int sensor_fov = 52;
+ static constexpr int point_count = TRACK_HAT_NUMBER_OF_POINTS;
+ static constexpr bool debug_mode = true;
+
+private:
+ trackhat_impl::setting_receiver sig{true};
+
+ [[nodiscard]] bool init_regs();
+ void set_pt_options();
+
+ camera_handle device;
+ pt_settings s{trackhat_metadata::module_name};
+ trackhat_settings t;
+};
+
+struct trackhat_frame final : pt_frame
+{
+ void init_points(const trackHat_ExtendedPoints_t& points, double min_size, double max_size);
+ trackhat_frame() = default;
+ ~trackhat_frame() override = default;
+
+ std::array<point, trackhat_camera::point_count> points;
+};
+
+struct trackhat_preview final : pt_preview
+{
+ QImage get_bitmap() override;
+ void draw_head_center(f x, f y) override;
+ void set_last_frame(const pt_frame&) override; // NOLINT(misc-unconventional-assign-operator)
+
+ trackhat_preview(int w, int h);
+ ~trackhat_preview() override = default;
+ void draw_points();
+ void draw_center();
+
+ OTR_DISABLE_MOVE_COPY(trackhat_preview);
+
+ cv::Mat frame_bgr, frame_bgra;
+ numeric_types::vec2 center{-1, -1};
+ std::array<point, trackhat_camera::point_count> points;
+};
+
+struct trackhat_extractor final : pt_point_extractor
+{
+ void extract_points(const pt_frame& data, pt_preview&, bool, std::vector<vec2>& points) override;
+
+ OTR_DISABLE_MOVE_COPY(trackhat_extractor);
+
+ trackhat_extractor() = default;
+ ~trackhat_extractor() override = default;
+};