summaryrefslogtreecommitdiffhomepage
path: root/tracker-pt/module
diff options
context:
space:
mode:
Diffstat (limited to 'tracker-pt/module')
-rw-r--r--tracker-pt/module/CMakeLists.txt6
-rw-r--r--tracker-pt/module/Resources/Logo_IR.pngbin0 -> 10386 bytes
-rw-r--r--tracker-pt/module/camera.cpp171
-rw-r--r--tracker-pt/module/camera.h69
-rw-r--r--tracker-pt/module/frame.cpp83
-rw-r--r--tracker-pt/module/frame.hpp36
-rw-r--r--tracker-pt/module/ftnoir_tracker_pt.qrc5
-rw-r--r--tracker-pt/module/lang/nl_NL.ts4
-rw-r--r--tracker-pt/module/lang/ru_RU.ts4
-rw-r--r--tracker-pt/module/lang/stub.ts4
-rw-r--r--tracker-pt/module/module.cpp69
-rw-r--r--tracker-pt/module/point_extractor.cpp377
-rw-r--r--tracker-pt/module/point_extractor.h59
13 files changed, 887 insertions, 0 deletions
diff --git a/tracker-pt/module/CMakeLists.txt b/tracker-pt/module/CMakeLists.txt
new file mode 100644
index 00000000..1d1b4458
--- /dev/null
+++ b/tracker-pt/module/CMakeLists.txt
@@ -0,0 +1,6 @@
+find_package(OpenCV 3.0 QUIET)
+if(OpenCV_FOUND)
+ otr_module(tracker-pt)
+ target_link_libraries(opentrack-tracker-pt opentrack-tracker-pt-base)
+ target_include_directories(opentrack-tracker-pt PRIVATE "${CMAKE_SOURCE_DIR}/tracker-pt")
+endif()
diff --git a/tracker-pt/module/Resources/Logo_IR.png b/tracker-pt/module/Resources/Logo_IR.png
new file mode 100644
index 00000000..95032a25
--- /dev/null
+++ b/tracker-pt/module/Resources/Logo_IR.png
Binary files differ
diff --git a/tracker-pt/module/camera.cpp b/tracker-pt/module/camera.cpp
new file mode 100644
index 00000000..ba4583da
--- /dev/null
+++ b/tracker-pt/module/camera.cpp
@@ -0,0 +1,171 @@
+/* Copyright (c) 2012 Patrick Ruoff
+ *
+ * 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 "camera.h"
+#include "frame.hpp"
+
+#include "compat/sleep.hpp"
+#include "compat/camera-names.hpp"
+#include "compat/math-imports.hpp"
+
+#include <opencv2/imgproc.hpp>
+
+#include "cv/video-property-page.hpp"
+
+using namespace pt_module;
+
+Camera::Camera(const QString& module_name) : s { module_name }
+{
+}
+
+QString Camera::get_desired_name() const
+{
+ return desired_name;
+}
+
+QString Camera::get_active_name() const
+{
+ return active_name;
+}
+
+void Camera::show_camera_settings()
+{
+ const int idx = camera_name_to_index(s.camera_name);
+
+ if (bool(*this))
+ video_property_page::show_from_capture(*cap, idx);
+ else
+ {
+ video_property_page::show(idx);
+ }
+}
+
+Camera::result Camera::get_info() const
+{
+ if (cam_info.res_x == 0 || cam_info.res_y == 0)
+ return result(false, pt_camera_info());
+ return result(true, cam_info);
+}
+
+Camera::result Camera::get_frame(pt_frame& frame_)
+{
+ cv::Mat& frame = frame_.as<Frame>()->mat;
+
+ const bool new_frame = _get_frame(frame);
+
+ if (new_frame)
+ {
+ const double dt = t.elapsed_seconds();
+ t.start();
+
+ // measure fps of valid frames
+ constexpr double RC = .1; // seconds
+ const double alpha = dt/(dt + RC);
+
+ if (dt_mean < dt_eps)
+ dt_mean = dt;
+ else
+ dt_mean = (1-alpha) * dt_mean + alpha * dt;
+
+ cam_info.fps = dt_mean > dt_eps ? 1 / dt_mean : 0;
+ cam_info.res_x = frame.cols;
+ cam_info.res_y = frame.rows;
+ cam_info.fov = fov;
+
+ return result(true, cam_info);
+ }
+ else
+ return result(false, pt_camera_info());
+}
+
+pt_camera_open_status Camera::start(int idx, int fps, int res_x, int res_y)
+{
+ if (idx >= 0 && fps >= 0 && res_x >= 0 && res_y >= 0)
+ {
+ if (cam_desired.idx != idx ||
+ cam_desired.fps != fps ||
+ cam_desired.res_x != res_x ||
+ cam_desired.res_y != res_y ||
+ !cap || !cap->isOpened() || !cap->grab())
+ {
+ stop();
+
+ desired_name = get_camera_names().value(idx);
+ cam_desired.idx = idx;
+ cam_desired.fps = fps;
+ cam_desired.res_x = res_x;
+ cam_desired.res_y = res_y;
+ cam_desired.fov = fov;
+
+ cap = camera_ptr(new cv::VideoCapture(cam_desired.idx));
+
+ if (cam_desired.res_x)
+ cap->set(cv::CAP_PROP_FRAME_WIDTH, cam_desired.res_x);
+ if (cam_desired.res_y)
+ cap->set(cv::CAP_PROP_FRAME_HEIGHT, cam_desired.res_y);
+ if (cam_desired.fps)
+ cap->set(cv::CAP_PROP_FPS, cam_desired.fps);
+
+ if (cap->isOpened() && cap->grab())
+ {
+ cam_info = pt_camera_info();
+ active_name = QString();
+ cam_info.idx = idx;
+ dt_mean = 0;
+ active_name = desired_name;
+
+ t.start();
+
+ return cam_open_ok_change;
+ }
+ else
+ {
+ stop();
+ return cam_open_error;
+ }
+ }
+
+ return cam_open_ok_no_change;
+ }
+
+ stop();
+ return cam_open_error;
+}
+
+void Camera::stop()
+{
+ cap = nullptr;
+ desired_name = QString();
+ active_name = QString();
+ cam_info = pt_camera_info();
+ cam_desired = pt_camera_info();
+}
+
+bool Camera::_get_frame(cv::Mat& frame)
+{
+ if (cap && cap->isOpened())
+ {
+ for (int i = 0; i < 5; i++)
+ {
+ if (cap->read(frame))
+ return true;
+ portable::sleep(1);
+ }
+ }
+ return false;
+}
+
+void Camera::camera_deleter::operator()(cv::VideoCapture* cap)
+{
+ if (cap)
+ {
+ if (cap->isOpened())
+ cap->release();
+ delete cap;
+ }
+}
+
diff --git a/tracker-pt/module/camera.h b/tracker-pt/module/camera.h
new file mode 100644
index 00000000..96234840
--- /dev/null
+++ b/tracker-pt/module/camera.h
@@ -0,0 +1,69 @@
+/* Copyright (c) 2012 Patrick Ruoff
+ *
+ * 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.
+ */
+
+#pragma once
+
+#include "pt-api.hpp"
+
+#include "compat/timer.hpp"
+
+#include <functional>
+#include <memory>
+#include <tuple>
+
+#include <opencv2/core.hpp>
+#include <opencv2/videoio.hpp>
+
+#include <QString>
+
+namespace pt_module {
+
+struct Camera final : pt_camera
+{
+ Camera(const QString& module_name);
+
+ pt_camera_open_status start(int idx, int fps, int res_x, int res_y) override;
+ void stop() override;
+
+ result get_frame(pt_frame& Frame) override;
+ result get_info() const override;
+
+ pt_camera_info get_desired() const override { return cam_desired; }
+ QString get_desired_name() const override;
+ QString get_active_name() const override;
+
+ operator bool() const override { return cap && cap->isOpened(); }
+
+ void set_fov(double value) override { fov = value; }
+ void show_camera_settings() override;
+
+private:
+ warn_result_unused bool _get_frame(cv::Mat& Frame);
+
+ double dt_mean = 0, fov = 30;
+
+ Timer t;
+
+ pt_camera_info cam_info;
+ pt_camera_info cam_desired;
+ QString desired_name, active_name;
+
+ struct camera_deleter final
+ {
+ void operator()(cv::VideoCapture* cap);
+ };
+
+ using camera_ptr = std::unique_ptr<cv::VideoCapture, camera_deleter>;
+
+ camera_ptr cap;
+
+ pt_settings s;
+
+ static constexpr inline double dt_eps = 1./384;
+};
+
+} // ns pt_module
diff --git a/tracker-pt/module/frame.cpp b/tracker-pt/module/frame.cpp
new file mode 100644
index 00000000..e403af07
--- /dev/null
+++ b/tracker-pt/module/frame.cpp
@@ -0,0 +1,83 @@
+#include "frame.hpp"
+
+#include "compat/math.hpp"
+
+#include <cstring>
+#include <tuple>
+
+#include <opencv2/imgproc.hpp>
+
+using namespace pt_module;
+
+Preview& Preview::operator=(const pt_frame& frame_)
+{
+ const cv::Mat& frame = frame_.as_const<const Frame>()->mat;
+ ensure_size(frame_copy, frame_out.cols, frame_out.rows, CV_8UC3);
+
+ if (frame.channels() != 3)
+ {
+ once_only(qDebug() << "tracker/pt: camera frame depth: 3 !=" << frame.channels());
+ return *this;
+ }
+
+ const bool need_resize = frame.cols != frame_out.cols || frame.rows != frame_out.rows;
+ if (need_resize)
+ cv::resize(frame, frame_copy, cv::Size(frame_out.cols, frame_out.rows), 0, 0, cv::INTER_NEAREST);
+ else
+ frame.copyTo(frame_copy);
+
+ return *this;
+}
+
+Preview::Preview(int w, int h)
+{
+ ensure_size(frame_out, w, h, CV_8UC4);
+
+ frame_out.setTo(cv::Scalar(0, 0, 0, 0));
+}
+
+QImage Preview::get_bitmap()
+{
+ int stride = frame_out.step.p[0];
+
+ if (stride < 64 || stride < frame_out.cols * 4)
+ {
+ once_only(qDebug() << "bad stride" << stride
+ << "for bitmap size" << frame_copy.cols << frame_copy.rows);
+ return QImage();
+ }
+
+ cv::cvtColor(frame_copy, frame_out, cv::COLOR_BGR2BGRA);
+
+ return QImage((const unsigned char*) frame_out.data,
+ frame_out.cols, frame_out.rows,
+ stride,
+ QImage::Format_ARGB32);
+}
+
+void Preview::draw_head_center(double x, double y)
+{
+ double px_, py_;
+
+ std::tie(px_, py_) = to_pixel_pos(x, y, frame_copy.cols, frame_copy.rows);
+
+ int px = iround(px_), py = iround(py_);
+
+ constexpr int len = 9;
+
+ static const cv::Scalar color(0, 255, 255);
+ cv::line(frame_copy,
+ cv::Point(px - len, py),
+ cv::Point(px + len, py),
+ color, 1);
+ cv::line(frame_copy,
+ cv::Point(px, py - len),
+ cv::Point(px, py + len),
+ color, 1);
+}
+
+void Preview::ensure_size(cv::Mat& frame, int w, int h, int type)
+{
+ if (frame.cols != w || frame.rows != h)
+ frame = cv::Mat(h, w, type);
+}
diff --git a/tracker-pt/module/frame.hpp b/tracker-pt/module/frame.hpp
new file mode 100644
index 00000000..9e4f809a
--- /dev/null
+++ b/tracker-pt/module/frame.hpp
@@ -0,0 +1,36 @@
+#pragma once
+
+#include "pt-api.hpp"
+
+#include <opencv2/core.hpp>
+#include <QImage>
+
+namespace pt_module {
+
+struct Frame final : pt_frame
+{
+ cv::Mat mat;
+
+ operator const cv::Mat&() const& { return mat; }
+ operator cv::Mat&() & { return mat; }
+};
+
+struct Preview final : pt_preview
+{
+ Preview(int w, int h);
+
+ Preview& operator=(const pt_frame& frame) override;
+ QImage get_bitmap() override;
+ void draw_head_center(double x, double y) override;
+
+ operator cv::Mat&() { return frame_copy; }
+ operator cv::Mat const&() const { return frame_copy; }
+
+private:
+ static void ensure_size(cv::Mat& frame, int w, int h, int type);
+
+ bool fresh = true;
+ cv::Mat frame_copy, frame_color, frame_out;
+};
+
+} // ns pt_module
diff --git a/tracker-pt/module/ftnoir_tracker_pt.qrc b/tracker-pt/module/ftnoir_tracker_pt.qrc
new file mode 100644
index 00000000..dfeb7369
--- /dev/null
+++ b/tracker-pt/module/ftnoir_tracker_pt.qrc
@@ -0,0 +1,5 @@
+<RCC>
+ <qresource prefix="/">
+ <file>Resources/Logo_IR.png</file>
+ </qresource>
+</RCC>
diff --git a/tracker-pt/module/lang/nl_NL.ts b/tracker-pt/module/lang/nl_NL.ts
new file mode 100644
index 00000000..9e739505
--- /dev/null
+++ b/tracker-pt/module/lang/nl_NL.ts
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="nl_NL">
+</TS>
diff --git a/tracker-pt/module/lang/ru_RU.ts b/tracker-pt/module/lang/ru_RU.ts
new file mode 100644
index 00000000..f62cf2e1
--- /dev/null
+++ b/tracker-pt/module/lang/ru_RU.ts
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="ru_RU">
+</TS>
diff --git a/tracker-pt/module/lang/stub.ts b/tracker-pt/module/lang/stub.ts
new file mode 100644
index 00000000..6401616d
--- /dev/null
+++ b/tracker-pt/module/lang/stub.ts
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1">
+</TS>
diff --git a/tracker-pt/module/module.cpp b/tracker-pt/module/module.cpp
new file mode 100644
index 00000000..5c298ca5
--- /dev/null
+++ b/tracker-pt/module/module.cpp
@@ -0,0 +1,69 @@
+#include "ftnoir_tracker_pt.h"
+#include "api/plugin-api.hpp"
+
+#include "camera.h"
+#include "frame.hpp"
+#include "point_extractor.h"
+#include "ftnoir_tracker_pt_dialog.h"
+
+#include "pt-api.hpp"
+
+#include <memory>
+
+static const QString module_name = "tracker-pt";
+
+using namespace pt_module;
+
+struct pt_module_traits final : pt_runtime_traits
+{
+ pointer<pt_camera> make_camera() const override
+ {
+ return pointer<pt_camera>(new Camera(module_name));
+ }
+
+ pointer<pt_point_extractor> make_point_extractor() const override
+ {
+ return pointer<pt_point_extractor>(new PointExtractor(module_name));
+ }
+
+ QString get_module_name() const override
+ {
+ return module_name;
+ }
+
+ pointer<pt_frame> make_frame() const override
+ {
+ return pointer<pt_frame>(new Frame);
+ }
+
+ pointer<pt_preview> make_preview(int w, int h) const override
+ {
+ return pointer<pt_preview>(new Preview(w, h));
+ }
+};
+
+struct tracker_pt : Tracker_PT
+{
+ tracker_pt() : Tracker_PT(pointer<pt_runtime_traits>(new pt_module_traits))
+ {
+ }
+};
+
+struct dialog_pt : TrackerDialog_PT
+{
+ dialog_pt();
+};
+
+class metadata_pt : public Metadata
+{
+ QString name() { return _("PointTracker 1.1"); }
+ QIcon icon() { return QIcon(":/Resources/Logo_IR.png"); }
+};
+
+// ns pt_module
+
+using namespace pt_module;
+
+dialog_pt::dialog_pt() : TrackerDialog_PT(module_name) {}
+
+OPENTRACK_DECLARE_TRACKER(tracker_pt, dialog_pt, metadata_pt)
diff --git a/tracker-pt/module/point_extractor.cpp b/tracker-pt/module/point_extractor.cpp
new file mode 100644
index 00000000..b67c4036
--- /dev/null
+++ b/tracker-pt/module/point_extractor.cpp
@@ -0,0 +1,377 @@
+/* Copyright (c) 2012 Patrick Ruoff
+ * Copyright (c) 2015-2017 Stanislaw Halik <sthalik@misaki.pl>
+ *
+ * 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 "point_extractor.h"
+#include "point_tracker.h"
+#include "frame.hpp"
+
+#include "cv/numeric.hpp"
+#include "compat/math.hpp"
+
+#include <opencv2/videoio.hpp>
+
+#undef PREVIEW
+//#define PREVIEW
+
+#if defined PREVIEW
+# include <opencv2/highgui.hpp>
+#endif
+
+#include <cmath>
+#include <algorithm>
+#include <cinttypes>
+#include <memory>
+
+#include <QDebug>
+
+using namespace types;
+using namespace pt_module;
+
+/*
+http://en.wikipedia.org/wiki/Mean-shift
+In this application the idea, is to eliminate any bias of the point estimate
+which is introduced by the rather arbitrary thresholded area. One must recognize
+that the thresholded area can only move in one pixel increments since it is
+binary. Thus, its center of mass might make "jumps" as pixels are added/removed
+from the thresholded area.
+With mean-shift, a moving "window" or kernel is multiplied with the gray-scale
+image, and the COM is calculated of the result. This is iterated where the
+kernel center is set the previously computed COM. Thus, peaks in the image intensity
+distribution "pull" the kernel towards themselves. Eventually it stops moving, i.e.
+then the computed COM coincides with the kernel center. We hope that the
+corresponding location is a good candidate for the extracted point.
+The idea similar to the window scaling suggested in Berglund et al. "Fast, bias-free
+algorithm for tracking single particles with variable size and shape." (2008).
+*/
+static cv::Vec2d MeanShiftIteration(const cv::Mat &frame_gray, const vec2 &current_center, f filter_width)
+{
+ // Most amazingly this function runs faster with doubles than with floats.
+ const f s = 1.0 / filter_width;
+
+ f m = 0;
+ vec2 com { 0, 0 };
+ for (int i = 0; i < frame_gray.rows; i++)
+ {
+ auto frame_ptr = (uint8_t const* restrict_ptr)frame_gray.ptr(i);
+ for (int j = 0; j < frame_gray.cols; j++)
+ {
+ f val = frame_ptr[j];
+ val = val * val; // taking the square wights brighter parts of the image stronger.
+ {
+ f dx = (j - current_center[0])*s;
+ f dy = (i - current_center[1])*s;
+ f f = std::fmax(0, 1 - dx*dx - dy*dy);
+ val *= f;
+ }
+ m += val;
+ com[0] += j * val;
+ com[1] += i * val;
+ }
+ }
+ if (m > f(.1))
+ {
+ com *= f(1) / m;
+ return com;
+ }
+ else
+ return current_center;
+}
+
+PointExtractor::PointExtractor(const QString& module_name) : s(module_name)
+{
+ blobs.reserve(max_blobs);
+}
+
+void PointExtractor::ensure_channel_buffers(const cv::Mat& orig_frame)
+{
+ if (ch[0].rows != orig_frame.rows || ch[0].cols != orig_frame.cols)
+ for (unsigned k = 0; k < 3; k++)
+ ch[k] = cv::Mat1b(orig_frame.rows, orig_frame.cols);
+}
+
+void PointExtractor::ensure_buffers(const cv::Mat& frame)
+{
+ const int W = frame.cols, H = frame.rows;
+
+ if (frame_gray.rows != W || frame_gray.cols != H)
+ {
+ frame_gray = cv::Mat1b(H, W);
+ frame_bin = cv::Mat1b(H, W);
+ frame_blobs = cv::Mat1b(H, W);
+ }
+}
+
+void PointExtractor::extract_single_channel(const cv::Mat& orig_frame, int idx, cv::Mat& dest)
+{
+ ensure_channel_buffers(orig_frame);
+
+ const int from_to[] = {
+ idx, 0,
+ };
+
+ cv::mixChannels(&orig_frame, 1, &dest, 1, from_to, 1);
+}
+
+void PointExtractor::extract_channels(const cv::Mat& orig_frame, const int* order, int order_npairs)
+{
+ ensure_channel_buffers(orig_frame);
+
+ cv::mixChannels(&orig_frame, 1, (cv::Mat*) ch, order_npairs, order, order_npairs);
+}
+
+void PointExtractor::color_to_grayscale(const cv::Mat& frame, cv::Mat1b& output)
+{
+ switch (s.blob_color)
+ {
+ case pt_color_blue_only:
+ {
+ extract_single_channel(frame, 0, output);
+ break;
+ }
+ case pt_color_red_only:
+ {
+ extract_single_channel(frame, 2, output);
+ break;
+ }
+ case pt_color_average:
+ {
+ const int W = frame.cols, H = frame.rows;
+ const cv::Mat tmp = frame.reshape(1, W * H);
+ cv::Mat output_ = output.reshape(1, W * H);
+ cv::reduce(tmp, output_, 1, cv::REDUCE_AVG);
+ break;
+ }
+ default:
+ once_only(qDebug() << "wrong pt_color_type enum value" << int(s.blob_color));
+ /*FALLTHROUGH*/
+ case pt_color_natural:
+ cv::cvtColor(frame, output, cv::COLOR_BGR2GRAY);
+ break;
+ }
+}
+
+void PointExtractor::threshold_image(const cv::Mat& frame_gray, cv::Mat1b& output)
+{
+ const int threshold_slider_value = s.threshold_slider.to<int>();
+
+ if (!s.auto_threshold)
+ {
+ cv::threshold(frame_gray, output, threshold_slider_value, 255, cv::THRESH_BINARY);
+ }
+ else
+ {
+ const int hist_size = 256;
+ const float ranges_[] = { 0, 256 };
+ float const* ranges = (const float*) ranges_;
+
+ cv::calcHist(&frame_gray,
+ 1,
+ nullptr,
+ cv::noArray(),
+ hist,
+ 1,
+ (int const*) &hist_size,
+ &ranges);
+
+ const f radius = (f) threshold_radius_value(frame_gray.cols, frame_gray.rows, threshold_slider_value);
+
+ auto ptr = (float const* const restrict_ptr) hist.ptr(0);
+ const unsigned area = uround(3 * M_PI * radius*radius);
+ const unsigned sz = unsigned(hist.cols * hist.rows);
+ unsigned thres = 32;
+ for (unsigned i = sz-1, cnt = 0; i > 32; i--)
+ {
+ cnt += ptr[i];
+ if (cnt >= area)
+ break;
+ thres = i;
+ }
+
+ cv::threshold(frame_gray, output, thres, 255, CV_THRESH_BINARY);
+ }
+}
+
+void PointExtractor::extract_points(const pt_frame& frame_, pt_preview& preview_frame_, std::vector<vec2>& points)
+{
+ const cv::Mat& frame = frame_.as_const<Frame>()->mat;
+ cv::Mat& preview_frame = *preview_frame_.as<Preview>();
+
+ ensure_buffers(frame);
+ color_to_grayscale(frame, frame_gray);
+
+#if defined PREVIEW
+ cv::imshow("capture", frame_gray);
+ cv::waitKey(1);
+#endif
+
+ threshold_image(frame_gray, frame_bin);
+
+ blobs.clear();
+ frame_bin.copyTo(frame_blobs);
+
+ const f region_size_min = s.min_point_size;
+ const f region_size_max = s.max_point_size;
+
+ unsigned idx = 0;
+ for (int y=0; y < frame_blobs.rows; y++)
+ {
+ const unsigned char* ptr_bin = frame_blobs.ptr(y);
+ for (int x=0; x < frame_blobs.cols; x++)
+ {
+ if (ptr_bin[x] != 255)
+ continue;
+ idx = blobs.size() + 1;
+
+ cv::Rect rect;
+ cv::floodFill(frame_blobs,
+ cv::Point(x,y),
+ cv::Scalar(idx),
+ &rect,
+ cv::Scalar(0),
+ cv::Scalar(0),
+ 4 | cv::FLOODFILL_FIXED_RANGE);
+
+ unsigned cnt = 0;
+ unsigned norm = 0;
+
+ const int ymax = rect.y+rect.height,
+ xmax = rect.x+rect.width;
+
+ for (int i=rect.y; i < ymax; i++)
+ {
+ unsigned char const* const restrict_ptr ptr_blobs = frame_blobs.ptr(i);
+ unsigned char const* const restrict_ptr ptr_gray = frame_gray.ptr(i);
+ for (int j=rect.x; j < xmax; j++)
+ {
+ if (ptr_blobs[j] != idx)
+ continue;
+
+ //ptr_blobs[j] = 0;
+ norm += ptr_gray[j];
+ cnt++;
+ }
+ }
+
+ const double radius = std::sqrt(cnt / M_PI);
+ if (radius > region_size_max || radius < region_size_min)
+ continue;
+
+ blobs.emplace_back(radius,
+ vec2(rect.width/2., rect.height/2.),
+ std::pow(f(norm), f(1.1))/cnt,
+ rect);
+
+ if (idx >= max_blobs)
+ goto end;
+
+ // XXX we could go to the next scanline unless the points are really small.
+ // i'd expect each point being present on at least one unique scanline
+ // but it turns out some people are using 2px points -sh 20180110
+#if BROKEN && 0
+ break;
+#endif
+ }
+ }
+end:
+
+ const int W = frame_gray.cols;
+ const int H = frame_gray.rows;
+
+ const unsigned sz = blobs.size();
+
+ std::sort(blobs.begin(), blobs.end(), [](const blob& b1, const blob& b2) { return b2.brightness < b1.brightness; });
+
+ for (idx = 0; idx < sz; ++idx)
+ {
+ blob &b = blobs[idx];
+ cv::Rect rect = b.rect;
+
+ rect.x -= rect.width / 2;
+ rect.y -= rect.height / 2;
+ rect.width *= 2;
+ rect.height *= 2;
+ rect &= cv::Rect(0, 0, W, H); // crop at frame boundaries
+
+ cv::Mat frame_roi = frame_gray(rect);
+
+ // smaller values mean more changes. 1 makes too many changes while 1.5 makes about .1
+ static constexpr f radius_c = f(1.75);
+
+ const f kernel_radius = b.radius * radius_c;
+ vec2 pos(rect.width/2., rect.height/2.); // position relative to ROI.
+
+ for (int iter = 0; iter < 10; ++iter)
+ {
+ vec2 com_new = MeanShiftIteration(frame_roi, pos, kernel_radius);
+ vec2 delta = com_new - pos;
+ pos = com_new;
+ if (delta.dot(delta) < 1e-2)
+ break;
+ }
+
+ b.pos[0] = pos[0] + rect.x;
+ b.pos[1] = pos[1] + rect.y;
+ }
+
+ for (unsigned k = 0; k < blobs.size(); k++)
+ {
+ blob& b = blobs[k];
+
+ const f dpi = preview_frame.cols / f(320);
+ const f offx = 10 * dpi, offy = 7.5 * dpi;
+
+ const f cx = preview_frame.cols / f(frame.cols),
+ cy = preview_frame.rows / f(frame.rows),
+ c_ = (cx+cy)/2;
+
+ static constexpr unsigned fract_bits = 16;
+ static constexpr double c_fract(1 << fract_bits);
+
+ cv::Point p(iround(b.pos[0] * cx * c_fract), iround(b.pos[1] * cy * c_fract));
+
+ auto circle_color = k >= PointModel::N_POINTS
+ ? cv::Scalar(192, 192, 192)
+ : cv::Scalar(255, 255, 0);
+
+ const f overlay_size = dpi > 1.5 ? 2 : 1;
+
+ cv::circle(preview_frame, p, iround((b.radius + 3.3) * c_ * c_fract), circle_color, overlay_size, cv::LINE_AA, fract_bits);
+
+ char buf[16];
+ buf[sizeof(buf)-1] = '\0';
+ std::snprintf(buf, sizeof(buf) - 1, "%.2fpx", b.radius);
+
+ auto text_color = k >= PointModel::N_POINTS
+ ? cv::Scalar(160, 160, 160)
+ : cv::Scalar(0, 0, 255);
+
+ cv::Point pos(iround(b.pos[0]*cx+offx), iround(b.pos[1]*cy+offy));
+ cv::putText(preview_frame, buf, pos,
+ cv::FONT_HERSHEY_PLAIN, overlay_size, text_color,
+ 1);
+ }
+
+ // End of mean shift code. At this point, blob positions are updated with hopefully less noisy less biased values.
+ points.reserve(max_blobs);
+ points.clear();
+
+ for (const auto& b : blobs)
+ {
+ // note: H/W is equal to fx/fy
+
+ vec2 p;
+ std::tie(p[0], p[1]) = to_screen_pos(b.pos[0], b.pos[1], W, H);
+ points.push_back(p);
+ }
+}
+
+blob::blob(f radius, const vec2& pos, f brightness, const cv::Rect& rect) :
+ radius(radius), brightness(brightness), pos(pos), rect(rect)
+{
+ //qDebug() << "radius" << radius << "pos" << pos[0] << pos[1];
+}
diff --git a/tracker-pt/module/point_extractor.h b/tracker-pt/module/point_extractor.h
new file mode 100644
index 00000000..1b6f55a2
--- /dev/null
+++ b/tracker-pt/module/point_extractor.h
@@ -0,0 +1,59 @@
+/* Copyright (c) 2012 Patrick Ruoff
+ * Copyright (c) 2015-2016 Stanislaw Halik <sthalik@misaki.pl>
+ *
+ * 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.
+ */
+
+#pragma once
+
+#include "pt-api.hpp"
+
+#include <vector>
+
+#include <opencv2/core.hpp>
+#include <opencv2/imgproc.hpp>
+
+namespace pt_module {
+
+using namespace types;
+
+struct blob
+{
+ f radius, brightness;
+ vec2 pos;
+ cv::Rect rect;
+
+ blob(f radius, const vec2& pos, f brightness, const cv::Rect& rect);
+};
+
+class PointExtractor final : public pt_point_extractor
+{
+public:
+ // extracts points from frame and draws some processing info into frame, if draw_output is set
+ // dt: time since last call in seconds
+ void extract_points(const pt_frame& frame, pt_preview& preview_frame, std::vector<vec2>& points) override;
+ PointExtractor(const QString& module_name);
+private:
+ static constexpr int max_blobs = 16;
+
+ pt_settings s;
+
+ cv::Mat1b frame_gray, frame_bin, frame_blobs;
+ cv::Mat1f hist;
+ std::vector<blob> blobs;
+ cv::Mat1b ch[3];
+
+ void ensure_channel_buffers(const cv::Mat& orig_frame);
+ void ensure_buffers(const cv::Mat& frame);
+
+ void extract_single_channel(const cv::Mat& orig_frame, int idx, cv::Mat& dest);
+ void extract_channels(const cv::Mat& orig_frame, const int* order, int order_npairs);
+
+ void color_to_grayscale(const cv::Mat& frame, cv::Mat1b& output);
+ void threshold_image(const cv::Mat& frame_gray, cv::Mat1b& output);
+};
+
+} // ns impl
+