Compare commits

..

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

15 changed files with 392 additions and 828 deletions

6
.gitignore vendored
View File

@ -46,7 +46,7 @@ target_wrapper.*
# QtCreator CMake # QtCreator CMake
CMakeLists.txt.user* CMakeLists.txt.user*
# QtCreator 4.8< compilation database # QtCreator 4.8< compilation database
compile_commands.json compile_commands.json
# QtCreator local machine specific files for imported projects # QtCreator local machine specific files for imported projects
@ -57,3 +57,7 @@ compile_commands.json
# Build folders # Build folders
/.build-mow /.build-mow
/.build-opencv
# Opencv src folder
/src/opencv

View File

@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 2.8.12) cmake_minimum_required(VERSION 2.8.12)
project( MotionWatch ) project( MotionWatch )
find_package( OpenCV REQUIRED ) 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} ) 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} ) 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> Usage: mow <argument>
-h : display usage information about this application. -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. -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 ### ### Config File ###
The config file is a simple text file that contain parameters that dictate the 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 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 # 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 # this is the url to the main stream of the IP camera that will be used
# to record footage. # 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 # 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. # from the camera. the file naming convention uses the current date for
# warning: this will overwrite any existing index.html files so be sure # playlist files which points to hidden video clips taken from the camera.
# to choose a directory that doesn't have an existing website.
# #
buff_dir = /tmp buff_dir = /tmp/ramdisk/cam_name
# this application records small clips of the footage from the camera and # this application records small clips of the footage from the camera and
# then stores them into this directory. any clips with motion detected in # 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. # them are moved to output_dir, if no motion they are deleted. highly
# it is highly recommend to use a ramdisk tempfs for this since this # recommend to use a ramdisk tempfs for this since this directory is used
# directory is used for large amounts of writes. # for lots of writes.
# #
cam_name = cam-1 consec_threshold = 512
# this is the optional camera name parameter to identify the camera. this # motion is detected by comparing each frame in the camera feed for
# name will be used to form the directory structure in the web_root as # differences in the pixels. this value determine how many consecutive
# well as buff_dir. if not defined, the name of the config file will be # pixels need to different or how large the suspect object in motion
# used. # needs to be.
# #
pix_thresh = 150 block_threshold = 1024
# this value tells the application how far different the pixels need to be # this value tells the application how many "lines" of pixels need to
# before the pixels are actually considered different. think of this as # exceed consec_threshold before being considered motion.
# pixel diff sensitivity, the higher the value the lesser the sensitivity.
# #
frame_gap = 20 block_x = 64
# this value is used to tell the application how far in between frames to # this is the x coordinate size or horizontal size of a block of pixels
# check the pixel diffs for motion. the lower the value, the more frames # that the application will use to detect the presents of motion.
# will be checked, however with that comes higher cpu usage.
# #
img_thresh = 80000 block_y = 60
# this indicates how many pixels need to be different in between frame_gap # this is the y coordinate size or vertical size of a block of pixels
# before it is considered motion. any video clips found with frames # that the application will use to detect the presents of motion.
# exceeding this value will be moved from buff_dir to web_root.
# #
clip_len = 20 duration = 60
# this parameter indicate the amount of seconds to record in each video # this sets the internal timer used to reset to the detection loop and
# clip from the camera that will be stored and then processed in buff_dir. # 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
num_of_clips = 3 # to the settings will be applied without restarting the application.
# 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.
# #
post_cmd = move_the_ptz_camera.py post_cmd = move_the_ptz_camera.py
# this an optional command to run after num_of_clips is met. one great use # this an optional command to run after the internal timer duration has
# for this is to move a ptz camera to the next position of it's patrol # elapsed. one great use for this is to move a ptz camera to the next
# pattern. note: the call to this command will be delayed if motion was # position of it's patrol pattern. note: the call to this command will be
# detected. # delayed if motion was detected.
# #
max_days = 15 max_days = 15
# this defines the maximum amount of days worth of video clips that is # 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 # allowed to be stored in the output_dir. whenever this limit is met,
# oldest day and all of it's associated video clips will be deleted. # the oldest day is 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.
# #
vid_container = mp4 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 # camera. the format support depends entirely on the under laying ffmpeg
# installation. # 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 ### ### Setup/Build/Install ###
This application is currently only compatible with a Linux based operating This application is currently only compatible with a Linux based operating
systems that are capable of installing opencv. The following 3 scripts make systems that are capable of building and installing the opencv API from source.
building and then installing convenient.
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 ./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 ./build.sh time or if upgrading from the ground up.
sh ./install.sh 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 #!/bin/sh
mkdir -p ./.build-mow mkdir -p ./.build-mow
cd ./.build-mow cd ./.build-mow
cmake .. cmake ..
make -j4 make -j4

View File

@ -1,13 +1,3 @@
#!/bin/sh #!/bin/sh
if [ ! -d "/opt/mow" ]; then
mkdir /opt/mow cp ./.build-mow/mow /usr/bin/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

View File

@ -1,28 +1,19 @@
#!/bin/sh #!/bin/sh
apt update -y
apt install -y pkg-config apt update
apt install -y cmake apt install -y cmake g++ wget unzip git ffmpeg libavcodec-dev libavformat-dev libavutil-dev libswscale-dev
apt install -y make cd ./src
apt install -y g++ if [ -d "./opencv" ]
apt install -y wget then
apt install -y unzip cd ./opencv
apt install -y git git pull origin
apt install -y ffmpeg cd ..
apt install -y gstreamer1.0* else
apt install -y libavcodec-dev git clone https://github.com/opencv/opencv.git
apt install -y libavformat-dev fi
apt install -y libavutil-dev cd ..
apt install -y libswscale-dev mkdir -p ./.build-opencv
apt install -y libgstreamer1.0-dev cd ./.build-opencv
apt install -y x264 cmake ../src/opencv
apt install -y libx264-dev make -j4
apt install -y libopencv-dev make install
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

View File

@ -52,24 +52,48 @@ bool createDirTree(const string &full_path)
return ret; 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) vector<string> lsFilesInDir(const string &path, const string &ext)
{ {
DIR *dir;
struct dirent *ent;
vector<string> names; 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 = string(ent->d_name);
{
auto name = entry.path().filename().string();
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); names.push_back(name);
} }
} }
} }
closedir(dir);
} }
sort(names.begin(), names.end()); sort(names.begin(), names.end());
@ -77,53 +101,18 @@ vector<string> lsFilesInDir(const string &path, const string &ext)
return names; 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)) auto name = names[0];
{ auto plsFile = cleanDir(share->outDir) + "/" + name;
if (entry.is_directory()) auto vidFold = cleanDir(share->outDir) + "/." + name.substr(0, name.size() - 4);
{
names.push_back(entry.path().filename().string());
}
}
}
sort(names.begin(), names.end()); remove(plsFile.c_str());
remove_all(vidFold.c_str());
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);
names.erase(names.begin()); 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; 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) 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)
{ {
*value = line.substr(param.size()); *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) if (line.rfind(param.c_str(), 0) == 0)
{ {
*value = strtol(line.substr(param.size()).c_str(), NULL, 10); *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()) if (!varFile.is_open())
{ {
share->retCode = ENOENT; 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 else
{ {
string line; 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 do
{ {
getline(varFile, line); getline(varFile, line);
if (line.rfind("#", 0) != 0) if (line.rfind("#", 0) != 0)
{ {
rdLine("cam_name = ", line, &share->camName);
rdLine("recording_stream = ", line, &share->recordUrl); rdLine("recording_stream = ", line, &share->recordUrl);
rdLine("web_root = ", line, &share->webRoot); rdLine("output_dir = ", line, &share->outDir);
rdLine("web_text = ", line, &share->webTxt);
rdLine("web_bg = ", line, &share->webBg);
rdLine("web_font = ", line, &share->webFont);
rdLine("post_cmd = ", line, &share->postCmd); rdLine("post_cmd = ", line, &share->postCmd);
rdLine("clip_len = ", line, &share->clipLen); rdLine("consec_threshold = ", line, &share->consecThresh);
rdLine("num_of_clips = ", line, &share->numOfClips); rdLine("duration = ", line, &share->secs);
rdLine("buff_dir = ", line, &share->buffDir); rdLine("buff_dir = ", line, &share->buffDir);
rdLine("frame_gap = ", line, &share->frameGap); rdLine("block_x = ", line, &share->blockX);
rdLine("pix_thresh = ", line, &share->pixThresh); rdLine("block_y = ", line, &share->blockY);
rdLine("img_thresh = ", line, &share->imgThresh); rdLine("block_threshold = ", line, &share->blockThresh);
rdLine("max_days = ", line, &share->maxDays); 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_container = ", line, &share->vidExt);
rdLine("vid_codec = ", line, &share->vidCodec);
} }
} while(!line.empty()); } while(!line.empty());
}
return share->retCode == 0; // 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.
bool rdConf(shared_t *share) if (share->blockX == 0) share->blockX = 32;
{ if (share->blockY == 0) share->blockY = 32;
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";
if (share->init) if (share->init)
{ {
if (exists(share->buffDir)) remove_all(share->buffDir.c_str());
{
remove_all(share->buffDir);
}
share->init = false; share->init = false;
} }
new thread(enforceMaxDays, share);
createDirTree(cleanDir(share->buffDir)); 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 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 (int i = 0; i < argc; ++i)
for (; offs < argc; ++offs)
{ {
auto argInParams = string(argv[offs]); auto argInParams = string(argv[i]);
if (arg.compare(argInParams) == 0) if (arg.compare(argInParams) == 0)
{ {
if (!argOnly) if (!argOnly)
{ {
offs++; // check ahead, make sure i + 1 won't cause out-of-range exception
// check ahead, make sure offs + 1 won't cause out-of-range exception if ((i + 1) <= (argc - 1))
if (offs <= (argc - 1))
{ {
ret = string(argv[offs]); return string(argv[i + 1]);
} }
} }
else 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); auto path = string(cleanDir(share->buffDir) + "/stat");
} auto fd = open(path.c_str(), fstream::out | fstream::trunc);
vector<string> parseForList(const string &arg, int argc, char** argv) write(fd, share->stat.c_str(), share->stat.size() + 1);
{ close(fd);
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();
} }

View File

@ -22,6 +22,7 @@
#include <errno.h> #include <errno.h>
#include <vector> #include <vector>
#include <thread> #include <thread>
#include <dirent.h>
#include <filesystem> #include <filesystem>
#include <mutex> #include <mutex>
#include <sys/types.h> #include <sys/types.h>
@ -35,57 +36,46 @@ using namespace cv;
using namespace std; using namespace std;
using namespace std::filesystem; using namespace std::filesystem;
#define APP_VER "1.6" #define BUF_SZ 10
#define APP_NAME "Motion Watch" #define APP_VER "1.5"
struct shared_t struct shared_t
{ {
vector<thread> detThreads; string stat;
vector<string> conf; string recordUrl;
ofstream recLogFile; string outDir;
ofstream detLogFile; string postCmd;
string recLogPath; string conf;
string detLogPath; string buffDir;
string recordUrl; string vidExt;
string outDir; bool init;
string postCmd; bool recLoopWait;
string buffDir; bool skipCmd;
string vidExt; int consecThresh;
string vidCodec; int secs;
string camName; int blockThresh;
string webBg; int blockX;
string webTxt; int blockY;
string webFont; int maxDays;
string webRoot; int retCode;
bool init;
bool skipCmd;
int clipLen;
int frameGap;
int pixThresh;
int imgThresh;
int numOfClips;
int maxDays;
int maxClips;
int maxLogSize;
int retCode;
}; };
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);
string parseForParam(const string &arg, int argc, char** argv, bool argOnly, int &offs);
string parseForParam(const string &arg, int argc, char** argv, bool argOnly); string parseForParam(const string &arg, int argc, char** argv, bool argOnly);
bool createDir(const string &dir); bool createDir(const string &dir);
bool createDirTree(const string &full_path); bool createDirTree(const string &full_path);
void enforceMaxDays(const string &dirPath, shared_t *share); bool fileExists(const string& name);
void enforceMaxClips(const string &dirPath, shared_t *share); 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, string *value);
void rdLine(const string &param, const string &line, int *value); void rdLine(const string &param, const string &line, int *value);
void statOut(shared_t *share); void statOut(shared_t *share);
void waitForDetThreads(shared_t *share);
bool rdConf(shared_t *share); bool rdConf(shared_t *share);
vector<string> parseForList(const string &arg, int argc, char** argv); bool capPair(Mat &prev, Mat &next, VideoCapture &capture, shared_t *share);
vector<string> lsFilesInDir(const string &path, const string &ext = string()); Mat toGray(const Mat &src);
vector<string> lsDirsInDir(const string &path); vector<string> lsFilesInDir(const string &path, const string &ext);
#endif // COMMON_H #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,119 +11,63 @@
// GNU General Public License for more details. // GNU General Public License for more details.
#include "mo_detect.h" #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)
if (moDetect(bufPath, thumbNail, share))
{ {
share->skipCmd = true; bufFiles = lsFilesInDir(share->buffDir, "." + share->vidExt);
wrOut(bufPath, thumbNail, share); if ((bufFiles.size() >= 2) || (share->recLoopWait && !bufFiles.empty()))
} {
else if (exists(bufPath)) auto fullPath = cleanDir(share->buffDir) + "/" + bufFiles[0];
{
remove(bufPath);
}
detLog("detect_mo_in_file() -- finished", share); share->stat.clear();
if (moDetect(fullPath, share))
{
share->skipCmd = true;
wrOut(fullPath, share);
}
else
{
remove(fullPath.c_str());
}
statOut(share);
}
else
{
sleep(1);
}
}
} }
void recLoop(shared_t *share) void recLoop(shared_t *share)
{ {
while (rdConf(share)) while (rdConf(share))
{ {
recLog("rec_loop() -- start", share); createDirTree(share->buffDir);
enforceMaxLogSize(share->recLogPath, share); auto bufPath = cleanDir(share->buffDir) + "/%03d." + share->vidExt;
enforceMaxLogSize(share->detLogPath, share); 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); thread th2(detectLoop, share);
initLogFile(share->detLogPath, share->detLogFile);
initLogFrontPages(share); system(cmd.c_str());
if (!exists("/tmp/mow-lock")) share->recLoopWait = true;
{
system("touch /tmp/mow-lock");
genCSS(share); th2.join();
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);
if (!share->skipCmd) if (!share->skipCmd)
{ {
recLog("no motion detected", share); system(share->postCmd.c_str());
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;
} }
} }
} }
@ -132,35 +76,34 @@ int main(int argc, char** argv)
{ {
struct shared_t sharedRes; struct shared_t sharedRes;
sharedRes.conf = parseForList("-c", argc, argv); sharedRes.conf = parseForParam("-c", argc, argv, false);
if (parseForParam("-h", argc, argv, true) == "true") if (parseForParam("-h", argc, argv, true) == "true")
{ {
cout << "Motion Watch " << APP_VER << endl << endl; cout << "Motion Watch " << APP_VER << endl << endl;
cout << "Usage: mow <argument>" << endl << endl; cout << "Usage: mow <argument>" << endl << endl;
cout << "-h : display usage information about this application." << endl; cout << "-h : display usage information about this application." << endl;
cout << "-c : path to a config file." << endl; cout << "-c : path to the config file." << endl;
cout << "-v : display the current version." << endl << endl; cout << "-v : display the current version." << 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;
} }
else if (parseForParam("-v", argc, argv, true) == "true") if (parseForParam("-v", argc, argv, true) == "true")
{ {
cout << APP_VER << endl; cout << APP_VER << endl;
} }
else if (sharedRes.conf.empty()) 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 else
{ {
sharedRes.retCode = 0; sharedRes.retCode = 0;
sharedRes.skipCmd = false; sharedRes.recLoopWait = false;
sharedRes.init = true; sharedRes.skipCmd = false;
sharedRes.init = true;
recLoop(&sharedRes); thread th1(recLoop, &sharedRes);
th1.join();
return sharedRes.retCode; return sharedRes.retCode;
} }

View File

@ -12,131 +12,124 @@
#include "mo_detect.h" #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) 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); for (auto x = 0; x < prev.cols; x += share->blockX)
if (next.empty()) detLog("next_frame is empty -- EOF assumed.", share);
if (!prev.empty() && !next.empty())
{ {
Mat diff; // spawn all of the block motion detection threads.
for (auto y = 0; y < prev.rows; y += share->blockY, id += 1)
absdiff(prev, next, diff); {
threshold(diff, diff, share->pixThresh, 255, THRESH_BINARY); threads.push_back(thread(secDiff, prev, next, id, share->blockY, share->blockX, y, x, &results, &secMutex, share));
}
auto diffScore = countNonZero(diff);
detLog("diff_score: " + to_string(diffScore), share);
ret = diffScore >= share->imgThresh;
} }
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; auto mod = false;
VideoCapture capture(buffFile.c_str(), CAP_FFMPEG); VideoCapture capture(buffFile.c_str(), CAP_FFMPEG);
if (capture.isOpened()) if (capture.isOpened())
{ {
Mat prev; Mat prev;
Mat next; Mat next;
auto frameCount = capture.get(CAP_PROP_FRAME_COUNT);
auto frameGaps = frameCount / share->frameGap;
detLog("frame_count: " + to_string(frameCount), share); while (capPair(prev, next, capture, share))
detLog("frame_gaps: " + to_string(frameGaps), share);
for (auto i = 0; i < frameGaps; i++)
{ {
if (prev.empty()) prev = frameFF(&capture, 1); if (imgDiff(toGray(prev), toGray(next), share))
else prev = next.clone();
next = frameFF(&capture, share->frameGap);
if (imgDiff(prev, next, share))
{ {
resize(next, vidThumb, Size(720, 480), INTER_LINEAR); mod = true; break;
mod = true; break;
} }
} }
} }
else 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; return mod;
} }

View File

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