#include <regex>
#include <cstdio>

#include "grabber.h"
#include "misc/Timer.h"
#include "processing/RawProcessing.h"
#include "misc/TestCamera.h"
#include <misc/InteractiveCamera.h>
#include <misc/ocl.h>
#include <processing/CPULabeling.h>
#include <misc/PVBlob.h>
#include <tracking/Tracker.h>
#include <tracker/misc/OutputLibrary.h>
#include <tracking/Export.h>
#include <tracker/misc/Output.h>
#include <python/GPURecognition.h>
#include <tracking/VisualField.h>
#include <pybind11/numpy.h>
#include <grabber/default_config.h>
#include <tracking/Recognition.h>

track::Tracker* tracker = nullptr;

using conversion_range_t = std::pair<long_t,long_t>;
CREATE_STRUCT(GrabSettings,
  (bool, grabber_use_threads),
  (bool, cam_undistort),
  (Rangef,        blob_size_range),
  (int,        threshold),
  (int,        threshold_maximum),
  (bool,        terminate),
  (bool,        reset_average),
  (bool,        image_invert),
  (bool,        correct_luminance),
  (bool,        recording),
  (size_t,        color_channel),
  (bool,        quit_after_average),
  (uint32_t,        stop_after_minutes),
  (float,        cam_scale),
  (conversion_range_t, video_conversion_range),
  (bool,        image_adjust),
  (bool,        equalize_histogram),
  (bool,        image_square_brightness),
  (float,        image_contrast_increase),
  (float,        image_brightness_increase),
  (bool,        enable_closed_loop)
)

#define GRAB_SETTINGS(NAME) GrabSettings::copy< GrabSettings:: NAME >()

static std::deque<std::shared_ptr<track::PPFrame>> unused_pp;
static std::deque<std::shared_ptr<track::PPFrame>> ppframe_queue;
static std::mutex ppframe_mutex, ppqueue_mutex;
static std::condition_variable ppvar;

long_t image_nr = 0;
#if !CAM_LOAD_FILE && WITH_PYLON
using namespace Pylon;
#endif

ENUM_CLASS(CLFeature,
           POSITION,
           VISUAL_FIELD,
           MIDLINE
);

IMPLEMENT(FrameGrabber::instance) = NULL;
IMPLEMENT(FrameGrabber::gpu_average);
IMPLEMENT(FrameGrabber::gpu_average_original);

std::unique_ptr<Image> FrameGrabber::latest_image() {
    decltype(_current_image) current;
    {
        std::unique_lock<std::mutex> lock(_frame_lock);
        current = std::move(_current_image);
    }
    return current;
}

cv::Size FrameGrabber::determine_resolution() {
    if(_video)
        return _video->size();
    
    std::lock_guard<std::mutex> guard(_camera_lock);
    if(_camera)
        return (cv::Size)_camera->size();
    
    return cv::Size(-1, -1);
}

track::Tracker* FrameGrabber::tracker_instance() {
    return tracker;
}


void FrameGrabber::apply_filters(gpuMat& gpu_buffer) {
    if(GRAB_SETTINGS(image_adjust)) {
        /*if(key == std::string("image_square_brightness"))
            image_square_brightness = value.template value<bool>();
        else if(key == std::string("image_contrast_increase"))
            image_contrast_increase = value.template value<float>();
        else if(key == std::string("image_brightness_increase"))
            image_brightness_increase = value.template value<float>();*/
        
        float alpha = GRAB_SETTINGS(image_contrast_increase) / 255.f;
        float beta = GRAB_SETTINGS(image_brightness_increase);
        
        static gpuMat buffer;
        
        //cv::Mat local;
       // gpu_buffer.copyTo(local);
        //tf::imshow("before", local);
        
        gpu_buffer.convertTo(buffer, CV_32FC1, alpha, beta);
        
        //gpu_buffer.convertTo(local, CV_8UC1, 255);
        //tf::imshow("contrast", local);
        
        if(GRAB_SETTINGS(image_square_brightness)) {
            cv::multiply(buffer, buffer, buffer);
            cv::multiply(buffer, buffer, buffer);

            //gpu_buffer.convertTo(local, CV_8UC1, 255);
            //tf::imshow("square", local);
        }
        
        // normalize resulting values between 0 and 1
        cv::threshold(buffer, buffer, 1, 1, cv::THRESH_TRUNC);
        
        //_buffer1.convertTo(_buffer0, CV_32FC1, 1./255.f);
        
        //cv::add(_buffer0, 1, _buffer1);
        //cv::multiply(_buffer1, _buffer1, _buffer0);
        //cv::multiply(_buffer0, _buffer0, _buffer1);
        
        
        //cv::multiply(_buffer1, _buffer1, _buffer1);
        //cv::subtract(_buffer1, 1, _buffer0);
        
        //cv::threshold(_buffer0, _buffer0, 1, 1, CV_THRESH_TRUNC);
        //cv::multiply(_buffer0, 255, _buffer0);
        
        buffer.convertTo(gpu_buffer, CV_8UC1, 255);
        
        if(GRAB_SETTINGS(equalize_histogram)) {
            cv::equalizeHist(gpu_buffer, gpu_buffer);
            //gpu_buffer.copyTo(local);
            //tf::imshow("histogram", local);
        }
        //_buffer1.copyTo(local);
    }
}

void ImageThreads::loading() {
    Image_t *last_loaded = NULL;

    while(!_terminate) {
        // retrieve images from camera
        _image_lock.lock();
        if(_unused.empty()) {
            // skip this image. queue is full...
            _image_lock.unlock();
            std::this_thread::sleep_for(std::chrono::microseconds(10));
            
        } else {
            Image_t *current = _unused.front();
            _unused.pop_front();
            _image_lock.unlock();
            
            _fn_prepare(last_loaded, *current);
            last_loaded = current;
            
            if(_fn_load(*current)) {
                // loading was successful, so push to processing
                _image_lock.lock();
                _used.push_front(current);
                _image_lock.unlock();
                
                _condition.notify_one();
                
            } else {
                _image_lock.lock();
                _unused.push_front(current);
                _image_lock.unlock();
            }
        }
    }
}

void ImageThreads::processing() {
    std::unique_lock<std::mutex> lock(_image_lock);
    
    while(!_terminate) {
        // process images and write to file
        _condition.wait(lock, [this](){ return !_used.empty() || _terminate; });
        
        if(!_used.empty()) {
            Image_t *current = _used.back();
            _used.pop_back();
            lock.unlock();
            
            _fn_process(*current);
            
            lock.lock();
            assert(!contains(_unused, current));
            _unused.push_back(current);
        }
    }
}

file::Path FrameGrabber::make_filename() {
    auto path = pv::DataLocation::parse("output", SETTING(filename).value<file::Path>());
    if(path.extension().to_string() == "pv")
        return path.remove_extension();
    
    return path;
}

void FrameGrabber::prepare_average() {
    Debug("Copying _original_average (%dx%d) back to _average and preparing...", _original_average.cols, _original_average.rows);
    _original_average.copyTo(_average);
    _processed.undistort(_average, _average);
    
    if(_crop_rect.width != _cam_size.width || _crop_rect.height != _cam_size.height)
    {
        Debug("Cropping %dx%d", _average.cols, _average.rows);
        _average(_crop_rect).copyTo(_average);
    }
    
    Debug("cam_scale = %f", GRAB_SETTINGS(cam_scale));
    if(GRAB_SETTINGS(cam_scale) != 1)
        resize_image(_average, GRAB_SETTINGS(cam_scale));
    
    if(GRAB_SETTINGS(image_invert))
        cv::subtract(cv::Scalar(255), _average, _average);
    
    if(GRAB_SETTINGS(correct_luminance)) {
        Debug("Calculating relative luminance...");
        if(_grid)
            delete _grid;
        cv::Mat tmp;
        _average.copyTo(tmp);
        _grid = new LuminanceGrid(tmp);
        _grid->correct_image(_average);
        
        //cv::Mat corrected;
        //gpu_average.copyTo(corrected);
        
    } //else
        //tmp.copyTo(gpu_average);
    
    /*if(scale != 1) {
        cv::Mat temp;
        //cv::resize(_average, temp, cv::Size(), scale, scale, cv::INTER_NEAREST);
        _processed.set_average(temp);
        if(tracker)
            tracker->set_average(temp);
    } else {*/
    
    apply_filters(_average);
    
    Debug("Copying _average %dx%d", _average.cols, _average.rows);
    cv::Mat temp;
    _average.copyTo(temp);
    _processed.set_average(temp);
    if(tracker)
        tracker->set_average(std::make_shared<Image>(temp));
    //}
    
    if(_video) {
        _video->processImage(_average, _average, false);
    }
    
    Debug("--- done preparing");
}

FrameGrabber::FrameGrabber(std::function<void(FrameGrabber&)> callback_before_starting)
  : //_current_image(NULL),
    _current_average_timestamp(0),
    _average_finished(false),
    _average_samples(0),
    _video(NULL), _video_mask(NULL),
    _camera(NULL),
    _current_fps(0), _fps(0),
    _processed(make_filename()),
    previous_time(0), _loading_timing(0), _grid(NULL), file(NULL), _terminate_tracker(false)
#if WITH_FFMPEG
, mp4_thread(NULL), mp4_queue(NULL)
#endif
{
    FrameGrabber::instance = this;
    GrabSettings::init();
    
    _pool = std::make_unique<GenericThreadPool>(max(1u, cmn::hardware_concurrency()), [](auto e) { std::rethrow_exception(e); }, "ocl_threads", [](){
        ocl::init_ocl();
    });
    
    if(!_processed.filename().remove_filename().empty() && !_processed.filename().remove_filename().exists())
        _processed.filename().remove_filename().create_folder();
    
    std::string source = SETTING(video_source);
    
#if WITH_PYLON
    if(utils::lowercase(source) == "basler") {
        std::lock_guard<std::mutex> guard(_camera_lock);
        _camera = new fg::PylonCamera;
        if(SETTING(cam_framerate).value<int>() > 0 && SETTING(frame_rate).value<int>() <= 0) {
            SETTING(frame_rate) = SETTING(cam_framerate).value<int>();
        }
        
        auto path = average_name();
        Debug("Saving average at or loading from '%S'.", &path.str());
        
        if(path.exists()) {
            if(SETTING(reset_average)) {
                Warning("Average exists, but will not be used because 'reset_average' is set to true.");
                SETTING(reset_average) = false;
            } else {
                cv::Mat file = cv::imread(path.str());
                if(file.rows == _camera->size().height && file.cols == _camera->size().width) {
                    cv::cvtColor(file, _average, cv::COLOR_BGR2GRAY);
                    _average_finished = true;
                    _current_average_timestamp = 1337;
                } else
                    Warning("Loaded average has wrong dimensions (%dx%d), overwriting...", file.cols, file.rows);
            }
        } else {
            Debug("Average image at '%S' doesnt exist.", &path.str());
            if(SETTING(reset_average))
                SETTING(reset_average) = false;
        }
        
    }
    else
#else
    if (utils::lowercase(source) == "basler") {
        U_EXCEPTION("Software was not compiled with basler API.");

    } else
#endif

    if(utils::lowercase(source) == "webcam") {
        std::lock_guard<std::mutex> guard(_camera_lock);
        _camera = new fg::Webcam;
        _processed.set_resolution(_camera->size() * GRAB_SETTINGS(cam_scale));
        
    } else if(utils::lowercase(source) == "test_image") {
        std::lock_guard<std::mutex> guard(_camera_lock);
        _camera = new fg::TestCamera(SETTING(cam_resolution).value<cv::Size>());
        cv::Mat background = cv::Mat::ones(_camera->size().height, _camera->size().width, CV_8UC1) * 255;
        
        _average_finished = true;
        background.copyTo(_average);
        _current_average_timestamp = 1337;
        
    } else if(utils::lowercase(source) == "interactive") {
        if(SETTING(cam_framerate).value<int>() > 0 && SETTING(frame_rate).value<int>() <= 0) {
            SETTING(frame_rate) = SETTING(cam_framerate).value<int>();
        } else
            SETTING(frame_rate).value<int>() = 30;
        
        std::lock_guard<std::mutex> guard(_camera_lock);
        _camera = new fg::InteractiveCamera();
        cv::Mat background = cv::Mat::zeros(_camera->size().height, _camera->size().width, CV_8UC1);
        _average_finished = true;
        background.copyTo(_average);
        _current_average_timestamp = 1337;
        
    } else {
        initialize_video();
    }
    
    // determine recording resolution and set it
	_cam_size = determine_resolution();
    SETTING(video_size) = Size2(_cam_size) * GRAB_SETTINGS(cam_scale);
    
#if WITH_FFMPEG
    if(SETTING(save_raw_movie)) {
        auto path = _processed.filename();
        if(path.has_extension())
            path = path.replace_extension("mov");
        else
            path = path.add_extension("mov");
        mp4_queue = new FFMPEGQueue(true, Size2(_cam_size), path);
        Debug("Encoding mp4 into '%S'...", &path.str());
        mp4_thread = new std::thread([this](){
            mp4_queue->loop();
        });
    }
#endif
    
    if(_video) {
        SETTING(cam_resolution).value<cv::Size>() = cv::Size(Size2(_cam_size) * GRAB_SETTINGS(cam_scale));
    }

    if(GRAB_SETTINGS(enable_closed_loop) && !SETTING(enable_live_tracking)) {
        Warning("Forcing enable_live_tracking = true because closed loop has been enabled.");
        SETTING(enable_live_tracking) = true;
    }
    
    if (SETTING(enable_live_tracking)) {
        tracker = new track::Tracker();
        Output::Library::Init();
    }
    
    if (GRAB_SETTINGS(enable_closed_loop)) {
        track::PythonIntegration::set_settings(GlobalSettings::instance());
        track::PythonIntegration::set_display_function([](auto& name, auto& mat) { tf::imshow(name, mat); });

        track::Recognition::fix_python();
        track::PythonIntegration::instance();
        track::PythonIntegration::ensure_started();
    }

    if (tracker) {
        _tracker_thread = new std::thread([this]() {
            update_tracker_queue();
        });
    }

    cv::Mat map1, map2;
    cv::Size size = _cam_size;
    
    cv::Mat cam_matrix = cv::Mat(3, 3, CV_32FC1, SETTING(cam_matrix).value<std::vector<float>>().data());
    cv::Mat cam_undistort_vector = cv::Mat(1, 5, CV_32FC1, SETTING(cam_undistort_vector).value<std::vector<float>>().data());
    
    cv::Mat drawtransform = cv::getOptimalNewCameraMatrix(cam_matrix, cam_undistort_vector, size, 1.0, size);
    print_mat("draw_transform", drawtransform);
    print_mat("cam", cam_matrix);
    //drawtransform = SETTING(cam_matrix).value<cv::Mat>();
    cv::initUndistortRectifyMap(
                                cam_matrix,
                                cam_undistort_vector,
                                cv::Mat(),
                                drawtransform,
                                size,
                                CV_32FC1,
                                map1, map2);
    
    GlobalSettings::get("cam_undistort1") = map1;
    GlobalSettings::get("cam_undistort2") = map2;
    
    if(GlobalSettings::map().has("meta_real_width") && GlobalSettings::map().has("cam_scale") && SETTING(cam_scale).value<float>() != 1) {
        Warning("Scaling `meta_real_width` (%f) due to `cam_scale` (%f) being set.", SETTING(meta_real_width).value<float>(), SETTING(cam_scale).value<float>());
        //SETTING(meta_real_width) = SETTING(meta_real_width).value<float>() * SETTING(cam_scale).value<float>();
    }
    
    // setting cm_per_pixel after average has been generated (and offsets have been set)
    if(!GlobalSettings::map().has("cm_per_pixel") || SETTING(cm_per_pixel).value<float>() == 0)
        SETTING(cm_per_pixel) = SETTING(meta_real_width).value<float>() / SETTING(video_size).value<Size2>().width;
    
    _average.copyTo(_original_average);
    callback_before_starting(*this);
    
    // determine offsets
    CropOffsets roff = SETTING(crop_offsets);
    _processed.set_offsets(roff);
    
    _crop_rect = roff.toPixels(_cam_size);
    _cropped_size = cv::Size(_crop_rect.width * GRAB_SETTINGS(cam_scale), _crop_rect.height * GRAB_SETTINGS(cam_scale));
    
    {
        std::lock_guard<std::mutex> guard(_camera_lock);
        if(_camera) {
            _processed.set_resolution(_cropped_size);
            _camera->set_crop(_crop_rect);
        }
    }
    
    if(_video) {
        _average.copyTo(_original_average);
        prepare_average();
        _average_finished = true;
        _current_average_timestamp = 42;
    }
    
    // create mask if necessary
    if(SETTING(cam_circle_mask)) {
        cv::Mat mask = cv::Mat::zeros(_cropped_size.height, _cropped_size.width, CV_8UC1);
        cv::circle(mask, cv::Point(mask.cols/2, mask.rows/2), min(mask.cols, mask.rows)/2, cv::Scalar(1), -1);
        _processed.set_mask(mask);
    }
    
    //auto epoch = std::chrono::time_point<std::chrono::system_clock>();
    _start_timing = _video && !_video->has_timestamps() ? 0 : UINT64_MAX;//Image::clock_::now();
    _real_timing = std::chrono::system_clock::now();
	
    _analysis = new std::decay<decltype(*_analysis)>::type(
          [&]() -> Image_t* { // create object
              return new Image_t(_cam_size.height, _cam_size.width);
          },
          [&](const Image_t* prev, Image_t& current) -> bool { // prepare object
              if(_video) {
                  current.set_index(prev != NULL ? prev->index() + 1 : (GRAB_SETTINGS(video_conversion_range).first != -1 ? GRAB_SETTINGS(video_conversion_range).first : 0));
                  
                  if(GRAB_SETTINGS(video_conversion_range).second != -1) {
                      if(current.index() >= GRAB_SETTINGS(video_conversion_range).second) {
                          if(!GRAB_SETTINGS(terminate))
                              SETTING(terminate) = true;
                          return false;
                      }
                  } else {
                      if(current.index() >= long_t(_video->length())) {
                          if(!GRAB_SETTINGS(terminate))
                              SETTING(terminate) = true;
                          return false;
                      }
                  }
                  
                  if(!_video->has_timestamps()) {
                      double percent = double(current.index()) / double(SETTING(frame_rate).value<int>()) * 1000.0;
                      
                      size_t fake_delta = size_t(percent * 1000.0);
                      current.set_timestamp(_start_timing + fake_delta);//std::chrono::microseconds(fake_delta));
                  } else {
                      current.set_timestamp(_video->timestamp(current.index()));
                  }
                  
                  if(GRAB_SETTINGS(terminate))
                      return false;
                  
              } else {
                  current.set_index(prev != NULL ? prev->index() + 1 : 0);
              }
              
              return true;
          },
          [&](Image_t& current) -> bool { return load_image(current); },
          [&](Image_t& current) -> Queue::Code { return process_image(current); });
    
    Debug("ThreadedAnalysis started (%dx%d | %dx%d).", _cam_size.width, _cam_size.height, _cropped_size.width, _cropped_size.height);
}

FrameGrabber::~FrameGrabber() {
    // stop processing
    Debug("Terminating...");
    _analysis->terminate();
    delete _analysis;

    {
        std::unique_lock<std::mutex> guard(_log_lock);
        if(file)
            fclose(file);
        file = NULL;
    }
    
    // wait for all the threads to finish
    while(true) {
        std::unique_lock<std::mutex> lock(_lock);
        if(_image_queue.empty())
            break;
    }
    
    /*for (auto t : _pool) {
        t->join();
        delete t;
    }*/
    
    _terminate_tracker = true;
    _multi_variable.notify_all();
    for(auto &thread: _multi_pool) {
        thread->join();
    }
    _multi_pool.clear();
    
	//delete _analysis;
    
    if(_processed.open())
        _processed.stop_writing();
	
    if(_video)
        delete _video;
    
    {
        std::lock_guard<std::mutex> guard(_camera_lock);
        if(_camera)
            delete _camera;
    }
    
#if WITH_FFMPEG
    if(mp4_queue) {
        mp4_queue->terminate() = true;
        mp4_queue->notify();
        mp4_thread->join();
        delete mp4_queue;
        delete mp4_thread;
    }
#endif
    
    if(tracker) {
        ppvar.notify_all();
        _tracker_thread->join();
        delete _tracker_thread;
        
        {
            track::Tracker::LockGuard guard("GUI::save_state");
            tracker->wait();
            
            if(!SETTING(auto_no_tracking_data))
                track::export_data(*tracker, -1, Rangel());
            
            std::vector<std::string> additional_exclusions;
            sprite::Map config_grabber, config_tracker;
            GlobalSettings::docs_map_t docs;
            grab::default_config::get(config_grabber, docs, nullptr);
            ::default_config::get(config_tracker, docs, nullptr);
            
            auto tracker_keys = config_tracker.keys();
            for(auto &key : config_grabber.keys()) {
                if(!contains(tracker_keys, key)) {
                    additional_exclusions.push_back(key);
                }
            }
            
            additional_exclusions.insert(additional_exclusions.end(), {
                "cam_undistort1",
                "cam_undistort2",
                "frame_rate",
                "web_time_threshold",
                "auto_no_tracking_data",
                "auto_no_results",
                "auto_no_memory_stats"
            });
            
            auto add = Meta::toStr(additional_exclusions);
            Debug("Excluding fields %S", &add);
            
            auto filename = file::Path(pv::DataLocation::parse("output_settings").str());
            if(!filename.exists() || SETTING(grabber_force_settings)) {
                auto text = default_config::generate_delta_config(false, additional_exclusions);
                
                FILE *f = fopen(filename.str().c_str(), "wb");
                if(f) {
                    if(filename.exists())
                        Warning("Overwriting file '%S'.", &filename.str());
                    else
                        Debug("Writing settings file '%S'.", &filename.str());
                    fwrite(text.data(), 1, text.length(), f);
                    fclose(f);
                } else {
                    Except("Dont have write permissions for file '%S'.", &filename.str());
                }
            }
            
            if(!SETTING(auto_no_results)) {
                try {
                    Output::TrackingResults results(*tracker);
                    results.save([](const std::string&, float, const std::string&){  }, Output::TrackingResults::expected_filename(), additional_exclusions);
                } catch(const UtilsException&) { Except("Something went wrong saving program state. Maybe no write permissions?"); }
            }
        }
        
        delete tracker;
    }
    
    FrameGrabber::instance = NULL;
    
    file::Path filename = make_filename().add_extension("pv");
    if(filename.exists()) {
        pv::File file(filename);
        file.start_reading();
        file.print_info();
    } else {
        Error("No file has been written.");
    }
}

file::Path FrameGrabber::average_name() const {
    auto path = pv::DataLocation::parse("output", "average_" + SETTING(filename).value<file::Path>().filename().to_string() + ".png");
    return path;
}

void FrameGrabber::initialize_video() {
    std::vector<file::Path> filenames;
    auto video_source = SETTING(video_source).value<std::string>();
    try {
        filenames = Meta::fromStr<std::vector<file::Path>>(video_source);
        if(filenames.size() > 1) {
            Debug("Found an array of filenames (%d).", filenames.size());
        } else if(filenames.size() == 1) {
            SETTING(video_source) = filenames.front();
            filenames.clear();
        } else
            U_EXCEPTION("Empty input filename '%S'. Please specify an input name.", &video_source);
        
    } catch(const illegal_syntax& e) {
        // ... do nothing
    }
    
    if(filenames.empty()) {
        auto filepath = file::Path(SETTING(video_source).value<std::string>());
        if(filepath.remove_filename().empty()) {
            auto path = (SETTING(output_dir).value<file::Path>() / filepath);
            filenames.push_back(path);
        } else
            filenames.push_back(filepath);
    }
    
    for(auto &name : filenames) {
        name = pv::DataLocation::parse("input", name);
    }
    
    if(filenames.size() == 1) {
        std::string source = filenames.front().str();
        std::smatch m;
        std::regex rplaceholder ("%[0-9]+(\\.[0-9]+(.[1-9][0-9]*)?)?d$"), rext(".*(\\..+)$");
        
        long_t number_length = -1, start_number = 0, end_number = VIDEO_SEQUENCE_UNSPECIFIED_VALUE;
        
        std::string base_name, extension;
        if(std::regex_search(source,m,rext)) {
            auto x = m[1];
            extension = x.str().substr(1);
            base_name = source.substr(0, m.position(1));
            
            Debug("Extension '%S' basename '%S'", &extension, &base_name);
            
        } else {
            U_EXCEPTION("File extension not found in '%S'", &source);
        }
        
        if(std::regex_search (base_name,m,rplaceholder)) {
            auto x = m[0];
            
            std::string s = x.str();
            auto p = m.position();
            
            s = s.substr(1, s.length()-2);
            auto split = utils::split(s, '.');
            
            if(split.size()>1) {
                start_number = std::stoi(split[1]);
            }
            if(split.size()>2) {
                end_number = std::stoi(split[2]);
            }
            
            number_length = std::stoi(split[0]);
            base_name = base_name.substr(0, p);
            Debug("match '%S' at %d with nr %d", &s, p, number_length);
        }
        
        if(number_length != -1) {
            // no placeholders found, just load file.
            _video = new VideoSource(base_name, extension, start_number, end_number, number_length);
        } else {
            _video = new VideoSource(base_name, extension);
        }
        
    } else {
        _video = new VideoSource(filenames);
    }
    
    int frame_rate = _video->framerate();
    if(frame_rate == -1) {
        frame_rate = 25;
    }
    
    auto path = average_name();
    Debug("Saving average at or loading from '%S'.", &path.str());
    
    if(SETTING(frame_rate).value<int>() == -1) {
        Debug("Setting frame rate to %d (from video).", frame_rate);
        SETTING(frame_rate) = frame_rate;
    } else if(SETTING(frame_rate).value<int>() != frame_rate) {
        Warning("Overwriting default frame rate of %d with %d.", frame_rate, SETTING(frame_rate).value<int>());
    }
    
    if(!SETTING(mask_path).value<file::Path>().empty()) {
        auto path = pv::DataLocation::parse("input", SETTING(mask_path).value<file::Path>());
        if(path.exists()) {
            auto folder = path.remove_filename();
            auto ext = path.extension().to_string();
            auto base = path.remove_extension();
            
            _video_mask = new VideoSource(base.str(), ext);
        }
    }
    
    if(path.exists()) {
        if(SETTING(reset_average)) {
            Warning("Average exists, but will not be used because 'reset_average' is set to true.");
            SETTING(reset_average) = false;
        } else {
            cv::Mat file = cv::imread(path.str());
            if(file.rows == _video->size().height && file.cols == _video->size().width) {
                cv::cvtColor(file, _average, cv::COLOR_BGR2GRAY);
            } else
                Warning("Loaded average has wrong dimensions (%dx%d), overwriting...", file.cols, file.rows);
        }
    } else {
        Debug("Average image at '%S' doesnt exist.", &path.str());
        if(SETTING(reset_average))
            SETTING(reset_average) = false;
    }
    
    if(_average.empty()) {
        Debug("Generating new average.");
        _video->generate_average(_video->average(), 0);
        _video->average().copyTo(_average);
        
        if(!SETTING(terminate))
            cv::imwrite(path.str(), _average);
        
    } else {
        Debug("Reusing previously generated average.");
    }
    
    //_video->undistort(_average, _average);
    
    if(SETTING(quit_after_average))
        SETTING(terminate) = true;
    
    _current_average_timestamp = 1336;
}

bool FrameGrabber::add_image_to_average(const Image_t& current) {
    // Create average image, whenever average_finished is not set
    if(!_average_finished || GRAB_SETTINGS(reset_average)) {
        file::Path fname;
        if(!_average_finished)
            fname = average_name();
        
        if(GRAB_SETTINGS(reset_average)) {
            SETTING(reset_average) = false;
            
            // to protect _last_frame
            std::lock_guard<std::mutex> guard(_frame_lock);
            _average_samples = 0;
            _average_finished = false;
            if(fname.exists()) {
                if(!fname.delete_file()) {
                    U_EXCEPTION("Cannot delete file '%S'.", &fname.str());
                }
                else Debug("Deleted file '%S'.", &fname.str());
            }
            
            _last_frame = nullptr;
            _current_image = nullptr;
        }
        
        static std::string averaging_method = GlobalSettings::has("averaging_method") ?  utils::lowercase(SETTING(averaging_method).value<std::string>()) : "mean";
        static bool use_mean = averaging_method != "max" && averaging_method != "min";
        static gpuMat empty_image;
        if(empty_image.empty())
            empty_image = gpuMat::zeros(_cropped_size.height, _cropped_size.width, CV_8UC1);
        current.get().copyTo(empty_image);
        //current.get(empty_image);
        
        if(use_mean) {
            cv::Mat tmp;
            empty_image.convertTo(tmp, CV_32FC1, 1.0/255.0);
            
            if(_current_average.empty() || _current_average.type() != CV_32FC1)
                tmp.copyTo(_current_average);
            else
                _current_average += tmp;
        } else {
            if(_current_average.empty())
                empty_image.copyTo(_current_average);
            else {
                cv::Mat local, local_av;
                _current_average.copyTo(local_av);
                empty_image.copyTo(local);
                
                if(averaging_method == "max")
                    _current_average = cv::max(local, local_av);
                else if(averaging_method == "min")
                    _current_average = cv::min(local, local_av);
            }
        }
        
        _average_samples++;
        
        if(_average_samples >= SETTING(average_samples).value<int>()) { //|| fname.is_regular()) {
            if(use_mean) {
                _current_average /= float(_average_samples);
                std::lock_guard<std::mutex> guard(_frame_lock);
                _current_average.convertTo(_average, CV_8UC1, 255.0);
                
            } else {
                std::lock_guard<std::mutex> guard(_frame_lock);
                _current_average.copyTo(_average);
            }
            
            _current_average_timestamp = std::chrono::steady_clock::now().time_since_epoch().count();
            
            if(_average.channels() > 1) {
                static const size_t color_channel = SETTING(color_channel).value<size_t>();
                if(color_channel >= 3) {
                    // turn into HUE
                    if(_average.channels() == 3) {
                        cv::cvtColor(_average, _average, cv::COLOR_BGR2HSV);
                        cv::extractChannel(_average, _average, 0);
                    } else Error("Cannot copy to read frame with %d channels.", _average.channels());
                } else {
                    cv::Mat copy;
                    cv::extractChannel(_average, copy, color_channel);
                    copy.copyTo(_average);
                }
            }
            
            assert(_average.type() == CV_8UC1);
            _average.copyTo(_original_average);
            
            cv::Mat tmp;
            _average.copyTo(tmp);
            
            if(!cv::imwrite(fname.str(), tmp))
                Error("Cannot write '%S'.", &fname.str());
            else
                Debug("Saved new average image at '%S'.", &fname.str());
            /*} else {
                cv::Mat f = cv::imread(fname.str());
                if(f.cols != _current_average.cols || f.rows != _current_average.rows) {
                    // invalid size
                    if(!fname.delete_file())
                        U_EXCEPTION("Cannot delete file '%S'.", &fname.str());
                    _average = gpuMat();
                    
                    return Queue::ITEM_NEXT;
                } else {
                    f.copyTo(_average);
                }
            }*/
            
            /*cv::Mat copied = _average;
            _processed.undistort(_average, copied);
            
            auto scale = SETTING(cam_scale).value<float>();
            if(scale != 1) {
                cv::Mat temp;
                resize_image(copied, temp, scale);
                _processed.set_average(temp);
            } else {
                _processed.set_average(copied);
            }
            //_processed.processImage(_average, _average, false);*/
            
            prepare_average();
            
            _average_finished = true;
            
            if(SETTING(quit_after_average))
                SETTING(terminate) = true;
        }
        
        std::unique_lock<std::mutex> lock(_frame_lock);
        _current_image = std::make_unique<Image>(current);
        
        return true;
    }
    
    return false;
}

bool FrameGrabber::load_image(Image_t& current) {
    Timer timer;
    
    if(GRAB_SETTINGS(terminate))
        return false;
    
    if(_video) {
        if(current.index() >= long_t(_video->length())) {
            if(!GRAB_SETTINGS(terminate)) {
                SETTING(terminate) = true;
            }
            return false;
        }
        
        assert(!current.empty());
        
        cv::Mat m = current.get();
        
        try {
            _video->frame(current.index(), m);
            
            Image *mask_ptr = NULL;
            if(_video_mask) {
                static cv::Mat mask;
                _video_mask->frame(current.index(), mask);
                assert(mask.channels() == 1);
                
                mask_ptr = new Image(mask.rows, mask.cols, 1);
                mask_ptr->set(-1, mask);
            }
            
            current.set_mask(mask_ptr);
            
        } catch(const UtilsException& e) {
            Except("Skipping frame %d and ending conversion.", current.index());
            if(!GRAB_SETTINGS(terminate)) {
                SETTING(terminate) = true;
            }
            
            return false;
        }
        
    } else {
        std::lock_guard<std::mutex> guard(_camera_lock);
        if(_camera) {
            //static Image_t image(_camera->size().height, _camera->size().width, 1);
            if(!_camera->next(current)) {
                return false;
            }
            //current.set(image);
        }
    }
    
    if(add_image_to_average(current))
        return false;
    
    if(GRAB_SETTINGS(image_invert)) {
        cv::subtract(cv::Scalar(255), current.get(), current.get());
    }
    
    _loading_timing = _loading_timing * 0.75 + timer.elapsed() * 0.25;
    return true;
}

void FrameGrabber::add_tracker_queue(const pv::Frame& frame, long_t index) {
    std::shared_ptr<track::PPFrame> ptr;
    static size_t created_items = 0;
    static Timer print_timer;
    
    {
        std::unique_lock<std::mutex> guard(ppframe_mutex);
        while (!GRAB_SETTINGS(enable_closed_loop) && video() && created_items > 100 && unused_pp.empty()) {
            if(print_timer.elapsed() > 5) {
                Debug("Waiting (%d images cached) for tracking...", created_items);
                print_timer.reset();
            }
            guard.unlock();
            std::this_thread::sleep_for(std::chrono::milliseconds(5));
            guard.lock();
        }
        
        if(!unused_pp.empty()) {
            ptr = unused_pp.front();
            unused_pp.pop_front();
        } else
            ++created_items;
    }
    
    if(!ptr) {
        ptr = std::make_shared<track::PPFrame>();
    }
    
    ptr->frame() = frame;
    ptr->frame().set_index(index);
    ptr->frame().set_timestamp(frame.timestamp());
    ptr->set_index(index);
    
    {
        std::lock_guard<std::mutex> guard(ppqueue_mutex);
        ppframe_queue.push_back(ptr);
    }
    
    ppvar.notify_one();
}

void FrameGrabber::update_tracker_queue() {
    set_thread_name("Tracker::Thread");
    Debug("Starting tracker thread.");
    _terminate_tracker = false;
    
    //pybind11::module closed_loop;
    Timer content_timer;
    
    std::mutex feature_mutex;
    std::set<CLFeature::Class> selected_features;
    auto request_features = [&feature_mutex, &selected_features](std::string features)
    {
        std::lock_guard<std::mutex> guard(feature_mutex);
        selected_features.clear();
        
        auto array = utils::split(features, ',');
        for(auto &a : array) {
            a = utils::uppercase(utils::trim(a));
            if(CLFeature::has(a)) {
                auto feature = CLFeature::get(a);
                selected_features.insert(feature);
                Debug("Feature '%s' will be sent to python.", feature.name());
            } else
                Warning("CLFeature '%S' is unknown and will be ignored.", &a);
        }
    };
    
    if(GRAB_SETTINGS(enable_closed_loop)) {
        track::PythonIntegration::async_python_function([&request_features]()
        {
            try {
                track::PythonIntegration::import_module("closed_loop");
                request_features(track::PythonIntegration::run_retrieve_str("closed_loop", "request_features"));
            }
            catch (const SoftException&) {

            }
            return true;
            
        }).get();
    }
    
    static Timer print_quit_timer;
    static Timer loop_timer;
    
    std::unique_lock<std::mutex> guard(ppqueue_mutex);
    while (!_terminate_tracker || (!GRAB_SETTINGS(enable_closed_loop) && !ppframe_queue.empty() /* we cannot skip frames */)) {
        if(ppframe_queue.empty())
            ppvar.wait(guard);
        
        if(GRAB_SETTINGS(enable_closed_loop) && ppframe_queue.size() > 1) {
            if (print_quit_timer.elapsed() > 1) {
                Debug("Skipping %d frames for tracking.", ppframe_queue.size() - 1);
                print_quit_timer.reset();
            }
            ppframe_queue.erase(ppframe_queue.begin(), ppframe_queue.begin() + ppframe_queue.size() - 1);
        }
        
        while(!ppframe_queue.empty()) {
            if(_terminate_tracker && !GRAB_SETTINGS(enable_closed_loop) /* we cannot skip frames */) {
                if(print_quit_timer.elapsed() > 5) {
                    Debug("[Tracker] Adding remaining frames (%d)...", ppframe_queue.size());
                    print_quit_timer.reset();
                }
            }
            
            loop_timer.reset();

            auto copy = ppframe_queue.front();
            ppframe_queue.pop_front();
            guard.unlock();
            
            if(copy && !tracker) {
                U_EXCEPTION("Cannot track frame %d since tracker has been deleted.", copy->index());
            }
            
            if(copy && tracker) {
                track::Tracker::LockGuard guard("update_tracker_queue");
                //copy->set_index(track::Tracker::end_frame()+1);
                //copy->frame().set_index(copy->index());
                track::Tracker::preprocess_frame(*copy, {}, NULL, NULL, false);
                tracker->add(*copy);

                
                //std::this_thread::sleep_for(std::chrono::seconds(1));
                static Timer test_timer;
                if (test_timer.elapsed() > 10) {
                    test_timer.reset();
                    Debug("(tracker) %d individuals", tracker->individuals().size());
                }
                
#define CL_HAS_FEATURE(NAME) (selected_features.find(CLFeature:: NAME) != selected_features.end())
                if(GRAB_SETTINGS(enable_closed_loop)) {
                    std::map<long_t, std::shared_ptr<track::VisualField>> visual_fields;
                    std::map<long_t, track::Midline::Ptr> midlines;
                    
                    if(CL_HAS_FEATURE(VISUAL_FIELD)) {
                        for(auto fish : tracker->active_individuals(copy->frame().index())) {
                            if(fish->head(copy->frame().index()))
                                visual_fields[fish->identity().ID()] = std::make_shared<track::VisualField>(fish->identity().ID(), copy->frame().index(), fish->basic_stuff(copy->frame().index()), fish->posture_stuff(copy->frame().index()), false);
                            
                        }
                    }
                    
                    if(CL_HAS_FEATURE(MIDLINE)) {
                        for(auto fish : tracker->active_individuals(copy->frame().index())) {
                            auto midline = fish->midline(copy->frame().index());
                            if(midline)
                                midlines[fish->identity().ID()] = midline;
                        }
                    }
                    
                    static Timing timing("python::closed_loop", 0.1);
                    TakeTiming take(timing);
                    
                    track::PythonIntegration::async_python_function([&content_timer, &copy, &visual_fields, &midlines, frame = copy->index(), &request_features, &selected_features]()
                    {
                        std::vector<long_t> ids;
                        std::vector<float> midline_points;
                        std::vector<long_t> colors;
                        std::vector<float> xy;
                        std::vector<float> centers;

                        size_t number_fields = 0;
                        std::vector<long_t> vids;
                        
                        for(auto & fish : tracker->active_individuals(copy->frame().index()))
                        {
                            auto basic = fish->basic_stuff(copy->frame().index());
                            if(basic) {
                                
                                ids.push_back(fish->identity().ID());
                                colors.insert(colors.end(), { (long_t)fish->identity().color().r, (long_t)fish->identity().color().g, (long_t)fish->identity().color().b });

                                auto bounds = basic->blob.calculate_bounds();
                                auto pos = bounds.pos();
                                centers.insert(centers.end(), { float(bounds.width * 0.5), float(bounds.height * 0.5) });
                                xy.insert(xy.end(), { pos.x, pos.y });
                                
                                if (!visual_fields.count(fish->identity().ID()))
                                    continue;

                                ++number_fields;

                                auto &eye0 = visual_fields[fish->identity().ID()]->eyes().front()._visible_ids;
                                auto &eye1 = visual_fields[fish->identity().ID()]->eyes().back()._visible_ids;

                                vids.insert(vids.end(), eye0.begin(), eye0.begin() + track::VisualField::field_resolution);
                                vids.insert(vids.end(), eye1.begin(), eye1.begin() + track::VisualField::field_resolution);
                            }
                        }
                        
                        // buffer
                        size_t number_midlines = 0;
                        if(!midlines.empty() && CL_HAS_FEATURE(MIDLINE)) {
                            std::vector<float> points;
                            
                            for(auto id : ids) {
                                auto it = midlines.find(id);
                                if(it == midlines.end() || it->second->segments().size() != FAST_SETTINGS(midline_resolution))
                                {
                                    points.resize(0);
                                    for(uint32_t i=0; i<FAST_SETTINGS(midline_resolution); ++i)
                                        points.push_back(infinity<float>());
                                    midline_points.insert(midline_points.end(), points.begin(), points.end());
                                    
                                } else {
                                    gui::Transform tf = it->second->transform(default_config::recognition_normalization_t::none, true);
                                    
                                    points.resize(0);
                                    for(auto &seg : it->second->segments()) {
                                        auto pt = tf.transformPoint(seg.pos);
                                        points.push_back(pt.x);
                                        points.push_back(pt.y);
                                    }
                                    
                                    midline_points.insert(midline_points.end(), points.begin(), points.end());
                                }
                                
                                ++number_midlines;
                            }
                        }

                        using py = track::PythonIntegration;

                        if (content_timer.elapsed() > 1) {
                            if(track::PythonIntegration::check_module("closed_loop")) {
                                request_features(track::PythonIntegration::run_retrieve_str("closed_loop", "request_features"));
                            }
                            content_timer.reset();
                        }

                        try {
                            py::set_variable("ids", ids, "closed_loop");
                            py::set_variable("colors", colors, "closed_loop");
                            py::set_variable("positions", xy, "closed_loop",
                                std::vector<size_t>{ ids.size(), 2 },
                                std::vector<size_t>{
                                    2 * sizeof(float),
                                    sizeof(float)
                                }
                            );
                            py::set_variable("centers", centers, "closed_loop",
                                std::vector<size_t>{ ids.size(), 2 },
                                std::vector<size_t>{
                                    2 * sizeof(float),
                                    sizeof(float)
                                }
                            );
                            py::set_variable("frame", frame, "closed_loop");
                            py::set_variable("visual_field", vids, "closed_loop",
                                std::vector<size_t>{ number_fields, 2, track::VisualField::field_resolution },
                                std::vector<size_t>{ 2 * track::VisualField::field_resolution * sizeof(long_t), track::VisualField::field_resolution * sizeof(long_t), sizeof(long_t) });
                            py::set_variable("midlines", midline_points, "closed_loop",
                                             std::vector<size_t>{ min(number_midlines, ids.size()), FAST_SETTINGS(midline_resolution), 2 },
                                std::vector<size_t>{ 2 * FAST_SETTINGS(midline_resolution) * sizeof(float), 2 * sizeof(float), sizeof(float) });

                            track::PythonIntegration::run("closed_loop", "update_tracking");
                        } catch(const SoftException& e) {
                            Except("Python runtime exception: '%s'", e.what());
                        }
                        
                        return true;
                    }).get();
                }
            }
            
            if(copy) {
                std::lock_guard<std::mutex> guard(ppframe_mutex);
                copy->clear();
                unused_pp.push_back(copy);
            }
            
            guard.lock();
            _tracking_time = _tracking_time * 0.75 + loop_timer.elapsed() * 0.25;//uint64_t(loop_timer.elapsed() * 1000 * 1000);
            
            if(GRAB_SETTINGS(enable_closed_loop))
                break;
        }
    }
    
    Debug("Ending tracker thread.");
}

void FrameGrabber::ensure_average_is_ready() {
    // make a local copy of the average, so that its thread-safe
    // (will only be read by the threads, and the memory should always be the same - so no
    // harm in accessing it for reads while something else is being memcypied over it)
    static uint64_t last_average = 0;
    
    if(last_average < _current_average_timestamp) {
        std::lock_guard<std::mutex> guard(_frame_lock);
        
        last_average = _current_average_timestamp;
        
        Warning("Copying average to GPU.");
        _pool->wait();
        ocl::init_ocl();
        
        static cv::Mat tmp;
        prepare_average();
        
        assert(_average.type() == CV_8UC1);
        assert(_average.channels() == 1);
        if (_processed.has_mask()) {
            if(_processed.mask().rows == _average.rows
               && _processed.mask().cols == _average.cols)
            {
                _average.copyTo(tmp, _processed.mask());
                
            } else {
                Debug("Does not match dimensions.");
                _average.copyTo(tmp);
            }
            
        } else {
            _average.copyTo(tmp);
        }
        
        tmp.copyTo(gpu_average_original);
        tmp.copyTo(gpu_average);
    }
}

void FrameGrabber::crop_and_scale(gpuMat& gpu) {
    if (GRAB_SETTINGS(cam_undistort)) {
        static gpuMat undistorted;
        _processed.undistort(gpu, undistorted);
        undistorted(_crop_rect).copyTo(gpu);
        
    } else {
        if(_crop_rect.width != _cam_size.width || _crop_rect.height != _cam_size.height)
            gpu(_crop_rect).copyTo(gpu);
    }
    
    if(GRAB_SETTINGS(cam_scale) != 1)
        resize_image(gpu, GRAB_SETTINGS(cam_scale));
}

void FrameGrabber::update_fps(long_t index, uint64_t stamp, uint64_t tdelta, uint64_t now) {
    {
        std::unique_lock<std::mutex> fps_lock(_fps_lock);
        _current_fps++;
        
        float elapsed = _fps_timer.elapsed();
        if(elapsed >= 1) {
            _fps = _current_fps / elapsed;
            _current_fps = 0;
            _fps_timer.reset();
            
            std::string ETA = "";
            if(_video && index > 0) {
                auto duration = std::chrono::system_clock::now() - _real_timing;
                auto ms = std::chrono::duration_cast<std::chrono::microseconds>(duration);
                auto per_frame = ms / index;
                auto eta = per_frame * (_video->length() - index);
                ETA = Meta::toStr(DurationUS{eta.count()});
            }
            
            auto save = uint64_t(_saving_time.load() * 1000 * 1000);
            DurationUS processing{uint64_t(_processing_timing.load() * 1000 * 1000 - save)};
            DurationUS loading{uint64_t(_loading_timing.load() * 1000 * 1000)};
            
            auto tracking_str = Meta::toStr(DurationUS{ uint64_t(_tracking_time.load() * 1000 * 1000) });
            auto saving_str = Meta::toStr(DurationUS{ save });
            auto processing_str = Meta::toStr(processing);
            auto loading_str = Meta::toStr(loading);
            
            auto str = Meta::toStr(DurationUS{stamp});
            
            if(_video)
                Debug("%d/%d (t+%S) @ %.1ffps (eta:%S load:%S proc:%S track:%S save:%S)", index, _video->length(), &str, _fps.load(), &ETA, &loading_str, &processing_str, &tracking_str, &saving_str);
            else
                Debug("%d (t+%S) @ %.1ffps (load:%S proc:%S track:%S save:%S)", index, &str, _fps.load(), &loading_str, &processing_str, &tracking_str, &saving_str);
        }
        
        if(GlobalSettings::map().has("write_fps"))
            write_fps(tdelta, now);
    }
}

Queue::Code FrameGrabber::process_image(Image_t& current) {
    static Timing timing("process_image", 10);
    TakeTiming take(timing);
    
    ensure_average_is_ready();
    
    // make timestamp relative to _start_timing
    if(_start_timing == UINT64_MAX)
        _start_timing = current.timestamp();
    current.set_timestamp(current.timestamp() - _start_timing);
    
    double minutes = double(current.timestamp()) / 1000.0 / 1000.0 / 60.0;
    if(GRAB_SETTINGS(stop_after_minutes) > 0 && minutes >= GRAB_SETTINGS(stop_after_minutes) && !GRAB_SETTINGS(terminate)) {
        SETTING(terminate) = true;
        Debug("Terminating program because stop_after_minutes (%d) has been reached.", GRAB_SETTINGS(stop_after_minutes));
        
    } else if(GRAB_SETTINGS(stop_after_minutes) > 0) {
        static double last_minutes = 0;
        if(minutes - last_minutes >= 0.1) {
            Debug("%f / %d minutes", minutes, GRAB_SETTINGS(stop_after_minutes));
            last_minutes = minutes;
        }
    }

    Timer timer;
    
    auto image = current.get();
    assert(image.type() == CV_8UC1);
    
    const bool use_corrected = GRAB_SETTINGS(correct_luminance);
    static cv::Mat local;
    
    if(!current.mask()) {
        //static std::deque<std::shared_ptr<RawProcessing>> processings;
        static std::mutex mute;
        
        static RawProcessing raw(gpu_average, nullptr);
        static gpuMat gpu_buffer;
        
        image.copyTo(gpu_buffer);
        
        crop_and_scale(gpu_buffer);

        if (processed().has_mask()) {
            static gpuMat mask;
            if (mask.empty())
                processed().mask().copyTo(mask);
            assert(processed().mask().cols == gpu_buffer.cols && processed().mask().rows == gpu_buffer.rows);
            gpu_buffer = gpu_buffer.mul(mask);
        }

        if(use_corrected && _grid) {
            _grid->correct_image(gpu_buffer);
        }
        
        apply_filters(gpu_buffer);
        raw.generate_binary(gpu_buffer, local);
        
    } else {
        static gpuMat gpu, mask;
        image.copyTo(gpu);
        
        if(current.rows != current.mask()->rows || current.cols != current.mask()->cols)
            cv::resize(current.mask()->get(), mask, cv::Size(current.rows, current.cols), 0, 0, cv::INTER_LINEAR);
        else
            current.mask()->get().copyTo(mask);
        
        cv::threshold(mask, mask, 0, 1, cv::THRESH_BINARY);
        cv::multiply(gpu, mask, gpu);
        
        crop_and_scale(gpu);
        gpu.copyTo(local);
    }
    
    {
        std::lock_guard<std::mutex> guard(_frame_lock);
        _current_image = std::make_unique<Image>(current);
        
        //tf::imshow("current", _current_image->get());
        //tf::imshow("local", local);
    }
    
    /**
     * ==============
     * Threadable
     * ==============
     */
    struct Task {
        size_t index;
        std::unique_ptr<Image> current;
        std::unique_ptr<pv::Frame> frame;
        Timer timer;
        std::vector<pv::BlobPtr> filtered, filtered_out;
    };
    
    static std::mutex to_pool_mutex, to_main_mutex;
    static std::queue<Task> for_the_pool;
    static std::once_flag flag;
    static std::vector<std::thread*> thread_pool;
    static std::condition_variable single_variable;
    
    static const auto in_main_thread = [&](Task&& task){
        long_t used_index_here = infinity<long_t>();
        bool added = false;
        
        static size_t last_task_processed = 0;
        {
            std::unique_lock<std::mutex> guard(to_main_mutex);
            Timer timer;
            while(last_task_processed + 1 != task.index && !_terminate_tracker) {
                single_variable.wait_for(guard, std::chrono::seconds(30));
                
                if(timer.elapsed() >= 1) {
                    //Debug("Still waiting to finalize task %d (%d)", task.index, last_task_processed);
                    timer.reset();
                }
            }
        }
        
        if(_terminate_tracker)
            return;
        
        // write frame to file if recording (and if there's anything in the frame)
        if(task.frame->n() > 0 && GRAB_SETTINGS(recording) && !GRAB_SETTINGS(quit_after_average)) {
            if(!_processed.open()) {
                // set (real time) timestamp for video start
                // (just for the user to read out later)
                auto epoch = std::chrono::time_point<std::chrono::system_clock>();
                _processed.set_start_time(!_video || !_video->has_timestamps() ? std::chrono::system_clock::now() : (epoch + std::chrono::microseconds(_video->start_timestamp())));
                _processed.start_writing(true);
            }
            
            static Timer timer;
            timer.reset();
            
            used_index_here = _processed.length();
            _processed.add_individual(*task.frame);
            _saving_time = _saving_time * 0.75 + timer.elapsed() * 0.25;
            
            _paused = false;
            added = true;
        } else {
            _paused = true;
        }
        
        auto stamp = task.current->timestamp();
        auto index = task.current->index();
        
        _last_index = index;
        
        if(added && tracker) {
            add_tracker_queue(*task.frame, used_index_here);
        }
        
        uint64_t tdelta, tdelta_camera, now;
        {
            std::lock_guard<std::mutex> guard(_frame_lock);
            if(_last_frame) {
                tdelta_camera = task.frame->timestamp() - _last_frame->timestamp();
                
                now = std::chrono::steady_clock::now().time_since_epoch().count();
                if(previous_time == 0)
                    previous_time = now;
                tdelta = now - previous_time;
                
                previous_time = now;
            } else {
                tdelta = 0;
                tdelta_camera = 0;
                now = 0;
            }
            
            _last_frame = std::move(task.frame);
            _noise = nullptr;
            
            if(!task.filtered_out.empty() && task.index % 10 == 0) {
                _noise = std::make_unique<pv::Frame>(task.current->timestamp(), task.filtered_out.size());
                for (auto b: task.filtered_out) {
                    _noise->add_object(b->lines(), b->pixels());
                }
            }
        }
        
    #if WITH_FFMPEG
        if(mp4_queue && used_index_here != -1) {
            current.set_index(used_index_here);
            mp4_queue->add(ptr);
            
            // try and get images back
            std::lock_guard<std::mutex> guard(process_image_mutex);
            mp4_queue->refill_queue(_unused_process_images);
        } else
    #endif
        _processing_timing = _processing_timing * 0.75 + task.timer.elapsed() * 0.25;
        
        {
            std::lock_guard<std::mutex> guard(to_main_mutex);
            last_task_processed = task.index; //! allow next task
        }
        
        update_fps(index, stamp, tdelta, now);
        single_variable.notify_all();
    };
    
    static const auto threadable_task = [in_main_thread = &in_main_thread](Task&& task) {
        const Rangef min_max = GRAB_SETTINGS(blob_size_range);
        static const float cm_per_pixel = SQR(SETTING(cm_per_pixel).value<float>());
        
        Timer _sub_timer;
        auto rawblobs = CPULabeling::run_fast(task.current->get(), true);
        
        for(auto  && [lines, pixels] : rawblobs) {
            //b->calculate_properties();
            
            size_t num_pixels;
            if(pixels)
                num_pixels = pixels->size();
            else {
                num_pixels = 0;
                for(auto &line : *lines) {
                    num_pixels += line.x1 - line.x0 + 1;
                }
            }
            if(num_pixels * cm_per_pixel >= min_max.start
               && num_pixels * cm_per_pixel <= min_max.end)
            {
                //b->calculate_moments();
                task.filtered.push_back(std::make_shared<pv::Blob>(lines, pixels));
                
            }
            else {
                task.filtered_out.push_back(std::make_shared<pv::Blob>(lines, pixels));
            }
        }
        
        // create pv::Frame object for this frame
        // (creating new object so it can be swapped with _last_frame)
        task.frame = std::make_unique<pv::Frame>(task.current->timestamp(), task.filtered.size());
        {
            static Timing timing("adding frame");
            TakeTiming take(timing);
            
            for (auto b: task.filtered) {
                if(b->hor_lines().size() < UINT16_MAX) {
                    if(b->hor_lines().size() < UINT16_MAX)
                        task.frame->add_object(b->lines(), b->pixels());
                    else
                        Warning("Lots of lines!");
                }
                else
                    Warning("Probably a lot of noise with %lu lines!", b->hor_lines().size());
            }
        }
        
        (*in_main_thread)(std::move(task));
    };
    
    std::call_once(flag, [&](){
        Debug("Creating queue...");
        auto blob = std::make_shared<pv::Blob>();
        for (size_t i=0; i<8; ++i) {
            _multi_pool.push_back(std::make_unique<std::thread>([&](size_t i){
                set_thread_name("MultiPool"+Meta::toStr(i));
                
                std::unique_lock<std::mutex> guard(to_pool_mutex);
                Timer timer;
                while(!_terminate_tracker) {
                    _multi_variable.wait_for(guard, std::chrono::milliseconds(1));
                    
                    if(!for_the_pool.empty()) {
                        timer.reset();
                        
                        auto task = std::move(for_the_pool.front());
                        for_the_pool.pop();
                        
                        guard.unlock();
                        _multi_variable.notify_one();
                        
                        try {
                            auto index = task.index;
                            threadable_task(std::move(task));
                            
                        } catch(const std::exception& ex) {
                            Except("std::exception from threadable task: %s", ex.what());
                        } catch(...) {
                            Except("Unknown exception from threadable task.");
                        }
                        
                        _multi_variable.notify_one();
                        guard.lock();
                        
                    } /*else if(timer.elapsed() > 1) {
                        Debug("Still waiting to process anything.");
                    }*/
                }
            }, i));
        }
        Debug("Done. %d", blob->blob_id());
        
        _multi_variable.notify_all();
    });
    
    if(GRAB_SETTINGS(grabber_use_threads)) {
        {
            std::unique_lock<std::mutex> guard(to_pool_mutex);
            static size_t global_index = 1;
            Timer timer;
            while(for_the_pool.size() >= 8 && !_terminate_tracker) {
                _multi_variable.wait_for(guard, std::chrono::milliseconds(1));
                
                if(timer.elapsed() > 1) {
                    //Debug("Still waiting to push task %d", global_index);
                    timer.reset();
                }
            }
            
            if(!_terminate_tracker) {
                for_the_pool.push(Task{global_index++, std::make_unique<Image>(local, current.index(), current.timestamp()), nullptr});
            }
        }
        
        _multi_variable.notify_one();
        
    } else {
        static size_t global_index = 1;
        threadable_task(Task{global_index++, std::make_unique<Image>(local, current.index(), current.timestamp()), nullptr});
    }
    
    /**
     * ==============
     * / Threadable
     * ==============
     */
    
    return Queue::ITEM_NEXT;
}

void FrameGrabber::safely_close() {
    try {
        if(_analysis && _analysis->analysis_thread() && _analysis->loading_thread()) {
            //auto tid = std::this_thread::get_id();
            
            /*for (auto p : _pool) {
                if(p->get_id() == std::this_thread::get_id()) {
                    tid = _analysis->loading_thread()->get_id();
                    break;
                }
            }*/
            
            //_analysis->pause_from_thread(tid);
            _analysis->terminate();
        
            _lock.lock();
            
            printf("Closing camera/video...\n");
            
            {
                std::lock_guard<std::mutex> guard(_camera_lock);
                //if(std::this_thread::get_id() != CrashProgram::main_pid && _camera)
                //    delete _camera;
                _camera = NULL;
            }
            
            if(_video)
                delete _video;
            _video = NULL;
        
        
        //printf("Terminating analysis\n");
        //_analysis->terminate();
        
        } else {
            _lock.lock();
        }
        
        file::Path filename = SETTING(filename).value<file::Path>().add_extension("pv");
        if(filename.exists()){
            pv::File file(filename);
            file.start_reading();
        }
        
    } catch(const std::system_error& e) {
        printf("A system error occurred when closing the framegrabber: '%s'. This might not mean anything, telling you just in case.\n", e.what());
    }
}