Compare commits
No commits in common. "528b4105f75e5ab314f7528ab4ea76251c6bfc96" and "01a11741c780113becbea2ce5cec8a27bbe5a1bc" have entirely different histories.
528b4105f7
...
01a11741c7
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -57,3 +57,7 @@ compile_commands.json
|
|||
|
||||
# Build folders
|
||||
/.build-mow
|
||||
/.build-opencv
|
||||
|
||||
# Opencv src folder
|
||||
/src/opencv
|
||||
|
|
|
@ -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
131
README.md
|
@ -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.
|
||||
```
|
||||
|
|
2
build.sh
2
build.sh
|
@ -1,5 +1,7 @@
|
|||
#!/bin/sh
|
||||
|
||||
mkdir -p ./.build-mow
|
||||
cd ./.build-mow
|
||||
cmake ..
|
||||
make -j4
|
||||
|
||||
|
|
14
install.sh
14
install.sh
|
@ -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
|
||||
|
|
45
setup.sh
45
setup.sh
|
@ -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
|
||||
|
|
302
src/common.cpp
302
src/common.cpp
|
@ -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 ¶m, 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 ¶m, 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);
|
||||
}
|
||||
|
|
46
src/common.h
46
src/common.h
|
@ -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 ¶m, const string &line, string *value);
|
||||
void rdLine(const string ¶m, 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
|
||||
|
|
108
src/logger.cpp
108
src/logger.cpp
|
@ -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());
|
||||
}
|
24
src/logger.h
24
src/logger.h
|
@ -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
|
137
src/main.cpp
137
src/main.cpp
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
134
src/web.cpp
134
src/web.cpp
|
@ -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 &®Name : 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();
|
||||
}
|
22
src/web.h
22
src/web.h
|
@ -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
|
Loading…
Reference in New Issue
Block a user