diff --git a/CMakeLists.txt b/CMakeLists.txt index 3b58453..fa46d25 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 2.8.12) project( MotionWatch ) find_package( OpenCV REQUIRED ) -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++17 -pthread") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++20 -pthread") include_directories( ${OpenCV_INCLUDE_DIRS} ) -add_executable( mow src/main.cpp src/common.cpp src/mo_detect.cpp ) +add_executable( mow src/main.cpp src/common.cpp src/mo_detect.cpp src/web.cpp ) target_link_libraries( mow ${OpenCV_LIBS} ) diff --git a/setup.sh b/setup.sh index 34b5bf2..66b042c 100644 --- a/setup.sh +++ b/setup.sh @@ -1,7 +1,11 @@ #!/bin/sh -apt update +apt update -y apt install -y cmake g++ wget unzip git ffmpeg libavcodec-dev libavformat-dev libavutil-dev libswscale-dev +add-apt-repository -y ppa:ubuntu-toolchain-r/test +apt update -y +apt install -y gcc-10 gcc-10-base gcc-10-doc g++-10 +apt install -y libstdc++-10-dev libstdc++-10-doc cd ./src if [ -d "./opencv" ] then diff --git a/src/common.cpp b/src/common.cpp index 2cd5c61..5ed3569 100644 --- a/src/common.cpp +++ b/src/common.cpp @@ -57,19 +57,20 @@ bool fileExists(const string& name) return access(name.c_str(), F_OK) != -1; } -void replaceAll(string &str, const string &from, const string &to) +string replaceAll(string str, const string &from, const string &to) { - if(from.empty()) - return; + if (from.empty()) return str; size_t startPos = 0; - while((startPos = str.find(from, startPos)) != string::npos) + while ((startPos = str.find(from, startPos)) != string::npos) { str.replace(startPos, from.length(), to); startPos += to.length(); } + + return str; } vector lsFilesInDir(const string &path, const string &ext) @@ -82,11 +83,11 @@ vector lsFilesInDir(const string &path, const string &ext) { while ((ent = readdir(dir)) != NULL) { - auto name = string(ent->d_name); - - if ((name.size() >= 4) && (ent->d_type & DT_REG)) + if (ent->d_type & DT_REG) { - if (name.substr(name.size() - 4) == ext) + auto name = string(ent->d_name); + + if (name.ends_with(ext.c_str()) || ext.empty()) { names.push_back(name); } @@ -101,19 +102,54 @@ vector lsFilesInDir(const string &path, const string &ext) return names; } -void enforceMaxDays(shared_t *share) +vector lsDirsInDir(const string &path) { - auto names = lsFilesInDir(share->outDir, ".m3u"); + DIR *dir; + struct dirent *ent; + vector names; - while (names.size() > share->maxDays) + if ((dir = opendir(path.c_str())) != NULL) { - auto name = names[0]; - auto plsFile = cleanDir(share->outDir) + "/" + name; - auto vidFold = cleanDir(share->outDir) + "/." + name.substr(0, name.size() - 4); + while ((ent = readdir(dir)) != NULL) + { + if (ent->d_type & DT_DIR) + { + names.push_back(string(ent->d_name)); + } + } - remove(plsFile.c_str()); - remove_all(vidFold.c_str()); + closedir(dir); + } + sort(names.begin(), names.end()); + + return names; +} + +void enforceMaxDays(const string &dirPath, shared_t *share) +{ + auto names = lsDirsInDir(dirPath); + + while (names.size() > (share->maxDays - 1)) + { + remove_all(string(cleanDir(dirPath) + "/" + names[0]).c_str()); + + names.erase(names.begin()); + } +} + +void enforceMaxClips(const string &dirPath, shared_t *share) +{ + auto names = lsDirsInDir(dirPath); + + while ((names.size() * 3) > ((share->maxClips - 1) * 3)) + { + remove(string(cleanDir(dirPath) + "/" + names[0]).c_str()); + remove(string(cleanDir(dirPath) + "/" + names[1]).c_str()); + remove(string(cleanDir(dirPath) + "/" + names[2]).c_str()); + + names.erase(names.begin()); + names.erase(names.begin()); names.erase(names.begin()); } } @@ -140,22 +176,11 @@ string genDstFile(const string &dirOut, const char *fmt, const string &ext) return cleanDir(dirOut) + string("/") + genTimeStr(fmt) + ext; } -Mat toGray(const Mat &src) -{ - Mat ret; - - cvtColor(src, ret, COLOR_BGR2GRAY); - - return ret; -} - void rdLine(const string ¶m, const string &line, string *value) { if (line.rfind(param.c_str(), 0) == 0) { *value = line.substr(param.size()); - - //cout << param << *value << endl; } } @@ -164,8 +189,6 @@ void rdLine(const string ¶m, const string &line, int *value) if (line.rfind(param.c_str(), 0) == 0) { *value = strtol(line.substr(param.size()).c_str(), NULL, 10); - - //cout << param << *value << endl; } } @@ -184,19 +207,20 @@ bool rdConf(shared_t *share) string line; share->recordUrl.clear(); - share->outDir.clear(); share->postCmd.clear(); share->buffDir.clear(); - share->consecThresh = 512; - share->secs = 60; - share->blockX = 32; - share->blockY = 32; - share->blockThresh = 1024; - share->maxDays = 5; - share->vidExt = "mp4"; - share->recLoopWait = false; - share->skipCmd = false; + share->frameGap = 10; + share->pixThresh = 30; + share->imgThresh = 512; + share->secs = 60; + share->maxDays = 15; + share->maxClips = 30; + share->camName = path(share->conf.c_str()).filename(); + share->outDir = "/var/www/hmtl"; + share->vidExt = "mp4"; + share->recLoopWait = false; + share->skipCmd = false; do { @@ -204,26 +228,23 @@ bool rdConf(shared_t *share) if (line.rfind("#", 0) != 0) { + rdLine("cam_name = ", line, &share->camName); rdLine("recording_stream = ", line, &share->recordUrl); rdLine("output_dir = ", line, &share->outDir); rdLine("post_cmd = ", line, &share->postCmd); - rdLine("consec_threshold = ", line, &share->consecThresh); rdLine("duration = ", line, &share->secs); rdLine("buff_dir = ", line, &share->buffDir); - rdLine("block_x = ", line, &share->blockX); - rdLine("block_y = ", line, &share->blockY); - rdLine("block_threshold = ", line, &share->blockThresh); + rdLine("frame_gap = ", line, &share->frameGap); + rdLine("pix_thresh = ", line, &share->pixThresh); + rdLine("img_thresh = ", line, &share->imgThresh); rdLine("max_days = ", line, &share->maxDays); + rdLine("max_clips = ", line, &share->maxClips); rdLine("vid_container = ", line, &share->vidExt); } } while(!line.empty()); - // it's imperative that blockX/Y are not zero or it will cause - // an infinte loop. if bad data is read from the conf, default - // values will be used. - if (share->blockX == 0) share->blockX = 32; - if (share->blockY == 0) share->blockY = 32; + share->outDir = cleanDir(share->outDir) + "/" + share->camName; if (share->init) { @@ -232,10 +253,7 @@ bool rdConf(shared_t *share) share->init = false; } - new thread(enforceMaxDays, share); - createDirTree(cleanDir(share->buffDir)); - system(string("touch " + cleanDir(share->buffDir) + "/stat").c_str()); share->retCode = 0; } @@ -245,40 +263,6 @@ bool rdConf(shared_t *share) return share->retCode == 0; } -bool capPair(Mat &prev, Mat &next, VideoCapture &capture, shared_t *share) -{ - capture >> prev; - capture >> next; - - return !prev.empty() && !next.empty(); -} - -void wrOut(const string &buffFile, shared_t *share) -{ - auto clnDir = cleanDir(share->outDir); - auto vidOut = genDstFile(clnDir + "/." + genTimeStr("%Y-%m-%d"), "%H%M%S", "." + share->vidExt); - auto m3uOut = vidOut.substr(clnDir.size() + 1); - auto lisOut = genDstFile(share->outDir, "%Y-%m-%d", ".m3u"); - - copy_file(buffFile.c_str(), vidOut.c_str()); - remove(buffFile.c_str()); - - ofstream file; - - if (fileExists(lisOut)) - { - file.open(lisOut.c_str(), ios_base::app); - } - else - { - file.open(lisOut.c_str()); - } - - file << m3uOut << endl; - - file.close(); -} - string parseForParam(const string &arg, int argc, char** argv, bool argOnly) { for (int i = 0; i < argc; ++i) @@ -304,14 +288,3 @@ string parseForParam(const string &arg, int argc, char** argv, bool argOnly) return string(); } - -void statOut(shared_t *share) -{ - system(string("touch " + cleanDir(share->buffDir) + "/stat").c_str()); - - auto path = string(cleanDir(share->buffDir) + "/stat"); - auto fd = open(path.c_str(), fstream::out | fstream::trunc); - - write(fd, share->stat.c_str(), share->stat.size() + 1); - close(fd); -} diff --git a/src/common.h b/src/common.h index 76f60ac..42e6378 100644 --- a/src/common.h +++ b/src/common.h @@ -36,27 +36,27 @@ using namespace cv; using namespace std; using namespace std::filesystem; -#define BUF_SZ 10 -#define APP_VER "1.5" +#define APP_VER "1.5.t1" +#define APP_NAME "Motion Watch" struct shared_t { - string stat; string recordUrl; string outDir; string postCmd; string conf; string buffDir; string vidExt; + string camName; bool init; bool recLoopWait; bool skipCmd; - int consecThresh; + int frameGap; + int pixThresh; + int imgThresh; int secs; - int blockThresh; - int blockX; - int blockY; int maxDays; + int maxClips; int retCode; }; @@ -64,18 +64,17 @@ string genDstFile(const string &dirOut, const char *fmt, const string &e string genTimeStr(const char *fmt); string cleanDir(const string &path); string parseForParam(const string &arg, int argc, char** argv, bool argOnly); +string replaceAll(string str, const string &from, const string &to); bool createDir(const string &dir); bool createDirTree(const string &full_path); bool fileExists(const string& name); -void wrOut(const string &buffFile, shared_t *share); -void replaceAll(string &str, const string &from, const string &to); -void enforceMaxDays(shared_t *share); +void enforceMaxDays(const string &dirPath, shared_t *share); +void enforceMaxClips(const string &dirPath, shared_t *share); void rdLine(const string ¶m, const string &line, string *value); void rdLine(const string ¶m, const string &line, int *value); void statOut(shared_t *share); bool rdConf(shared_t *share); -bool capPair(Mat &prev, Mat &next, VideoCapture &capture, shared_t *share); -Mat toGray(const Mat &src); -vector lsFilesInDir(const string &path, const string &ext); +vector lsFilesInDir(const string &path, const string &ext = string()); +vector lsDirsInDir(const string &path); #endif // COMMON_H diff --git a/src/main.cpp b/src/main.cpp index 5b9ab03..2870b6c 100755 --- a/src/main.cpp +++ b/src/main.cpp @@ -16,40 +16,54 @@ void detectLoop(shared_t *share) { vector bufFiles; - while (!bufFiles.empty() || !share->recLoopWait) + do { bufFiles = lsFilesInDir(share->buffDir, "." + share->vidExt); + // this loop will not process the last buffile while recLoop is still actively + // pulling footage from ffmpeg. it is assumed the last file is not finished. + // share->recLoopWait is used to detect if the recloop is still pulling. if not + // then the last file is finally processed. + if ((bufFiles.size() >= 2) || (share->recLoopWait && !bufFiles.empty())) { auto fullPath = cleanDir(share->buffDir) + "/" + bufFiles[0]; - share->stat.clear(); + Mat thumbNail; - if (moDetect(fullPath, share)) + if (moDetect(fullPath, thumbNail, share)) { share->skipCmd = true; - wrOut(fullPath, share); + wrOut(fullPath, thumbNail, share); } else { remove(fullPath.c_str()); } - - statOut(share); } else { sleep(1); } } + while (!bufFiles.empty()); } void recLoop(shared_t *share) { while (rdConf(share)) { + if (!fileExists("/tmp/mow-lock")) + { + system("touch /tmp/mow-lock"); + + auto webRoot = path(share->outDir).parent_path().string(); + + genHTMLul(webRoot, string(APP_NAME) + " " + string(APP_VER)); + system("rm /tmp/mow-lock"); + } + createDirTree(share->buffDir); auto bufPath = cleanDir(share->buffDir) + "/%03d." + share->vidExt; @@ -59,11 +73,19 @@ void recLoop(shared_t *share) thread th2(detectLoop, share); - system(cmd.c_str()); + if (system(cmd.c_str()) != 0) + { + share->recLoopWait = true; - share->recLoopWait = true; + th2.join(); + } + else + { + // simulate that ffmpeg is running even after it has failed. + sleep(share->secs); - th2.join(); + share->recLoopWait = true; + } if (!share->skipCmd) { @@ -86,7 +108,7 @@ int main(int argc, char** argv) cout << "-c : path to the config file." << endl; cout << "-v : display the current version." << endl; } - if (parseForParam("-v", argc, argv, true) == "true") + else if (parseForParam("-v", argc, argv, true) == "true") { cout << APP_VER << endl; } @@ -101,9 +123,7 @@ int main(int argc, char** argv) sharedRes.skipCmd = false; sharedRes.init = true; - thread th1(recLoop, &sharedRes); - - th1.join(); + recLoop(&sharedRes); return sharedRes.retCode; } diff --git a/src/mo_detect.cpp b/src/mo_detect.cpp index 7d7ac4b..9d23270 100644 --- a/src/mo_detect.cpp +++ b/src/mo_detect.cpp @@ -12,101 +12,64 @@ #include "mo_detect.h" -bool pixDiff(const uchar &pixA, const uchar &pixB, shared_t *share) -{ - if (pixA > pixB) return true; - if (pixB > pixA) return true; - - return false; -} - -void secDiff(const Mat &imgA, const Mat &imgB, int id, int rows, int cols, int rowOffs, int colOffs, vector *results, mutex *secMutex, shared_t *share) -{ - auto diff = 0; - auto pnts = 0; - - for (auto y = rowOffs; y < (rowOffs + rows); y++) - { - for (auto x = colOffs; x < (colOffs + cols); x++) - { - auto pixA = imgA.at(Point(x, y)); - auto pixB = imgB.at(Point(x, y)); - - if (pixDiff(pixA, pixB, share)) pnts += 1; - else pnts = 0; - - if (pnts >= share->consecThresh) - { - diff += 1; - } - } - } - - struct sec_t res; - - res.x = colOffs; - res.y = rowOffs; - res.xSize = cols; - res.ySize = rows; - res.pixDiff = diff; - res.id = id; - - lock_guard guard(*secMutex); - - results->push_back(res); -} - bool imgDiff(const Mat &prev, const Mat &next, shared_t *share) { - vector threads; - vector results; - mutex secMutex; + Mat diff; - auto id = 0; + absdiff(prev, next, diff); + threshold(diff, diff, share->pixThresh, 255, THRESH_BINARY); - for (auto x = 0; x < prev.cols; x += share->blockX) - { - // spawn all of the block motion detection threads. - for (auto y = 0; y < prev.rows; y += share->blockY, id += 1) - { - threads.push_back(thread(secDiff, prev, next, id, share->blockY, share->blockX, y, x, &results, &secMutex, share)); - } - } - - for (auto &&thr : threads) - { - // wait for all of the threads to finish. - thr.join(); - } - - auto maxPixDiff = 0; - auto blockPick = 0; - - for (auto i = 0; i < results.size(); ++i) - { - // out of all of the results returned form the threads, pick - // the block with the highest amount of pixDiff. - auto x = results[i].x; - auto y = results[i].y; - auto diff = results[i].pixDiff; - auto id = results[i].id; - - if (diff > 0) - { - share->stat += string("block_thread:") + " id=" + to_string(id) + " diff=" + to_string(diff) + "\n"; - } - - if ((results[i].pixDiff >= share->blockThresh) && (results[i].pixDiff > maxPixDiff)) - { - maxPixDiff = results[i].pixDiff; - blockPick = i; - } - } - - return maxPixDiff >= share->blockThresh; + return countNonZero(diff) >= share->imgThresh; } -bool moDetect(const string &buffFile, shared_t *share) +Mat frameFF(VideoCapture *cap, int gap) +{ + Mat ret; + + if (gap == 0) gap = 1; + + for (int i = 0; i < gap; ++i) + { + cap->grab(); + } + + cap->retrieve(ret); + + if (!ret.empty()) + { + cvtColor(ret, ret, COLOR_BGR2GRAY); + } + + return ret; +} + +void wrOut(const string &buffFile, const Mat &vidThumb, shared_t *share) +{ + auto dayStr = genTimeStr("%Y-%m-%d"); + auto timStr = genTimeStr("%H%M%S"); + auto outDir = cleanDir(share->outDir) + "/" + dayStr; + + if (!exists(outDir)) + { + enforceMaxDays(share->outDir, share); + } + + auto vidOut = genDstFile(outDir, timStr.c_str(), "." + share->vidExt); + auto imgOut = genDstFile(outDir, timStr.c_str(), ".jpg"); + + enforceMaxClips(outDir, share); + + copy_file(buffFile.c_str(), vidOut.c_str()); + remove(buffFile.c_str()); + + imwrite(imgOut.c_str(), vidThumb); + + genHTMLvid(vidOut, share); + genHTMLul(outDir, share->camName + ": " + dayStr); + genHTMLul(share->outDir, share->camName); +} + +bool moDetect(const string &buffFile, Mat &vidThumb, shared_t *share) { auto mod = false; @@ -117,13 +80,21 @@ bool moDetect(const string &buffFile, shared_t *share) Mat prev; Mat next; - while (capPair(prev, next, capture, share)) + do { - if (imgDiff(toGray(prev), toGray(next), share)) + if (prev.empty()) prev = frameFF(&capture, 1); + else prev = next.clone(); + + next = frameFF(&capture, share->frameGap); + + if (imgDiff(prev, next, share)) { - mod = true; break; + resize(next, vidThumb, Size(360, 202), INTER_LINEAR); + + mod = true; break; } } + while (!prev.empty() && !next.empty()); } else { diff --git a/src/mo_detect.h b/src/mo_detect.h index fd35186..8799101 100644 --- a/src/mo_detect.h +++ b/src/mo_detect.h @@ -14,20 +14,11 @@ // GNU General Public License for more details. #include "common.h" +#include "web.h" -struct sec_t -{ - int id; - int x; - int y; - int xSize; - int ySize; - int pixDiff; -}; - -void secDiff(const Mat &imgA, const Mat &imgB, int id, int rows, int cols, int rowOffs, int colOffs, vector *results, mutex *secMutex, shared_t *share); -bool pixDiff(const uchar &pixA, const uchar &pixB, shared_t *share); bool imgDiff(const Mat &prev, const Mat &next, shared_t *share); -bool moDetect(const string &buffFile, shared_t *share); +bool moDetect(const string &buffFile, Mat &vidThumb, shared_t *share); +void wrOut(const string &buffFile, const Mat &vidThumb, shared_t *share); +Mat frameFF(VideoCapture *cap, int gap); #endif // MO_DETECT_H diff --git a/src/web.cpp b/src/web.cpp new file mode 100644 index 0000000..7208781 --- /dev/null +++ b/src/web.cpp @@ -0,0 +1,94 @@ +// This file is part of Motion Watch. + +// Motion Watch is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Motion Watch is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +#include "web.h" + +void genHTMLul(const string &outputDir, const string &title) +{ + DIR *dir; + struct dirent *ent; + vector regNames; + vector dirNames; + + string htmlText = "\n"; + + htmlText += "\n"; + htmlText += "\n"; + htmlText += "

" + title + "

\n"; + htmlText += "
    \n"; + + if ((dir = opendir(outputDir.c_str())) != NULL) + { + while ((ent = readdir(dir)) != NULL) + { + if (ent->d_type & DT_REG) + { + regNames.push_back(string(ent->d_name)); + } + else if (ent->d_type & DT_DIR) + { + dirNames.push_back(string(ent->d_name)); + } + } + + closedir(dir); + } + + sort(regNames.begin(), regNames.end()); + sort(dirNames.begin(), dirNames.end()); + + for (auto dirName : dirNames) + { + htmlText += "
  • " + dirName + "
  • \n"; + } + + for (auto regName : regNames) + { + if (regName.ends_with(".html") && !regName.ends_with("index.html")) + { + htmlText += "
  • \n"; + } + } + + htmlText += "
\n"; + htmlText += "\n"; + htmlText += ""; + + ofstream file(string(cleanDir(outputDir) + "/index.html").c_str()); + + file << htmlText << endl; + + file.close(); +} + +void genHTMLvid(const string &outputVid, shared_t *share) +{ + auto filename = path(outputVid).filename().string(); + auto filePath = path(outputVid).parent_path().string(); + string htmlText = "\n"; + + filename = replaceAll(filename, share->vidExt, "html"); + + htmlText += "\n"; + htmlText += "\n"; + htmlText += "\n"; + htmlText += "\n"; + htmlText += ""; + + ofstream file(string(cleanDir(filePath) + "/" + filename + ".html").c_str()); + + file << htmlText << endl; + + file.close(); +} diff --git a/src/web.h b/src/web.h new file mode 100644 index 0000000..e36b27a --- /dev/null +++ b/src/web.h @@ -0,0 +1,21 @@ +#ifndef WEB_H +#define WEB_H + +// This file is part of Motion Watch. + +// Motion Watch is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Motion Watch is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +#include "common.h" + +void genHTMLul(const string &outputDir, const string &title); +void genHTMLvid(const string &outputVid, shared_t *share); + +#endif // WEB_H