diff --git a/README.md b/README.md index a2105f3..ec7fd91 100644 --- a/README.md +++ b/README.md @@ -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 ### diff --git a/src/main.cpp b/src/main.cpp index 113795a..87b9820 100755 --- a/src/main.cpp +++ b/src/main.cpp @@ -6,6 +6,9 @@ #include #include #include +#include +#include +#include #include #include @@ -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(Point(x, y)); auto pixB = imgB.at(Point(x, y)); + //cout << "pnts: " << pnts << endl; + if (pixDiff(pixA, pixB, share)) { pnts += 1; + + if (pnts >= share->blockThresh) + { + lock_guard 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 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 ¶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)) + 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 " << 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; }