Compare commits

..

No commits in common. "528b4105f75e5ab314f7528ab4ea76251c6bfc96" and "01a11741c780113becbea2ce5cec8a27bbe5a1bc" have entirely different histories.

15 changed files with 392 additions and 828 deletions

4
.gitignore vendored
View File

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

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++20 -pthread")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++17 -pthread")
include_directories( ${OpenCV_INCLUDE_DIRS} )
add_executable( mow src/main.cpp src/common.cpp src/mo_detect.cpp src/web.cpp src/logger.cpp )
add_executable( mow src/main.cpp src/common.cpp src/mo_detect.cpp )
target_link_libraries( mow ${OpenCV_LIBS} )

131
README.md
View File

@ -13,20 +13,15 @@ of this app can be used to operate multiple cameras.
Usage: mow <argument>
-h : display usage information about this application.
-c : path to the config file(s).
-c : path to the config file.
-v : display the current version.
note: multiple -c config files can be passed, reading from left
to right. any conflicting values between the files will
have the latest value from the latest file overwrite the
the earliest.
```
### Config File ###
The config file is a simple text file that contain parameters that dictate the
behavior of the application. Below is an example of a config file with all
parameters supported and descriptions of each.
parameters supported and descriptions of each parameter.
```
# Motion Watch config file
#
@ -38,109 +33,77 @@ 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.
#
web_root = /var/www/html
output_dir = /path/to/footage/directory
# this is the output directory that will be used to store recorded footage
# from the cameras as well as the web interface for the application.
# warning: this will overwrite any existing index.html files so be sure
# to choose a directory that doesn't have an existing website.
# from the camera. the file naming convention uses the current date for
# playlist files which points to hidden video clips taken from the camera.
#
buff_dir = /tmp
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 web_root; if no motion is detected, they are deleted.
# it is highly recommend to use a ramdisk tempfs for this since this
# directory is used for large amounts of writes.
# 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.
#
cam_name = cam-1
# this is the optional camera name parameter to identify the camera. this
# name will be used to form the directory structure in the web_root as
# well as buff_dir. if not defined, the name of the config file will be
# used.
consec_threshold = 512
# motion is detected by comparing each frame in the camera feed for
# differences in the pixels. this value determine how many consecutive
# pixels need to different or how large the suspect object in motion
# needs to be.
#
pix_thresh = 150
# this value tells the application how far different the pixels need to be
# before the pixels are actually considered different. think of this as
# pixel diff sensitivity, the higher the value the lesser the sensitivity.
block_threshold = 1024
# this value tells the application how many "lines" of pixels need to
# exceed consec_threshold before being considered motion.
#
frame_gap = 20
# this value is used to tell the application how far in between frames to
# check the pixel diffs for motion. the lower the value, the more frames
# will be checked, however with that comes higher cpu usage.
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.
#
img_thresh = 80000
# this indicates how many pixels need to be different in between frame_gap
# before it is considered motion. any video clips found with frames
# exceeding this value will be moved from buff_dir to web_root.
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.
#
clip_len = 20
# this parameter indicate the amount of seconds to record in each video
# clip from the camera that will be stored and then processed in buff_dir.
#
num_of_clips = 3
# this will tell the application how many video clips should be recorded
# to buff_dir from the camera before the recording loop pauses to do some
# house keeping. by house keeping, it will wait until all motion detection
# threads are finished, reload the config file and then call the post_cmd
# if no motion was detected in any of the video clips.
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 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.
#
post_cmd = move_the_ptz_camera.py
# this an optional command to run after num_of_clips is met. one great use
# for this 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.
# this an optional command to run after the internal timer duration has
# elapsed. one great use for this 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.
#
max_days = 15
# this defines the maximum amount of days worth of video clips that is
# allowed to be stored in the web_root. whenever this limit is met, the
# oldest day and all of it's associated video clips will be deleted.
#
max_clips = 30
# this is the maximum amount of video clips that is allowed to be stored
# in web_root per day. whenever this limit is met, the oldest clip is
# deleted.
#
max_log_size = 50000
# this is the maximum byte size of all log files that can be stored in
# web_root. whenever this limit is met, the log file will be deleted and
# then eventually recreated blank.
# allowed to be stored in the output_dir. whenever this limit is met,
# the oldest day is deleted.
#
vid_container = mp4
# this is the video file format to use for recording footage from the
# this is the video file format to use from recording footage from the
# camera. the format support depends entirely on the under laying ffmpeg
# installation.
#
vid_codec = copy
# this is the video codec to use when pulling footage from the camera
# via ffmpeg. the default is "copy" meaning it will just match the codec
# from the camera itself without trans-coding.
#
web_text = #dee5ee
# this can be used to customize the color of the text in the web
# interface. it can be defined as any color understood by html5 standard.
#
web_bg = #485564
# this can be used to customize the background color of the web
# interface. just like web_text, it also follows the html5 standard.
#
web_font = courier
# this will customize the text font family to use in the web interface.
# it is recommended to use mono-spaced font because this is also used to
# display logs and logs are best displayed in mono-spaced font.
```
### Setup/Build/Install ###
This application is currently only compatible with a Linux based operating
systems that are capable of installing opencv. The following 3 scripts make
building and then installing convenient.
systems that are capable of building and installing the opencv API from source.
The following 3 scripts make this convenient by downloading, compiling and then
installing the opencv API for you directly from opencv's git repository. This
also makes sure FFMPEG and all of it's dependencies are installed because this
application needs it to work properly.
```
note 1: be sure to run setup.sh and install.sh as root (or use sudo).
note 2: if building from scratch the following scripts will need to
be run in this order - setup.sh -> build.sh -> install.sh.
```
```
sh ./setup.sh <--- only need to run this once if compiling for the first
sh ./build.sh time or if upgrading from the ground up.
sh ./install.sh
```
```
note 1: be sure to run setup.sh and install.sh as root (or use sudo).
note 2: if building from scratch the following scripts will need to
be run in this order - setup.sh -> build.sh -> install.sh.
```

View File

@ -1,5 +1,7 @@
#!/bin/sh
mkdir -p ./.build-mow
cd ./.build-mow
cmake ..
make -j4

View File

@ -1,13 +1,3 @@
#!/bin/sh
if [ ! -d "/opt/mow" ]; then
mkdir /opt/mow
fi
cp ./.build-mow/mow /opt/mow/bin
printf "#!/bin/sh\n" > /opt/mow/run
printf "export OPENCV_LOG_LEVEL=DEBUG\n" >> /opt/mow/run
printf "export OPENCV_VIDEOIO_DEBUG=1\n" >> /opt/mow/run
printf "/opt/mow/bin \$1 \$2 \$3\n" >> /opt/mow/run
chmod +x /opt/mow/run
chmod +x /opt/mow/bin
rm /usr/bin/mow
ln -s /opt/mow/run /usr/bin/mow
cp ./.build-mow/mow /usr/bin/mow

View File

@ -1,28 +1,19 @@
#!/bin/sh
apt update -y
apt install -y pkg-config
apt install -y cmake
apt install -y make
apt install -y g++
apt install -y wget
apt install -y unzip
apt install -y git
apt install -y ffmpeg
apt install -y gstreamer1.0*
apt install -y libavcodec-dev
apt install -y libavformat-dev
apt install -y libavutil-dev
apt install -y libswscale-dev
apt install -y libgstreamer1.0-dev
apt install -y x264
apt install -y libx264-dev
apt install -y libopencv-dev
apt install -y apache2
add-apt-repository -y ppa:ubuntu-toolchain-r/test
apt update -y
apt install -y gcc-10
apt install -y gcc-10-base
apt install -y gcc-10-doc
apt install -y g++-10
apt install -y libstdc++-10-dev
apt install -y libstdc++-10-doc
apt update
apt install -y cmake g++ wget unzip git ffmpeg libavcodec-dev libavformat-dev libavutil-dev libswscale-dev
cd ./src
if [ -d "./opencv" ]
then
cd ./opencv
git pull origin
cd ..
else
git clone https://github.com/opencv/opencv.git
fi
cd ..
mkdir -p ./.build-opencv
cd ./.build-opencv
cmake ../src/opencv
make -j4
make install

View File

@ -52,24 +52,48 @@ 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();
}
}
vector<string> lsFilesInDir(const string &path, const string &ext)
{
DIR *dir;
struct dirent *ent;
vector<string> names;
if (exists(path))
if ((dir = opendir(path.c_str())) != NULL)
{
for (auto &entry : directory_iterator(path))
while ((ent = readdir(dir)) != NULL)
{
if (entry.is_regular_file())
{
auto name = entry.path().filename().string();
auto name = string(ent->d_name);
if (ext.empty() || name.ends_with(ext))
if ((name.size() >= 4) && (ent->d_type & DT_REG))
{
if (name.substr(name.size() - 4) == ext)
{
names.push_back(name);
}
}
}
closedir(dir);
}
sort(names.begin(), names.end());
@ -77,53 +101,18 @@ vector<string> lsFilesInDir(const string &path, const string &ext)
return names;
}
vector<string> lsDirsInDir(const string &path)
void enforceMaxDays(shared_t *share)
{
vector<string> names;
auto names = lsFilesInDir(share->outDir, ".m3u");
if (exists(path))
while (names.size() > share->maxDays)
{
for (auto &entry : directory_iterator(path))
{
if (entry.is_directory())
{
names.push_back(entry.path().filename().string());
}
}
}
auto name = names[0];
auto plsFile = cleanDir(share->outDir) + "/" + name;
auto vidFold = cleanDir(share->outDir) + "/." + name.substr(0, name.size() - 4);
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 = lsFilesInDir(dirPath, "." + share->vidExt);
while (names.size() > share->maxClips)
{
// removes the video file extension.
auto nameOnly = names[0].substr(0, names[0].size() - (share->vidExt.size() + 1));
auto imgFile = cleanDir(dirPath) + "/" + nameOnly + ".jpg";
auto webFile = cleanDir(dirPath) + "/" + nameOnly + ".html";
remove(cleanDir(dirPath) + "/" + names[0]);
if (exists(imgFile)) remove(imgFile);
if (exists(webFile)) remove(webFile);
remove(plsFile.c_str());
remove_all(vidFold.c_str());
names.erase(names.begin());
}
@ -151,11 +140,22 @@ 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,186 +164,154 @@ 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;
}
}
bool rdConf(const string &filePath, shared_t *share)
bool rdConf(shared_t *share)
{
ifstream varFile(filePath.c_str());
ifstream varFile(share->conf.c_str());
if (!varFile.is_open())
{
share->retCode = ENOENT;
cout << "wrn: config file: " << filePath << " does not exists or lack read permissions." << endl;
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->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;
do
{
getline(varFile, line);
if (line.rfind("#", 0) != 0)
{
rdLine("cam_name = ", line, &share->camName);
rdLine("recording_stream = ", line, &share->recordUrl);
rdLine("web_root = ", line, &share->webRoot);
rdLine("web_text = ", line, &share->webTxt);
rdLine("web_bg = ", line, &share->webBg);
rdLine("web_font = ", line, &share->webFont);
rdLine("output_dir = ", line, &share->outDir);
rdLine("post_cmd = ", line, &share->postCmd);
rdLine("clip_len = ", line, &share->clipLen);
rdLine("num_of_clips = ", line, &share->numOfClips);
rdLine("consec_threshold = ", line, &share->consecThresh);
rdLine("duration = ", line, &share->secs);
rdLine("buff_dir = ", line, &share->buffDir);
rdLine("frame_gap = ", line, &share->frameGap);
rdLine("pix_thresh = ", line, &share->pixThresh);
rdLine("img_thresh = ", line, &share->imgThresh);
rdLine("block_x = ", line, &share->blockX);
rdLine("block_y = ", line, &share->blockY);
rdLine("block_threshold = ", line, &share->blockThresh);
rdLine("max_days = ", line, &share->maxDays);
rdLine("max_clips = ", line, &share->maxClips);
rdLine("max_log_size = ", line, &share->maxLogSize);
rdLine("vid_container = ", line, &share->vidExt);
rdLine("vid_codec = ", line, &share->vidCodec);
}
} while(!line.empty());
}
return share->retCode == 0;
}
bool rdConf(shared_t *share)
{
share->recordUrl.clear();
share->postCmd.clear();
share->buffDir.clear();
share->camName.clear();
share->recLogPath.clear();
share->detLogPath.clear();
share->recLogFile.close();
share->detLogFile.close();
share->retCode = 0;
share->frameGap = 20;
share->pixThresh = 150;
share->imgThresh = 80000;
share->clipLen = 20;
share->numOfClips = 3;
share->maxDays = 15;
share->maxClips = 90;
share->maxLogSize = 50000;
share->webRoot = "/var/www/html";
share->buffDir = "/tmp";
share->vidExt = "mp4";
share->vidCodec = "copy";
share->skipCmd = false;
share->webBg = "#485564";
share->webTxt = "#dee5ee";
share->webFont = "courier";
auto ret = false;
for (auto &&confPath: share->conf)
{
if (rdConf(confPath, share)) ret = true;
}
if (ret)
{
if (share->camName.empty())
{
share->camName = path(share->conf.back()).filename();
}
share->outDir = cleanDir(share->webRoot) + "/" + share->camName;
share->buffDir = cleanDir(share->buffDir) + "/" + share->camName;
share->recLogPath = share->outDir + "/rec_log_lines.html";
share->detLogPath = share->outDir + "/det_log_lines.html";
// 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;
if (share->init)
{
if (exists(share->buffDir))
{
remove_all(share->buffDir);
}
remove_all(share->buffDir.c_str());
share->init = false;
}
new thread(enforceMaxDays, share);
createDirTree(cleanDir(share->buffDir));
createDirTree(share->outDir);
system(string("touch " + cleanDir(share->buffDir) + "/stat").c_str());
share->retCode = 0;
}
varFile.close();
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
{
cerr << "err: none of the expected config files could be read." << endl;
file.open(lisOut.c_str());
}
return ret;
file << m3uOut << endl;
file.close();
}
string parseForParam(const string &arg, int argc, char** argv, bool argOnly, int &offs)
string parseForParam(const string &arg, int argc, char** argv, bool argOnly)
{
auto ret = string();
for (; offs < argc; ++offs)
for (int i = 0; i < argc; ++i)
{
auto argInParams = string(argv[offs]);
auto argInParams = string(argv[i]);
if (arg.compare(argInParams) == 0)
{
if (!argOnly)
{
offs++;
// check ahead, make sure offs + 1 won't cause out-of-range exception
if (offs <= (argc - 1))
// check ahead, make sure i + 1 won't cause out-of-range exception
if ((i + 1) <= (argc - 1))
{
ret = string(argv[offs]);
return string(argv[i + 1]);
}
}
else
{
ret = string("true");
return string("true");
}
}
}
return ret;
return string();
}
string parseForParam(const string &arg, int argc, char** argv, bool argOnly)
void statOut(shared_t *share)
{
auto notUsed = 0;
system(string("touch " + cleanDir(share->buffDir) + "/stat").c_str());
return parseForParam(arg, argc, argv, argOnly, notUsed);
}
vector<string> parseForList(const string &arg, int argc, char** argv)
{
auto offs = 0;
auto ret = vector<string>();
string param;
do
{
param = parseForParam(arg, argc, argv, false, offs);
if (!param.empty())
{
ret.push_back(param);
}
}
while (!param.empty());
return ret;
}
void waitForDetThreads(shared_t *share)
{
for (auto &&thr : share->detThreads)
{
thr.join();
}
share->detThreads.clear();
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

@ -22,6 +22,7 @@
#include <errno.h>
#include <vector>
#include <thread>
#include <dirent.h>
#include <filesystem>
#include <mutex>
#include <sys/types.h>
@ -35,57 +36,46 @@ using namespace cv;
using namespace std;
using namespace std::filesystem;
#define APP_VER "1.6"
#define APP_NAME "Motion Watch"
#define BUF_SZ 10
#define APP_VER "1.5"
struct shared_t
{
vector<thread> detThreads;
vector<string> conf;
ofstream recLogFile;
ofstream detLogFile;
string recLogPath;
string detLogPath;
string stat;
string recordUrl;
string outDir;
string postCmd;
string conf;
string buffDir;
string vidExt;
string vidCodec;
string camName;
string webBg;
string webTxt;
string webFont;
string webRoot;
bool init;
bool recLoopWait;
bool skipCmd;
int clipLen;
int frameGap;
int pixThresh;
int imgThresh;
int numOfClips;
int consecThresh;
int secs;
int blockThresh;
int blockX;
int blockY;
int maxDays;
int maxClips;
int maxLogSize;
int retCode;
};
string genDstFile(const string &dirOut, const char *fmt, const string &ext);
string genTimeStr(const char *fmt);
string cleanDir(const string &path);
string parseForParam(const string &arg, int argc, char** argv, bool argOnly, int &offs);
string parseForParam(const string &arg, int argc, char** argv, bool argOnly);
bool createDir(const string &dir);
bool createDirTree(const string &full_path);
void enforceMaxDays(const string &dirPath, shared_t *share);
void enforceMaxClips(const string &dirPath, shared_t *share);
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 rdLine(const string &param, const string &line, string *value);
void rdLine(const string &param, const string &line, int *value);
void statOut(shared_t *share);
void waitForDetThreads(shared_t *share);
bool rdConf(shared_t *share);
vector<string> parseForList(const string &arg, int argc, char** argv);
vector<string> lsFilesInDir(const string &path, const string &ext = string());
vector<string> lsDirsInDir(const string &path);
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);
#endif // COMMON_H

View File

@ -1,108 +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 "logger.h"
void recLog(const string &line, shared_t *share)
{
share->recLogFile << genTimeStr("[%Y-%m-%d-%H-%M-%S] ") << line << "<br>" << endl;
}
void detLog(const string &line, shared_t *share)
{
share->detLogFile << genTimeStr("[%Y-%m-%d-%H-%M-%S] ") << line << "<br>" << endl;
}
void enforceMaxLogSize(const string &filePath, shared_t *share)
{
if (exists(filePath))
{
if (file_size(filePath) >= share->maxLogSize)
{
remove(filePath);
}
}
}
void initLogFile(const string &filePath, ofstream &fileObj)
{
if (!fileObj.is_open())
{
if (!exists(filePath))
{
system(string("touch " + filePath).c_str());
}
fileObj.open(filePath.c_str(), ofstream::app | ofstream::out);
}
}
void initLogFrontPage(const string &filePath, const string &logLinesFile)
{
string htmlText = "<!DOCTYPE html>\n";
htmlText += "<html>\n";
htmlText += "<script>\n";
htmlText += "function includeHTML() {\n";
htmlText += " var z, i, elmnt, file, xhttp;\n";
htmlText += " z = document.getElementsByTagName(\"*\");\n";
htmlText += " for (i = 0; i < z.length; i++) {\n";
htmlText += " elmnt = z[i];\n";
htmlText += " file = elmnt.getAttribute(\"include-html\");\n";
htmlText += " if (file) {\n";
htmlText += " xhttp = new XMLHttpRequest();\n";
htmlText += " xhttp.onreadystatechange = function() {\n";
htmlText += " if (this.readyState == 4) {\n";
htmlText += " if (this.status == 200) {elmnt.innerHTML = this.responseText;}\n";
htmlText += " if (this.status == 404) {elmnt.innerHTML = \"Page not found.\";}\n";
htmlText += " elmnt.removeAttribute(\"include-html\");\n";
htmlText += " includeHTML();\n";
htmlText += " }\n";
htmlText += " }\n";
htmlText += " xhttp.open(\"GET\", file, true);\n";
htmlText += " xhttp.send();\n";
htmlText += " return;\n";
htmlText += " }\n";
htmlText += " }\n";
htmlText += "};\n";
htmlText += "</script>\n";
htmlText += "<head>\n";
htmlText += "<meta http-equiv=\"Cache-Control\" content=\"no-cache, no-store, must-revalidate\" />\n";
htmlText += "<meta http-equiv=\"Pragma\" content=\"no-cache\" />\n";
htmlText += "<meta http-equiv=\"Expires\" content=\"0\" />\n";
htmlText += "<link rel='stylesheet' href='/theme.css'>\n";
htmlText += "</head>\n";
htmlText += "<body>\n";
htmlText += "<p>\n";
htmlText += "<div include-html='" + logLinesFile + "'></div>\n";
htmlText += "<script>\n";
htmlText += "includeHTML();\n";
htmlText += "</script>\n";
htmlText += "</p>\n";
htmlText += "</body>\n";
htmlText += "</html>\n";
ofstream outFile(filePath);
outFile << htmlText;
outFile.close();
}
void initLogFrontPages(shared_t *share)
{
auto recLogFilePath = share->outDir + "/recording_log.html";
auto detLogFilePath = share->outDir + "/detection_log.html";
initLogFrontPage(recLogFilePath, path(share->recLogPath).filename().string());
initLogFrontPage(detLogFilePath, path(share->detLogPath).filename().string());
}

View File

@ -1,24 +0,0 @@
#ifndef lOGGER_H
#define lOGGER_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 recLog(const string &line, shared_t *share);
void detLog(const string &line, shared_t *share);
void enforceMaxLogSize(const string &filePath, shared_t *share);
void initLogFile(const string &filePath, ofstream &fileObj);
void initLogFrontPages(shared_t *share);
#endif // lOGGER_H

View File

@ -11,156 +11,99 @@
// GNU General Public License for more details.
#include "mo_detect.h"
#include "logger.h"
void detectMoInFile(const string &bufPath, shared_t *share)
void detectLoop(shared_t *share)
{
detLog("detect_mo_in_file() -- start", share);
vector<string> bufFiles;
Mat thumbNail;
while (!bufFiles.empty() || !share->recLoopWait)
{
bufFiles = lsFilesInDir(share->buffDir, "." + share->vidExt);
if (moDetect(bufPath, thumbNail, share))
if ((bufFiles.size() >= 2) || (share->recLoopWait && !bufFiles.empty()))
{
auto fullPath = cleanDir(share->buffDir) + "/" + bufFiles[0];
share->stat.clear();
if (moDetect(fullPath, share))
{
share->skipCmd = true;
wrOut(bufPath, thumbNail, share);
wrOut(fullPath, share);
}
else if (exists(bufPath))
else
{
remove(bufPath);
remove(fullPath.c_str());
}
detLog("detect_mo_in_file() -- finished", share);
statOut(share);
}
else
{
sleep(1);
}
}
}
void recLoop(shared_t *share)
{
while (rdConf(share))
{
recLog("rec_loop() -- start", share);
createDirTree(share->buffDir);
enforceMaxLogSize(share->recLogPath, share);
enforceMaxLogSize(share->detLogPath, share);
auto bufPath = cleanDir(share->buffDir) + "/%03d." + share->vidExt;
auto secs = to_string(share->secs);
auto limSecs = to_string(share->secs + 3);
auto cmd = "timeout -k 1 " + limSecs + " ffmpeg -hide_banner -i " + share->recordUrl + " -y -vcodec copy -map 0 -segment_time 00:00:10 -f segment -t " + secs + " " + bufPath;
initLogFile(share->recLogPath, share->recLogFile);
initLogFile(share->detLogPath, share->detLogFile);
thread th2(detectLoop, share);
initLogFrontPages(share);
system(cmd.c_str());
if (!exists("/tmp/mow-lock"))
{
system("touch /tmp/mow-lock");
share->recLoopWait = true;
genCSS(share);
genHTMLul(share->webRoot, string(APP_NAME) + " " + string(APP_VER), share);
remove("/tmp/mow-lock");
recLog("webroot page updated: " + cleanDir(share->webRoot) + "/index.html", share);
}
else
{
recLog("skipping update of the webroot page, it is busy.", share);
}
genHTMLul(share->outDir, share->camName, share);
recLog("camera specific webroot page updated: " + share->outDir + "/index.html", share);
for (auto i = 0; i < share->numOfClips; ++i)
{
auto bufPath = cleanDir(share->buffDir) + "/" + to_string(i) + "." + share->vidExt;
auto cmd = "timeout -k 1 " + to_string(share->clipLen + 2) + " ";
cmd += "ffmpeg -hide_banner -i " + share->recordUrl + " -y -vcodec " + share->vidCodec + " -movflags faststart -t " + to_string(share->clipLen) + " " + bufPath;
recLog("ffmpeg_run: " + cmd, share);
auto retCode = system(cmd.c_str());
recLog("ffmpeg_retcode: " + to_string(retCode), share);
if (retCode == 0)
{
recLog("detect_mo_in_file() -- started in a seperate thread.", share);
share->detThreads.push_back(thread(detectMoInFile, bufPath, share));
}
else
{
recLog("ffmpeg returned non zero, indicating failure. please check stderr output.", share);
if (exists(bufPath))
{
remove(bufPath);
}
sleep(share->clipLen);
}
}
waitForDetThreads(share);
th2.join();
if (!share->skipCmd)
{
recLog("no motion detected", share);
if (share->postCmd.empty())
{
recLog("post command not defined, skipping.", share);
}
else
{
recLog("running post command: " + share->postCmd, share);
system(share->postCmd.c_str());
}
}
else
{
recLog("motion detected, skipping the post command.", share);
}
recLog("rec_loop() -- finished", share);
if (share->retCode != 0)
{
break;
}
}
}
int main(int argc, char** argv)
{
struct shared_t sharedRes;
sharedRes.conf = parseForList("-c", argc, argv);
sharedRes.conf = parseForParam("-c", argc, argv, false);
if (parseForParam("-h", argc, argv, true) == "true")
{
cout << "Motion Watch " << APP_VER << endl << endl;
cout << "Usage: mow <argument>" << endl << endl;
cout << "-h : display usage information about this application." << endl;
cout << "-c : path to a config file." << endl;
cout << "-v : display the current version." << endl << endl;
cout << "note: multiple -c config files can be passed, reading from left" << endl;
cout << " to right. any conflicting values between the files will" << endl;
cout << " have the latest value from the latest file overwrite the" << endl;
cout << " the earliest." << endl;
cout << "-c : path to the config file." << endl;
cout << "-v : display the current version." << endl;
}
else if (parseForParam("-v", argc, argv, true) == "true")
if (parseForParam("-v", argc, argv, true) == "true")
{
cout << APP_VER << endl;
}
else if (sharedRes.conf.empty())
{
cerr << "err: no config file(s) were given in -c" << endl;
cerr << "err: A config file was not given in -c" << endl;
}
else
{
sharedRes.retCode = 0;
sharedRes.recLoopWait = false;
sharedRes.skipCmd = false;
sharedRes.init = true;
recLoop(&sharedRes);
thread th1(recLoop, &sharedRes);
th1.join();
return sharedRes.retCode;
}

View File

@ -12,94 +12,102 @@
#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)
{
auto ret = false;
vector<thread> threads;
vector<sec_t> results;
mutex secMutex;
detLog("img_diff() -- start()", share);
auto id = 0;
if (prev.empty()) detLog("prev_frame is empty -- Borken frame from the camera assumed.", share);
if (next.empty()) detLog("next_frame is empty -- EOF assumed.", share);
if (!prev.empty() && !next.empty())
for (auto x = 0; x < prev.cols; x += share->blockX)
{
Mat diff;
absdiff(prev, next, diff);
threshold(diff, diff, share->pixThresh, 255, THRESH_BINARY);
auto diffScore = countNonZero(diff);
detLog("diff_score: " + to_string(diffScore), share);
ret = diffScore >= share->imgThresh;
// 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));
}
}
detLog("img_diff() -- finished()", share);
for (auto &&thr : threads)
{
// wait for all of the threads to finish.
thr.join();
}
return ret;
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;
}
Mat frameFF(VideoCapture *cap, int gap)
bool moDetect(const string &buffFile, shared_t *share)
{
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)
{
detLog("wr_out() -- start()", share);
detLog("buff_file: " + buffFile, 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");
detLog("write_out_vid: " + vidOut, share);
detLog("write_out_img: " + imgOut, share);
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, share);
genHTMLul(share->outDir, share->camName, share);
detLog("wr_out() -- finished()", share);
}
bool moDetect(const string &buffFile, Mat &vidThumb, shared_t *share)
{
detLog("mo_detect() -- start()", share);
detLog("buff_file: " + buffFile, share);
auto mod = false;
VideoCapture capture(buffFile.c_str(), CAP_FFMPEG);
@ -108,35 +116,20 @@ bool moDetect(const string &buffFile, Mat &vidThumb, shared_t *share)
{
Mat prev;
Mat next;
auto frameCount = capture.get(CAP_PROP_FRAME_COUNT);
auto frameGaps = frameCount / share->frameGap;
detLog("frame_count: " + to_string(frameCount), share);
detLog("frame_gaps: " + to_string(frameGaps), share);
for (auto i = 0; i < frameGaps; i++)
while (capPair(prev, next, capture, share))
{
if (prev.empty()) prev = frameFF(&capture, 1);
else prev = next.clone();
next = frameFF(&capture, share->frameGap);
if (imgDiff(prev, next, share))
if (imgDiff(toGray(prev), toGray(next), share))
{
resize(next, vidThumb, Size(720, 480), INTER_LINEAR);
mod = true; break;
}
}
}
else
{
detLog("failed to open the buff file for reading. check permissions and/or opencv's video-io support (gstreamer/ffmpeg).", share);
cerr << "err: Could not open buff file: " << buffFile << " for reading. check formatting/permissions." << endl;
cerr << " Also check if opencv was compiled with FFMPEG encoding enabled." << endl;
}
capture.release();
detLog("mo_detect() -- finished()", share);
return mod;
}

View File

@ -14,12 +14,20 @@
// GNU General Public License for more details.
#include "common.h"
#include "web.h"
#include "logger.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, Mat &vidThumb, shared_t *share);
void wrOut(const string &buffFile, const Mat &vidThumb, shared_t *share);
Mat frameFF(VideoCapture *cap, int gap);
bool moDetect(const string &buffFile, shared_t *share);
#endif // MO_DETECT_H

View File

@ -1,134 +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 "web.h"
void genHTMLul(const string &outputDir, const string &title, shared_t *share)
{
vector<string> logNames;
vector<string> regNames = lsFilesInDir(outputDir);
vector<string> dirNames = lsDirsInDir(outputDir);
string htmlText = "<!DOCTYPE html>\n";
htmlText += "<html>\n";
htmlText += "<head>\n";
htmlText += "<meta http-equiv=\"Cache-Control\" content=\"no-cache, no-store, must-revalidate\" />\n";
htmlText += "<meta http-equiv=\"Pragma\" content=\"no-cache\" />\n";
htmlText += "<meta http-equiv=\"Expires\" content=\"0\" />\n";
htmlText += "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n";
htmlText += "<link rel='stylesheet' href='/theme.css'>\n";
htmlText += "</head>\n";
htmlText += "<body>\n";
htmlText += "<h3>" + title + "</h3>\n";
if (!dirNames.empty())
{
htmlText += "<ul>\n";
for (auto &&dirName : dirNames)
{
htmlText += " <li><a href='" + dirName + "/index.html'>" + dirName + "</a></li>\n";
}
htmlText += "</ul>\n";
}
for (auto &&regName : regNames)
{
if (regName.ends_with("_log.html"))
{
logNames.push_back(regName);
}
else if (regName.ends_with(".html") &&
!regName.ends_with("index.html") &&
!regName.ends_with("rec_log_lines.html") &&
!regName.ends_with("det_log_lines.html"))
{
// regName.substr(0, regName.size() - 5) removes .html
auto name = regName.substr(0, regName.size() - 5);
htmlText += "<a href='" + regName + "'><img src='" + name + ".jpg" + "' style='width:25%;height:25%;'</a>\n";
}
}
if (!logNames.empty())
{
htmlText += "<h4>Logs</h4>\n";
htmlText += "<ul>\n";
for (auto &&name : logNames)
{
// name.substr(0, name.size() - 9) removes _log.html
htmlText += " <li><a href='" + name + "'>" + name.substr(0, name.size() - 9) + "</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 vidName = path(outputVid).filename().string();
auto filePath = path(outputVid).parent_path().string();
auto fileName = vidName.substr(0, vidName.size() - (share->vidExt.size() + 1));
string htmlText = "<!DOCTYPE html>\n";
htmlText += "<html>\n";
htmlText += "<head>\n";
htmlText += "<meta http-equiv=\"Cache-Control\" content=\"no-cache, no-store, must-revalidate\" />\n";
htmlText += "<meta http-equiv=\"Pragma\" content=\"no-cache\" />\n";
htmlText += "<meta http-equiv=\"Expires\" content=\"0\" />\n";
htmlText += "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n";
htmlText += "<link rel='stylesheet' href='/theme.css'>\n";
htmlText += "</head>\n";
htmlText += "<body>\n";
htmlText += "<video width=100% height=100% controls autoplay>\n";
htmlText += " <source src='" + vidName + "' type='video/" + share->vidExt + "'>\n";
htmlText += "</video>\n";
htmlText += "</body>\n";
htmlText += "</html>";
ofstream file(string(filePath + "/" + fileName + ".html").c_str());
file << htmlText << endl;
file.close();
}
void genCSS(shared_t *share)
{
string cssText = "body {\n";
cssText += " background-color: " + share->webBg + ";\n";
cssText += " color: " + share->webTxt + ";\n";
cssText += " font-family: " + share->webFont + ";\n";
cssText += "}\n";
cssText += "a {\n";
cssText += " color: " + share->webTxt + ";\n";
cssText += "}\n";
ofstream file(string(cleanDir(share->webRoot) + "/theme.css").c_str());
file << cssText << endl;
file.close();
}

View File

@ -1,22 +0,0 @@
#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, shared_t *share);
void genHTMLvid(const string &outputVid, shared_t *share);
void genCSS(shared_t *share);
#endif // WEB_H