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:
Maurice ONeal 2022-08-12 21:46:36 -04:00
parent a36d4e93c0
commit 48e55b9721
2 changed files with 252 additions and 153 deletions

View File

@ -30,50 +30,49 @@ 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.
# to the settings will be applied without restarting the application.
#
post_cmd = move_the_ptz_camera.py
# this an optional command to run after the internal timer duration has
@ -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 ###

View File

@ -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,24 +16,27 @@
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;
int colorThresh;
int secs;
int consec;
int consecThresh;
int pixThresh;
int postMoIncr;
int retCode;
string recordUrl;
string outDir;
string postCmd;
string conf;
string buffDir;
string concatTxtTmp;
string concatShTmp;
string createShTmp;
mutex thrMutex;
bool init;
int tmpId;
int colorThresh;
int secs;
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,50 +161,45 @@ 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;
if (pnts >= share->blockThresh)
{
lock_guard<mutex> guard(share->thrMutex);
*mod = true; return;
}
}
}
}
return pnts;
}
bool imgDiff(Mat curImg, shared_t *share)
bool imgDiff(Mat prev, Mat next, shared_t *share)
{
if (share->baseImg.empty())
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)
{
share->baseImg = toGray(curImg);
return false;
}
else
{
curImg = toGray(curImg);
auto pnts = secDiff(share->baseImg, curImg, curImg.rows, curImg.cols, 0, 0, share);
if (share->diffVerb == "Y")
for (auto y = 0; (y < numOfYBlocks) && !moInBlock; y += share->blockY)
{
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;
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 &param, 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 &param, 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))
capture >> prev;
capture >> next;
return !prev.empty() && !next.empty();
}
void wrOut(const string &buffFile, const string &dstPath, shared_t *share)
{
ofstream file;
auto scriptFile = genTmpFile(share->buffDir, ".sh", share);
auto scriptData = string();
if (fileExists(dstPath))
{
for (auto i = 0; i < (share->secs * share->detectFps); ++i)
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
{
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))
{
Mat frame;
if (!share->camera.isOpened())
if (imgDiff(toGray(prev), toGray(next), share))
{
share->camera.open(share->detectUrl, CAP_FFMPEG);
}
share->camera >> frame;
if (frame.empty())
{
// 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;
}
else
{
usleep(1000000 / share->detectFps);
mod = true; break;
}
}
system(share->postCmd.c_str());
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;
}