diff options
author | Michael Welter <michael@welter-4d.de> | 2021-03-13 11:27:03 +0100 |
---|---|---|
committer | Michael Welter <michael@welter-4d.de> | 2021-05-02 18:20:41 +0200 |
commit | 7e582247ab0942398225b3e190d9fadafcf19950 (patch) | |
tree | 394d8ca7c1aedf058208191b19b8e3e9fe4d134c /tracker-neuralnet/ftnoir_tracker_neuralnet.cpp | |
parent | 058942f40e17e091b91df5436d771d61203ccc73 (diff) |
Add a face alignment based tracker
Powered by AI!
Models were generated with code from
https://github.com/DaWelter/neuralnet-tracker-traincode/releases/tag/v0.1
Diffstat (limited to 'tracker-neuralnet/ftnoir_tracker_neuralnet.cpp')
-rw-r--r-- | tracker-neuralnet/ftnoir_tracker_neuralnet.cpp | 917 |
1 files changed, 917 insertions, 0 deletions
diff --git a/tracker-neuralnet/ftnoir_tracker_neuralnet.cpp b/tracker-neuralnet/ftnoir_tracker_neuralnet.cpp new file mode 100644 index 00000000..2fad17aa --- /dev/null +++ b/tracker-neuralnet/ftnoir_tracker_neuralnet.cpp @@ -0,0 +1,917 @@ +/* Copyright (c) 2021 Michael Welter <michael@welter-4d.de> + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + */ + +#include "ftnoir_tracker_neuralnet.h" +#include "compat/sleep.hpp" +#include "compat/math-imports.hpp" +#include "cv/init.hpp" +#include <opencv2/core.hpp> +#include <opencv2/core/hal/interface.h> +#include <opencv2/core/types.hpp> +#include <opencv2/calib3d.hpp> +#include <opencv2/imgcodecs.hpp> +#include "compat/timer.hpp" +#include <omp.h> + +#ifdef _MSC_VER +# pragma warning(disable : 4702) +#endif + +#include <QMutexLocker> +#include <QDebug> +#include <QFile> + +#include <cstdio> +#include <cmath> +#include <algorithm> +#include <chrono> + +// Some demo code for onnx +// https://github.com/microsoft/onnxruntime/blob/master/csharp/test/Microsoft.ML.OnnxRuntime.EndToEndTests.Capi/C_Api_Sample.cpp +// https://github.com/leimao/ONNX-Runtime-Inference/blob/main/src/inference.cpp + +namespace +{ + +using numeric_types::vec3; +using numeric_types::vec2; +using numeric_types::mat33; + +// Minimal difference if at all going from 1 to 2 threads. +static constexpr int num_threads = 1; + + +#if _MSC_VER +std::wstring convert(const QString &s) { return s.toStdWString(); } +#else +std::string convert(const QString &s) { return s.toStdString(); } +#endif + + +float sigmoid(float x) +{ + return 1.f/(1.f + std::exp(-x)); +} + + +template<class T> +cv::Rect_<T> squarize(const cv::Rect_<T> &r) +{ + cv::Point_<T> c{r.x + r.width/T(2), r.y + r.height/T(2)}; + const T sz = std::max(r.height, r.width); + return {c.x - sz/T(2), c.y - sz/T(2), sz, sz}; +} + + +int compute_padding(const cv::Rect &r, int w, int h) +{ + using std::max; + return max({ + max(-r.x, 0), + max(-r.y, 0), + max(r.x+r.width-w, 0), + max(r.y+r.height-h, 0) + }); +} + + +cv::Rect2f unnormalize(const cv::Rect2f &r, int h, int w) +{ + auto unnorm = [](float x) -> float { return 0.5*(x+1); }; + auto tl = r.tl(); + auto br = r.br(); + auto x0 = unnorm(tl.x)*w; + auto y0 = unnorm(tl.y)*h; + auto x1 = unnorm(br.x)*w; + auto y1 = unnorm(br.y)*h; + return { + x0, y0, x1-x0, y1-y0 + }; +} + +cv::Point2f normalize(const cv::Point2f &p, int h, int w) +{ + return { + p.x/w*2.f-1.f, + p.y/h*2.f-1.f + }; +} + + +mat33 rotation_from_two_vectors(const vec3 &a, const vec3 &b) +{ + vec3 axis = a.cross(b); + const float len_a = cv::norm(a); + const float len_b = cv::norm(b); + const float len_axis = cv::norm(axis); + const float sin_angle = std::clamp(len_axis / (len_a * len_b), -1.f, 1.f); + const float angle = std::asin(sin_angle); + axis *= angle/(1.e-12 + len_axis); + mat33 out; + cv::Rodrigues(axis, out); + return out; +} + + +/* Computes correction due to head being off screen center. + x, y: In screen space, i.e. in [-1,1] + focal_length_x: In screen space +*/ +mat33 compute_rotation_correction(const cv::Point2f &p, float focal_length_x) +{ + return rotation_from_two_vectors( + {1.f,0.f,0.f}, + {focal_length_x, p.y, p.x}); +} + + +mat33 quaternion_to_mat33(const std::array<float,4> quat) +{ + mat33 m; + const float w = quat[0]; + const float i = quat[1]; + const float j = quat[2]; + const float k = quat[3]; + m(0,0) = 1.f - 2.f*(j*j + k*k); + m(1,0) = 2.f*(i*j + k*w); + m(2,0) = 2.f*(i*k - j*w); + m(0,1) = 2.f*(i*j - k*w); + m(1,1) = 1.f - 2.f*(i*i + k*k); + m(2,1) = 2.f*(j*k + i*w); + m(0,2) = 2.f*(i*k + j*w); + m(1,2) = 2.f*(j*k - i*w); + m(2,2) = 1.f - 2.f*(i*i + j*j); + return m; +} + + +template<class T> +T iou(const cv::Rect_<T> &a, const cv::Rect_<T> &b) +{ + auto i = a & b; + return double{i.area()} / (a.area()+b.area()-i.area()); +} + + +} // namespace + + +namespace neuralnet_tracker_ns +{ + + +int enum_to_fps(int value) +{ + switch (value) + { + case fps_30: return 30; + case fps_60: return 60; + default: [[fallthrough]]; + case fps_default: return 0; + } +} + + +Localizer::Localizer(Ort::MemoryInfo &allocator_info, Ort::Session &&session) : + session{std::move(session)}, + scaled_frame(input_img_height, input_img_width, CV_8U), + input_mat(input_img_height, input_img_width, CV_32F) +{ + // Only works when input_mat does not reallocated memory ...which it should not. + // Non-owning memory reference to input_mat? + // Note: shape = (bach x channels x h x w) + const std::int64_t input_shape[4] = { 1, 1, input_img_height, input_img_width }; + input_val = Ort::Value::CreateTensor<float>(allocator_info, input_mat.ptr<float>(0), input_mat.total(), input_shape, 4); + + const std::int64_t output_shape[2] = { 1, 5 }; + output_val = Ort::Value::CreateTensor<float>(allocator_info, results.data(), results.size(), output_shape, 2); +} + + +std::pair<float, cv::Rect2f> Localizer::run( + const cv::Mat &frame) +{ + auto p = input_mat.ptr(0); + + cv::resize(frame, scaled_frame, { input_img_width, input_img_height }, 0, 0, cv::INTER_AREA); + scaled_frame.convertTo(input_mat, CV_32F, 1./255., -0.5); + + assert (input_mat.ptr(0) == p); + assert (!input_mat.empty() && input_mat.isContinuous()); + assert (input_mat.cols == input_img_width && input_mat.rows == input_img_height); + + const char* input_names[] = {"x"}; + const char* output_names[] = {"logit_box"}; + + //Timer t_; t_.start(); + + const auto nt = omp_get_num_threads(); + omp_set_num_threads(num_threads); + session.Run(Ort::RunOptions{nullptr}, input_names, &input_val, 1, output_names, &output_val, 1); + omp_set_num_threads(nt); + + //qDebug() << "localizer: " << t_.elapsed_ms() << " ms\n"; + + const cv::Rect2f roi = unnormalize(cv::Rect2f{ + results[1], + results[2], + results[3]-results[1], // Width + results[4]-results[2] // Height + }, frame.rows, frame.cols); + const float score = sigmoid(results[0]); + + return { score, roi }; +} + + +PoseEstimator::PoseEstimator(Ort::MemoryInfo &allocator_info, Ort::Session &&session) : + session{std::move(session)}, + scaled_frame(input_img_height, input_img_width, CV_8U), + input_mat(input_img_height, input_img_width, CV_32F) +{ + { + const std::int64_t input_shape[4] = { 1, 1, input_img_height, input_img_width }; + input_val = Ort::Value::CreateTensor<float>(allocator_info, input_mat.ptr<float>(0), input_mat.total(), input_shape, 4); + } + + { + const std::int64_t output_shape[2] = { 1, 3 }; + output_val[0] = Ort::Value::CreateTensor<float>( + allocator_info, &output_coord[0], output_coord.rows, output_shape, 2); + } + + { + const std::int64_t output_shape[2] = { 1, 4 }; + output_val[1] = Ort::Value::CreateTensor<float>( + allocator_info, &output_quat[0], output_quat.rows, output_shape, 2); + } + + { + const std::int64_t output_shape[2] = { 1, 4 }; + output_val[2] = Ort::Value::CreateTensor<float>( + allocator_info, &output_box[0], output_box.rows, output_shape, 2); + } +} + + +int PoseEstimator::find_input_intensity_90_pct_quantile() const +{ + const int channels[] = { 0 }; + const int hist_size[] = { 255 }; + float range[] = { 0, 256 }; + const float* ranges[] = { range }; + cv::Mat hist; + cv::calcHist(&scaled_frame, 1, channels, cv::Mat(), hist, 1, hist_size, ranges, true, false); + int gray_level = 0; + const int num_pixels_quantile = scaled_frame.total()*0.9f; + int num_pixels_accum = 0; + for (int i=0; i<hist_size[0]; ++i) + { + num_pixels_accum += hist.at<float>(i); + if (num_pixels_accum > num_pixels_quantile) + { + gray_level = i; + break; + } + } + return gray_level; +} + + +std::optional<PoseEstimator::Face> PoseEstimator::run( + const cv::Mat &frame, const cv::Rect &box) +{ + cv::Mat cropped; + + const int patch_size = std::max(box.width, box.height)*1.05; + const cv::Point2f patch_center = { + std::clamp<float>(box.x + 0.5f*box.width, 0.f, frame.cols), + std::clamp<float>(box.y + 0.5f*box.height, 0.f, frame.rows) + }; + cv::getRectSubPix(frame, {patch_size, patch_size}, patch_center, cropped); + + // Will get failure if patch_center is outside image boundaries. + // Have to catch this case. + if (cropped.rows != patch_size || cropped.cols != patch_size) + return {}; + + auto p = input_mat.ptr(0); + + cv::resize(cropped, scaled_frame, { input_img_width, input_img_height }, 0, 0, cv::INTER_AREA); + + // Automatic brightness amplification. + const int brightness = find_input_intensity_90_pct_quantile(); + const double alpha = brightness<127 ? 0.5/std::max(5,brightness) : 1./255; + const double beta = -0.5; + + scaled_frame.convertTo(input_mat, CV_32F, alpha, beta); + + assert (input_mat.ptr(0) == p); + assert (!input_mat.empty() && input_mat.isContinuous()); + assert (input_mat.cols == input_img_width && input_mat.rows == input_img_height); + + const char* input_names[] = {"x"}; + const char* output_names[] = {"pos_size", "quat", "box"}; + + //Timer t_; t_.start(); + + const auto nt = omp_get_num_threads(); + omp_set_num_threads(num_threads); + session.Run(Ort::RunOptions{nullptr}, input_names, &input_val, 1, output_names, output_val, 3); + omp_set_num_threads(nt); + + // FIXME: Execution time fluctuates wildly. 19 to 26 ms. Why??? + // The instructions are always the same. Maybe a memory allocation + // issue. The ONNX api suggests that tensor are allocated in an + // arena. Does that matter? Maybe the issue is something else? + + //qDebug() << "pose net: " << t_.elapsed_ms() << " ms\n"; + + // Perform coordinate transformation. + // From patch-local normalized in [-1,1] to + // frame unnormalized pixel coordinates. + + const cv::Point2f center = patch_center + + (0.5f*patch_size)*cv::Point2f{output_coord[0], output_coord[1]}; + + const float size = patch_size*0.5f*output_coord[2]; + + // Following Eigen which uses quat components in the order w, x, y, z. + const std::array<float,4> rotation = { + output_quat[3], + output_quat[0], + output_quat[1], + output_quat[2] }; + + const cv::Rect2f outbox = { + patch_center.x + (0.5f*patch_size)*output_box[0], + patch_center.y + (0.5f*patch_size)*output_box[1], + 0.5f*patch_size*(output_box[2]-output_box[0]), + 0.5f*patch_size*(output_box[3]-output_box[1]) + }; + + return std::optional<Face>({ + rotation, outbox, center, size + }); +} + + +cv::Mat PoseEstimator::last_network_input() const +{ + cv::Mat ret; + if (!input_mat.empty()) + { + input_mat.convertTo(ret, CV_8U, 255., 127.); + cv::cvtColor(ret, ret, cv::COLOR_GRAY2RGB); + } + return ret; +} + + +bool neuralnet_tracker::detect() +{ + // Note: BGR colors! + if (!last_localizer_roi || !last_roi || + iou(*last_localizer_roi,*last_roi)<0.25) + { + auto [p, rect] = localizer->run(grayscale); + if (p > 0.5) + { + last_localizer_roi = rect; + last_roi = rect; + } + } + + if (!last_roi) + return false; + + auto face = poseestimator->run(grayscale, *last_roi); + + if (!face) + { + last_roi.reset(); + return false; + } + + last_roi = face->box; + + Affine pose = compute_pose(*face); + + draw_gizmos(frame, *face, pose); + + { + QMutexLocker lck(&mtx); + this->pose_ = pose; + } + + return true; +} + + +Affine neuralnet_tracker::compute_pose(const PoseEstimator::Face &face) const +{ + const mat33 rot_correction = compute_rotation_correction( + normalize(face.center, frame.rows, frame.cols), + intrinsics.focal_length_w); + + const mat33 m = rot_correction * quaternion_to_mat33(face.rotation); + + /* + + hhhhhh <- head size (meters) + \ | ----------------------- + \ | \ + \ | | + \ | |- tz (meters) + ____ <- face.size / width | + \ | | | + \| |- focal length / + ------------------------ + */ + + // Compute the location the network outputs in 3d space. + const vec3 face_world_pos = image_to_world(face.center.x, face.center.y, face.size, head_size_mm); + + // But this is in general not the location of the rotation joint in the neck. + // So we need an extra offset. Which we determine by solving + // z,y,z-pos = head_joint_loc + R_face * offset + + const vec3 pos = face_world_pos + + m * vec3{ + static_cast<float>(s.offset_fwd), + static_cast<float>(s.offset_up), + static_cast<float>(s.offset_right)}; + + return { m, pos }; +} + + +void neuralnet_tracker::draw_gizmos( + cv::Mat frame, + const PoseEstimator::Face &face, + const Affine& pose) const +{ + if (last_roi) + { + const int col = 255; + cv::rectangle(frame, *last_roi, cv::Scalar(0, 255, 0), /*thickness=*/1); + } + if (last_localizer_roi) + { + const int col = 255; + cv::rectangle(frame, *last_localizer_roi, cv::Scalar(col, 0, 255-col), /*thickness=*/1); + } + + if (face.size>=1.f) + cv::circle(frame, static_cast<cv::Point>(face.center), int(face.size), cv::Scalar(255,255,255), 2); + cv::circle(frame, static_cast<cv::Point>(face.center), 3, cv::Scalar(255,255,255), -1); + + auto draw_coord_line = [&](int i, const cv::Scalar& color) + { + const float vx = -pose.R(2,i); + const float vy = -pose.R(1,i); + static constexpr float len = 100.f; + cv::Point q = face.center + len*cv::Point2f{vx, vy}; + cv::line(frame, static_cast<cv::Point>(face.center), static_cast<cv::Point>(q), color, 2); + }; + draw_coord_line(0, {0, 0, 255}); + draw_coord_line(1, {0, 255, 0}); + draw_coord_line(2, {255, 0, 0}); + + if (s.show_network_input) + { + cv::Mat netinput = poseestimator->last_network_input(); + if (!netinput.empty()) + { + const int w = std::min(netinput.cols, frame.cols); + const int h = std::min(netinput.rows, frame.rows); + cv::Rect roi(0, 0, w, h); + netinput(roi).copyTo(frame(roi)); + } + } + { + // Draw the computed joint position + auto xy = world_to_image(pose.t); + cv::circle(frame, cv::Point(xy[0],xy[1]), 5, cv::Scalar(0,0,255), -1); + } + + char buf[128]; + ::snprintf(buf, sizeof(buf), "%d Hz, Max: %d ms", clamp(int(fps), 0, 9999), int(max_frame_time*1000.)); + cv::putText(frame, buf, cv::Point(10, frame.rows-10), cv::FONT_HERSHEY_PLAIN, 1, cv::Scalar(0, 255, 0), 1); +} + + +neuralnet_tracker::neuralnet_tracker() +{ + opencv_init(); + cv::setNumThreads(num_threads); +} + + +neuralnet_tracker::~neuralnet_tracker() +{ + requestInterruption(); + wait(); + // fast start/stop causes breakage + portable::sleep(1000); +} + + +module_status neuralnet_tracker::start_tracker(QFrame* videoframe) +{ + videoframe->show(); + videoWidget = std::make_unique<cv_video_widget>(videoframe); + layout = std::make_unique<QHBoxLayout>(); + layout->setContentsMargins(0, 0, 0, 0); + layout->addWidget(videoWidget.get()); + videoframe->setLayout(layout.get()); + videoWidget->show(); + start(); + return status_ok(); +} + + +bool neuralnet_tracker::load_and_initialize_model() +{ + const QString localizer_model_path_enc = + OPENTRACK_BASE_PATH+"/" OPENTRACK_LIBRARY_PATH "/models/head-localizer.onnx"; + const QString poseestimator_model_path_enc = + OPENTRACK_BASE_PATH+"/" OPENTRACK_LIBRARY_PATH "/models/head-pose.onnx"; + + try + { + env = Ort::Env{ + OrtLoggingLevel::ORT_LOGGING_LEVEL_ERROR, + "tracker-neuralnet" + }; + auto opts = Ort::SessionOptions{}; + // Do thread settings here do anything? + // There is a warning which says to control number of threads via + // openmp settings. Which is what we do. omp_set_num_threads directly + // before running the inference pass. + opts.SetIntraOpNumThreads(num_threads); + opts.SetInterOpNumThreads(num_threads); + opts.SetGraphOptimizationLevel( + GraphOptimizationLevel::ORT_ENABLE_EXTENDED); + + opts.EnableCpuMemArena(); + allocator_info = Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault); + + localizer.emplace( + allocator_info, + Ort::Session{env, convert(localizer_model_path_enc).c_str(), opts}); + + poseestimator.emplace( + allocator_info, + Ort::Session{env, convert(poseestimator_model_path_enc).c_str(), opts}); + } + catch (const Ort::Exception &e) + { + qDebug() << "Failed to initialize the neural network models. ONNX error message: " + << e.what(); + return false; + } + return true; +} + + +bool neuralnet_tracker::open_camera() +{ + int fps = enum_to_fps(s.force_fps); + + QMutexLocker l(&camera_mtx); + + camera = video::make_camera(s.camera_name); + + if (!camera) + return false; + + video::impl::camera::info args {}; + + args.width = 320; + args.height = 240; + + if (fps) + args.fps = fps; + + if (!camera->start(args)) + { + qDebug() << "neuralnet tracker: can't open camera"; + return false; + } + return true; +} + + +void neuralnet_tracker::set_intrinsics() +{ + const int w = grayscale.cols, h = grayscale.rows; + const double diag_fov = s.fov * M_PI / 180.; + const double fov_w = 2.*atan(tan(diag_fov/2.)/sqrt(1. + h/(double)w * h/(double)w)); + const double fov_h = 2.*atan(tan(diag_fov/2.)/sqrt(1. + w/(double)h * w/(double)h)); + const double focal_length_w = 1. / tan(.5 * fov_w); + const double focal_length_h = 1. / tan(.5 * fov_h); + + intrinsics.fov_h = fov_h; + intrinsics.fov_w = fov_w; + intrinsics.focal_length_w = focal_length_w; + intrinsics.focal_length_h = focal_length_h; +} + + +vec3 neuralnet_tracker::image_to_world(float x, float y, float size, float real_size) const +{ + // Compute the location the network outputs in 3d space. + const float xpos = -(intrinsics.focal_length_w * frame.cols * 0.5f) / size * real_size; + const float zpos = (x / frame.cols * 2.f - 1.f) * xpos / intrinsics.focal_length_w; + const float ypos = (y / frame.rows * 2.f - 1.f) * xpos / intrinsics.focal_length_h; + return {xpos, ypos, zpos}; +} + + +vec2 neuralnet_tracker::world_to_image(const vec3& pos) const +{ + const float xscr = pos[2] / pos[0] * intrinsics.focal_length_w; + const float yscr = pos[1] / pos[0] * intrinsics.focal_length_h; + const float x = (xscr+1.)*0.5f*frame.cols; + const float y = (yscr+1.)*0.5f*frame.rows; + return {x, y}; +} + + +void neuralnet_tracker::run() +{ + if (!open_camera()) + return; + + if (!load_and_initialize_model()) + return; + + std::chrono::high_resolution_clock clk; + + while (!isInterruptionRequested()) + { + auto t = clk.now(); + { + QMutexLocker l(&camera_mtx); + + auto [ img, res ] = camera->get_frame(); + + if (!res) + { + l.unlock(); + portable::sleep(100); + continue; + } + + auto color = cv::Mat(img.height, img.width, CV_8UC(img.channels), (void*)img.data, img.stride); + color.copyTo(frame); + + switch (img.channels) + { + case 1: + grayscale.setTo(color); + break; + case 3: + cv::cvtColor(color, grayscale, cv::COLOR_BGR2GRAY); + break; + default: + qDebug() << "Can't handle" << img.channels << "color channels"; + return; + } + } + + set_intrinsics(); + + detect(); + + if (frame.rows > 0) + videoWidget->update_image(frame); + + update_fps( + std::chrono::duration_cast<std::chrono::milliseconds>( + clk.now() - t).count()*1.e-3); + } +} + + +void neuralnet_tracker::update_fps(double dt) +{ + const double alpha = dt/(dt + RC); + + if (dt > 1e-6) + { + fps *= 1 - alpha; + fps += alpha * 1./dt; + } + + max_frame_time = std::max(max_frame_time, dt); +} + + +void neuralnet_tracker::data(double *data) +{ + Affine tmp = [&]() + { + QMutexLocker lck(&mtx); + return pose_; + }(); + + const auto& mx = tmp.R.col(0); + const auto& my = tmp.R.col(1); + const auto& mz = -tmp.R.col(2); + + const float yaw = std::atan2(mx(2), mx(0)); + const float pitch = -std::atan2(-mx(1), std::sqrt(mx(2)*mx(2)+mx(0)*mx(0))); + const float roll = std::atan2(-my(2), mz(2)); + { + constexpr double rad2deg = 180/M_PI; + data[Yaw] = rad2deg * yaw; + data[Pitch] = rad2deg * pitch; + data[Roll] = rad2deg * roll; + + // convert to cm + data[TX] = -tmp.t[2] * 0.1; + data[TY] = tmp.t[1] * 0.1; + data[TZ] = -tmp.t[0] * 0.1; + } +} + + +Affine neuralnet_tracker::pose() +{ + QMutexLocker lck(&mtx); + return pose_; +} + + +void neuralnet_dialog::make_fps_combobox() +{ + for (int k = 0; k < fps_MAX; k++) + { + const int hz = enum_to_fps(k); + const QString name = (hz == 0) ? tr("Default") : QString::number(hz); + ui.cameraFPS->addItem(name, k); + } +} + + +neuralnet_dialog::neuralnet_dialog() : + trans_calib(1, 2) +{ + ui.setupUi(this); + + make_fps_combobox(); + tie_setting(s.force_fps, ui.cameraFPS); + + for (const auto& str : video::camera_names()) + ui.cameraName->addItem(str); + + tie_setting(s.camera_name, ui.cameraName); + tie_setting(s.fov, ui.cameraFOV); + tie_setting(s.offset_fwd, ui.tx_spin); + tie_setting(s.offset_up, ui.ty_spin); + tie_setting(s.offset_right, ui.tz_spin); + tie_setting(s.show_network_input, ui.showNetworkInput); + + connect(ui.buttonBox, SIGNAL(accepted()), this, SLOT(doOK())); + connect(ui.buttonBox, SIGNAL(rejected()), this, SLOT(doCancel())); + connect(ui.camera_settings, SIGNAL(clicked()), this, SLOT(camera_settings())); + + connect(&s.camera_name, value_::value_changed<QString>(), this, &neuralnet_dialog::update_camera_settings_state); + + update_camera_settings_state(s.camera_name); + + connect(&calib_timer, &QTimer::timeout, this, &neuralnet_dialog::trans_calib_step); + calib_timer.setInterval(35); + connect(ui.tcalib_button,SIGNAL(toggled(bool)), this, SLOT(startstop_trans_calib(bool))); +} + + +void neuralnet_dialog::doOK() +{ + s.b->save(); + close(); +} + + +void neuralnet_dialog::doCancel() +{ + close(); +} + + +void neuralnet_dialog::camera_settings() +{ + if (tracker) + { + QMutexLocker l(&tracker->camera_mtx); + (void)tracker->camera->show_dialog(); + } + else + (void)video::show_dialog(s.camera_name); +} + + +void neuralnet_dialog::update_camera_settings_state(const QString& name) +{ + (void)name; + ui.camera_settings->setEnabled(true); +} + + +void neuralnet_dialog::register_tracker(ITracker * x) +{ + tracker = static_cast<neuralnet_tracker*>(x); + ui.tcalib_button->setEnabled(true); +} + + +void neuralnet_dialog::unregister_tracker() +{ + tracker = nullptr; + ui.tcalib_button->setEnabled(false); +} + + +void neuralnet_dialog::trans_calib_step() +{ + if (tracker) + { + const Affine X_CM = [&]() { + QMutexLocker l(&calibrator_mutex); + return tracker->pose(); + }(); + trans_calib.update(X_CM.R, X_CM.t); + auto [_, nsamples] = trans_calib.get_estimate(); + + constexpr int min_yaw_samples = 15; + constexpr int min_pitch_samples = 12; + constexpr int min_samples = min_yaw_samples+min_pitch_samples; + + // Don't bother counting roll samples. Roll calibration is hard enough + // that it's a hidden unsupported feature anyway. + + QString sample_feedback; + if (nsamples[0] < min_yaw_samples) + sample_feedback = tr("%1 yaw samples. Yaw more to %2 samples for stable calibration.").arg(nsamples[0]).arg(min_yaw_samples); + else if (nsamples[1] < min_pitch_samples) + sample_feedback = tr("%1 pitch samples. Pitch more to %2 samples for stable calibration.").arg(nsamples[1]).arg(min_pitch_samples); + else + { + const int nsamples_total = nsamples[0] + nsamples[1]; + sample_feedback = tr("%1 samples. Over %2, good!").arg(nsamples_total).arg(min_samples); + } + ui.sample_count_display->setText(sample_feedback); + } + else + startstop_trans_calib(false); +} + + +void neuralnet_dialog::startstop_trans_calib(bool start) +{ + QMutexLocker l(&calibrator_mutex); + // FIXME: does not work ... + if (start) + { + qDebug() << "pt: starting translation calibration"; + calib_timer.start(); + trans_calib.reset(); + ui.sample_count_display->setText(QString()); + // Tracker must run with zero'ed offset for calibration. + s.offset_fwd = 0; + s.offset_up = 0; + s.offset_right = 0; + } + else + { + calib_timer.stop(); + qDebug() << "pt: stopping translation calibration"; + { + auto [tmp, nsamples] = trans_calib.get_estimate(); + s.offset_fwd = int(tmp[0]); + s.offset_up = int(tmp[1]); + s.offset_right = int(tmp[2]); + } + } + ui.tx_spin->setEnabled(!start); + ui.ty_spin->setEnabled(!start); + ui.tz_spin->setEnabled(!start); + + if (start) + ui.tcalib_button->setText(tr("Stop calibration")); + else + ui.tcalib_button->setText(tr("Start calibration")); +} + + +settings::settings() : opts("neuralnet-tracker") {} + +} // neuralnet_tracker_ns + +OPENTRACK_DECLARE_TRACKER(neuralnet_tracker, neuralnet_dialog, neuralnet_metadata) |