A working solution

This commit is contained in:
Dmitry Kokorin 2018-09-05 20:26:58 +03:00
parent 9016f8461e
commit 196957f3d0
8 changed files with 14213 additions and 10 deletions

View file

@ -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
View file

@ -0,0 +1,2 @@
DOXYFILE_ENCODING = UTF-8
PROJECT_NAME = "integral_image"

View file

@ -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
View 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
View 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
View 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));
}

View file

@ -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

File diff suppressed because it is too large Load diff