Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement and test camera prototype #16

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ endif ()

find_package(lcm REQUIRED)
find_package(GTest REQUIRED)
find_package(OpenCV REQUIRED)

include(${LCM_USE_FILE}) # lcm related cmake helper functions
include_directories(${OpenCV_INLUCDE_DIRS}) # OpenCV headers
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

include_directories should not be used unless this is required for every source file. use target_include_directories instead, for the module that actually depends on it.


include_directories(.)

Expand Down
2 changes: 2 additions & 0 deletions utils/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ add_subdirectory(gtest_utils)
add_subdirectory(lcm_utils)
add_subdirectory(boost_python)
add_subdirectory(communication)
add_subdirectory(camera)

irm_add_lcm_library(example_lcm SOURCES lcmtypes/example.lcm)

irm_add_cc_test(timing_test DEPENDS gtest_utils timing)
irm_add_cc_test(lcm_utils_test DEPENDS gtest_utils ${example_lcm} lcm_utils)
irm_add_cc_test(protocol_test DEPENDS gtest_utils communication)
irm_add_cc_test(camera_test DEPENDS gtest_utils camera timing ${OpenCV_LIBS})
irm_add_python_test(boost_python_example_test)

add_executable(lcm_file_logger executables/lcm_file_logger.cc)
Expand Down
7 changes: 7 additions & 0 deletions utils/camera/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
project(camera)

set(SOURCES camera_driver.cc)

add_library(camera ${SOURCES})

target_link_libraries(camera ${OpenCV_LIBS} pthread)
80 changes: 80 additions & 0 deletions utils/camera/camera_driver.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#include "camera_driver.h"
#include <thread>
#include <chrono>
#include <iostream>
#include <string>

namespace camera {

CameraBase::CameraBase(uint32_t n) {
this->n = n;
this->busy_index = 0;
this->is_capturing = false;
this->_buffer.resize(this->n);
}

void CameraBase::_start_capture(unsigned int sleep) {
if (this->is_capturing) {
// TODO: add legit exception handling
std::cerr << "DONT CALL ME TWICE!!!!!\r\n";
}
this->is_capturing = true;
while (true) {
this->_buffer[this->busy_index] = this->cam_read();
this->_index_lock.lock();
this->busy_index = (this->busy_index + 1) % this->n;
this->_index_lock.unlock();
if (sleep)
std::this_thread::sleep_for(std::chrono::milliseconds(sleep));
}
}

void CameraBase::get_img(cv::Mat &dst) {
this->_index_lock.lock();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider using modern C++ lock guard instead of this raw locking mechanism

uint32_t ind_to_read = (this->busy_index + this->n - 1) % this->n;
// this can be potentially improved by taking (diminishing) tradeoff with thread safety
// but on Jetson TX2, this implementation already runs faster than 120fps
// which is faster than almost every camera...
this->_buffer[ind_to_read].copyTo(dst);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the circular buffer is big enough and the processing frequency is faster than the circular capture frequency, we could get rid of this copy.

this->_index_lock.unlock();
}

void CameraBase::start() {
// fill the buffer
for (uint32_t i = 0; i < this->n; ++i){
this->_buffer[i] = this->cam_read();
}
std::thread t(&::camera::CameraBase::_start_capture, this, 0);
this->camera_thread_handle = t.native_handle();
t.detach();
}

void CameraBase::stop() {
this->_index_lock.lock();
pthread_cancel(this->camera_thread_handle);
this->_index_lock.unlock();
}

/* ---------------------------- SimpleCVCam ---------------------------*/

SimpleCVCam::SimpleCVCam(uint32_t n) : CameraBase(n) {
this->cap = cv::VideoCapture(0); // default camera
}

SimpleCVCam::SimpleCVCam(uint32_t n, unsigned short device_id) : CameraBase(n) {
this->cap = cv::VideoCapture(device_id);
}

SimpleCVCam::~SimpleCVCam() {
this->stop();
this->cap.release();
cv::destroyAllWindows();
}

cv::Mat SimpleCVCam::cam_read() {
cv::Mat frame;
this->cap >> frame;
return frame;
}

} // namespace camera
114 changes: 114 additions & 0 deletions utils/camera/camera_driver.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
#pragma once

#include <mutex>
#include <vector>
#include <thread>
#include <iostream>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <opencv2/opencv.hpp>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

using namespace std;

namespace camera {
/**
* A base class for generalized camera level processing
*/
class CameraBase {
public:
/**
* @brief constructor for CameraBase class
*
* @param n size of circular frame buffer
*/
CameraBase(uint32_t n);

/**
* @brief read an image and copy into src Mat
*
* @param dst destination place holder for the stored image
* @return none
*/
void get_img(cv::Mat &dst);

/**
* @brief start the camera in a seperate thread
*
* @return none
*/
void start();

/**
* @brief terminates the (already started) separate thread
*/
void stop();

/**
* @brief to be implemented according to the specs of a specific camera
*
* @return Mat object read directly from the camera
*/
virtual cv::Mat cam_read() = 0;

/**
* @brief determine if the camera is dead or alive. Should only be of use for video feed
*
* @return alive flag
*/
inline bool is_alive(void) { return this->is_capturing; }
protected:
/**
* @brief Invoke a separate thread to get images
*
* @param sleep time (in ms) to sleep between each capture. Default to zero
* @return none
*/
void _start_capture(unsigned int sleep = 0);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
void _start_capture(unsigned int sleep = 0);
void start_capture_(unsigned int sleep = 0);


bool is_capturing; // maybe use a lock?
uint32_t n;
// index indicating frames of interest
// increment by each capturing loop
uint32_t busy_index;
std::vector<cv::Mat> _buffer;
mutex _index_lock;
Comment on lines +78 to +79
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
std::vector<cv::Mat> _buffer;
mutex _index_lock;
std::vector<cv::Mat> buffer_;
std::mutex index_lock_;

pthread_t camera_thread_handle;
};

/**
* @brief a generalized opencv camera class
*/
class SimpleCVCam: public CameraBase {
public:
/**
* @brief constructor for SimpleCVCam class
*
* @param n size of circular frame buffer
*/
SimpleCVCam(uint32_t n);

/**
* @brief constructor for SimpleCVCam
*
* @param n size of the circular frame buffer
* @param device_id device id as in camera index [default to 0]
*/
SimpleCVCam(uint32_t n, unsigned short device_id);

~SimpleCVCam();

/**
* @brief virtual implementation of how to read images
*
* @return Mat object read directly from the camera
*/
cv::Mat cam_read();
protected:
cv::VideoCapture cap;
};
}
102 changes: 102 additions & 0 deletions utils/tests/camera_test.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
#include <iostream>
#include <chrono>
#include <opencv2/opencv.hpp>
#include "utils/timing/tic_toc.h"
#include "utils/camera/camera_driver.h"
#include "utils/gtest_utils/test_base.h"

/************************************
Camerat tests:
- uses metrics to compare camera implementation and visually confirm results are correct

Metrics
- Latency (How long does each call to get img take)?
- Throughput (In a given time interval e.g. 10sec). How many non-identical image can be obtained?

************************************/

using namespace std;
using namespace timing;
using namespace camera;

bool mat_equal(cv::Mat & a, cv::Mat & b) {
// vectorized Op
return cv::countNonZero(a != b) == 0;
}

class CameraTest : public TestBase {
public:
void OnePhotoTest() {
SimpleCVCam my_cam(4, 0);
my_cam.start();
cv::Mat test_frame;
ASSERT_TRUE(test_frame.rows == 0);
ASSERT_TRUE(test_frame.cols == 0);
my_cam.get_img(test_frame);
ASSERT_TRUE(test_frame.rows > 0);
ASSERT_TRUE(test_frame.cols > 0);
imwrite("./test.jpg", test_frame);
}

void BenchmarkTest() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this test necessary? It does not seem to verify the correctness of anything.

int n = 100; // number of frames to get
SimpleCVCam my_cam(4, 0);
TicTocGlobalReset();
TicToc tictoc_baseline("Standard get_img");
TicToc tictoc_ours("Our get_img");
vector<cv::Mat> baseline_arr;
vector<cv::Mat> novel_mat_arr;
// baseline
for (int i = 0; i < n; ++i) {
tictoc_baseline.Tic();
cv::Mat my_img = my_cam.cam_read();
tictoc_baseline.Toc();
baseline_arr.push_back(my_img);
}
my_cam.start();
// ours
for (int i = 0; i < n; ++i) {
cv::Mat my_img;
tictoc_ours.Tic();
my_cam.get_img(my_img);
tictoc_ours.Toc();
novel_mat_arr.push_back(my_img);
}
std::cout << "[ TicTocTest ] Time Summary:" << std::endl;
std::cout << TicTocGlobalSummary() << std::endl;
ASSERT_TRUE(baseline_arr.size() == ((size_t)n));
ASSERT_TRUE(novel_mat_arr.size() == ((size_t)n));
for (int i = 0; i < n; ++i) {
ASSERT_TRUE(baseline_arr[i].rows > 0);
ASSERT_TRUE(baseline_arr[i].cols > 0);
ASSERT_TRUE(novel_mat_arr[i].rows > 0);
ASSERT_TRUE(novel_mat_arr[i].cols > 0);
}
// latency
double baseline_time = TicTocStatsTime("Standard get_img");
double our_time = TicTocStatsTime("Our get_img");
ASSERT_TRUE(our_time < baseline_time);
// throughput
int baseline_valid_cnt = 0;
int our_valid_cnt = 0;
for (int i = 0; i < n - 1; ++i) {
if (!mat_equal(baseline_arr[i], baseline_arr[i + 1]))
baseline_valid_cnt++;
if (!mat_equal(novel_mat_arr[i], novel_mat_arr[i + 1]))
our_valid_cnt++;
}
std::cout << "Valid baseline mat cnt: " << baseline_valid_cnt << std::endl;
std::cout << "Valid our mat cnt: " << our_valid_cnt << std::endl;
double baseline_throughput = baseline_valid_cnt / baseline_time;
double our_throughput = our_valid_cnt / our_time;
std::cout << "Baseline throughput (fps): " << baseline_throughput << std::endl;
std::cout << "Our throughput (fps): " << our_throughput << std::endl;
}
};

// for some unknown reason, releasing a videocapture object
// and then reacquring it sometimes cause Illegal Hardware Instruction fault.
// This should not be a problem on the robot.
// Therefore, one test is commented out.
// TEST_FM(CameraTest, OnePhotoTest);
TEST_FM(CameraTest, BenchmarkTest);
10 changes: 10 additions & 0 deletions utils/timing/tic_toc.cc
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,16 @@ std::string TicTocGlobalSummary() {
return ss.str();
}

double TicTocStatsTime(std::string query) {
tictoc_t tictocs = global_tic_toc_bank.GetLCM();
// sort according to keys + find maximum channel name length
for (tictoc_channel_t &tictoc: tictocs.tictoc_channels) {
if (tictoc.name == query)
return ((TicTocStats)(tictoc.tictoc_stats)).TotalTime();
}
return -1; // not found
}

Comment on lines +56 to +65
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make this a TicTocBank method, and for generalization purposes, return the entire stats for the channel. Key access should be hash mapped not queried.

/***********************
* --- TicTocStats --- *
***********************/
Expand Down
17 changes: 13 additions & 4 deletions utils/timing/tic_toc.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class TicTocStats : public tictoc_stats_t {

void Update(int64_t duration_ns);
void Reset();

double TotalTime();
double AverageTime();
double MaxTime();
Expand Down Expand Up @@ -74,7 +74,7 @@ class TicTocBank {
* @return tic toc lcm data struct
*/
tictoc_t GetLCM();

private:
std::unordered_map<std::string, TicTocStats> channel_map_;
std::mutex lock_;
Expand Down Expand Up @@ -104,7 +104,7 @@ class TicToc {
* @remark calls Tic() to start the timer when constructing
*/
explicit TicToc(const std::string &name);

/**
* @brief start the timer
*/
Expand All @@ -114,7 +114,7 @@ class TicToc {
* @brief end the timer and upload duration to a global manager
*/
void Toc();

private:
std::chrono::time_point<std::chrono::steady_clock> start_;
std::string name_;
Expand Down Expand Up @@ -171,4 +171,13 @@ void TicTocGlobalReset();
*/
std::string TicTocGlobalSummary();

/**
* @brief return the TOTAL time of a TicToc stats in the global TicToc bank
*
* @param query name of desired TicToc
*
* @return total time of corresponding TicToc
*/
double TicTocStatsTime(std::string query);

} // namespace timing