Compare commits

...

8 Commits

12 changed files with 491 additions and 41 deletions

3
.gitignore vendored
View File

@ -1,2 +1,3 @@
.venv .venv
.idea .idea
build

View File

@ -3,56 +3,37 @@
This project is a demo for streaming video output from multiple "client" devices to one "server". This is a basic demo This project is a demo for streaming video output from multiple "client" devices to one "server". This is a basic demo
of what sauron-cv seeks to accomplish. of what sauron-cv seeks to accomplish.
## Installation ## Building
It is strongly recommended that you use a virtual environment. These instructions will assume you are using venv, you This project uses Meson for build management.
can substitute this with your preferred environment management. You will need to make sure that the `virtualenv` package
is installed globally, either via pip or the `python3-virtualenv` package in your system package manager.
When first cloning this repo, run the following: ### Install Dependencies
Install the following packages from your distribution package manager:
- `meson`
- `opencv`
- `boost`
A better procedure for this (hopefully involving Meson) will be added / documented at a later date.
### Setup Meson
```shell ```shell
python -m venv .venv meson setup build
source .venv/bin/activate
pip install -r requirements.txt
``` ```
This will create a virtual environment, enter that virtual environment, and install the required packages. *NOTE FOR JETBRAINS / CLION USERS: PLEASE SET YOUR MESON BUILD DIRECTORY TO `build` IN THE IDE SETTINGS UNDER "Build /
Execution / Deployment" -> "Meson"*
If you start a new shell, you will need to re-enter the virtual environment: ### Compiling
```shell ```shell
source .venv/bin/activate meson build client # for client only
meson build server # for server only
``` ```
## Running ## Running
### Client
To run the client with a localhost target:
```shell ```shell
python -m client ./build/client # for client application
``` ./build/server # for server application
```
To target an external server, provide its IP address using the `-s` flag:
```shell
python -m client -s [server IP address]
```
### Server
```shell
python -m server
```
### Common Flags
Make sure that these match between your client and server!!
| Short | Long | Description | Default |
|-------|---|---|---|
| `-p` | `--port` | The port for the client / server to communicate on. | `5005` |
| `-W` | `--width` | Image width in pixels. | `640` |
| `-H` | `--height` | Image height in pixels. | `480` |

43
client.cpp Normal file
View File

@ -0,0 +1,43 @@
#include <opencv2/videoio.hpp>
#include <cstring>
#include <iostream>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
#include "transfer.h"
using namespace std;
int main() {
// create video capture
cv::VideoCapture cap = cv::VideoCapture(0);
// create socket
int clientSocket = socket(AF_INET, SOCK_STREAM, 0);
// specifying address
sockaddr_in serverAddress;
serverAddress.sin_family = AF_INET;
serverAddress.sin_port = htons(8080);
serverAddress.sin_addr.s_addr = INADDR_ANY;
cv::Mat image = cv::Mat::zeros(cv::Size(640, 480), CV_8UC3);
info("Ready to connect.");
// sending connection request
connect(clientSocket, reinterpret_cast<sockaddr *>(&serverAddress),
sizeof(serverAddress));
info("Connected.");
// create buffer for serialization
vector<uchar> imgbuf;
while (true) {
cap.read(image);
trace("Sending image");
sendImage(clientSocket, image, imgbuf);
}
close(clientSocket);
return 0;
}

58
legacy/README.md Normal file
View File

@ -0,0 +1,58 @@
# Video Streaming Proof of Concept
This project is a demo for streaming video output from multiple "client" devices to one "server". This is a basic demo
of what sauron-cv seeks to accomplish.
## Installation
It is strongly recommended that you use a virtual environment. These instructions will assume you are using venv, you
can substitute this with your preferred environment management. You will need to make sure that the `virtualenv` package
is installed globally, either via pip or the `python3-virtualenv` package in your system package manager.
When first cloning this repo, run the following:
```shell
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
```
This will create a virtual environment, enter that virtual environment, and install the required packages.
If you start a new shell, you will need to re-enter the virtual environment:
```shell
source .venv/bin/activate
```
## Running
### Client
To run the client with a localhost target:
```shell
python -m client
```
To target an external server, provide its IP address using the `-s` flag:
```shell
python -m client -s [server IP address]
```
### Server
```shell
python -m server
```
### Common Flags
Make sure that these match between your client and server!!
| Short | Long | Description | Default |
|-------|---|---|---|
| `-p` | `--port` | The port for the client / server to communicate on. | `5005` |
| `-W` | `--width` | Image width in pixels. | `640` |
| `-H` | `--height` | Image height in pixels. | `480` |

178
logging.h Normal file
View File

@ -0,0 +1,178 @@
#ifndef LOGGING_H
#define LOGGING_H
#include <iostream>
#include <iomanip>
#include <chrono>
#include <ctime>
// Logging levels
#define LOG_ERROR 0
#define LOG_WARN 1
#define LOG_INFO 2
#define LOG_DEBUG 3
#define LOG_TRACE 4
// Set logging arguments
// TODO: set these by compiler arguments or env vars or something
#define LOGGING LOG_DEBUG // logging level
#define LOGGING_COLOR true // enable color
#define LOGGING_TIMESTAMP true // enable timestamp
#define LOGGING_TIMESTAMP_FMT "%Y-%m-%dT%H:%M:%S%z" // timestamp format (local time)
#define LOGGING_POSITION true // display position (only works on C++20 or newer)
// Color codes
#define ANSI_RESET "\033[0m"
#define ANSI_BLACK "\033[30m" /* Black */
#define ANSI_RED "\033[31m" /* Red */
#define ANSI_GREEN "\033[32m" /* Green */
#define ANSI_YELLOW "\033[33m" /* Yellow */
#define ANSI_BLUE "\033[34m" /* Blue */
#define ANSI_MAGENTA "\033[35m" /* Magenta */
#define ANSI_CYAN "\033[36m" /* Cyan */
#define ANSI_WHITE "\033[37m" /* White */
#define ANSI_BOLD "\033[1m" /* Bold */
template <typename T>
void print(T t) {
std::cout << t << std::endl;
}
template <typename T, typename... Args>
void print(T t, Args... args) {
std::cout << t;
print(args...);
}
inline void printTimestamp() {
#if LOGGING_TIMESTAMP
auto now = std::chrono::system_clock::now();
auto time_c = std::chrono::system_clock::to_time_t(now);
std::tm time_tm;
localtime_r(&time_c, &time_tm);
std::cout << std::put_time(&time_tm, LOGGING_TIMESTAMP_FMT) << ": ";
#endif
}
// if we're on C++20 or later, then use the source_location header and add source location to logs
#if __cplusplus >= 202002L
#include <source_location>
inline void printPosition(std::source_location& location) {
#if LOGGING_POSITION
std::cout << location.file_name() << ":" << location.function_name << ":" << ANSI_CYAN << location.line() << ANSI_RESET << ": ";
#endif
}
inline void printHeader(std::string name, std::string color, std::source_location& location) {
#if LOGGING_COLOR
std::cout << ANSI_BOLD << color << "[" << name << "] " << ANSI_RESET;
printTimestamp();
printPosition(location);
#else
printHeader(name);
#endif
}
inline void printHeader(std::string name, std::source_location& location) {
std::cout << "[" << name << "] ";
printTimestamp();
printPosition(location);
}
template <typename... Args, typename Sl = std::source_location>
void trace(Args... args, Sl location = std::source_location::current()) {
#if LOGGING >= LOG_TRACE
printHeader("TRACE", ANSI_CYAN, location);
print(args...);
#endif
}
template <typename... Args, typename Sl = std::source_location>
void debug(Args... args, Sl location = std::source_location::current()) {
#if LOGGING >= LOG_DEBUG
printHeader("DEBUG", ANSI_MAGENTA, location);
print(args...);
#endif
}
template <typename... Args, typename Sl = std::source_location>
void info(Args... args, Sl location = std::source_location::current()) {
#if LOGGING >= LOG_INFO
printHeader("INFO", ANSI_GREEN, location);
print(args...);
#endif
}
template <typename... Args, typename Sl = std::source_location>
void warn(Args... args, Sl location = std::source_location::current()) {
#if LOGGING >= LOG_WARN
printHeader("WARN", ANSI_YELLOW, location);
print(args...);
#endif
}
template <typename... Args, typename Sl = std::source_location>
void error(Args... args, Sl location = std::source_location::current()) {
#if LOGGING >= LOG_ERROR
printHeader("ERROR", ANSI_RED, location);
print(args...);
#endif
}
#else
inline void printHeader(std::string name, std::string color) {
#if LOGGING_COLOR
std::cout << ANSI_BOLD << color << "[" << name << "] " << ANSI_RESET;
printTimestamp();
#else
printHeader(name);
#endif
}
inline void printHeader(std::string name) {
std::cout << "[" << name << "] ";
printTimestamp();
}
template <typename... Args>
void trace(Args... args) {
#if LOGGING >= LOG_TRACE
printHeader("TRACE", ANSI_CYAN);
print(args...);
#endif
}
template <typename... Args>
void debug(Args... args) {
#if LOGGING >= LOG_DEBUG
printHeader("DEBUG", ANSI_MAGENTA);
print(args...);
#endif
}
template <typename... Args>
void info(Args... args) {
#if LOGGING >= LOG_INFO
printHeader("INFO", ANSI_GREEN);
print(args...);
#endif
}
template <typename... Args>
void warn(Args... args) {
#if LOGGING >= LOG_WARN
printHeader("WARN", ANSI_YELLOW);
print(args...);
#endif
}
template <typename... Args>
void error(Args... args) {
#if LOGGING >= LOG_ERROR
printHeader("ERROR", ANSI_RED);
print(args...);
#endif
}
#endif
#endif //LOGGING_H

33
meson.build Normal file
View File

@ -0,0 +1,33 @@
#=======================================================================================================================
# PROJECT SETTINGS
#=======================================================================================================================
project('video-streaming-poc', 'cpp', version : '0.0.1-SNAPSHOT',
default_options : ['c_std=c17', 'cpp_std=c++20'])
#=======================================================================================================================
# DEPENDENCIES
#=======================================================================================================================
# opencv dependency
opencv = dependency('opencv4', version : '>=4.0.0')
# boost dependency
boost = dependency('boost')
#=======================================================================================================================
# SOURCE FILES
#=======================================================================================================================
# common files between client / server
common = ['transfer.h', 'logging.h']
# client-only files
client = common + ['client.cpp']
# server-only files
server = common + ['server.cpp']
#=======================================================================================================================
# BUILD TARGETS
#=======================================================================================================================
# client executable
client_exe = executable('client', client,
dependencies : [opencv, boost])
# server executable
server_exe = executable('server', server,
dependencies : [opencv, boost])

55
server.cpp Normal file
View File

@ -0,0 +1,55 @@
#include <opencv2/highgui.hpp>
#include <opencv2/core/mat.hpp>
#include <cstring>
#include <iostream>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
#include "transfer.h"
using namespace std;
int main() {
// TODO: read image data from socket instead of VideoCapture
// creating socket
int serverSocket = socket(AF_INET, SOCK_STREAM, 0);
// specifying the address
sockaddr_in serverAddress;
serverAddress.sin_family = AF_INET;
serverAddress.sin_port = htons(8080);
serverAddress.sin_addr.s_addr = INADDR_ANY;
// binding socket.
bind(serverSocket, reinterpret_cast<sockaddr *>(&serverAddress),
sizeof(serverAddress));
info("Ready to accept connections.");
// listening to the assigned socket
listen(serverSocket, 5);
// accepting connection request
int clientSocket = accept(serverSocket, nullptr, nullptr);
info("Client connected.");
// TODO: handle multiple images
cv::Mat image = cv::Mat::zeros(cv::Size(640, 480), CV_8UC3);
bool running = true;
// TODO: make this asynchronous. probably do that in tandem with setting up networking
while (running) {
// receive data
vector<uchar> buffer;
trace("Receiving image");
recvImage(clientSocket, buffer);
trace("Applying new data to image");
applyImage(image, &buffer);
trace("Displaying image");
imshow("image", image);
running = cv::waitKey(30) != 27;
}
close(serverSocket);
return 0;
}

101
transfer.h Normal file
View File

@ -0,0 +1,101 @@
#ifndef TRANSFER_H
#define TRANSFER_H
#include <chrono>
#include <opencv2/core/mat.hpp>
#include <opencv2/imgcodecs.hpp>
#include <sys/socket.h>
#include <logging.h>
#include <vector>
using namespace std;
struct imageHeader {
size_t size;
chrono::milliseconds timestamp;
};
chrono::milliseconds getMillis() {
// Get current time
const auto now = chrono::system_clock::now();
// Get time since epoch in milliseconds
const chrono::duration ms = chrono::duration_cast<chrono::milliseconds>(now.time_since_epoch());
return chrono::milliseconds(ms.count());
}
inline void serializeImage(const cv::Mat& image, std::vector<uchar>& buffer) {
cv::imencode(".jpg", image, buffer);
}
int sendImage(int socket, const cv::Mat& image, std::vector<uchar>& buffer) {
serializeImage(image, buffer);
size_t totalSent = 0;
// first send the size of the serialized image
const size_t size = buffer.size();
const chrono::milliseconds timestamp = getMillis();
imageHeader header;
header.size = size;
header.timestamp = timestamp;
trace("Buffer size: ", size);
if (const ssize_t sent = send(socket, &header, sizeof(header), 0); sent == -1) {
error("Error sending data header");
return -1;
}
// then start sending the serialized image
while (totalSent < size) {
const ssize_t sent = send(socket, buffer.data() + totalSent, size - totalSent, 0);
if (sent == -1) {
error("Error sending data");
return -1;
}
totalSent += sent;
debug("Packet sent (", sent, " bytes, total ", totalSent, " bytes)");
}
return 0;
}
int recvImage(int socket, std::vector<uchar>& buffer) {
// first receive the size of the image
imageHeader header;
if (ssize_t recvd = recv(socket, &header, sizeof(imageHeader), 0); recvd <= 0) {
error("Error receiving data header");
return -1;
}
size_t dataSize = header.size;
chrono::milliseconds sentTime = header.timestamp;
trace("Buffer size: ", dataSize);
// resize the buffer to fit the whole image
buffer.resize(dataSize);
// start receiving the image until the whole thing arrives
size_t totalReceived = 0;
while (totalReceived < dataSize) {
ssize_t bytesReceived = recv(socket, buffer.data() + totalReceived, dataSize, 0);
if (bytesReceived <= 0) {
error("Error receiving data");
buffer.clear();
return -1;
}
totalReceived += bytesReceived;
debug("Packet received (", bytesReceived, " bytes, total ", totalReceived, " bytes)");
}
chrono::milliseconds currentTime = getMillis();
chrono::milliseconds diff = currentTime - sentTime;
debug("Packet latency: ", diff.count(), "ms");
return 0;
}
bool applyImage(cv::Mat& image, std::vector<uchar> *src) {
// decode the image into an OpenCV Mat
cv::imdecode(*src, cv::IMREAD_UNCHANGED, &image);
return true;
}
#endif //TRANSFER_H