The Qt approach to grabbing frames from the live stream was also
a failure.

- decided to switch to a combination ffmpeg and imagemagic was
  external commands to do motion detection. this approach
  elimates the need for opencv altogeather so it was removed
  from the project. system resource usage appears to be decent
  and perhaps better than opencv.
This commit is contained in:
Maurice ONeal 2023-05-26 16:12:53 -04:00
parent f850ec6a46
commit 4134d4befb
10 changed files with 272 additions and 377 deletions

2
.gitignore vendored
View File

@ -57,3 +57,5 @@ compile_commands.json
# Build folders # Build folders
/.build-mow /.build-mow
/.build-opencv
/src/opencv

View File

@ -11,18 +11,13 @@ set(CMAKE_AUTORCC ON)
set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
include_directories(${OpenCV_INCLUDE_DIRS}) find_package(QT NAMES Qt6 Qt5 COMPONENTS Core REQUIRED)
find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Core REQUIRED)
find_package(QT NAMES Qt6 COMPONENTS Core REQUIRED)
find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Core Multimedia REQUIRED)
find_package(OpenCV REQUIRED)
add_executable(mow add_executable(mow
src/main.cpp src/main.cpp
src/common.h src/common.h
src/common.cpp src/common.cpp
src/mo_detect.h
src/mo_detect.cpp
src/web.h src/web.h
src/web.cpp src/web.cpp
src/logger.h src/logger.h
@ -31,4 +26,4 @@ add_executable(mow
src/camera.cpp src/camera.cpp
) )
target_link_libraries(mow Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Multimedia ${OpenCV_LIBS}) target_link_libraries(mow Qt${QT_VERSION_MAJOR}::Core ${OpenCV_LIBS})

BIN
bin/magick Normal file

Binary file not shown.

View File

@ -1,4 +1,7 @@
#!/bin/sh #!/bin/sh
export DEBIAN_FRONTEND=noninteractive
apt update -y apt update -y
apt install -y pkg-config cmake make g++ wget unzip git apt install -y pkg-config cmake make g++
apt install -y ffmpeg libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libgstreamer1.0-dev x264 libx264-dev libilmbase-dev libopencv-dev qt6-base-dev qtchooser qmake6 qt6-base-dev-tools qt6-multimedia-dev libxkbcommon-dev apt install -y ffmpeg libavcodec-dev libavformat-dev libavutil-dev libswscale-dev x264 libx264-dev libilmbase-dev qt6-base-dev qtchooser qmake6 qt6-base-dev-tools libxkbcommon-dev
cp ./bin/magick /usr/bin/magick
chmod +x /usr/bin/magick

View File

@ -21,8 +21,8 @@ Camera::Camera(QObject *parent) : QObject(parent)
shared.retCode = 0; shared.retCode = 0;
shared.maxScore = 0; shared.maxScore = 0;
shared.pixThresh = 50; shared.pixThresh = 50;
shared.imgThresh = 800; shared.imgThresh = 8000;
shared.maxEvents = 40; shared.maxEvents = 100;
shared.maxLogSize = 100000; shared.maxLogSize = 100000;
shared.skipCmd = false; shared.skipCmd = false;
shared.postSecs = 60; shared.postSecs = 60;
@ -47,10 +47,10 @@ int Camera::start(const QStringList &args)
auto thr3 = new QThread(nullptr); auto thr3 = new QThread(nullptr);
auto thr4 = new QThread(nullptr); auto thr4 = new QThread(nullptr);
new RecLoop(&shared, thr1, this); new RecLoop(&shared, thr1, nullptr);
new Upkeep(&shared, thr2, this); new Upkeep(&shared, thr2, nullptr);
new EventLoop(&shared, thr3, this); new EventLoop(&shared, thr3, nullptr);
new DetectLoop(&shared, thr4, this); new DetectLoop(&shared, thr4, nullptr);
thr1->start(); thr1->start();
thr2->start(); thr2->start();
@ -61,25 +61,25 @@ int Camera::start(const QStringList &args)
return shared.retCode; return shared.retCode;
} }
Loop::Loop(shared_t *sharedRes, QThread *thr, QObject *parent) : QObject(0) Loop::Loop(shared_t *sharedRes, QThread *thr, QObject *parent) : QObject(parent)
{ {
shared = sharedRes; shared = sharedRes;
heartBeat = 10; heartBeat = 10;
loopTimer = new QTimer(nullptr); loopTimer = 0;
loopTimer->setSingleShot(false); connect(thr, &QThread::started, this, &Loop::init);
connect(this, &Loop::loopSig, this, &Loop::loopSlot);
connect(thr, &QThread::started, this, &Loop::init);
connect(parent, &QObject::destroyed, this, &Loop::deleteLater);
connect(parent, &QObject::destroyed, thr, &QThread::terminate);
connect(parent, &QObject::destroyed, loopTimer, &QTimer::deleteLater);
connect(loopTimer, &QTimer::timeout, this, &Loop::loopSlot);
moveToThread(thr); moveToThread(thr);
} }
void Loop::init() void Loop::init()
{ {
loopTimer = new QTimer(nullptr);
connect(loopTimer, &QTimer::timeout, this, &Loop::loopSlot);
loopTimer->setSingleShot(false);
loopTimer->start(heartBeat * 1000); loopTimer->start(heartBeat * 1000);
} }
@ -93,82 +93,127 @@ void Loop::loopSlot()
bool Loop::exec() bool Loop::exec()
{ {
if (loopTimer->interval() != heartBeat * 1000)
{
loopTimer->start(heartBeat * 1000);
}
return shared->retCode == 0; return shared->retCode == 0;
} }
RecLoop::RecLoop(shared_t *sharedRes, QThread *thr, QObject *parent) : Loop(sharedRes, thr, parent) RecLoop::RecLoop(shared_t *sharedRes, QThread *thr, QObject *parent) : Loop(sharedRes, thr, parent)
{ {
once = true; once = true;
baseListRdy = false;
recProc = 0;
imgProc = 0;
}
void RecLoop::init()
{
recProc = new QProcess(this);
imgProc = new QProcess(this);
Loop::init();
} }
void RecLoop::updateCmd() void RecLoop::updateCmd()
{ {
QStringList args; QStringList recArgs;
QStringList imgArgs;
args << "-hide_banner"; recArgs << "-hide_banner";
args << "-i" << shared->recordUrl; recArgs << "-i" << shared->recordUrl;
args << "-strftime" << "1"; recArgs << "-strftime" << "1";
args << "-strftime_mkdir" << "1"; recArgs << "-strftime_mkdir" << "1";
args << "-hls_segment_filename" << "live/%Y-%j-%H-%M-%S.ts"; recArgs << "-hls_segment_filename" << "live/" + QString(STRFTIME_FMT) + ".ts";
args << "-hls_flags" << "delete_segments"; recArgs << "-hls_flags" << "delete_segments";
args << "-y"; recArgs << "-y";
args << "-vcodec" << "copy"; recArgs << "-vcodec" << "copy";
args << "-f" << "hls"; recArgs << "-f" << "hls";
args << "-hls_time" << "2"; recArgs << "-hls_time" << "2";
args << "-hls_list_size" << "1000"; recArgs << "-hls_list_size" << "1000";
args << "stream.m3u8"; recArgs << "stream.m3u8";
proc.setProgram("ffmpeg"); imgArgs << "-hide_banner";
proc.setArguments(args); imgArgs << "-i" << shared->recordUrl;
imgArgs << "-strftime" << "1";
imgArgs << "-vf" << "fps=1,scale=320:240";
imgArgs << "img/" + QString(STRFTIME_FMT) + ".bmp";
recProc->setProgram("ffmpeg");
recProc->setArguments(recArgs);
imgProc->setProgram("ffmpeg");
imgProc->setArguments(imgArgs);
curUrl = shared->recordUrl; curUrl = shared->recordUrl;
recLog("rec_args_updated: " + args.join(" "), shared); recLog("rec_args_updated: " + recArgs.join(" "), shared);
recLog("img_args_updated: " + imgArgs.join(" "), shared);
} }
void RecLoop::reset() void RecLoop::reset()
{ {
recLog("--rec_cmd_resetting--", shared); recLog("--rec_and_img_cmds_resetting--", shared);
proc.kill(); baseListRdy = false;
proc.waitForFinished();
recProc->kill();
recProc->waitForFinished();
imgProc->kill();
imgProc->waitForFinished();
updateCmd(); updateCmd();
} }
bool RecLoop::exec() void RecLoop::startProc(const QString &desc, QProcess *proc)
{ {
auto md5 = genMD5(QString("stream.m3u8")); if (proc->state() == QProcess::NotRunning)
if (once)
{ {
updateCmd(); once = false; streamMD5 = genMD5(QByteArray("FIRST")); proc->start();
}
else if ((curUrl != shared->recordUrl) || (streamMD5 == md5))
{
reset();
}
auto hashLogLine = "stream_hash--prev:" + QString(streamMD5.toHex()) + "--new:" + QString(md5.toHex()); if (proc->waitForStarted())
streamMD5 = md5;
recLog(hashLogLine, shared);
if (proc.state() == QProcess::NotRunning)
{
proc.start();
if (proc.waitForStarted())
{ {
recLog("rec_cmd_start: ok", shared); recLog(desc + "_cmd_start: ok", shared);
} }
else else
{ {
recLog("rec_cmd_start: fail", shared); recLog(desc + "_cmd_start: fail", shared);
recLog("rec_cmd_stderr: " + QString(proc.readAllStandardError()), shared); recLog(desc + "_cmd_stderr: " + QString(proc->readAllStandardError()), shared);
} }
} }
}
bool RecLoop::exec()
{
if (once)
{
updateCmd(); once = false;
}
else if (!baseListRdy)
{
baseListRdy = true;
}
else if (backwardFacingFiles("live", ".ts", QDateTime::currentDateTime(), heartBeat).isEmpty())
{
recLog("backward facing files in the live stream are empty. cmd stall is suspected.", shared);
reset();
}
else if (backwardFacingFiles("img", ".bmp", QDateTime::currentDateTime(), heartBeat).isEmpty())
{
recLog("backward facing files in the image stream are empty. cmd stall is suspected.", shared);
reset();
}
else if (curUrl != shared->recordUrl)
{
recLog("a change in the recording URL was detected.", shared);
reset();
}
startProc("img", imgProc);
startProc("rec", recProc);
return Loop::exec(); return Loop::exec();
} }
@ -180,6 +225,7 @@ bool Upkeep::exec()
QDir().mkdir("live"); QDir().mkdir("live");
QDir().mkdir("events"); QDir().mkdir("events");
QDir().mkdir("logs"); QDir().mkdir("logs");
QDir().mkdir("img");
enforceMaxLogSize(QString("logs/") + REC_LOG_NAME, shared); enforceMaxLogSize(QString("logs/") + REC_LOG_NAME, shared);
enforceMaxLogSize(QString("logs/") + DET_LOG_NAME, shared); enforceMaxLogSize(QString("logs/") + DET_LOG_NAME, shared);
@ -195,6 +241,7 @@ bool Upkeep::exec()
initLogFrontPages(shared); initLogFrontPages(shared);
enforceMaxEvents(shared); enforceMaxEvents(shared);
enforceMaxImages();
genHTMLul(".", shared->camName, shared); genHTMLul(".", shared->camName, shared);
upkLog("camera specific webroot page updated: " + shared->outDir + "/index.html", shared); upkLog("camera specific webroot page updated: " + shared->outDir + "/index.html", shared);
@ -224,22 +271,43 @@ bool Upkeep::exec()
EventLoop::EventLoop(shared_t *sharedRes, QThread *thr, QObject *parent) : Loop(sharedRes, thr, parent) EventLoop::EventLoop(shared_t *sharedRes, QThread *thr, QObject *parent) : Loop(sharedRes, thr, parent)
{ {
heartBeat = 2; heartBeat = 5;
} }
bool EventLoop::exec() bool EventLoop::exec()
{ {
if (!shared->recList.isEmpty()) if (!shared->recList.isEmpty())
{ {
auto event = shared->recList[0]; auto event = shared->recList[0];
auto name = event.timeStamp.toString(DATETIME_FMT);
auto vidList = backwardFacingFiles("live", ".ts", event.timeStamp, shared->evMaxSecs / 2);
recLog("attempting write out of event: " + event.evName, shared); if (vidList.isEmpty())
if (wrOutVod(event))
{ {
genHTMLvod(event.evName); recLog("err: no backward faces files were found for event: " + name, shared);
}
else
{
vidList.removeLast();
event.thumbnail.save(QString("events/" + event.evName + ".jpg")); vidList += forwardFacingFiles("live", ".ts", event.timeStamp, shared->evMaxSecs / 2);
recLog("attempting write out of event: " + name, shared);
if (wrOutVod(name, vidList))
{
genHTMLvod(name);
QProcess proc;
QStringList args;
args << "convert";
args << event.imgPath;
args << "events/" + name + ".jpg";
proc.start("magick", args);
proc.waitForFinished();
}
} }
shared->recList.removeFirst(); shared->recList.removeFirst();
@ -248,22 +316,22 @@ bool EventLoop::exec()
return Loop::exec(); return Loop::exec();
} }
bool EventLoop::wrOutVod(const evt_t &event) bool EventLoop::wrOutVod(const QString &name, const QStringList &vids)
{ {
auto cnt = 0; auto cnt = 0;
auto concat = event.evName + ".tmp"; auto concat = name + ".tmp";
QFile file(concat); QFile file(concat);
file.open(QFile::WriteOnly); file.open(QFile::WriteOnly);
for (auto i = 0; i < event.srcPaths.size(); ++i) for (auto &&vid : vids)
{ {
recLog("event_src: " + event.srcPaths[i], shared); recLog("event_src: " + vid, shared);
if (QFile::exists(event.srcPaths[i])) if (QFile::exists(vid))
{ {
file.write(QString("file '" + event.srcPaths[i] + "'\n").toUtf8()); cnt++; file.write(QString("file '" + vid + "'\n").toUtf8()); cnt++;
} }
} }
@ -285,7 +353,7 @@ bool EventLoop::wrOutVod(const evt_t &event)
args << "-safe" << "0"; args << "-safe" << "0";
args << "-i" << concat; args << "-i" << concat;
args << "-c" << "copy"; args << "-c" << "copy";
args << "events/" + event.evName + ".mp4"; args << "events/" + name + ".mp4";
proc.setProgram("ffmpeg"); proc.setProgram("ffmpeg");
proc.setArguments(args); proc.setArguments(args);
@ -313,20 +381,16 @@ bool EventLoop::wrOutVod(const evt_t &event)
DetectLoop::DetectLoop(shared_t *sharedRes, QThread *thr, QObject *parent) : Loop(sharedRes, thr, parent) DetectLoop::DetectLoop(shared_t *sharedRes, QThread *thr, QObject *parent) : Loop(sharedRes, thr, parent)
{ {
heartBeat = 1; pcTimer = 0;
evId = 0; heartBeat = 3;
pcId = 0;
player = new QMediaPlayer(this);
frameAnalyzer = new MoDetect(shared, this);
player->setVideoSink(frameAnalyzer);
connect(player, &QMediaPlayer::errorOccurred, this, &DetectLoop::playError);
} }
void DetectLoop::init() void DetectLoop::init()
{ {
thread()->sleep(1); pcTimer = new QTimer(nullptr);
mod = false;
connect(pcTimer, &QTimer::timeout, this, &DetectLoop::pcBreak);
resetTimers(); resetTimers();
@ -335,134 +399,68 @@ void DetectLoop::init()
void DetectLoop::resetTimers() void DetectLoop::resetTimers()
{ {
if (evId != 0) killTimer(evId); pcTimer->start(shared->postSecs * 1000);
if (pcId != 0) killTimer(pcId);
evId = startTimer(shared->evMaxSecs * 1000);
pcId = startTimer(shared->postSecs * 1000);
} }
void DetectLoop::timerEvent(QTimerEvent *event) void DetectLoop::pcBreak()
{ {
if (event->timerId() == evId) if (!shared->postCmd.isEmpty())
{
detLog("---EVENT_BREAK---", shared);
if (!shared->curEvent.srcPaths.isEmpty())
{
shared->curEvent.evName = QDateTime::currentDateTime().toString("yyyyMMddmmss--") + QString::number(shared->maxScore);
shared->recList.append(shared->curEvent);
detLog("motion detected in " + QString::number(shared->curEvent.srcPaths.size()) + " file(s) in " + QString::number(shared->evMaxSecs) + " secs", shared);
detLog("all video clips queued for event generation under event name: " + shared->curEvent.evName, shared);
}
else
{
detLog("no motion detected in all files. none queued for event generation.", shared);
}
shared->curEvent.srcPaths.clear();
shared->curEvent.evName.clear();
shared->curEvent.thumbnail = QImage();
shared->maxScore = 0;
}
if ((event->timerId() == pcId) && (!shared->postCmd.isEmpty()))
{ {
detLog("---POST_BREAK---", shared); detLog("---POST_BREAK---", shared);
if (!shared->skipCmd) if (mod)
{
detLog("motion detected, skipping the post command.", shared);
}
else
{ {
detLog("no motion detected, running post command: " + shared->postCmd, shared); detLog("no motion detected, running post command: " + shared->postCmd, shared);
system(shared->postCmd.toUtf8().data()); system(shared->postCmd.toUtf8().data());
} }
else
{
shared->skipCmd = false;
detLog("motion detected, skipping the post command.", shared);
}
} }
}
void DetectLoop::playError(QMediaPlayer::Error error, const QString &errorString) mod = false;
{
Q_UNUSED(error)
detLog("err: media player error - " + errorString, shared);
}
void DetectLoop::playStateChanged(QMediaPlayer::PlaybackState newState)
{
if (newState == QMediaPlayer::PlayingState)
{
detLog("detection playback started.", shared);
}
else if (newState == QMediaPlayer::StoppedState)
{
detLog("detection playback stopped.", shared);
}
} }
bool DetectLoop::exec() bool DetectLoop::exec()
{ {
if (player->playbackState() != QMediaPlayer::PlayingState) auto curDT = QDateTime::currentDateTime();
auto images = backwardFacingFiles("img", ".bmp", curDT, 10);
if (images.size() < 3)
{ {
QFile fileIn("stream.m3u8"); detLog("wrn: didn't pick up enough image files from the image stream. number of files: " + QString::number(images.size()), shared);
QString tsPath; }
else
{
QProcess extComp;
QStringList args;
if (!fileIn.open(QFile::ReadOnly)) args << "compare";
args << "-metric" << "FUZZ";
args << images[0];
args << images[1];
args << "/dev/null";
extComp.start("magick", args);
extComp.waitForFinished();
QString output = extComp.readAllStandardError();
output = output.left(output.indexOf(' '));
detLog(extComp.program() + " " + args.join(" ") + " --result: " + output, shared);
if (output.toFloat() >= shared->imgThresh)
{ {
detLog("err: failed to open the stream hls file for reading. reason: " + fileIn.errorString(), shared); detLog("--threshold_breached: " + QString::number(shared->imgThresh), shared);
}
else if (fileIn.size() < 50)
{
detLog("the stream hls list is not big enough yet. waiting for more clips.", shared);
}
else if (!fileIn.seek(fileIn.size() - 50))
{
detLog("err: failed to seek to 'near end' of stream file. reason: " + fileIn.errorString(), shared);
}
else
{
QString line;
do evt_t event;
{
line = QString::fromUtf8(fileIn.readLine());
if (line.startsWith("live/")) event.timeStamp = curDT;
{ event.imgPath = images[2];
tsPath = line;
}
} while(!line.isEmpty()); shared->recList.append(event); mod = true;
}
if (tsPath.isEmpty())
{
detLog("wrn: didn't find the latest hls clip. previous failure? waiting 5secs.", shared);
thread()->sleep(5);
}
else if (prevTs == tsPath)
{
detLog("wrn: the lastest hls clip is the same as the previous clip. is the recording loop running? waiting 5secs.", shared);
thread()->sleep(5);
}
else
{
detLog("stream_clip: " + tsPath, shared);
prevTs = tsPath;
player->setSource(QUrl::fromLocalFile(tsPath));
frameAnalyzer->play(tsPath);
player->play();
} }
} }

View File

@ -16,7 +16,6 @@
#include "common.h" #include "common.h"
#include "logger.h" #include "logger.h"
#include "web.h" #include "web.h"
#include "mo_detect.h"
class Camera : public QObject class Camera : public QObject
{ {
@ -41,12 +40,17 @@ protected:
shared_t *shared; shared_t *shared;
QTimer *loopTimer; QTimer *loopTimer;
bool interruptible;
int heartBeat; int heartBeat;
protected slots: protected slots:
virtual void init(); virtual void init();
signals:
void loopSig();
private slots: private slots:
void loopSlot(); void loopSlot();
@ -64,13 +68,21 @@ class RecLoop : public Loop
private: private:
QProcess proc; QProcess *recProc;
QString curUrl; QProcess *imgProc;
QByteArray streamMD5; QStringList recList;
bool once; QStringList imgList;
QString curUrl;
bool baseListRdy;
bool once;
void updateCmd(); void updateCmd();
void reset(); void reset();
void startProc(const QString &desc, QProcess *proc);
private slots:
void init();
public: public:
@ -96,7 +108,7 @@ class EventLoop : public Loop
private: private:
bool wrOutVod(const evt_t &event); bool wrOutVod(const QString &name, const QStringList &vids);
public: public:
@ -111,21 +123,15 @@ class DetectLoop : public Loop
private: private:
int pcId; QTimer *pcTimer;
int evId; bool mod;
QString prevTs;
QMediaPlayer *player;
MoDetect *frameAnalyzer;
void resetTimers(); void resetTimers();
void resetDetect();
void timerEvent(QTimerEvent *event);
private slots: private slots:
void init(); void init();
void playError(QMediaPlayer::Error error, const QString &errorString); void pcBreak();
void playStateChanged(QMediaPlayer::PlaybackState newState);
public: public:

View File

@ -49,27 +49,6 @@ QString getParam(const QString &key, const QStringList &args)
return ret; return ret;
} }
QByteArray genMD5(const QByteArray &bytes)
{
QCryptographicHash hasher(QCryptographicHash::Md5);
hasher.addData(bytes); return hasher.result();
}
QByteArray genMD5(const QString &path)
{
QFile file(path);
if (file.open(QFile::ReadOnly))
{
return genMD5(file.readAll());
}
else
{
return genMD5(QByteArray("EMPTY"));
}
}
QStringList lsFilesInDir(const QString &path, const QString &ext) QStringList lsFilesInDir(const QString &path, const QString &ext)
{ {
QStringList filters; QStringList filters;
@ -95,6 +74,37 @@ QStringList lsDirsInDir(const QString &path)
return dirObj.entryList(); return dirObj.entryList();
} }
QStringList listFacingFiles(const QString &path, const QString &ext, const QDateTime &stamp, int secs, char dir)
{
QStringList ret;
for (auto i = 0; i < secs; ++i)
{
QString filePath;
if (dir == '-') filePath = path + "/" + stamp.addSecs(-i).toString(DATETIME_FMT) + ext;
if (dir == '+') filePath = path + "/" + stamp.addSecs(i).toString(DATETIME_FMT) + ext;
if (QFile::exists(filePath))
{
if (dir == '-') ret.insert(0, filePath);
if (dir == '+') ret.append(filePath);
}
}
return ret;
}
QStringList backwardFacingFiles(const QString &path, const QString &ext, const QDateTime &stamp, int secs)
{
return listFacingFiles(path, ext, stamp, secs, '-');
}
QStringList forwardFacingFiles(const QString &path, const QString &ext, const QDateTime &stamp, int secs)
{
return listFacingFiles(path, ext, stamp, secs, '+');
}
void enforceMaxEvents(shared_t *share) void enforceMaxEvents(shared_t *share)
{ {
auto names = lsFilesInDir("events", ".mp4"); auto names = lsFilesInDir("events", ".mp4");
@ -117,6 +127,18 @@ void enforceMaxEvents(shared_t *share)
} }
} }
void enforceMaxImages()
{
auto names = lsFilesInDir("img", ".bmp");
while (names.size() > MAX_IMAGES)
{
QFile::remove("img/" + names[0]);
names.removeFirst();
}
}
void rdLine(const QString &param, const QString &line, QString *value) void rdLine(const QString &param, const QString &line, QString *value)
{ {
if (line.startsWith(param)) if (line.startsWith(param))

View File

@ -23,31 +23,25 @@
#include <QFile> #include <QFile>
#include <QDateTime> #include <QDateTime>
#include <QThread> #include <QThread>
#include <QMediaPlayer>
#include <QVideoSink>
#include <QVideoFrame>
#include <QImage>
#include <QTimer> #include <QTimer>
#include <QStringList>
#include <opencv4/opencv2/opencv.hpp>
#include <opencv4/opencv2/videoio.hpp>
#include <opencv2/core/utils/logger.hpp>
using namespace cv;
using namespace std; using namespace std;
#define APP_VER "3.0.t5" #define APP_VER "3.0.t6"
#define APP_NAME "Motion Watch" #define APP_NAME "Motion Watch"
#define APP_BIN "mow" #define APP_BIN "mow"
#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"
#define DATETIME_FMT "yyyyMMddhhmmss"
#define STRFTIME_FMT "%Y%m%d%H%M%S"
#define MAX_IMAGES 40
struct evt_t struct evt_t
{ {
QString evName; QDateTime timeStamp;
QStringList srcPaths; QString imgPath;
QImage thumbnail;
}; };
struct shared_t struct shared_t
@ -82,16 +76,18 @@ struct shared_t
int evInd; int evInd;
}; };
QByteArray genMD5(const QString &path);
QByteArray genMD5(const QByteArray &bytes);
QString getParam(const QString &key, const QStringList &args); QString getParam(const QString &key, const QStringList &args);
QStringList lsFilesInDir(const QString &path, const QString &ext = QString()); QStringList lsFilesInDir(const QString &path, const QString &ext = QString());
QStringList lsDirsInDir(const QString &path); QStringList lsDirsInDir(const QString &path);
QStringList listFacingFiles(const QString &path, const QString &ext, const QDateTime &stamp, int secs, char dir);
QStringList backwardFacingFiles(const QString &path, const QString &ext, const QDateTime &stamp, int secs);
QStringList forwardFacingFiles(const QString &path, const QString &ext, const QDateTime &stamp, int secs);
bool rdConf(const QString &filePath, shared_t *share); bool rdConf(const QString &filePath, shared_t *share);
bool rdConf(shared_t *share); bool rdConf(shared_t *share);
void rdLine(const QString &param, const QString &line, QString *value); void rdLine(const QString &param, const QString &line, QString *value);
void rdLine(const QString &param, const QString &line, int *value); void rdLine(const QString &param, const QString &line, int *value);
void enforceMaxEvents(shared_t *share); void enforceMaxEvents(shared_t *share);
void enforceMaxImages();
class MultiInstance : public QObject class MultiInstance : public QObject
{ {

View File

@ -1,83 +0,0 @@
// 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 "mo_detect.h"
MoDetect::MoDetect(shared_t *share, QObject *parent) : QVideoSink(parent)
{
shared = share;
connect(this, &MoDetect::videoFrameChanged, this, &MoDetect::newFrame);
}
void MoDetect::stop()
{
baseImg = QImage(); gap = 0; shared->maxScore = 0;
}
void MoDetect::play(const QString &path)
{
filePath = path;
}
void MoDetect::newFrame()
{
if (!baseImg.isNull())
{
baseImg = videoFrame().toImage().convertToFormat(QImage::Format_Grayscale8);
}
else if (shared->frameGap > gap)
{
gap++;
}
else
{
gap = 0;
auto nextImg = videoFrame().toImage();
auto nextImgGray = nextImg.convertToFormat(QImage::Format_Grayscale8);
auto score = 0;
if (diff(baseImg, nextImgGray, &score))
{
shared->skipCmd = true;
if (!shared->curEvent.srcPaths.contains(filePath))
{
shared->curEvent.srcPaths.append(filePath);
if (shared->maxScore <= score)
{
shared->maxScore = score;
shared->curEvent.thumbnail = nextImg;
}
}
}
}
}
bool MoDetect::diff(QImage &base, QImage &next, int *score)
{
Mat baseMat = Mat(base.height(), base.width(), CV_8U, base.bits(), base.bytesPerLine());
Mat nextMat = Mat(next.height(), next.width(), CV_8U, next.bits(), next.bytesPerLine());
Mat diffMat;
absdiff(baseMat, nextMat, diffMat);
threshold(diffMat, diffMat, shared->pixThresh, 255, THRESH_BINARY);
*score = countNonZero(diffMat);
detLog("diff_score: " + QString::number(*score) + " thresh: " + QString::number(shared->imgThresh), shared);
return *score >= shared->imgThresh;
}

View File

@ -1,44 +0,0 @@
#ifndef MO_DETECT_H
#define MO_DETECT_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"
#include "logger.h"
class MoDetect : public QVideoSink
{
Q_OBJECT
private:
QImage baseImg;
QString filePath;
shared_t *shared;
int gap;
bool diff(QImage &base, QImage &next, int *score);
private slots:
void newFrame();
public:
void play(const QString &path);
void stop();
explicit MoDetect(shared_t *share, QObject *parent = nullptr);
};
#endif // MO_DETECT_H