Repository for OpenCV's extra modules
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

387 lines
16 KiB

#define CL_TARGET_OPENCL_VERSION 120
#include "../common/viz2d.hpp"
#include "../common/nvg.hpp"
#include "../common/util.hpp"
#include <cmath>
#include <vector>
#include <string>
#include <thread>
#include <opencv2/features2d.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/optflow.hpp>
#include <opencv2/core/ocl.hpp>
using std::cerr;
using std::endl;
using std::vector;
using std::string;
using namespace std::literals::chrono_literals;
/** Application parameters **/
constexpr unsigned int WIDTH = 1920;
constexpr unsigned int HEIGHT = 1080;
constexpr unsigned long DIAG = hypot(double(WIDTH), double(HEIGHT));
constexpr const char* OUTPUT_FILENAME = "optflow-demo.mkv";
constexpr bool OFFSCREEN = false;
constexpr int VA_HW_DEVICE_INDEX = 0;
/** Visualization parameters **/
// Generate the foreground at this scale.
float fg_scale = 0.5f;
// On every frame the foreground loses on brightness. specifies the loss in percent.
float fg_loss = 2.5;
//Convert the background to greyscale
bool grey_background = true;
// Peak thresholds for the scene change detection. Lowering them makes the detection more sensitive but
// the default should be fine.
float scene_change_thresh = 0.29f;
float scene_change_thresh_diff = 0.1f;
// The theoretical maximum number of points to track which is scaled by the density of detected points
// and therefor is usually much smaller.
int max_points = 250000;
// How many of the tracked points to lose intentionally, in percent.
float point_loss = 25;
// The theoretical maximum size of the drawing stroke which is scaled by the area of the convex hull
// of tracked points and therefor is usually much smaller.
int max_stroke = 14;
// Intensity of glow defined by kernel size. The default scales with the image diagonal.
int glow_kernel_size = std::max(int(DIAG / 138 % 2 == 0 ? DIAG / 138 + 1 : DIAG / 138), 1);
// Keep alpha separate for the GUI
float alpha = 0.05f;
// Red, green, blue and alpha. All from 0.0f to 1.0f
nanogui::Color effect_color(1.0f, 0.75f, 0.4f, 1.0f);
//display on-screen FPS
bool show_fps = true;
//Use OpenCL or not
bool use_opencl = true;
//Use a global bloom effect
bool use_bloom = false;
//The kernel size of the bloom effect
int bloom_kernel_size = 3;
//The lightness selection threshold
int bloom_thresh = 235;
//The intensity of the bloom filter
float bloom_gain = 4;
void prepare_motion_mask(const cv::UMat& srcGrey, cv::UMat& motionMaskGrey) {
static cv::Ptr<cv::BackgroundSubtractor> bg_subtrator = cv::createBackgroundSubtractorMOG2(100, 16.0, false);
bg_subtrator->apply(srcGrey, motionMaskGrey);
int morph_size = 1;
cv::Mat element = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(2 * morph_size + 1, 2 * morph_size + 1), cv::Point(morph_size, morph_size));
cv::morphologyEx(motionMaskGrey, motionMaskGrey, cv::MORPH_OPEN, element, cv::Point(element.cols >> 1, element.rows >> 1), 2, cv::BORDER_CONSTANT, cv::morphologyDefaultBorderValue());
}
void detect_points(const cv::UMat& srcMotionMaskGrey, vector<cv::Point2f>& points) {
static cv::Ptr<cv::FastFeatureDetector> detector = cv::FastFeatureDetector::create(1, false);
static vector<cv::KeyPoint> tmpKeyPoints;
tmpKeyPoints.clear();
detector->detect(srcMotionMaskGrey, tmpKeyPoints);
points.clear();
for (const auto &kp : tmpKeyPoints) {
points.push_back(kp.pt);
}
}
bool detect_scene_change(const cv::UMat& srcMotionMaskGrey, const float thresh, const float theshDiff) {
static float last_movement = 0;
float movement = cv::countNonZero(srcMotionMaskGrey) / double(srcMotionMaskGrey.cols * srcMotionMaskGrey.rows);
float relation = movement > 0 && last_movement > 0 ? std::max(movement, last_movement) / std::min(movement, last_movement) : 0;
float relM = relation * log10(1.0f + (movement * 9.0));
float relLM = relation * log10(1.0f + (last_movement * 9.0));
bool result = !((movement > 0 && last_movement > 0 && relation > 0)
&& (relM < thresh && relLM < thresh && fabs(relM - relLM) < theshDiff));
last_movement = (last_movement + movement) / 2.0f;
return result;
}
void visualize_sparse_optical_flow(const cv::UMat &prevGrey, const cv::UMat &nextGrey, vector<cv::Point2f> &detectedPoints,
const float scaleFactor, const int maxStrokeSize, const cv::Scalar color, const int maxPoints, const float pointLossPercent) {
static vector<cv::Point2f> hull, prevPoints, nextPoints, newPoints;
static vector<cv::Point2f> upPrevPoints, upNextPoints;
static std::vector<uchar> status;
static std::vector<float> err;
if (detectedPoints.size() > 4) {
cv::convexHull(detectedPoints, hull);
float area = cv::contourArea(hull);
float density = (detectedPoints.size() / area);
float stroke = maxStrokeSize * pow(area / (nextGrey.cols * nextGrey.rows), 0.33f);
size_t currentMaxPoints = density * maxPoints;
std::random_shuffle(prevPoints.begin(), prevPoints.end());
prevPoints.resize(ceil(prevPoints.size() * (1.0f - (pointLossPercent / 100.0f))));
size_t copyn = std::min(detectedPoints.size(), (size_t(std::ceil(currentMaxPoints)) - prevPoints.size()));
if (prevPoints.size() < currentMaxPoints) {
std::copy(detectedPoints.begin(), detectedPoints.begin() + copyn, std::back_inserter(prevPoints));
}
cv::calcOpticalFlowPyrLK(prevGrey, nextGrey, prevPoints, nextPoints, status, err);
newPoints.clear();
if (prevPoints.size() > 1 && nextPoints.size() > 1) {
upNextPoints.clear();
upPrevPoints.clear();
for (cv::Point2f pt : prevPoints) {
upPrevPoints.push_back(pt /= scaleFactor);
}
for (cv::Point2f pt : nextPoints) {
upNextPoints.push_back(pt /= scaleFactor);
}
using namespace kb::viz2d;
nvg::beginPath();
nvg::strokeWidth(stroke);
nvg::strokeColor(color);
for (size_t i = 0; i < prevPoints.size(); i++) {
if (status[i] == 1 && err[i] < (1.0 / density)
&& upNextPoints[i].y >= 0 && upNextPoints[i].x >= 0
&& upNextPoints[i].y < nextGrey.rows / scaleFactor
&& upNextPoints[i].x < nextGrey.cols / scaleFactor) {
float len = hypot(fabs(upPrevPoints[i].x - upNextPoints[i].x), fabs(upPrevPoints[i].y - upNextPoints[i].y));
if (len > 0 && len < sqrt(area)) {
newPoints.push_back(nextPoints[i]);
nvg::moveTo(upNextPoints[i].x, upNextPoints[i].y);
nvg::lineTo(upPrevPoints[i].x, upPrevPoints[i].y);
}
}
}
nvg::stroke();
}
prevPoints = newPoints;
}
}
void bloom(const cv::UMat& src, cv::UMat &dst, int ksize = 3, int threshValue = 235, float gain = 4) {
static cv::UMat hsv;
static cv::UMat sv16;
static cv::UMat sv;
static cv::UMat threshGrey;
static cv::UMat blur;
static std::vector<cv::UMat> hsvChannels;
cv::cvtColor(src, hsv, cv::COLOR_BGR2HSV);
cv::split(hsv, hsvChannels);
cv::bitwise_not(hsvChannels[1], hsvChannels[1]);
cv::multiply(hsvChannels[1], hsvChannels[2], sv16, 1, CV_16U);
cv::divide(sv16, cv::Scalar(255.0), sv, 1, CV_8U);
cv::threshold(sv, threshGrey, threshValue, 255, cv::THRESH_BINARY);
cv::boxFilter(threshGrey, blur, -1, cv::Size(ksize, ksize), cv::Point(-1,-1), true, cv::BORDER_REPLICATE);
cv::cvtColor(blur, blur, cv::COLOR_GRAY2BGRA);
addWeighted(src, 1.0, blur, gain, 0, dst);
}
void glow_effect(const cv::UMat &src, cv::UMat &dst, const int ksize) {
static cv::UMat resize;
static cv::UMat blur;
static cv::UMat dst16;
cv::bitwise_not(src, dst);
//Resize for some extra performance
cv::resize(dst, resize, cv::Size(), 0.5, 0.5);
//Cheap blur
cv::boxFilter(resize, resize, -1, cv::Size(ksize, ksize), cv::Point(-1,-1), true, cv::BORDER_REPLICATE);
//Back to original size
cv::resize(resize, blur, src.size());
//Multiply the src image with a blurred version of itself
cv::multiply(dst, blur, dst16, 1, CV_16U);
//Normalize and convert back to CV_8U
cv::divide(dst16, cv::Scalar::all(255.0), dst, 1, CV_8U);
cv::bitwise_not(dst, dst);
}
void composite_layers(const cv::UMat background, const cv::UMat foreground, const cv::UMat frameBuffer, cv::UMat dst, int glowKernelSize, float fgLossPercent, bool greyBackground) {
static cv::UMat glow;
static cv::UMat backgroundGrey;
cv::subtract(foreground, cv::Scalar::all(255.0f * (fgLossPercent / 100.0f)), foreground);
cv::add(foreground, frameBuffer, foreground);
glow_effect(foreground, glow, glowKernelSize);
if(greyBackground) {
cv::cvtColor(background, backgroundGrey, cv::COLOR_BGRA2GRAY);
cv::cvtColor(backgroundGrey, background, cv::COLOR_GRAY2BGRA);
}
cv::add(background, glow, dst);
}
void setup_gui(cv::Ptr<kb::viz2d::Viz2D> v2d) {
auto* effectWindow = v2d->makeWindow(5, 30, "Effects");
// nanogui::Button* btn = v2d->form()->add_button("Expand", [=]() {
// for(auto* child : effectWindow->children()) {
// child->set_visible(true);
// }
// btn->set_visible(false);
// v2d->perform_layout();
// });
effectWindow->button_panel()->add<nanogui::Button>("_")->set_callback([=](){
effectWindow->set_visible(false);
for(auto* child : effectWindow->children()) {
child->set_visible(false);
}
// btn->set_visible(true);
v2d->perform_layout();
});
v2d->makeGroup("Foreground");
v2d->makeFormVariable("Scale", fg_scale, 0.1f, 4.0f, true, "", "Generate the foreground at this scale");
v2d->makeFormVariable("Loss", fg_loss, 0.1f, 99.9f, true, "%", "On every frame the foreground loses on brightness");
v2d->makeGroup("Background");
v2d->makeFormVariable("Grey", grey_background, "Enable or disable global bloom effect");
v2d->makeGroup("Points");
v2d->makeFormVariable("Max. Points", max_points, 10, 1000000, true, "", "The theoretical maximum number of points to track which is scaled by the density of detected points and therefor is usually much smaller");
v2d->makeFormVariable("Point Loss", point_loss, 0.0f, 100.0f, true, "%", "How many of the tracked points to lose intentionally");
v2d->makeGroup("Optical flow");
v2d->makeFormVariable("Max. Stroke Size", max_stroke, 1, 100, true, "px", "The theoretical maximum size of the drawing stroke which is scaled by the area of the convex hull of tracked points and therefor is usually much smaller");
auto* glowKernel = v2d->makeFormVariable("Glow Kernel Size", glow_kernel_size, 1, 63, true, "", "Intensity of glow defined by kernel size");
glowKernel->set_callback([](const int& k) {
glow_kernel_size = std::max(int(k % 2 == 0 ? k + 1 : k), 1);
});
auto* color = v2d->form()->add_variable("Color", effect_color);
color->set_tooltip("The effect color");
color->set_final_callback([](const nanogui::Color &c) {
effect_color[0] = c[0];
effect_color[1] = c[1];
effect_color[2] = c[2];
});
v2d->makeFormVariable("Alpha", alpha, 0.0f, 1.0f, true, "", "The opacity of the effect");
v2d->makeGroup("Global Bloom");
v2d->makeFormVariable("Enable", use_bloom, "Enable or disable global bloom effect");
auto* bloomKernel = v2d->makeFormVariable("Bloom Kernel Size", bloom_kernel_size, 1, 63, true, "", "Size of global bloom effect defined by kernel size");
bloomKernel->set_callback([](const int& k) {
bloom_kernel_size = std::max(int(k % 2 == 0 ? k + 1 : k), 1);
});
v2d->makeFormVariable("Threshold", bloom_thresh, 1, 255, true, "", "The lightness selection threshold");
v2d->makeFormVariable("Gain", bloom_gain, 0.1f, 20.0f, true, "", "Intensity of the effect defined by gain");
auto* settingsWindow = v2d->makeWindow(240, 30, "Settings");
// settingsWindow->button_panel()->add<nanogui::Button>("_")->set_callback([=](){
// settingsWindow->set_visible(false);
// nanogui::Button* btn = lbl->add<nanogui::Button>("Settings");
// btn->set_callback([=](){
// settingsWindow->set_visible(true);
// });
// v2d->perform_layout();
// });
v2d->makeGroup("Acceleration");
v2d->makeFormVariable("Use OpenCL", use_opencl, "Enable or disable OpenCL acceleration");
v2d->makeGroup("Scene Change Detection");
v2d->makeFormVariable("Threshold", scene_change_thresh, 0.1f, 1.0f, true, "", "Peak threshold. Lowering it makes detection more sensitive");
v2d->makeFormVariable("Threshold Diff", scene_change_thresh_diff, 0.1f, 1.0f, true, "", "Difference of peak thresholds. Lowering it makes detection more sensitive");
v2d->makeGroup("Display");
v2d->makeFormVariable("Show FPS", show_fps, "Enable or disable the On-screen FPS display");
v2d->form()->add_button("Fullscreen", [=]() {
v2d->setFullscreen(!v2d->isFullscreen());
});
}
int main(int argc, char **argv) {
using namespace kb::viz2d;
if (argc != 2) {
std::cerr << "Usage: optflow <input-video-file>" << endl;
exit(1);
}
cv::Ptr<Viz2D> v2d = new Viz2D(cv::Size(WIDTH, HEIGHT), cv::Size(WIDTH, HEIGHT), OFFSCREEN, "Sparse Optical Flow Demo");
print_system_info();
if(!v2d->isOffscreen()) {
setup_gui(v2d);
v2d->setVisible(true);
}
auto capture = v2d->makeVACapture(argv[1], VA_HW_DEVICE_INDEX);
if (!capture.isOpened()) {
cerr << "ERROR! Unable to open video input" << endl;
exit(-1);
}
float fps = capture.get(cv::CAP_PROP_FPS);
float width = capture.get(cv::CAP_PROP_FRAME_WIDTH);
float height = capture.get(cv::CAP_PROP_FRAME_HEIGHT);
v2d->makeVAWriter(OUTPUT_FILENAME, cv::VideoWriter::fourcc('V', 'P', '9', '0'), fps, cv::Size{width, height}, VA_HW_DEVICE_INDEX);
//BGRA
cv::UMat background, foreground(v2d->getFrameBufferSize(), CV_8UC4, cv::Scalar::all(0));
//RGB
cv::UMat down;
//GREY
cv::UMat downPrevGrey, downNextGrey, downMotionMaskGrey;
vector<cv::Point2f> detectedPoints;
while (true) {
if(cv::ocl::useOpenCL() != use_opencl)
v2d->setUseOpenCL(use_opencl);
if(!v2d->captureVA())
break;
v2d->opencl([&](cv::UMat& frameBuffer){
cv::resize(frameBuffer, down, cv::Size(v2d->getFrameBufferSize().width * fg_scale, v2d->getFrameBufferSize().height * fg_scale));
frameBuffer.copyTo(background);
});
cv::cvtColor(down, downNextGrey, cv::COLOR_RGB2GRAY);
//Subtract the background to create a motion mask
prepare_motion_mask(downNextGrey, downMotionMaskGrey);
//Detect trackable points in the motion mask
detect_points(downMotionMaskGrey, detectedPoints);
v2d->nanovg([&](const cv::Size& sz) {
v2d->clear();
if (!downPrevGrey.empty()) {
//We don't want the algorithm to get out of hand when there is a scene change, so we suppress it when we detect one.
if (!detect_scene_change(downMotionMaskGrey, scene_change_thresh, scene_change_thresh_diff)) {
//Visualize the sparse optical flow using nanovg
cv::Scalar color = cv::Scalar(effect_color.b() * 255.0f, effect_color.g() * 255.0f, effect_color.r() * 255.0f, alpha * 255.0f);
visualize_sparse_optical_flow(downPrevGrey, downNextGrey, detectedPoints, fg_scale, max_stroke, color, max_points, point_loss);
}
}
});
downPrevGrey = downNextGrey.clone();
v2d->opencl([&](cv::UMat& frameBuffer){
//Put it all together (OpenCL)
composite_layers(background, foreground, frameBuffer, frameBuffer, glow_kernel_size, fg_loss, grey_background);
if(use_bloom)
bloom(frameBuffer, frameBuffer, bloom_kernel_size, bloom_thresh, bloom_gain);
});
update_fps(v2d, show_fps);
v2d->writeVA();
//If onscreen rendering is enabled it displays the framebuffer in the native window. Returns false if the window was closed.
if(!v2d->display())
break;
}
return 0;
}