A working solution
This commit is contained in:
parent
9016f8461e
commit
196957f3d0
8 changed files with 14213 additions and 10 deletions
|
|
@ -1,6 +1,9 @@
|
|||
cmake_minimum_required(VERSION 2.8)
|
||||
|
||||
set(PROJECT "integral_image")
|
||||
set(PROJECT_TESTS tests)
|
||||
|
||||
include(CTest)
|
||||
|
||||
find_package(OpenCV REQUIRED)
|
||||
|
||||
|
|
@ -10,11 +13,27 @@ if (OPENMP_FOUND)
|
|||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${OpenMP_CXX_FLAGS}")
|
||||
endif()
|
||||
|
||||
|
||||
set(SOURCES
|
||||
main.cpp
|
||||
integral_image.cpp
|
||||
)
|
||||
|
||||
add_executable(${PROJECT} ${SOURCES})
|
||||
target_link_libraries(${PROJECT}
|
||||
${OpenCV_LIBRARIES}
|
||||
)
|
||||
|
||||
target_link_libraries(${PROJECT} LINK_PUBLIC
|
||||
${OpenCV_LIBRARIES})
|
||||
set(TEST_SOURCES
|
||||
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 <stdexcept>
|
||||
|
||||
#include <omp.h>
|
||||
|
||||
#include <opencv2/opencv.hpp>
|
||||
|
||||
|
||||
#include "integral_image.h"
|
||||
|
||||
|
||||
|
||||
namespace {
|
||||
|
||||
|
||||
const std::string OUTPUT_FILE_POSTFIX = ".integral";
|
||||
|
||||
|
||||
struct Arguments
|
||||
{
|
||||
int thread_number = 0;
|
||||
|
|
@ -36,7 +42,7 @@ Arguments parse_arguments(int argc, char **argv)
|
|||
ss.str(argv[i]);
|
||||
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");
|
||||
}
|
||||
else if (IMAGE_ARGUMENT == argv[i]) {
|
||||
|
|
@ -55,17 +61,37 @@ Arguments parse_arguments(int argc, char **argv)
|
|||
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)
|
||||
{
|
||||
try {
|
||||
|
||||
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) {
|
||||
|
||||
|
|
@ -75,11 +101,46 @@ int main(int argc, char **argv)
|
|||
std::cerr << "Image file " << file_name << " is absent or damaged, skipping" << std::endl;
|
||||
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) {
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
|||
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