Compare commits

..

4 Commits

Author SHA1 Message Date
Maurice ONeal
f4ea944f97 v2.2
Updated the documentation, current iteration of the code is deemed
stable. Releasing to main.
2023-05-06 09:08:47 -04:00
Maurice ONeal
3f3cbcc75b v2.1.t3
-adjusted config defaults

Attempting to slow down motion detection again. This time it should
properly match the camera's fps.
2023-04-21 16:01:14 -04:00
Maurice ONeal
b4ca30b0e1 v2.1.t2
the delay on the motion detection loop slowed it down too much to
the point that it falls too far behind live. I removed the delay
and re-introduced the frame gap so all frames in the video files
need to be decoded.

post command and event timers are now seperate but still tied to
a single thread so they can still be synced.

fixed an issued that cuased several thmubnails to not generate.

added more log lines the aid with debugging.
2023-04-20 14:52:59 -04:00
Maurice ONeal
bafd2bf727 v2.1.t1
- reduced the hls size to 2 seconds.
- motion clips are not being combined. fixed it by making event
  loop track the size of shared_t::recList instead of system time.
- maxScore is now global inside of shared_t instead of locally
  inside detectMoInStream().
2023-04-15 08:23:49 -04:00
6 changed files with 204 additions and 122 deletions

View File

@ -60,15 +60,26 @@ img_thresh = 80000
# before it is considered motion. any video clips found with frames # before it is considered motion. any video clips found with frames
# exceeding this value will be copied from live footage to event footage. # exceeding this value will be copied from live footage to event footage.
# #
frame_gap = 10
# this is the amount of frames in between the comparison frames to check
# for pixel differences. the higher the value, the lower the cpu over
# head, however it does lower motion detection accuracy.
#
max_events = 40 max_events = 40
# this indicates the maximum amount of motion event video clips to keep # this indicates the maximum amount of motion event video clips to keep
# before deleting the oldest clip. # before deleting the oldest clip.
# #
sch_sec = 60 max_event_secs = 10
# this is the amount of seconds to wait in between running post_cmd. # this is the maximum amount of secs of video footage that can be
# recorded in a motion event.
#
post_secs = 60
# this is the amount of seconds to wait before running the command
# defined in post_cmd. the command will not run if motion was detected
# in the space before post_secs elapsed.
# #
post_cmd = move_the_ptz_camera.py post_cmd = move_the_ptz_camera.py
# this an optional command to run with sch_sec. one great use for this # this an optional command to run with post_secs. one great use for this
# is to move a ptz camera to the next position of it's patrol pattern. # is to move a ptz camera to the next position of it's patrol pattern.
# note: the call to this command will be delayed if motion was detected. # note: the call to this command will be delayed if motion was detected.
# #

View File

@ -175,6 +175,11 @@ string genDstFile(const string &dirOut, const char *fmt, const string &ext)
return cleanDir(dirOut) + string("/") + genTimeStr(fmt) + ext; return cleanDir(dirOut) + string("/") + genTimeStr(fmt) + ext;
} }
string genEventName(int score)
{
return genTimeStr(string("%Y-%j-%H-%M-%S--" + to_string(score)).c_str());
}
void rdLine(const string &param, const string &line, string *value) void rdLine(const string &param, const string &line, string *value)
{ {
if (line.rfind(param.c_str(), 0) == 0) if (line.rfind(param.c_str(), 0) == 0)
@ -217,10 +222,12 @@ bool rdConf(const string &filePath, shared_t *share)
rdLine("web_text = ", line, &share->webTxt); rdLine("web_text = ", line, &share->webTxt);
rdLine("web_bg = ", line, &share->webBg); rdLine("web_bg = ", line, &share->webBg);
rdLine("web_font = ", line, &share->webFont); rdLine("web_font = ", line, &share->webFont);
rdLine("sch_sec = ", line, &share->schSec); rdLine("max_event_secs = ", line, &share->evMaxSecs);
rdLine("post_secs = ", line, &share->postSecs);
rdLine("post_cmd = ", line, &share->postCmd); rdLine("post_cmd = ", line, &share->postCmd);
rdLine("pix_thresh = ", line, &share->pixThresh); rdLine("pix_thresh = ", line, &share->pixThresh);
rdLine("img_thresh = ", line, &share->imgThresh); rdLine("img_thresh = ", line, &share->imgThresh);
rdLine("frame_gap = ", line, &share->frameGap);
rdLine("max_events = ", line, &share->maxEvents); rdLine("max_events = ", line, &share->maxEvents);
rdLine("max_log_size = ", line, &share->maxLogSize); rdLine("max_log_size = ", line, &share->maxLogSize);
} }
@ -241,9 +248,11 @@ bool rdConf(shared_t *share)
share->pixThresh = 50; share->pixThresh = 50;
share->imgThresh = 800; share->imgThresh = 800;
share->maxEvents = 40; share->maxEvents = 40;
share->maxLogSize = 50000; share->maxLogSize = 100000;
share->skipCmd = false; share->skipCmd = false;
share->schSec = 60; share->postSecs = 60;
share->evMaxSecs = 10;
share->frameGap = 10;
share->webRoot = "/var/www/html"; share->webRoot = "/var/www/html";
share->webBg = "#485564"; share->webBg = "#485564";
share->webTxt = "#dee5ee"; share->webTxt = "#dee5ee";
@ -350,8 +359,3 @@ string genVidNameFromLive(const string &tsPath)
return string(); return string();
} }
} }
uint64_t genEpoch()
{
return duration_cast<seconds>(system_clock::now().time_since_epoch()).count();
}

View File

@ -34,47 +34,54 @@ using namespace std;
using namespace std::filesystem; using namespace std::filesystem;
using namespace std::chrono; using namespace std::chrono;
#define APP_VER "2.0" #define APP_VER "2.2"
#define APP_NAME "Motion Watch" #define APP_NAME "Motion Watch"
#define REC_LOG_NAME "rec_log_lines.html" #define REC_LOG_NAME "rec_log_lines.html"
#define DET_LOG_NAME "det_log_lines.html" #define DET_LOG_NAME "det_log_lines.html"
#define UPK_LOG_NAME "upk_log_lines.html" #define UPK_LOG_NAME "upk_log_lines.html"
struct pls_t struct evt_t
{ {
string evName; string evName;
vector<string> srcPaths; vector<string> srcPaths;
uint64_t createTime;
Mat thumbnail; Mat thumbnail;
}; };
struct shared_t struct shared_t
{ {
map<string, pls_t> recList; vector<evt_t> recList;
string conf; string conf;
string recLog; string recLog;
string detLog; string detLog;
string upkLog; string upkLog;
string recordUrl; string recordUrl;
string outDir; string outDir;
string postCmd; string postCmd;
string camName; string camName;
string webBg; string webBg;
string webTxt; string webTxt;
string webFont; string webFont;
string webRoot; string webRoot;
bool skipCmd; evt_t curEvent;
int procTime; bool skipCmd;
int schSec; int frameGap;
int pixThresh; int evMaxSecs;
int imgThresh; int postSecs;
int maxEvents; int maxScore;
int maxLogSize; int procCnt;
int retCode; int hlsCnt;
int pixThresh;
int imgThresh;
int maxEvents;
int maxLogSize;
int retCode;
int postInd;
int evInd;
}; };
string genVidNameFromLive(const string &tsPath); string genVidNameFromLive(const string &tsPath);
string genEventPath(const string &tsPath); string genEventPath(const string &tsPath);
string genEventName(int score);
string genDstFile(const string &dirOut, const char *fmt, const string &ext); string genDstFile(const string &dirOut, const char *fmt, const string &ext);
string genTimeStr(const char *fmt); string genTimeStr(const char *fmt);
string cleanDir(const string &path); string cleanDir(const string &path);
@ -90,6 +97,5 @@ void enforceMaxEvents(shared_t *share);
bool rdConf(shared_t *share); bool rdConf(shared_t *share);
vector<string> lsFilesInDir(const string &path, const string &ext = string()); vector<string> lsFilesInDir(const string &path, const string &ext = string());
vector<string> lsDirsInDir(const string &path); vector<string> lsDirsInDir(const string &path);
uint64_t genEpoch();
#endif // COMMON_H #endif // COMMON_H

View File

@ -14,6 +14,17 @@
#include "logger.h" #include "logger.h"
#include "web.h" #include "web.h"
void timer(shared_t *share)
{
while (share->retCode == 0)
{
sleep(1);
share->postInd += 1;
share->evInd += 1;
}
}
void detectMo(shared_t *share) void detectMo(shared_t *share)
{ {
while (share->retCode == 0) while (share->retCode == 0)
@ -27,40 +38,31 @@ void eventLoop(shared_t *share)
{ {
while (share->retCode == 0) while (share->retCode == 0)
{ {
while (!share->recList.empty()) if (!share->recList.empty())
{ {
auto it = share->recList.begin(); auto event = share->recList[0];
auto evName = it->first;
auto event = it->second;
auto timeDiff = genEpoch() - event.createTime;
// wait at least 62 seconds before processing the event in try
// queue.
if ((timeDiff > 0) && (timeDiff > 62))
{ {
try recLog("attempting write out of event: " + event.evName, share);
{
createDirTree("events");
wrOutVod(event, share);
genHTMLvod(evName);
if (!exists("events/" + evName + ".jpg")) createDirTree("events");
{
imwrite(string("events/" + evName + ".jpg").c_str(), event.thumbnail);
}
}
catch (filesystem_error &ex)
{
recLog(string("err: ") + ex.what(), share);
}
share->recList.erase(it); if (wrOutVod(event, share))
{
genHTMLvod(event.evName);
imwrite(string("events/" + event.evName + ".jpg").c_str(), event.thumbnail);
}
}
catch (filesystem_error &ex)
{
recLog(string("err: ") + ex.what(), share);
} }
sleep(5); share->recList.erase(share->recList.begin());
} }
sleep(5); sleep(10);
} }
} }
@ -106,7 +108,15 @@ void upkeep(shared_t *share)
upkLog("skipping update of the webroot page, it is busy.", share); upkLog("skipping update of the webroot page, it is busy.", share);
} }
sleep(60); sleep(10);
}
}
void rmLive()
{
if (exists("live"))
{
remove_all("live");
} }
} }
@ -114,11 +124,6 @@ void recLoop(shared_t *share)
{ {
while (share->retCode == 0) while (share->retCode == 0)
{ {
if (exists("live"))
{
remove_all("live");
}
auto cmd = "ffmpeg -hide_banner -rtsp_transport tcp -timeout 3000000 -i " + auto cmd = "ffmpeg -hide_banner -rtsp_transport tcp -timeout 3000000 -i " +
share->recordUrl + share->recordUrl +
" -strftime 1" + " -strftime 1" +
@ -126,10 +131,11 @@ void recLoop(shared_t *share)
" -hls_segment_filename 'live/%Y-%j-%H-%M-%S.ts'" + " -hls_segment_filename 'live/%Y-%j-%H-%M-%S.ts'" +
" -hls_flags delete_segments" + " -hls_flags delete_segments" +
" -y -vcodec copy" + " -y -vcodec copy" +
" -f hls -hls_time 10 -hls_list_size 400" + " -f hls -hls_time 2 -hls_list_size 1000" +
" stream.m3u8"; " stream.m3u8";
recLog("ffmpeg_run: " + cmd, share); recLog("ffmpeg_run: " + cmd, share);
rmLive();
auto retCode = system(cmd.c_str()); auto retCode = system(cmd.c_str());
@ -169,7 +175,9 @@ int main(int argc, char** argv)
else else
{ {
sharedRes.retCode = 0; sharedRes.retCode = 0;
sharedRes.procTime = 0; sharedRes.maxScore = 0;
sharedRes.postInd = 0;
sharedRes.evInd = 0;
sharedRes.skipCmd = false; sharedRes.skipCmd = false;
rdConf(&sharedRes); rdConf(&sharedRes);
@ -178,11 +186,13 @@ int main(int argc, char** argv)
auto thr2 = thread(upkeep, &sharedRes); auto thr2 = thread(upkeep, &sharedRes);
auto thr3 = thread(detectMo, &sharedRes); auto thr3 = thread(detectMo, &sharedRes);
auto thr4 = thread(eventLoop, &sharedRes); auto thr4 = thread(eventLoop, &sharedRes);
auto thr5 = thread(timer, &sharedRes);
thr1.join(); thr1.join();
thr2.join(); thr2.join();
thr3.join(); thr3.join();
thr4.join(); thr4.join();
thr5.join();
return sharedRes.retCode; return sharedRes.retCode;
} }

View File

@ -14,25 +14,55 @@
void detectMoInStream(const string &streamFile, shared_t *share) void detectMoInStream(const string &streamFile, shared_t *share)
{ {
if (share->procTime >= share->schSec) if (share->postInd >= share->postSecs)
{ {
if (!share->skipCmd) if (!share->postCmd.empty())
{ {
detLog("no motion detected, running post command: " + share->postCmd, share); detLog("---POST_BREAK---", share);
system(share->postCmd.c_str());
if (!share->skipCmd)
{
detLog("no motion detected, running post command: " + share->postCmd, share);
system(share->postCmd.c_str());
}
else
{
share->skipCmd = false;
detLog("motion detected, skipping the post command.", share);
}
}
share->postInd = 0;
}
if (share->evInd >= share->evMaxSecs)
{
detLog("---EVENT_BREAK---", share);
if (!share->curEvent.srcPaths.empty())
{
share->curEvent.evName = genEventName(share->maxScore);
share->recList.push_back(share->curEvent);
detLog("motion detected in " + to_string(share->curEvent.srcPaths.size()) + " file(s) in " + to_string(share->evMaxSecs) + " secs", share);
detLog("all video clips queued for event generation under event name: " + share->curEvent.evName, share);
} }
else else
{ {
share->skipCmd = false; detLog("no motion detected in all files. none queued for event generation.", share);
share->procTime = 0;
detLog("motion detected, skipping the post command.", share);
} }
share->curEvent.srcPaths.clear();
share->curEvent.evName.clear();
share->curEvent.thumbnail.release();
share->evInd = 0;
share->maxScore = 0;
} }
ifstream fileIn(streamFile); ifstream fileIn(streamFile);
string tsPath; string tsPath;
Mat thumbnail;
for (string line; getline(fileIn, line); ) for (string line; getline(fileIn, line); )
{ {
@ -44,31 +74,12 @@ void detectMoInStream(const string &streamFile, shared_t *share)
if (!tsPath.empty()) if (!tsPath.empty())
{ {
if (moDetect(tsPath, thumbnail, share)) if (moDetect(tsPath, share))
{ {
auto eventName = genTimeStr("%Y-%j-%H-%M"); share->curEvent.srcPaths.push_back(tsPath);
if (share->recList.find(eventName) != share->recList.end())
{
share->recList[eventName].srcPaths.push_back(tsPath);
}
else
{
pls_t event;
event.srcPaths.push_back(tsPath);
event.createTime = genEpoch();
event.thumbnail = thumbnail.clone();
event.evName = eventName;
share->recList.insert(pair{eventName, event});
}
share->skipCmd = true; share->skipCmd = true;
} }
share->procTime += 10;
} }
} }
@ -87,34 +98,43 @@ bool imgDiff(const Mat &prev, const Mat &next, int &score, shared_t *share)
score = countNonZero(diff); score = countNonZero(diff);
detLog("diff_score: " + to_string(score) + " tresh: " + to_string(share->imgThresh), share); detLog("diff_score: " + to_string(score) + " thresh: " + to_string(share->imgThresh), share);
return score >= share->imgThresh; return score >= share->imgThresh;
} }
bool moDetect(const string &buffFile, Mat &vidThumb, shared_t *share) bool moDetect(const string &buffFile, shared_t *share)
{ {
auto maxScore = 0; auto score = 0;
auto score = 0; auto mod = false;
detLog("stream_clip: " + buffFile, share); detLog("stream_clip: " + buffFile, share);
VideoCapture capture(buffFile.c_str(), CAP_FFMPEG); VideoCapture capture;
if (!capture.open(buffFile.c_str(), CAP_FFMPEG))
{
usleep(500);
capture.open(buffFile.c_str(), CAP_FFMPEG);
}
if (capture.isOpened()) if (capture.isOpened())
{ {
Mat prev; Mat prev;
Mat next; Mat next;
detLog("capture open successful.", share); int fps = capture.get(cv::CAP_PROP_FPS);
while (capture.grab()) for (auto gap = 0, frm = fps; capture.grab(); ++gap, ++frm)
{ {
if (frm == fps) sleep(1); frm = 1;
if (prev.empty()) if (prev.empty())
{ {
capture.retrieve(prev); capture.retrieve(prev);
} }
else else if (gap == (share->frameGap - 1))
{ {
capture.retrieve(next); capture.retrieve(next);
@ -122,45 +142,76 @@ bool moDetect(const string &buffFile, Mat &vidThumb, shared_t *share)
{ {
if (imgDiff(prev, next, score, share)) if (imgDiff(prev, next, score, share))
{ {
if (score > maxScore) mod = true;
{
maxScore = score;
resize(next, vidThumb, Size(720, 480), INTER_LINEAR); if (share->maxScore <= score)
{
share->maxScore = score;
resize(next, share->curEvent.thumbnail, Size(720, 480), INTER_LINEAR);
} }
} }
} }
prev.release(); prev = next.clone();
gap = 0;
next.release(); next.release();
} }
else
sleep(1); {
capture.grab();
}
} }
} }
else else
{ {
detLog("capture open failure, check debug output.", share); detLog("err: failed to open: " + buffFile + " after 500 msecs. giving up.", share);
} }
capture.release(); capture.release();
return maxScore > 0; return mod;
} }
void wrOutVod(const pls_t &event, shared_t *share) bool wrOutVod(const evt_t &event, shared_t *share)
{ {
auto cnt = 0;
auto concat = event.evName + ".tmp"; auto concat = event.evName + ".tmp";
ofstream file(concat.c_str()); ofstream file(concat.c_str());
for (auto i = 0; i < event.srcPaths.size(); ++i) for (auto i = 0; i < event.srcPaths.size(); ++i)
{ {
file << "file '" << event.srcPaths[i] << "''" << endl; recLog("event_src: " + event.srcPaths[i], share);
if (exists(event.srcPaths[i]))
{
file << "file '" << event.srcPaths[i] << "''" << endl; cnt++;
}
} }
file.close(); file.close();
system(string("ffmpeg -f concat -safe 0 -i " + concat + " -c copy events/" + event.evName + ".mp4").c_str()); if (cnt == 0)
remove(concat); {
recLog("err: none of the event hls clips exists, canceling write out.", share);
if (exists(concat)) remove(concat);
return false;
}
else
{
auto ret = system(string("ffmpeg -f concat -safe 0 -i " + concat + " -c copy events/" + event.evName + ".mp4").c_str());
if (ret != 0)
{
recLog("err: ffmpeg concat failure, canceling write out.", share);
}
if (exists(concat)) remove(concat);
return ret == 0;
}
} }

View File

@ -17,8 +17,8 @@
#include "logger.h" #include "logger.h"
bool imgDiff(const Mat &prev, const Mat &next, int &score, shared_t *share); bool imgDiff(const Mat &prev, const Mat &next, int &score, shared_t *share);
bool moDetect(const string &buffFile, Mat &vidThumb, shared_t *share); bool moDetect(const string &buffFile, shared_t *share);
void detectMoInStream(const string &streamFile, shared_t *share); void detectMoInStream(const string &streamFile, shared_t *share);
void wrOutVod(const pls_t &pls, shared_t *share); bool wrOutVod(const evt_t &pls, shared_t *share);
#endif // MO_DETECT_H #endif // MO_DETECT_H