Skip to content

Commit

Permalink
Add image reader (#9)
Browse files Browse the repository at this point in the history
  • Loading branch information
apertovs authored Mar 16, 2021
1 parent 4bc988b commit 52d916a
Show file tree
Hide file tree
Showing 9 changed files with 301 additions and 10 deletions.
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
########################################
option(BUILD_TESTS "Build deepworks with tests" ON)
option(WITH_EIGEN "Build deepworks with eigen backend" ON)
option(WITH_PNG "Build deepworks with libpng" ON)
option(WITH_JPEG "Build deepworks with libjpeg" ON)

add_subdirectory(thirdparty)
add_subdirectory(src)
Expand Down
13 changes: 13 additions & 0 deletions include/deepworks/image_reader.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#pragma once

#include <string>

namespace deepworks {

class Tensor;

namespace io {
deepworks::Tensor ReadImage(std::string_view);
} // namespace io

} // namespace deepworks
16 changes: 16 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ set(SRC_FILES
${CMAKE_CURRENT_LIST_DIR}/runtime/cpu/kernels/kernels.cpp

${CMAKE_CURRENT_LIST_DIR}/metrics.cpp

${CMAKE_CURRENT_LIST_DIR}/initializers.cpp

# I/O
${CMAKE_CURRENT_LIST_DIR}/io/image_reader.cpp
)

set(DeepWorks_INCLUDE_DIR "${PROJECT_SOURCE_DIR}/include/")
Expand All @@ -33,4 +37,16 @@ if (WITH_EIGEN)
target_link_libraries(${PROJECT_NAME} PRIVATE Eigen3::Eigen)
endif(WITH_EIGEN)

if (WITH_PNG)
find_package(PNG REQUIRED)
target_link_libraries(${PROJECT_NAME} PRIVATE PNG::PNG)
target_compile_definitions(${PROJECT_NAME} PRIVATE HAVE_PNG)
endif(WITH_PNG)

if (WITH_JPEG)
find_package(JPEG REQUIRED)
target_link_libraries(${PROJECT_NAME} PRIVATE JPEG::JPEG)
target_compile_definitions(${PROJECT_NAME} PRIVATE HAVE_JPEG)
endif(WITH_JPEG)

target_link_libraries(${PROJECT_NAME} PRIVATE ade)
149 changes: 149 additions & 0 deletions src/io/image_reader.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
#include <stdexcept>
#include <sstream>
#include <deepworks/tensor.hpp>
#include "util/assert.hpp"

#ifdef HAVE_JPEG
#include <jpeglib.h>
#endif

#ifdef HAVE_PNG
#include <png.h>
#include <algorithm>

#endif

namespace {
bool IsPngFile(std::string_view path) {
return path.substr(path.find_last_of(".") + 1) == "png";
}

bool IsJpegFile(std::string_view path) {
return path.substr(path.find_last_of(".") + 1) == "jpg" || path.substr(path.find_last_of(".") + 1) == "jpeg";
}

deepworks::Tensor ReadJpegFile(std::string_view path) {
#ifdef HAVE_JPEG
struct jpeg_decompress_struct cinfo{};
struct jpeg_error_mgr err{};
FILE *infile = fopen(path.data(), "rb");

if (!infile) {
std::stringstream fmt;
fmt << "can't open file: " << path;
DeepWorks_Assert(false && fmt.str().c_str());
}

cinfo.err = jpeg_std_error(&err);
jpeg_create_decompress(&cinfo);
jpeg_stdio_src(&cinfo, infile);

(void) jpeg_read_header(&cinfo, true);
(void) jpeg_start_decompress(&cinfo);

size_t row_stride = cinfo.output_width * cinfo.output_components;
JSAMPARRAY buffer = (*cinfo.mem->alloc_sarray)((j_common_ptr) &cinfo, JPOOL_IMAGE, row_stride, 1);

int width = static_cast<int>(cinfo.output_width);
int height = static_cast<int>(cinfo.output_height);
int channels = static_cast<int>(cinfo.output_components);

deepworks::Tensor out_tensor(deepworks::Shape{height, width, channels});

deepworks::Tensor::Type *dst_data = out_tensor.data();
deepworks::Strides tensor_strides = out_tensor.strides();

const size_t elements_per_h_channel = width * channels;
size_t h = 0;
// it's works if we have default hwc layout for tensor
while (cinfo.output_scanline < cinfo.output_height) {
(void) jpeg_read_scanlines(&cinfo, buffer, 1);
std::copy_n(buffer[0], elements_per_h_channel, &dst_data[h * tensor_strides[0]]);
++h;
}

(void) jpeg_finish_decompress(&cinfo);
jpeg_destroy_decompress(&cinfo);
fclose(infile);
return out_tensor;
#else
DeepWorks_Assert(false && "Couldn't find LIBJPEG");
return {};
#endif
}

deepworks::Tensor ReadPngFile(std::string_view path) {
#ifdef HAVE_PNG
FILE *infile = fopen(path.data(), "rb");
if (!infile) {
std::stringstream fmt;
fmt << "can't open file: " << path;
DeepWorks_Assert(false && fmt.str().c_str());
}
char header[8];
fread(header, 1, 8, infile);
if (png_sig_cmp(reinterpret_cast<png_const_bytep>(header), 0, 8)) {
std::stringstream fmt;
fmt << "File is not a PNG file: " << path;
DeepWorks_Assert(false && fmt.str().c_str());
}
png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);

png_infop info_ptr = png_create_info_struct(png_ptr);

DeepWorks_Assert(!setjmp(png_jmpbuf(png_ptr)) && "Error during init_io");

png_init_io(png_ptr, infile);
png_set_sig_bytes(png_ptr, 8);

png_read_info(png_ptr, info_ptr);

int width = static_cast<int>(png_get_image_width(png_ptr, info_ptr));
int height = static_cast<int>(png_get_image_height(png_ptr, info_ptr));
int channels = static_cast<int>(png_get_channels(png_ptr, info_ptr));

png_read_update_info(png_ptr, info_ptr);

deepworks::Tensor out_tensor(deepworks::Shape{height, width, channels});

deepworks::Tensor::Type *dst_data = out_tensor.data();
deepworks::Strides tensor_strides = out_tensor.strides();
/* read file */
DeepWorks_Assert(!setjmp(png_jmpbuf(png_ptr)) && "Error during read image");

auto *row_pointers = (png_bytep *) malloc(sizeof(png_bytep) * height);
for (int y = 0; y < height; y++) {
row_pointers[y] = (png_byte *) malloc(png_get_rowbytes(png_ptr, info_ptr));
}
png_read_image(png_ptr, row_pointers);
// it's works if we have default hwc layout for tensor
const size_t elements_per_h_channel = width * channels;
for (int h = 0; h < height; ++h) {
std::copy_n(row_pointers[h], elements_per_h_channel, &dst_data[h * tensor_strides[0]]);
}
for (int y = 0; y < height; y++) {
free(row_pointers[y]);
}
free(row_pointers);
fclose(infile);
png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
return out_tensor;
#else
DeepWorks_Assert(false && "Couldn't find LIBPNG");
return {};
#endif
}
}

namespace deepworks::io {
Tensor ReadImage(std::string_view path) {
if (IsPngFile(path)) {
return ReadPngFile(path);
}
if (IsJpegFile(path)) {
return ReadJpegFile(path);
}
DeepWorks_Assert(false && "image format not supported");
return {};
}
}
12 changes: 7 additions & 5 deletions src/runtime/tensor.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include <deepworks/tensor.hpp>
#include <util/assert.hpp>

#include <numeric>
#include <algorithm>
Expand All @@ -11,14 +12,14 @@ Tensor::Descriptor::Descriptor(const Shape &shape) : m_shape(shape) {

void Tensor::Descriptor::copyTo(Tensor::Descriptor &descriptor) {
if (this == &descriptor) {
throw std::runtime_error("Tensor cannot copy itself.");
DeepWorks_Assert(false && "Tensor cannot copy itself.");
}
if (m_shape != descriptor.m_shape || m_strides != descriptor.m_strides) {
throw std::runtime_error("Copy to another layout isn't supported.");
DeepWorks_Assert(false && "Copy to another layout isn't supported.");
}

if (descriptor.m_data == nullptr) {
throw std::runtime_error("copyTo: Output tensor should be allocated.");
DeepWorks_Assert(false && "copyTo: Output tensor should be allocated.");
}

m_shape = descriptor.m_shape;
Expand All @@ -32,10 +33,11 @@ void Tensor::Descriptor::allocate(const Shape &shape) {
return dim < 0;
});
if (have_negative_dim) {
throw std::runtime_error("Cannot allocate tensor dynamic shape.");
DeepWorks_Assert(false && "Cannot allocate tensor dynamic shape.");

}
if (m_data != nullptr) {
throw std::runtime_error("Tensor already allocated, cannot allocate twice.");
DeepWorks_Assert(false && "Tensor already allocated, cannot allocate twice.");
}
m_shape = shape;
m_total = std::accumulate(shape.begin(),
Expand Down
1 change: 1 addition & 0 deletions tests/unit/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ include_directories(${gtest_SOURCE_DIR}/include ${gtest_SOURCE_DIR})
file(GLOB TEST_SRC_FILES ${PROJECT_SOURCE_DIR}/tests/unit/*.cpp)

add_executable(${TEST_NAME} ${TEST_SRC_FILES})
add_definitions(-DTEST_DATA_PATH=\"${PROJECT_SOURCE_DIR}/tests/testdata\")

target_link_libraries(${TEST_NAME} gtest gtest_main)
target_link_libraries(${TEST_NAME} ${PROJECT_NAME})
86 changes: 86 additions & 0 deletions tests/unit/test_image_reader.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
#include <gtest/gtest.h>
#include <fstream>

#include <deepworks/shape.hpp>
#include <deepworks/tensor.hpp>
#include <deepworks/image_reader.hpp>
#include <numeric>

#include "test_utils.hpp"

namespace {
// the reference image data was written in H, W, C loop
deepworks::Tensor GetTensorFromBinary(std::istream &expected_stream, const deepworks::Shape& shape) {
deepworks::Tensor tensor(shape);
const size_t total_elements = tensor.total();
auto it = std::istreambuf_iterator<char>(expected_stream);
// copy works only when the layout of the reference and tested tensor matches

auto* tensor_dst = tensor.data();
for (size_t index = 0; index < total_elements; ++index) {
tensor_dst[index] = static_cast<uint8_t>(*it);
++it;
}
return tensor;
}
}

TEST(ImageReader, ReadRGBPng) {
std::string image_path = deepworks::testutils::GetTestDataPath();
image_path += "/image/lenna.png";
std::string reference_path = deepworks::testutils::GetTestDataPath();
reference_path += "/image/lenna_reference.bin";

const deepworks::Tensor actual_tensor = deepworks::io::ReadImage(image_path);
const auto expected_shape = deepworks::Shape{512, 512, 3};

std::fstream stream(reference_path, std::ios_base::binary | std::ios_base::in);
auto expected_tensor = GetTensorFromBinary(stream, expected_shape);

deepworks::testutils::AssertTensorEqual(actual_tensor, expected_tensor);
}

TEST(ImageReader, ReadTransparentPng) {
std::string image_path = deepworks::testutils::GetTestDataPath();
image_path += "/image/transparent.png";
std::string reference_path = deepworks::testutils::GetTestDataPath();
reference_path += "/image/transparent_reference.bin";

const deepworks::Tensor actual_tensor = deepworks::io::ReadImage(image_path);
const auto expected_shape = deepworks::Shape{600, 800, 4};

std::fstream stream(reference_path, std::ios_base::binary | std::ios_base::in);
auto expected_tensor = GetTensorFromBinary(stream, expected_shape);

deepworks::testutils::AssertTensorEqual(actual_tensor, expected_tensor);
}

TEST(ImageReader, ReadRGBJPEG) {
std::string image_path = deepworks::testutils::GetTestDataPath();
image_path += "/image/sunset.jpg";
std::string reference_path = deepworks::testutils::GetTestDataPath();
reference_path += "/image/sunset_reference.bin";

const deepworks::Tensor actual_tensor = deepworks::io::ReadImage(image_path);
const auto expected_shape = deepworks::Shape{600, 800, 3};

std::fstream stream(reference_path, std::ios_base::binary | std::ios_base::in);
auto expected_tensor = GetTensorFromBinary(stream, expected_shape);

deepworks::testutils::AssertTensorEqual(actual_tensor, expected_tensor);
}

TEST(ImageReader, ReadGrayScaleJPEG) {
std::string image_path = deepworks::testutils::GetTestDataPath();
image_path += "/image/grayscale.jpg";
std::string reference_path = deepworks::testutils::GetTestDataPath();
reference_path += "/image/grayscale_reference.bin";

const deepworks::Tensor actual_tensor = deepworks::io::ReadImage(image_path);
const auto expected_shape = deepworks::Shape{600, 600, 1};

std::fstream stream(reference_path, std::ios_base::binary | std::ios_base::in);
auto expected_tensor = GetTensorFromBinary(stream, expected_shape);

deepworks::testutils::AssertTensorEqual(actual_tensor, expected_tensor);
}
10 changes: 5 additions & 5 deletions tests/unit/test_tensor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ TEST(TensorTest, DefaultCtor) {
EXPECT_EQ(tensor.shape(), deepworks::Shape{});
EXPECT_EQ(tensor.strides(), deepworks::Strides{});
EXPECT_EQ(tensor.data(), nullptr);
ASSERT_THROW(tensor.copyTo(tensor), std::runtime_error);
ASSERT_ANY_THROW(tensor.copyTo(tensor));
}

TEST(TensorTest, Reassignment) {
Expand All @@ -73,10 +73,10 @@ TEST(TensorTest, Reassignment) {
}

TEST(TensorTest, DynamicShape) {
ASSERT_THROW(deepworks::Tensor src_tensor({-1, 3, 16, 16}), std::runtime_error);
ASSERT_ANY_THROW(deepworks::Tensor src_tensor({-1, 3, 16, 16}));

deepworks::Tensor tensor;
ASSERT_THROW(tensor.allocate({1, 1, 1, -1, 1}), std::runtime_error);
ASSERT_ANY_THROW(tensor.allocate({1, 1, 1, -1, 1}));
}

TEST(TensorTest, CopyTo) {
Expand All @@ -100,11 +100,11 @@ TEST(TensorTest, CopyTo) {
}

deepworks::Tensor non_empty_tensor({1, 3, 16, 16});
ASSERT_THROW(src_tensor.copyTo(non_empty_tensor), std::runtime_error);
ASSERT_ANY_THROW(src_tensor.copyTo(non_empty_tensor));
}
{
deepworks::Tensor src_tensor({1, 3, 224, 224});
deepworks::Tensor dst_tensor;
ASSERT_THROW(dst_tensor.copyTo(src_tensor), std::runtime_error);
ASSERT_ANY_THROW(dst_tensor.copyTo(src_tensor));
}
}
22 changes: 22 additions & 0 deletions tests/unit/test_utils.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#pragma once

#include <string>

namespace deepworks::testutils {
std::string GetTestDataPath() {
return TEST_DATA_PATH;
}

void AssertTensorEqual(const deepworks::Tensor& actual, const deepworks::Tensor& expected) {
ASSERT_EQ(actual.shape() , expected.shape());
ASSERT_EQ(actual.strides(), expected.strides());

auto* actual_p = actual.data();
auto* expected_p = expected.data();

auto total = actual.total();
for (int i = 0; i < total; ++i) {
ASSERT_FLOAT_EQ(expected_p[i], actual_p[i]);
}
}
} // deepworks::testutils

0 comments on commit 52d916a

Please sign in to comment.