| /** |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| #include "MotionDetector.h" |
| |
| namespace org { |
| namespace apache { |
| namespace nifi { |
| namespace minifi { |
| namespace processors { |
| |
| core::Property MotionDetector::ImageEncoding( |
| core::PropertyBuilder::createProperty("Image Encoding") |
| ->withDescription("The encoding that should be applied to the output") |
| ->isRequired(true) |
| ->withAllowableValues<std::string>({".jpg", ".png"}) |
| ->withDefaultValue(".jpg")->build()); |
| core::Property MotionDetector::MinInterestArea( |
| core::PropertyBuilder::createProperty("Minimum Area") |
| ->withDescription("We only consider the movement regions with area greater than this.") |
| ->isRequired(true) |
| ->withDefaultValue<uint32_t>(100)->build()); |
| core::Property MotionDetector::Threshold( |
| core::PropertyBuilder::createProperty("Threshold for segmentation") |
| ->withDescription("Pixel greater than this will be white, otherwise black.") |
| ->isRequired(true) |
| ->withDefaultValue<uint32_t>(42)->build()); |
| core::Property MotionDetector::BackgroundFrame( |
| core::PropertyBuilder::createProperty("Path to background frame") |
| ->withDescription("If not provided then the processor will take the first input frame as background") |
| ->isRequired(true) |
| ->build()); |
| core::Property MotionDetector::DilateIter( |
| core::PropertyBuilder::createProperty("Dilate iteration") |
| ->withDescription("For image processing, if an object is detected as 2 separate objects, increase this value") |
| ->isRequired(true) |
| ->withDefaultValue<uint32_t>(10)->build()); |
| |
| core::Relationship MotionDetector::Success("success", "Successful to detect motion"); |
| core::Relationship MotionDetector::Failure("failure", "Failure to detect motion"); |
| |
| void MotionDetector::initialize() { |
| std::set<core::Property> properties; |
| properties.insert(ImageEncoding); |
| properties.insert(MinInterestArea); |
| properties.insert(Threshold); |
| properties.insert(BackgroundFrame); |
| properties.insert(DilateIter); |
| setSupportedProperties(std::move(properties)); |
| |
| setSupportedRelationships({Success, Failure}); |
| } |
| |
| void MotionDetector::onSchedule(const std::shared_ptr<core::ProcessContext> &context, |
| const std::shared_ptr<core::ProcessSessionFactory> &sessionFactory) { |
| std::string value; |
| |
| if (context->getProperty(ImageEncoding.getName(), value)) { |
| image_encoding_ = value; |
| } |
| |
| if (context->getProperty(MinInterestArea.getName(), value)) { |
| core::Property::StringToInt(value, min_area_); |
| } |
| |
| if (context->getProperty(Threshold.getName(), value)) { |
| core::Property::StringToInt(value, threshold_); |
| } |
| |
| if (context->getProperty(DilateIter.getName(), value)) { |
| core::Property::StringToInt(value, dil_iter_); |
| } |
| |
| if (context->getProperty(BackgroundFrame.getName(), value) && !value.empty()) { |
| bg_img_ = cv::imread(value, cv::IMREAD_GRAYSCALE); |
| double scale = IMG_WIDTH / bg_img_.size().width; |
| cv::resize(bg_img_, bg_img_, cv::Size(0, 0), scale, scale); |
| cv::GaussianBlur(bg_img_, bg_img_, cv::Size(21, 21), 0, 0); |
| bg_img_.convertTo(background_, CV_32F); |
| } |
| |
| logger_->log_trace("MotionDetector processor scheduled"); |
| } |
| |
| bool MotionDetector::detectAndDraw(cv::Mat &frame) { |
| cv::Mat gray; |
| cv::Mat img_diff, thresh; |
| std::vector<cv::Mat> contours; |
| |
| logger_->log_trace("Detect and Draw"); |
| |
| cv::cvtColor(frame, gray, cv::COLOR_BGR2GRAY); |
| cv::GaussianBlur(gray, gray, cv::Size(21, 21), 0, 0); |
| |
| // Get difference between current frame and background |
| logger_->log_trace("Get difference [%d x %d] [%d x %d]", bg_img_.rows, bg_img_.cols, gray.rows, gray.cols); |
| cv::absdiff(gray, bg_img_, img_diff); |
| logger_->log_trace("Apply threshold"); |
| cv::threshold(img_diff, thresh, threshold_, 255, cv::THRESH_BINARY); |
| // Image processing. |
| logger_->log_trace("Dilation"); |
| cv::dilate(thresh, thresh, cv::Mat(), cv::Point(-1, -1), dil_iter_); |
| cv::findContours(thresh, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE); |
| |
| // Finish process |
| logger_->log_debug("Draw contours"); |
| bool moved = false; |
| for (const auto &contour : contours) { |
| auto area = cv::contourArea(contour); |
| if (area < min_area_) { |
| continue; |
| } |
| moved = true; |
| cv::Rect bbox = cv::boundingRect(contour); |
| cv::rectangle(frame, bbox.tl(), bbox.br(), cv::Scalar(0, 255, 0), 2, 8, 0); |
| } |
| logger_->log_trace("Updating background"); |
| if (!moved) { |
| logger_->log_debug("Not moved"); |
| // Adaptive background, update background so that the illumnation does not affect that much. |
| cv::accumulateWeighted(gray, background_, 0.5); |
| cv::convertScaleAbs(background_, bg_img_); |
| } |
| logger_->log_trace("Finish Detect and Draw"); |
| return moved; |
| } |
| |
| void MotionDetector::onTrigger(const std::shared_ptr<core::ProcessContext> &context, |
| const std::shared_ptr<core::ProcessSession> &session) { |
| std::unique_lock<std::mutex> lock(mutex_, std::try_to_lock); |
| if (!lock.owns_lock()) { |
| logger_->log_info("Cannot process due to an unfinished onTrigger"); |
| context->yield(); |
| return; |
| } |
| |
| auto flow_file = session->get(); |
| if (flow_file->getSize() == 0) { |
| logger_->log_info("Empty flow file"); |
| return; |
| } |
| cv::Mat frame; |
| |
| opencv::FrameReadCallback cb(frame); |
| session->read(flow_file, &cb); |
| |
| if (frame.empty()) { |
| logger_->log_error("Empty frame."); |
| session->transfer(flow_file, Failure); |
| } |
| |
| double scale = IMG_WIDTH / frame.size().width; |
| cv::resize(frame, frame, cv::Size(0, 0), scale, scale); |
| |
| if (background_.empty()) { |
| logger_->log_info("Background is missing, update and yield."); |
| cv::cvtColor(frame, bg_img_, cv::COLOR_BGR2GRAY); |
| cv::GaussianBlur(bg_img_, bg_img_, cv::Size(21, 21), 0, 0); |
| bg_img_.convertTo(background_, CV_32F); |
| return; |
| } |
| logger_->log_trace("Start motion detecting"); |
| |
| auto t = std::time(nullptr); |
| auto tm = *std::localtime(&t); |
| std::ostringstream oss; |
| oss << std::put_time(&tm, "%Y-%m-%d %H-%M-%S"); |
| auto filename = oss.str(); |
| filename.append(image_encoding_); |
| |
| detectAndDraw(frame); |
| |
| opencv::FrameWriteCallback write_cb(frame, image_encoding_); |
| |
| session->putAttribute(flow_file, "filename", filename); |
| |
| session->write(flow_file, &write_cb); |
| session->transfer(flow_file, Success); |
| logger_->log_trace("Finish motion detecting"); |
| } |
| |
| void MotionDetector::notifyStop() { |
| } |
| |
| } /* namespace processors */ |
| } /* namespace minifi */ |
| } /* namespace nifi */ |
| } /* namespace apache */ |
| } /* namespace org */ |