v1.1 Update
major changes to the motion detection scheme and re-introduced multi-threading. this further sped up the motion detection to a point that it can now be called in line with the recording loop without loosing any extra camera footage due to heavy cpu usage. pixels are now read in blocks to further increase efficiency and to filter out movements of small objects. the footage clip size is now hard coded to 3 seconds instead of it being external adjustable. changed the way footage with motion is now stored. its now down to single level files with the current date. if footage of the same date already exists, new footage will be appended to it. the version number shall be updated going forward.
This commit is contained in:
parent
a36d4e93c0
commit
48e55b9721
64
README.md
64
README.md
|
@ -30,48 +30,47 @@ parameters supported and descriptions of each parameter.
|
|||
#
|
||||
recording_stream = rtsp://1.2.3.4:554/h264
|
||||
# this is the url to the main stream of the IP camera that will be used
|
||||
# to record footage if it contains motion.
|
||||
#
|
||||
detection_stream = rtsp://1.2.3.4:554/h264cif
|
||||
# this is the low resolution secondary stream url of the IP camera the
|
||||
# will be used to detect motion. it is never recorded. note: consider
|
||||
# matching the fps of both streams for best results.
|
||||
# to record footage.
|
||||
#
|
||||
output_dir = /path/to/footage/directory
|
||||
# this is the output directory that will be used to store recorded footage
|
||||
# from the camera. the file naming convention uses date codes. it creates
|
||||
# a sub-folder for the date if it needs to and then stores the video file
|
||||
# using the time.
|
||||
# from the camera. the file naming convention uses the current date. if
|
||||
# the file already exists, new footage is appended to it.
|
||||
#
|
||||
diff_verbose = N
|
||||
# this is a boolean Y or N option that turns on/off the option to output
|
||||
# the pixel diff values that the application is reading from the camera in
|
||||
# real time out into stdout. this is useful for determining the best value
|
||||
# to use in pix_threshold, color_threshold or consec_threshold.
|
||||
buff_dir = /tmp/ramdisk/cam_name
|
||||
# this application records small clips of the footage from the camera and
|
||||
# then stores them into this directory. any clips with motion detected in
|
||||
# them are moved to output_dir, if no motion they are deleted. highly
|
||||
# recommend to use a ramdisk tempfs for this since this directory is used
|
||||
# for lots of writes.
|
||||
#
|
||||
pix_threshold = 8
|
||||
# this application detects motion by loading back to back frames from the
|
||||
# detection stream and then compares the color spectrum levels of each
|
||||
# pixel of those frames. if the levels are significantly different, that
|
||||
# will maybe considered motion. this threshold indicates how many pixels
|
||||
# in the image needs to be different before triggering a potential motion
|
||||
# event.
|
||||
#
|
||||
color_threshold = 190
|
||||
color_threshold = 8
|
||||
# the color levels in each pixel of the detection stream can range from
|
||||
# 0-255. in an ideal world the color differences in between frames should
|
||||
# be 0 if there is no motion but must cameras can't do this. the threshold
|
||||
# value here is used to filter if the pixels are truly different or if its
|
||||
# seeing color differences of small objects that are of no interest.
|
||||
#
|
||||
consec_threshold = 10
|
||||
# this setting is used to tell the application how many consecutive frames
|
||||
# need to have pixel differences over the pix_threshold before triggering
|
||||
# a motion event and then record to storage.
|
||||
block_threshold = 3456
|
||||
# this application detects motion by loading frames from the camera and
|
||||
# then compare the pixels of each back to back frame for any significant
|
||||
# differences between the pixels based on color_threshold. it loads the
|
||||
# pixels of each frame in blocks. the size of the blocks are adjustable
|
||||
# below. it counts how many pixels are different in the block and this is
|
||||
# used to tell if the footage has motion if the different pixel count
|
||||
# exceeds it.
|
||||
#
|
||||
block_x = 64
|
||||
# this is the x coordinate size or horizontal size of a block of pixels
|
||||
# that the application will use to detect the presents of motion.
|
||||
#
|
||||
block_y = 60
|
||||
# this is the y coordinate size or vertical size of a block of pixels
|
||||
# that the application will use to detect the presents of motion.
|
||||
#
|
||||
duration = 60
|
||||
# this sets the internal timer used to reset to the detection loop and
|
||||
# then call post_cmd if it is defined. note: this time can be extended if
|
||||
# then call post_cmd if it is defined. note: this time is extended if
|
||||
# motion was detected. this will also reload the config file so changes
|
||||
# to the settings will be applied without restarting the application.
|
||||
#
|
||||
|
@ -81,15 +80,6 @@ post_cmd = move_the_ptz_camera.py
|
|||
# position of it's patrol pattern. note: the call to this command can be
|
||||
# delayed if motion was detected.
|
||||
#
|
||||
detect_fps = 20
|
||||
# this is how many frames to read from the detection stream per second.
|
||||
# setting this any higher the camera's actual fps will just waste cpu
|
||||
# cycles but setting it too low makes detecting motion inaccurate.
|
||||
#
|
||||
secs_post_motion = 10
|
||||
# this is the minimum amount of seconds to capture after motion was
|
||||
# detected.
|
||||
#
|
||||
```
|
||||
|
||||
### Build Setup ###
|
||||
|
|
323
src/main.cpp
323
src/main.cpp
|
@ -6,6 +6,9 @@
|
|||
#include <stdlib.h>
|
||||
#include <sys/stat.h>
|
||||
#include <errno.h>
|
||||
#include <vector>
|
||||
#include <thread>
|
||||
#include <mutex>
|
||||
|
||||
#include <opencv4/opencv2/opencv.hpp>
|
||||
#include <opencv4/opencv2/videoio.hpp>
|
||||
|
@ -13,23 +16,26 @@
|
|||
using namespace cv;
|
||||
using namespace std;
|
||||
|
||||
#define BUF_SZ 3
|
||||
|
||||
struct shared_t
|
||||
{
|
||||
VideoCapture camera;
|
||||
Mat baseImg;
|
||||
string detectUrl;
|
||||
string recordUrl;
|
||||
string diffVerb;
|
||||
string outDir;
|
||||
string postCmd;
|
||||
string conf;
|
||||
int detectFps;
|
||||
string buffDir;
|
||||
string concatTxtTmp;
|
||||
string concatShTmp;
|
||||
string createShTmp;
|
||||
mutex thrMutex;
|
||||
bool init;
|
||||
int tmpId;
|
||||
int colorThresh;
|
||||
int secs;
|
||||
int consec;
|
||||
int consecThresh;
|
||||
int pixThresh;
|
||||
int postMoIncr;
|
||||
int blockThresh;
|
||||
int blockX;
|
||||
int blockY;
|
||||
int retCode;
|
||||
|
||||
} sharedRes;
|
||||
|
@ -74,6 +80,26 @@ bool createDirTree(const string &full_path)
|
|||
return ret;
|
||||
}
|
||||
|
||||
bool fileExists(const string& name)
|
||||
{
|
||||
return access(name.c_str(), F_OK) != -1;
|
||||
}
|
||||
|
||||
void replaceAll(string &str, const string &from, const string &to)
|
||||
{
|
||||
if(from.empty())
|
||||
return;
|
||||
|
||||
size_t startPos = 0;
|
||||
|
||||
while((startPos = str.find(from, startPos)) != string::npos)
|
||||
{
|
||||
str.replace(startPos, from.length(), to);
|
||||
|
||||
startPos += to.length();
|
||||
}
|
||||
}
|
||||
|
||||
string genDstFile(const string &dirOut, const string &ext)
|
||||
{
|
||||
time_t rawtime;
|
||||
|
@ -82,27 +108,22 @@ string genDstFile(const string &dirOut, const string &ext)
|
|||
|
||||
auto timeinfo = localtime(&rawtime);
|
||||
|
||||
char dirName[20];
|
||||
char fileName[20];
|
||||
char dateC[20];
|
||||
|
||||
strftime(dirName, 20, "%Y%m%d", timeinfo);
|
||||
strftime(fileName, 20, "%H%M%S", timeinfo);
|
||||
strftime(dateC, 20, "%Y-%m-%d", timeinfo);
|
||||
|
||||
createDirTree(cleanDir(dirOut) + string("/") + string(dirName));
|
||||
createDirTree(cleanDir(dirOut));
|
||||
|
||||
return cleanDir(dirOut) + string("/") + string(dirName) + string("/") + string(fileName) + ext;
|
||||
return cleanDir(dirOut) + string("/") + string(dateC) + ext;
|
||||
}
|
||||
|
||||
void wrOut(shared_t *share)
|
||||
string genTmpFile(const string &dirOut, const string &ext, shared_t *share)
|
||||
{
|
||||
share->baseImg.release();
|
||||
createDirTree(cleanDir(dirOut));
|
||||
|
||||
share->consec = 0;
|
||||
share->tmpId += 1;
|
||||
|
||||
auto dstPath = genDstFile(share->outDir, ".mp4");
|
||||
auto cmd = "ffmpeg -i " + share->recordUrl + " -y -vcodec copy -t " + to_string(share->postMoIncr) + " " + dstPath;
|
||||
|
||||
system(cmd.c_str());
|
||||
return cleanDir(dirOut) + string("/") + to_string(share->tmpId) + ext;
|
||||
}
|
||||
|
||||
Mat toGray(const Mat &src)
|
||||
|
@ -129,7 +150,7 @@ bool pixDiff(const uchar &pixA, const uchar &pixB, shared_t *share)
|
|||
return diff != 0;
|
||||
}
|
||||
|
||||
int secDiff(Mat imgA, Mat imgB, int rows, int cols, int rowOffs, int colOffs, shared_t *share)
|
||||
void secDiff(Mat imgA, Mat imgB, int rows, int cols, int rowOffs, int colOffs, bool *mod, shared_t *share)
|
||||
{
|
||||
auto pnts = 0;
|
||||
|
||||
|
@ -140,51 +161,46 @@ int secDiff(Mat imgA, Mat imgB, int rows, int cols, int rowOffs, int colOffs, sh
|
|||
auto pixA = imgA.at<uchar>(Point(x, y));
|
||||
auto pixB = imgB.at<uchar>(Point(x, y));
|
||||
|
||||
//cout << "pnts: " << pnts << endl;
|
||||
|
||||
if (pixDiff(pixA, pixB, share))
|
||||
{
|
||||
pnts += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return pnts;
|
||||
}
|
||||
|
||||
bool imgDiff(Mat curImg, shared_t *share)
|
||||
if (pnts >= share->blockThresh)
|
||||
{
|
||||
if (share->baseImg.empty())
|
||||
{
|
||||
share->baseImg = toGray(curImg);
|
||||
lock_guard<mutex> guard(share->thrMutex);
|
||||
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
curImg = toGray(curImg);
|
||||
|
||||
auto pnts = secDiff(share->baseImg, curImg, curImg.rows, curImg.cols, 0, 0, share);
|
||||
|
||||
if (share->diffVerb == "Y")
|
||||
{
|
||||
cout << "diff: " << pnts << endl;
|
||||
}
|
||||
|
||||
share->baseImg = curImg.clone();
|
||||
|
||||
if (pnts >= share->pixThresh)
|
||||
{
|
||||
share->consec += 1;
|
||||
|
||||
return share->consec >= share->consecThresh;
|
||||
}
|
||||
else
|
||||
{
|
||||
share->consec = 0;
|
||||
|
||||
return false;
|
||||
*mod = true; return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool imgDiff(Mat prev, Mat next, shared_t *share)
|
||||
{
|
||||
auto numOfXBlocks = prev.cols / share->blockX;
|
||||
auto numOfYBlocks = prev.rows / share->blockY;
|
||||
auto moInBlock = false;
|
||||
|
||||
vector<thread> threads;
|
||||
|
||||
for (auto x = 0; (x < numOfXBlocks) && !moInBlock; x += share->blockX)
|
||||
{
|
||||
for (auto y = 0; (y < numOfYBlocks) && !moInBlock; y += share->blockY)
|
||||
{
|
||||
threads.push_back(thread(secDiff, prev, next, share->blockY, share->blockX, y, x, &moInBlock, share));
|
||||
}
|
||||
}
|
||||
|
||||
for (auto &&thr : threads)
|
||||
{
|
||||
thr.join();
|
||||
}
|
||||
|
||||
return moInBlock;
|
||||
}
|
||||
|
||||
string parseForParam(const string &arg, int argc, char** argv, bool argOnly)
|
||||
{
|
||||
|
@ -218,7 +234,7 @@ void rdLine(const string ¶m, const string &line, string *value)
|
|||
{
|
||||
*value = line.substr(param.size());
|
||||
|
||||
cout << param << *value << endl;
|
||||
//cout << param << *value << endl;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -228,40 +244,34 @@ void rdLine(const string ¶m, const string &line, int *value)
|
|||
{
|
||||
*value = strtol(line.substr(param.size()).c_str(), NULL, 10);
|
||||
|
||||
cout << param << *value << endl;
|
||||
//cout << param << *value << endl;
|
||||
}
|
||||
}
|
||||
|
||||
bool rdConf(shared_t *share)
|
||||
{
|
||||
auto ret = false;
|
||||
|
||||
share->retCode = ENOENT;
|
||||
|
||||
ifstream varFile(share->conf.c_str());
|
||||
|
||||
if (!varFile.is_open())
|
||||
{
|
||||
cerr << "err: failed to open the config file: " << share->conf << " for reading. please check file permissions or if it exists." << endl;
|
||||
share->retCode = ENOENT;
|
||||
|
||||
cerr << "err: Failed to open the config file: " << share->conf << " for reading. please check file permissions or if it exists." << endl;
|
||||
}
|
||||
else
|
||||
{
|
||||
string line;
|
||||
|
||||
share->recordUrl.clear();
|
||||
share->detectUrl.clear();
|
||||
share->outDir.clear();
|
||||
share->postCmd.clear();
|
||||
share->diffVerb.clear();
|
||||
share->baseImg.release();
|
||||
share->buffDir.clear();
|
||||
|
||||
share->pixThresh = 8;
|
||||
share->consecThresh = 10;
|
||||
share->colorThresh = 60;
|
||||
share->colorThresh = 5;
|
||||
share->secs = 60;
|
||||
share->detectFps = 20;
|
||||
share->postMoIncr = 5;
|
||||
share->consec = 0;
|
||||
share->blockX = 32;
|
||||
share->blockY = 32;
|
||||
share->blockThresh = 900;
|
||||
|
||||
do
|
||||
{
|
||||
|
@ -270,70 +280,147 @@ bool rdConf(shared_t *share)
|
|||
if (line.rfind("#", 0) != 0)
|
||||
{
|
||||
rdLine("recording_stream = ", line, &share->recordUrl);
|
||||
rdLine("detection_stream = ", line, &share->detectUrl);
|
||||
rdLine("output_dir = ", line, &share->outDir);
|
||||
rdLine("diff_verbose = ", line, &share->diffVerb);
|
||||
rdLine("post_cmd = ", line, &share->postCmd);
|
||||
rdLine("pix_threshold = ", line, &share->pixThresh);
|
||||
rdLine("color_threshold = ", line, &share->colorThresh);
|
||||
rdLine("consec_threshold = ", line, &share->consecThresh);
|
||||
rdLine("duration = ", line, &share->secs);
|
||||
rdLine("secs_post_motion = ", line, &share->postMoIncr);
|
||||
rdLine("detect_fps = ", line, &share->detectFps);
|
||||
rdLine("buff_dir = ", line, &share->buffDir);
|
||||
rdLine("block_x = ", line, &share->blockX);
|
||||
rdLine("block_y = ", line, &share->blockY);
|
||||
rdLine("block_threshold = ", line, &share->blockThresh);
|
||||
}
|
||||
|
||||
} while(!line.empty());
|
||||
|
||||
ret = true;
|
||||
if (share->init)
|
||||
{
|
||||
system(string("rm -r " + share->buffDir).c_str());
|
||||
|
||||
share->init = false;
|
||||
}
|
||||
|
||||
share->retCode = 0;
|
||||
}
|
||||
|
||||
varFile.close();
|
||||
|
||||
return ret;
|
||||
return share->retCode == 0;
|
||||
}
|
||||
|
||||
void moDetect(shared_t *share)
|
||||
bool capPair(Mat &prev, Mat &next, VideoCapture &capture, shared_t *share)
|
||||
{
|
||||
while (rdConf(share))
|
||||
{
|
||||
for (auto i = 0; i < (share->secs * share->detectFps); ++i)
|
||||
{
|
||||
Mat frame;
|
||||
capture >> prev;
|
||||
capture >> next;
|
||||
|
||||
if (!share->camera.isOpened())
|
||||
{
|
||||
share->camera.open(share->detectUrl, CAP_FFMPEG);
|
||||
return !prev.empty() && !next.empty();
|
||||
}
|
||||
|
||||
share->camera >> frame;
|
||||
void wrOut(const string &buffFile, const string &dstPath, shared_t *share)
|
||||
{
|
||||
ofstream file;
|
||||
|
||||
if (frame.empty())
|
||||
auto scriptFile = genTmpFile(share->buffDir, ".sh", share);
|
||||
auto scriptData = string();
|
||||
|
||||
if (fileExists(dstPath))
|
||||
{
|
||||
// broken frames returned from the cameras i've tested this with would cause
|
||||
// the entire capture connection to drop, hence why this bit of code is here
|
||||
// to detect empty frames (signs of a dropped connection) and attempt
|
||||
// re-connect to the cammera.
|
||||
share->camera.open(share->detectUrl, CAP_FFMPEG);
|
||||
}
|
||||
else if (imgDiff(frame, share))
|
||||
{
|
||||
wrOut(share); i = 0;
|
||||
auto concatFile = genTmpFile(share->buffDir, ".txt", share);
|
||||
auto existsFile = genTmpFile(share->outDir, ".ts", share);
|
||||
auto concatData = share->concatTxtTmp;
|
||||
|
||||
scriptData = share->concatShTmp;
|
||||
|
||||
replaceAll(concatData, "%existsFile%", existsFile);
|
||||
replaceAll(concatData, "%buffFile%", buffFile);
|
||||
|
||||
replaceAll(scriptData, "%existsFile%", existsFile);
|
||||
replaceAll(scriptData, "%concatFile%", concatFile);
|
||||
|
||||
file.open(concatFile.c_str());
|
||||
file << concatData;
|
||||
file.close();
|
||||
}
|
||||
else
|
||||
{
|
||||
usleep(1000000 / share->detectFps);
|
||||
scriptData = share->createShTmp;
|
||||
}
|
||||
|
||||
replaceAll(scriptData, "%buffFile%", buffFile);
|
||||
replaceAll(scriptData, "%dstPath%", dstPath);
|
||||
replaceAll(scriptData, "%scriptFile%", scriptFile);
|
||||
|
||||
file.open(scriptFile.c_str());
|
||||
file << scriptData;
|
||||
file.close();
|
||||
|
||||
system(string("sh " + scriptFile + " &").c_str());
|
||||
}
|
||||
|
||||
bool moDetect(const string &buffFile, shared_t *share)
|
||||
{
|
||||
auto mod = false;
|
||||
|
||||
VideoCapture capture(buffFile.c_str(), CAP_FFMPEG);
|
||||
|
||||
if (capture.isOpened())
|
||||
{
|
||||
Mat prev;
|
||||
Mat next;
|
||||
|
||||
while (capPair(prev, next, capture, share))
|
||||
{
|
||||
if (imgDiff(toGray(prev), toGray(next), share))
|
||||
{
|
||||
mod = true; break;
|
||||
}
|
||||
}
|
||||
|
||||
if (mod)
|
||||
{
|
||||
auto dstPath = genDstFile(share->outDir, ".ts");
|
||||
|
||||
wrOut(buffFile, dstPath, share);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
cerr << "err: Could not open buff file: " << buffFile << " for reading. check formatting/permissions." << endl;
|
||||
}
|
||||
|
||||
if (!mod)
|
||||
{
|
||||
system(string("rm " + buffFile + " &").c_str());
|
||||
}
|
||||
|
||||
return mod;
|
||||
}
|
||||
|
||||
void recLoop(shared_t *share)
|
||||
{
|
||||
while (rdConf(share))
|
||||
{
|
||||
auto mod = false;
|
||||
|
||||
for (auto i = 0; i < share->secs; i += BUF_SZ)
|
||||
{
|
||||
auto dstPath = genTmpFile(share->buffDir, ".ts", share);
|
||||
auto cmd = "ffmpeg -hide_banner -loglevel error -i " + share->recordUrl + " -y -vcodec copy -t " + to_string(BUF_SZ) + " " + dstPath;
|
||||
|
||||
system(cmd.c_str());
|
||||
|
||||
mod = moDetect(dstPath, share);
|
||||
}
|
||||
|
||||
if (!mod)
|
||||
{
|
||||
system(share->postCmd.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void showHelp()
|
||||
{
|
||||
cout << "Motion Watch v1.0" << endl << endl;
|
||||
cout << "Motion Watch v1.1" << endl << endl;
|
||||
cout << "Usage: mow <argument>" << endl << endl;
|
||||
cout << "-h : display usage information about this application." << endl;
|
||||
cout << "-c : path to the config file." << endl;
|
||||
|
@ -349,11 +436,33 @@ int main(int argc, char** argv)
|
|||
}
|
||||
else if (sharedRes.conf.empty())
|
||||
{
|
||||
cerr << "err: a config file was not given in -c" << endl;
|
||||
cerr << "err: A config file was not given in -c" << endl;
|
||||
}
|
||||
else
|
||||
{
|
||||
moDetect(&sharedRes);
|
||||
sharedRes.retCode = 0;
|
||||
sharedRes.tmpId = 0;
|
||||
sharedRes.init = true;
|
||||
|
||||
sharedRes.concatTxtTmp += "file '%existsFile%'\n";
|
||||
sharedRes.concatTxtTmp += "file '%buffFile%'\n";
|
||||
|
||||
sharedRes.concatShTmp += "#!/bin/sh\n";
|
||||
sharedRes.concatShTmp += "cp '%dstPath%' '%existsFile%'\n";
|
||||
sharedRes.concatShTmp += "ffmpeg -hide_banner -loglevel error -y -f concat -safe 0 -i '%concatFile%' -c copy '%dstPath%'\n";
|
||||
sharedRes.concatShTmp += "rm '%concatFile%'\n";
|
||||
sharedRes.concatShTmp += "rm '%existsFile%'\n";
|
||||
sharedRes.concatShTmp += "rm '%buffFile%'\n";
|
||||
sharedRes.concatShTmp += "rm '%scriptFile%'\n";
|
||||
|
||||
sharedRes.createShTmp += "#!/bin/sh\n";
|
||||
sharedRes.createShTmp += "cp '%buffFile%' '%dstPath%'\n";
|
||||
sharedRes.createShTmp += "rm '%buffFile%'\n";
|
||||
sharedRes.createShTmp += "rm '%scriptFile%'\n";
|
||||
|
||||
thread th1(recLoop, &sharedRes);
|
||||
|
||||
th1.join();
|
||||
|
||||
return sharedRes.retCode;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user