A working solution
This commit is contained in:
parent
9016f8461e
commit
196957f3d0
8 changed files with 14213 additions and 10 deletions
|
|
@ -1,20 +1,39 @@
|
||||||
cmake_minimum_required(VERSION 2.8)
|
cmake_minimum_required(VERSION 2.8)
|
||||||
|
|
||||||
set(PROJECT "integral_image")
|
set(PROJECT "integral_image")
|
||||||
|
set(PROJECT_TESTS tests)
|
||||||
|
|
||||||
|
include(CTest)
|
||||||
|
|
||||||
find_package(OpenCV REQUIRED)
|
find_package(OpenCV REQUIRED)
|
||||||
|
|
||||||
find_package(OpenMP REQUIRED)
|
find_package(OpenMP REQUIRED)
|
||||||
if (OPENMP_FOUND)
|
if (OPENMP_FOUND)
|
||||||
set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${OpenMP_C_FLAGS}")
|
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${OpenMP_C_FLAGS}")
|
||||||
set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${OpenMP_CXX_FLAGS}")
|
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${OpenMP_CXX_FLAGS}")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
|
||||||
set(SOURCES
|
set(SOURCES
|
||||||
main.cpp
|
main.cpp
|
||||||
|
integral_image.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
add_executable(${PROJECT} ${SOURCES})
|
add_executable(${PROJECT} ${SOURCES})
|
||||||
|
target_link_libraries(${PROJECT}
|
||||||
|
${OpenCV_LIBRARIES}
|
||||||
|
)
|
||||||
|
|
||||||
target_link_libraries(${PROJECT} LINK_PUBLIC
|
set(TEST_SOURCES
|
||||||
${OpenCV_LIBRARIES})
|
integral_image_tests.cpp
|
||||||
|
integral_image.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
set(CMAKE_INCLUDE_CURRENT_DIR ON)
|
||||||
|
|
||||||
|
add_executable(${PROJECT_TESTS} ${TEST_SOURCES})
|
||||||
|
target_link_libraries(${PROJECT_TESTS}
|
||||||
|
${OpenCV_LIBRARIES}
|
||||||
|
)
|
||||||
|
enable_testing()
|
||||||
|
add_test(NAME ${PROJECT_TESTS} COMMAND ${PROJECT_TESTS})
|
||||||
|
|
|
||||||
2
Doxyfile
Normal file
2
Doxyfile
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
DOXYFILE_ENCODING = UTF-8
|
||||||
|
PROJECT_NAME = "integral_image"
|
||||||
49
README.txt
49
README.txt
|
|
@ -0,0 +1,49 @@
|
||||||
|
Необходимо написать программу, считающую интегральные изображения входных изображений.
|
||||||
|
|
||||||
|
Интегральное изображение (integral image):
|
||||||
|
|
||||||
|
для C-канального изображения размером H строк на W столбцов Image интегральным изображением является следующее C-канальное изображение Integral размером H строк на W столбцов с типом данных double, для которого Integral(channel, row, col) = Sum(Image(channel, [0:row], [0:col]));
|
||||||
|
то есть значение пикселя в новом изображении равно сумме пикселей выше и левее него (нестрого);
|
||||||
|
если в исходном изображении каналов несколько, то, например, первый канал интегрального изображения будет равен интегральному изображению первого канала;
|
||||||
|
пример: для одноканального изображения
|
||||||
|
0 1
|
||||||
|
2 3
|
||||||
|
4 5
|
||||||
|
интегральным будет являться изображение
|
||||||
|
0.0 1.0
|
||||||
|
2.0 6.0
|
||||||
|
6.0 15.0
|
||||||
|
Теперь про приложение:
|
||||||
|
|
||||||
|
приложение должно называться integral_image;
|
||||||
|
запускаться должно следующим образом: ./integral_image -i <path_to_image2> [-i <path_to_image2> […]] [-t <threads number>], где -i указывает путь до изображения (таких аргументов может быть несколько), -t указывает количество потоков, которое желательно использовать;
|
||||||
|
аргумент -t может быть равен 0, в этом случае необходимо автоматически выбрать количество потоков, исходя из возможностей процессора;
|
||||||
|
аргумент -t может отстутствовать, в этом случае его считать равным 0;
|
||||||
|
гарантируется, что будет не более одного аргумента -t;
|
||||||
|
аргументы могут идти в любом порядке;
|
||||||
|
аргументов -i может быть сколько угодно;
|
||||||
|
при указании некорректного количества потоков приложение должно ничего не сделать, вывести сообщение об ошибке и корректно завершиться;
|
||||||
|
при указании некорректного пути, например, path_to_image2, оно не должно обрабатываться, должно быть выведено сообщение об ошибке с этим изображением, при этом результат должен быть посчитан для всех изображений с корректными путями;
|
||||||
|
интегральное изображение для изображения path_to_image2 стоит записать в текстовый файл path_to_image2.integral в следующем формате: интегральное изображение для первого канала, пустая строка, интегральное изображение для второго канала, если оно есть, и пустая строка и т.д. (для всех каналов);
|
||||||
|
для изображения из примера выше приложение должно вывести следующее:
|
||||||
|
0.0 1.0
|
||||||
|
2.0 6.0
|
||||||
|
6.0 15.0
|
||||||
|
|
||||||
|
Требования к реализации:
|
||||||
|
|
||||||
|
должны присутствовать юнит-тесты. Допускается использование любого юнит-тест фремворка (можно Catch (https://github.com/philsquared/Catch) или GoogleTest (https://github.com/google/googletest) как наиболее легко подключаемые);
|
||||||
|
код должен быть документирован в формате Doxygen, комментарии в реализации должны пояснять другим разработчикам детали неочевидных особенностей реализации;
|
||||||
|
в качестве системы сборки должен использоваться CMake (https://cmake.org/).
|
||||||
|
Можно использовать любые опенсорсные библиотеки со следующими условиями:
|
||||||
|
|
||||||
|
нельзя использовать готовую реализацию алгоритма вычисления интегрального изображения;
|
||||||
|
они должны автоматически подключаться через CMake (разумеется, при условии выставления необходимых флагов командной строки CMake и их наличия в ОС);
|
||||||
|
для считывания изображений и базовой работы с ними рекомендуется использовать OpenCV 3.x.
|
||||||
|
Приветствуется:
|
||||||
|
|
||||||
|
максимальная кроссплатформенность кода, в качестве целевых компиляторов можно рассматривать Visual Studio 2013 (или выше) и GCC 5.x (или выше);
|
||||||
|
максимальное (но разумное) использование структур и алгоритмов стандартной библиотеки;
|
||||||
|
использование стандартов C++11 и C++14;
|
||||||
|
распараллеливание даже если на вход подаётся всего одно изображение.
|
||||||
|
|
||||||
67
integral_image.cpp
Normal file
67
integral_image.cpp
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
#include "integral_image.h"
|
||||||
|
|
||||||
|
#include <omp.h>
|
||||||
|
|
||||||
|
|
||||||
|
namespace integral_image {
|
||||||
|
|
||||||
|
|
||||||
|
Mat integral_image_serial(const Mat &image)
|
||||||
|
{
|
||||||
|
if (image.cols == 0 || image.rows == 0)
|
||||||
|
return Mat();
|
||||||
|
|
||||||
|
Mat result = image.clone();
|
||||||
|
|
||||||
|
for (size_t row = 1; row < result.rows; ++row)
|
||||||
|
result[row][0] += result[row - 1][0];
|
||||||
|
|
||||||
|
for (size_t col = 1; col < result.cols; ++col)
|
||||||
|
result[0][col] += result[0][col - 1];
|
||||||
|
|
||||||
|
for (size_t row = 1; row < result.rows; ++row)
|
||||||
|
for (size_t col = 1; col < result.cols; ++col)
|
||||||
|
result[row][col] += result[row - 1][col] + result[row][col - 1] - result[row - 1][col - 1];
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Mat integral_image_openmp(const Mat &image, int thread_number)
|
||||||
|
{
|
||||||
|
if (image.cols == 0 || image.rows == 0)
|
||||||
|
return Mat();
|
||||||
|
|
||||||
|
if (0 != thread_number) {
|
||||||
|
|
||||||
|
omp_set_dynamic(0);
|
||||||
|
omp_set_num_threads(thread_number);
|
||||||
|
}
|
||||||
|
|
||||||
|
Mat result = image.clone();
|
||||||
|
|
||||||
|
#pragma omp parallel for
|
||||||
|
for (int row = 0; row < result.rows; ++row) {
|
||||||
|
|
||||||
|
for (int col = 1; col < result.cols; ++col) {
|
||||||
|
|
||||||
|
result[row][col] += result[row][col - 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//This loop is likely to have lots of cache misses that can probably be avoided by transposing data, processing data
|
||||||
|
//in a way similar to the previous loop, and than transposing data again.
|
||||||
|
//TODO: benchmark
|
||||||
|
|
||||||
|
#pragma omp parallel for
|
||||||
|
for (int col = 0; col < result.cols; ++col) {
|
||||||
|
|
||||||
|
for (int row = 1; row < result.rows; ++row) {
|
||||||
|
|
||||||
|
result[row][col] += result[row - 1][col];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
34
integral_image.h
Normal file
34
integral_image.h
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <opencv2/opencv.hpp>
|
||||||
|
|
||||||
|
|
||||||
|
/*! \file integral_image.h
|
||||||
|
\brief The file provides functions that calculate an integral image.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
namespace integral_image {
|
||||||
|
|
||||||
|
|
||||||
|
using Mat = cv::Mat_<double>;
|
||||||
|
|
||||||
|
//! A reference serial implementation of the integral image algorithm.
|
||||||
|
/*!
|
||||||
|
\param image an input 1-channel image.
|
||||||
|
\return The integral image of the input
|
||||||
|
\sa integral_image_openmp
|
||||||
|
*/
|
||||||
|
Mat integral_image_serial(const Mat &image);
|
||||||
|
|
||||||
|
//! An OpenMP-accelerated function that calculates an integral image.
|
||||||
|
/*!
|
||||||
|
\param image an input 1-channel image.
|
||||||
|
\param thread_number number of worker threads. If \p thread_number is equal to 0, the threads are created dynamically. Defaults to 0.
|
||||||
|
\return The integral image of the input
|
||||||
|
\sa integral_image_serial
|
||||||
|
*/
|
||||||
|
Mat integral_image_openmp(const Mat &image, int thread_number = 0);
|
||||||
|
|
||||||
|
}
|
||||||
49
integral_image_tests.cpp
Normal file
49
integral_image_tests.cpp
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
#define CATCH_CONFIG_MAIN
|
||||||
|
#include <thirdparty/catch.hpp>
|
||||||
|
|
||||||
|
#include "integral_image.h"
|
||||||
|
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
bool are_equal(const integral_image::Mat &lhv, const integral_image::Mat &rhv)
|
||||||
|
{
|
||||||
|
auto diff = (lhv != rhv);
|
||||||
|
return cv::countNonZero(diff) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST_CASE("Integral image of an empty Mat is an empty Mat")
|
||||||
|
{
|
||||||
|
using namespace integral_image;
|
||||||
|
|
||||||
|
Mat input;
|
||||||
|
auto output = integral_image_openmp(input);
|
||||||
|
|
||||||
|
REQUIRE(output.data == NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Integral image of a zero-valued Mat is the same sized zero-valued Mat")
|
||||||
|
{
|
||||||
|
using namespace integral_image;
|
||||||
|
|
||||||
|
Mat input = Mat::zeros(20, 18);
|
||||||
|
auto output = integral_image_openmp(input);
|
||||||
|
REQUIRE(are_equal(input, output));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("")
|
||||||
|
{
|
||||||
|
using namespace integral_image;
|
||||||
|
|
||||||
|
double input_values[] = {0, 1, 2, 3, 4, 5};
|
||||||
|
double expected_output_values[] = {0., 1., 2., 6., 6., 15.};
|
||||||
|
|
||||||
|
Mat input(3, 2, input_values);
|
||||||
|
Mat expected_output(3, 2, expected_output_values);
|
||||||
|
|
||||||
|
auto output = integral_image_openmp(input);
|
||||||
|
REQUIRE(are_equal(expected_output, output));
|
||||||
|
}
|
||||||
73
main.cpp
73
main.cpp
|
|
@ -4,13 +4,19 @@
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
|
|
||||||
#include <omp.h>
|
|
||||||
|
|
||||||
#include <opencv2/opencv.hpp>
|
#include <opencv2/opencv.hpp>
|
||||||
|
|
||||||
|
|
||||||
|
#include "integral_image.h"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
|
||||||
|
const std::string OUTPUT_FILE_POSTFIX = ".integral";
|
||||||
|
|
||||||
|
|
||||||
struct Arguments
|
struct Arguments
|
||||||
{
|
{
|
||||||
int thread_number = 0;
|
int thread_number = 0;
|
||||||
|
|
@ -36,7 +42,7 @@ Arguments parse_arguments(int argc, char **argv)
|
||||||
ss.str(argv[i]);
|
ss.str(argv[i]);
|
||||||
ss >> args.thread_number;
|
ss >> args.thread_number;
|
||||||
|
|
||||||
if (ss.bad() || args.thread_number < 0)
|
if (ss.fail() || args.thread_number < 0)
|
||||||
throw std::invalid_argument("thread number is invalid");
|
throw std::invalid_argument("thread number is invalid");
|
||||||
}
|
}
|
||||||
else if (IMAGE_ARGUMENT == argv[i]) {
|
else if (IMAGE_ARGUMENT == argv[i]) {
|
||||||
|
|
@ -55,17 +61,37 @@ Arguments parse_arguments(int argc, char **argv)
|
||||||
return args;
|
return args;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::ostream &operator<<(std::ostream &os, const integral_image::Mat &mat)
|
||||||
|
{
|
||||||
|
//The specification doesn't require any precision or data presentation format,
|
||||||
|
//so here we use a default one
|
||||||
|
|
||||||
|
if (mat.data) {
|
||||||
|
|
||||||
|
for (size_t row = 0; row < mat.rows; ++row) {
|
||||||
|
|
||||||
|
os << mat[row][0];
|
||||||
|
for (size_t col = 0; col < mat.cols; ++col)
|
||||||
|
os << ' ' << mat[row][col];
|
||||||
|
|
||||||
|
os << '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
os << std::endl;
|
||||||
|
|
||||||
|
return os;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
int main(int argc, char **argv)
|
int main(int argc, char **argv)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
|
||||||
Arguments args = parse_arguments(argc, argv);
|
Arguments args = parse_arguments(argc, argv);
|
||||||
|
|
||||||
if (0 == args.thread_number)
|
|
||||||
args.thread_number = omp_get_max_threads();
|
|
||||||
|
|
||||||
for (const auto &file_name : args.file_names) {
|
for (const auto &file_name : args.file_names) {
|
||||||
|
|
||||||
|
|
@ -75,11 +101,46 @@ int main(int argc, char **argv)
|
||||||
std::cerr << "Image file " << file_name << " is absent or damaged, skipping" << std::endl;
|
std::cerr << "Image file " << file_name << " is absent or damaged, skipping" << std::endl;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<cv::Mat> channels(mat.channels());
|
||||||
|
cv::split(mat, &channels[0]);
|
||||||
|
mat.release();
|
||||||
|
|
||||||
|
|
||||||
|
auto output_file_name = file_name + OUTPUT_FILE_POSTFIX;
|
||||||
|
std::fstream fs(output_file_name, std::ios_base::out);
|
||||||
|
if (fs.bad()) {
|
||||||
|
|
||||||
|
std::cerr << "Can't open output file " << output_file_name << " , skipping" << std::endl;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto &channel : channels) {
|
||||||
|
|
||||||
|
using namespace integral_image;
|
||||||
|
|
||||||
|
//The specification implicitly assumes that an output consists of floating-point values.
|
||||||
|
//Since the reasons of that decision are unknown, we convert data to 'double' format before
|
||||||
|
//calculations. Pros: no overflow, cons: precision loss.
|
||||||
|
Mat float_channel;
|
||||||
|
channel.convertTo(float_channel, CV_64FC1);
|
||||||
|
|
||||||
|
float_channel = integral_image_openmp(float_channel, args.thread_number);
|
||||||
|
|
||||||
|
fs << float_channel;
|
||||||
|
if (fs.fail()) {
|
||||||
|
|
||||||
|
std::cerr << "Failed to write data to file " << output_file_name << std::endl;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (const std::invalid_argument &e) {
|
catch (const std::invalid_argument &e) {
|
||||||
|
|
||||||
std::cerr << "Invalid argument: " << e.what() << std::endl;
|
std::cerr << "Invalid argument: " << e.what() << '\n'
|
||||||
|
<< "Usage: " << argv[0] << " [-t threads] [-i image_file_name]..." << std::endl;
|
||||||
|
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
13922
thirdparty/catch.hpp
vendored
Normal file
13922
thirdparty/catch.hpp
vendored
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue