From 52d916a7a092f0ef324250d732ecf4acecfac83f Mon Sep 17 00:00:00 2001 From: Aleksandr Pertovsky Date: Tue, 16 Mar 2021 23:22:54 +0300 Subject: [PATCH] Add image reader (#9) --- CMakeLists.txt | 2 + include/deepworks/image_reader.hpp | 13 +++ src/CMakeLists.txt | 16 ++++ src/io/image_reader.cpp | 149 +++++++++++++++++++++++++++++ src/runtime/tensor.cpp | 12 ++- tests/unit/CMakeLists.txt | 1 + tests/unit/test_image_reader.cpp | 86 +++++++++++++++++ tests/unit/test_tensor.cpp | 10 +- tests/unit/test_utils.hpp | 22 +++++ 9 files changed, 301 insertions(+), 10 deletions(-) create mode 100644 include/deepworks/image_reader.hpp create mode 100644 src/io/image_reader.cpp create mode 100644 tests/unit/test_image_reader.cpp create mode 100644 tests/unit/test_utils.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 6e95accf6b20fc..e28867da871afe 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/include/deepworks/image_reader.hpp b/include/deepworks/image_reader.hpp new file mode 100644 index 00000000000000..0dba42f6f76035 --- /dev/null +++ b/include/deepworks/image_reader.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include + +namespace deepworks { + +class Tensor; + +namespace io { +deepworks::Tensor ReadImage(std::string_view); +} // namespace io + +} // namespace deepworks diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 986506968bb5c0..1e52b900dd4b15 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -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/") @@ -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) diff --git a/src/io/image_reader.cpp b/src/io/image_reader.cpp new file mode 100644 index 00000000000000..0390d463d8bdd3 --- /dev/null +++ b/src/io/image_reader.cpp @@ -0,0 +1,149 @@ +#include +#include +#include +#include "util/assert.hpp" + +#ifdef HAVE_JPEG +#include +#endif + +#ifdef HAVE_PNG +#include +#include + +#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(cinfo.output_width); + int height = static_cast(cinfo.output_height); + int channels = static_cast(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(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(png_get_image_width(png_ptr, info_ptr)); + int height = static_cast(png_get_image_height(png_ptr, info_ptr)); + int channels = static_cast(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 {}; +} +} diff --git a/src/runtime/tensor.cpp b/src/runtime/tensor.cpp index dda3f04be96e85..fae4edcfde5783 100644 --- a/src/runtime/tensor.cpp +++ b/src/runtime/tensor.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -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; @@ -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(), diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index f6751189904678..4e4a6318637946 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -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}) diff --git a/tests/unit/test_image_reader.cpp b/tests/unit/test_image_reader.cpp new file mode 100644 index 00000000000000..4701653163f90f --- /dev/null +++ b/tests/unit/test_image_reader.cpp @@ -0,0 +1,86 @@ +#include +#include + +#include +#include +#include +#include + +#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(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(*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); +} diff --git a/tests/unit/test_tensor.cpp b/tests/unit/test_tensor.cpp index 79f2e65d7387f0..14398d1871c1dc 100644 --- a/tests/unit/test_tensor.cpp +++ b/tests/unit/test_tensor.cpp @@ -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) { @@ -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) { @@ -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)); } } diff --git a/tests/unit/test_utils.hpp b/tests/unit/test_utils.hpp new file mode 100644 index 00000000000000..ce4b097ec6ae42 --- /dev/null +++ b/tests/unit/test_utils.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include + +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