Decided to switch using opencv's builtin pixel diff motion detection via
absdiff and thresh. Doing this should increase efficiency instead of
using the home brewed pixel loops and threads.

Added a web interface of sorts by having html files output along with
the video clips. These files are designed to link together with the
assumption that the output directory is a web root like /var/www/html
that apache2 uses. The interface is crude at best but at least allow
playback of recorded footage.

Added max_clips config variable that can limit the amount of motion
events that can recorded to storage on a single day.
This commit is contained in:
Maurice ONeal 2022-12-04 15:13:39 -05:00
parent 01a11741c7
commit 9816ba339f
9 changed files with 303 additions and 230 deletions

View File

@ -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} )

View File

@ -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

View File

@ -57,10 +57,9 @@ 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;
@ -70,6 +69,8 @@ void replaceAll(string &str, const string &from, const string &to)
startPos += to.length();
}
return str;
}
vector<string> lsFilesInDir(const string &path, const string &ext)
@ -81,12 +82,12 @@ vector<string> lsFilesInDir(const string &path, const string &ext)
if ((dir = opendir(path.c_str())) != NULL)
{
while ((ent = readdir(dir)) != NULL)
{
if (ent->d_type & DT_REG)
{
auto name = string(ent->d_name);
if ((name.size() >= 4) && (ent->d_type & DT_REG))
{
if (name.substr(name.size() - 4) == ext)
if (name.ends_with(ext.c_str()) || ext.empty())
{
names.push_back(name);
}
@ -101,19 +102,54 @@ vector<string> lsFilesInDir(const string &path, const string &ext)
return names;
}
void enforceMaxDays(shared_t *share)
vector<string> lsDirsInDir(const string &path)
{
auto names = lsFilesInDir(share->outDir, ".m3u");
DIR *dir;
struct dirent *ent;
vector<string> 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 &param, 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 &param, 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,16 +207,17 @@ bool rdConf(shared_t *share)
string line;
share->recordUrl.clear();
share->outDir.clear();
share->postCmd.clear();
share->buffDir.clear();
share->consecThresh = 512;
share->frameGap = 10;
share->pixThresh = 30;
share->imgThresh = 512;
share->secs = 60;
share->blockX = 32;
share->blockY = 32;
share->blockThresh = 1024;
share->maxDays = 5;
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;
@ -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);
}

View File

@ -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 &param, const string &line, string *value);
void rdLine(const string &param, 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<string> lsFilesInDir(const string &path, const string &ext);
vector<string> lsFilesInDir(const string &path, const string &ext = string());
vector<string> lsDirsInDir(const string &path);
#endif // COMMON_H

View File

@ -16,40 +16,54 @@ void detectLoop(shared_t *share)
{
vector<string> 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;
th2.join();
}
else
{
// simulate that ffmpeg is running even after it has failed.
sleep(share->secs);
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;
}

View File

@ -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<sec_t> *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<uchar>(Point(x, y));
auto pixB = imgB.at<uchar>(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<mutex> guard(*secMutex);
results->push_back(res);
}
bool imgDiff(const Mat &prev, const Mat &next, shared_t *share)
{
vector<thread> threads;
vector<sec_t> 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)
return countNonZero(diff) >= share->imgThresh;
}
Mat frameFF(VideoCapture *cap, int gap)
{
// spawn all of the block motion detection threads.
for (auto y = 0; y < prev.rows; y += share->blockY, id += 1)
Mat ret;
if (gap == 0) gap = 1;
for (int i = 0; i < gap; ++i)
{
threads.push_back(thread(secDiff, prev, next, id, share->blockY, share->blockX, y, x, &results, &secMutex, share));
}
cap->grab();
}
for (auto &&thr : threads)
cap->retrieve(ret);
if (!ret.empty())
{
// wait for all of the threads to finish.
thr.join();
cvtColor(ret, ret, COLOR_BGR2GRAY);
}
auto maxPixDiff = 0;
auto blockPick = 0;
return ret;
}
for (auto i = 0; i < results.size(); ++i)
void wrOut(const string &buffFile, const Mat &vidThumb, shared_t *share)
{
// 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;
auto dayStr = genTimeStr("%Y-%m-%d");
auto timStr = genTimeStr("%H%M%S");
auto outDir = cleanDir(share->outDir) + "/" + dayStr;
if (diff > 0)
if (!exists(outDir))
{
share->stat += string("block_thread:") + " id=" + to_string(id) + " diff=" + to_string(diff) + "\n";
enforceMaxDays(share->outDir, share);
}
if ((results[i].pixDiff >= share->blockThresh) && (results[i].pixDiff > maxPixDiff))
{
maxPixDiff = results[i].pixDiff;
blockPick = i;
}
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);
}
return maxPixDiff >= share->blockThresh;
}
bool moDetect(const string &buffFile, shared_t *share)
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))
{
resize(next, vidThumb, Size(360, 202), INTER_LINEAR);
mod = true; break;
}
}
while (!prev.empty() && !next.empty());
}
else
{

View File

@ -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<sec_t> *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

94
src/web.cpp Normal file
View File

@ -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<string> regNames;
vector<string> dirNames;
string htmlText = "<!DOCTYPE html>\n";
htmlText += "<html>\n";
htmlText += "<body>\n";
htmlText += "<h2>" + title + "</h2>\n";
htmlText += "<ul>\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 += " <li><a href='" + dirName + "'>" + dirName + "</a></li>\n";
}
for (auto regName : regNames)
{
if (regName.ends_with(".html") && !regName.ends_with("index.html"))
{
htmlText += " <li><a href='" + regName + "'> <img src='" + replaceAll(regName, ".html", ".jpg") + "' </a></li>\n";
}
}
htmlText += "</ul>\n";
htmlText += "</body>\n";
htmlText += "</html>";
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 = "<!DOCTYPE html>\n";
filename = replaceAll(filename, share->vidExt, "html");
htmlText += "<html>\n";
htmlText += "<body>\n";
htmlText += "<video width='420' height='320' controls autoplay>\n";
htmlText += " <source src='" + filename + "' type='video/" + share->vidExt + "'>\n";
htmlText += "</video>\n";
htmlText += "</body>\n";
htmlText += "</html>";
ofstream file(string(cleanDir(filePath) + "/" + filename + ".html").c_str());
file << htmlText << endl;
file.close();
}

21
src/web.h Normal file
View File

@ -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