From bf48b4540583805b56a6286db13ceb30f2b0635b Mon Sep 17 00:00:00 2001 From: Sameer Sheorey <41028320+ssheorey@users.noreply.github.com> Date: Sat, 11 May 2024 23:45:03 -0700 Subject: [PATCH] Robust image I/O (#6765) - Use file signatures (beginning bytes) instead of filename extension (png/jpg) to select decoder. jpg files with png extension and vice versa are read correctly. - Catch libjpeg errors and report as normal Open3D exceptions. Change Open3D warnings to errors (exceptions) on jpeg I/O error. - Simplify demo_scene example - Clear image on read error - Fix for github release macOS bug. --- .github/workflows/documentation.yml | 3 +- .github/workflows/macos.yml | 2 - cpp/open3d/io/ImageIO.cpp | 56 +++-- cpp/open3d/io/ImageIO.h | 9 +- cpp/open3d/io/file_format/FileJPG.cpp | 258 ++++++++++++-------- cpp/open3d/io/file_format/FilePNG.cpp | 2 + cpp/open3d/t/io/ImageIO.cpp | 66 ++--- cpp/open3d/t/io/file_format/FileJPG.cpp | 176 +++++++------ cpp/open3d/t/io/file_format/FilePNG.cpp | 4 +- examples/python/visualization/demo_scene.py | 45 +--- 10 files changed, 350 insertions(+), 271 deletions(-) diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 6e4c21bc61f..00580af33d9 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -92,7 +92,7 @@ jobs: tar_file="open3d-${GITHUB_SHA}-docs.tar.gz" rm -rf ${tar_file} # Docs in docs/_out/html - tar -C docs/_out -czf ${tar_file} html + tar -C docs/_out -cvzf ${tar_file} html echo "Waiting for other release assets..." this_sha=$(echo ${GITHUB_SHA} | cut -c 1-6) @@ -110,7 +110,6 @@ jobs: echo "\nAll assets ready. Removing release assets except from last 3 commits: ${last_shas[@]}" release_assets=($(gh release view main-devel --json assets --jq '.assets[] | .name')) last_shas=($(git log --pretty=format:%h --abbrev-commit -n 3)) - echo "Waiting for all assets to be ready..." for relass in "${release_assets[@]}"; do found=false for last_sha in "${last_shas[@]}"; do diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 20af6fdd767..f0234f975ee 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -168,8 +168,6 @@ jobs: path: open3d-*-app-macosx-10_15-universal2.zip if-no-files-found: error - - name: Checkout source code - uses: actions/checkout@v4 - name: Update viewer devel release if: ${{ github.ref == 'refs/heads/main' }} env: diff --git a/cpp/open3d/io/ImageIO.cpp b/cpp/open3d/io/ImageIO.cpp index a4126df14e6..c00361a449a 100644 --- a/cpp/open3d/io/ImageIO.cpp +++ b/cpp/open3d/io/ImageIO.cpp @@ -7,24 +7,25 @@ #include "open3d/io/ImageIO.h" +#include +#include #include #include "open3d/utility/FileSystem.h" #include "open3d/utility/Logging.h" namespace open3d { +namespace io { namespace { -using namespace io; -static const std::unordered_map< - std::string, - std::function> - file_extension_to_image_read_function{ - {"png", ReadImageFromPNG}, - {"jpg", ReadImageFromJPG}, - {"jpeg", ReadImageFromJPG}, - }; +using signature_decoder_t = + std::pair>; +static const std::array signature_decoder_list{ + {{"\x89\x50\x4e\x47\xd\xa\x1a\xa", ReadImageFromPNG}, + {"\xFF\xD8\xFF", ReadImageFromJPG}}}; +static constexpr uint8_t MAX_SIGNATURE_LEN = 8; static const std::unordered_map< std::string, @@ -34,11 +35,8 @@ static const std::unordered_map< {"jpg", WriteImageToJPG}, {"jpeg", WriteImageToJPG}, }; - } // unnamed namespace -namespace io { - std::shared_ptr CreateImageFromFile( const std::string &filename) { auto image = std::make_shared(); @@ -47,21 +45,27 @@ std::shared_ptr CreateImageFromFile( } bool ReadImage(const std::string &filename, geometry::Image &image) { - std::string filename_ext = - utility::filesystem::GetFileExtensionInLowerCase(filename); - if (filename_ext.empty()) { - utility::LogWarning( - "Read geometry::Image failed: missing file extension."); - return false; - } - auto map_itr = file_extension_to_image_read_function.find(filename_ext); - if (map_itr == file_extension_to_image_read_function.end()) { - utility::LogWarning( - "Read geometry::Image failed: file extension {} unknown", - filename_ext); - return false; + std::string signature_buffer(MAX_SIGNATURE_LEN, 0); + std::ifstream file(filename, std::ios::binary); + file.read(&signature_buffer[0], MAX_SIGNATURE_LEN); + std::string err_msg; + if (!file) { + err_msg = "Read geometry::Image failed for file {}. I/O error."; + } else { + file.close(); + for (const auto &signature_decoder : signature_decoder_list) { + if (signature_buffer.compare(0, signature_decoder.first.size(), + signature_decoder.first) == 0) { + return signature_decoder.second(filename, image); + } + } + err_msg = + "Read geometry::Image failed for file {}. Unknown file " + "signature, only PNG and JPG are supported."; } - return map_itr->second(filename, image); + image.Clear(); + utility::LogWarning(err_msg.c_str(), filename); + return false; } bool WriteImage(const std::string &filename, diff --git a/cpp/open3d/io/ImageIO.h b/cpp/open3d/io/ImageIO.h index 3c6b62be3d8..45b0a3f71ed 100644 --- a/cpp/open3d/io/ImageIO.h +++ b/cpp/open3d/io/ImageIO.h @@ -69,11 +69,18 @@ bool WriteImageToJPG(const std::string &filename, const geometry::Image &image, int quality = kOpen3DImageIODefaultQuality); -/// The general entrance for reading an Image from memory +/// Read a PNG image from memory. +/// \param image_data_ptr the pointer to image data in memory. +/// \param image_data_size the size of image data in memory. +/// \return return true if the read function is successful, false otherwise. bool ReadPNGFromMemory(const unsigned char *image_data_ptr, size_t image_data_size, geometry::Image &image); +/// Read a JPG image from memory. +/// \param image_data_ptr the pointer to image data in memory. +/// \param image_data_size the size of image data in memory. +/// \return return true if the read function is successful, false otherwise. bool ReadJPGFromMemory(const unsigned char *image_data_ptr, size_t image_data_size, geometry::Image &image); diff --git a/cpp/open3d/io/file_format/FileJPG.cpp b/cpp/open3d/io/file_format/FileJPG.cpp index 7de155575bf..cb9cf6760e9 100644 --- a/cpp/open3d/io/file_format/FileJPG.cpp +++ b/cpp/open3d/io/file_format/FileJPG.cpp @@ -17,6 +17,23 @@ namespace open3d { namespace io { +namespace { + +/// Convert libjpeg error messages to std::runtime_error. This prevents +/// libjpeg from exit() in case of errors. +void jpeg_error_throw(j_common_ptr p_cinfo) { + if (p_cinfo->is_decompressor) + jpeg_destroy_decompress( + reinterpret_cast(p_cinfo)); + else + jpeg_destroy_compress( + reinterpret_cast(p_cinfo)); + char buffer[JMSG_LENGTH_MAX]; + (*p_cinfo->err->format_message)(p_cinfo, buffer); + throw std::runtime_error(buffer); +} + +} // namespace bool ReadImageFromJPG(const std::string &filename, geometry::Image &image) { struct jpeg_decompress_struct cinfo; @@ -27,53 +44,64 @@ bool ReadImageFromJPG(const std::string &filename, geometry::Image &image) { if ((file_in = utility::filesystem::FOpen(filename, "rb")) == NULL) { utility::LogWarning("Read JPG failed: unable to open file: {}", filename); + image.Clear(); return false; } - cinfo.err = jpeg_std_error(&jerr); - jpeg_create_decompress(&cinfo); - jpeg_stdio_src(&cinfo, file_in); - jpeg_read_header(&cinfo, TRUE); - - // We only support two channel types: gray, and RGB. - int num_of_channels = 3; - int bytes_per_channel = 1; - switch (cinfo.jpeg_color_space) { - case JCS_RGB: - case JCS_YCbCr: - cinfo.out_color_space = JCS_RGB; - cinfo.out_color_components = 3; - num_of_channels = 3; - break; - case JCS_GRAYSCALE: - cinfo.jpeg_color_space = JCS_GRAYSCALE; - cinfo.out_color_components = 1; - num_of_channels = 1; - break; - case JCS_CMYK: - case JCS_YCCK: - default: - utility::LogWarning("Read JPG failed: color space not supported."); - jpeg_destroy_decompress(&cinfo); - fclose(file_in); - return false; - } - jpeg_start_decompress(&cinfo); - image.Prepare(cinfo.output_width, cinfo.output_height, num_of_channels, - bytes_per_channel); - int row_stride = cinfo.output_width * cinfo.output_components; - buffer = (*cinfo.mem->alloc_sarray)((j_common_ptr)&cinfo, JPOOL_IMAGE, - row_stride, 1); - uint8_t *pdata = image.data_.data(); - while (cinfo.output_scanline < cinfo.output_height) { - jpeg_read_scanlines(&cinfo, buffer, 1); - memcpy(pdata, buffer[0], row_stride); - pdata += row_stride; + try { + cinfo.err = jpeg_std_error(&jerr); + jerr.error_exit = jpeg_error_throw; + jpeg_create_decompress(&cinfo); + jpeg_stdio_src(&cinfo, file_in); + jpeg_read_header(&cinfo, TRUE); + + // We only support two channel types: gray, and RGB. + int num_of_channels = 3; + int bytes_per_channel = 1; + switch (cinfo.jpeg_color_space) { + case JCS_RGB: + case JCS_YCbCr: + cinfo.out_color_space = JCS_RGB; + cinfo.out_color_components = 3; + num_of_channels = 3; + break; + case JCS_GRAYSCALE: + cinfo.jpeg_color_space = JCS_GRAYSCALE; + cinfo.out_color_components = 1; + num_of_channels = 1; + break; + case JCS_CMYK: + case JCS_YCCK: + default: + utility::LogWarning( + "Read JPG failed: color space not supported."); + jpeg_destroy_decompress(&cinfo); + fclose(file_in); + image.Clear(); + return false; + } + jpeg_start_decompress(&cinfo); + image.Prepare(cinfo.output_width, cinfo.output_height, num_of_channels, + bytes_per_channel); + int row_stride = cinfo.output_width * cinfo.output_components; + buffer = (*cinfo.mem->alloc_sarray)((j_common_ptr)&cinfo, JPOOL_IMAGE, + row_stride, 1); + uint8_t *pdata = image.data_.data(); + while (cinfo.output_scanline < cinfo.output_height) { + jpeg_read_scanlines(&cinfo, buffer, 1); + memcpy(pdata, buffer[0], row_stride); + pdata += row_stride; + } + jpeg_finish_decompress(&cinfo); + jpeg_destroy_decompress(&cinfo); + fclose(file_in); + return true; + } catch (const std::runtime_error &err) { + fclose(file_in); + image.Clear(); + utility::LogWarning("libjpeg error: {}", err.what()); + return false; } - jpeg_finish_decompress(&cinfo); - jpeg_destroy_decompress(&cinfo); - fclose(file_in); - return true; } bool WriteImageToJPG(const std::string &filename, @@ -108,30 +136,37 @@ bool WriteImageToJPG(const std::string &filename, return false; } - cinfo.err = jpeg_std_error(&jerr); - jpeg_create_compress(&cinfo); - jpeg_stdio_dest(&cinfo, file_out); - cinfo.image_width = image.width_; - cinfo.image_height = image.height_; - cinfo.input_components = image.num_of_channels_; - cinfo.in_color_space = - (cinfo.input_components == 1 ? JCS_GRAYSCALE : JCS_RGB); - jpeg_set_defaults(&cinfo); - jpeg_set_quality(&cinfo, quality, TRUE); - jpeg_start_compress(&cinfo, TRUE); - int row_stride = image.width_ * image.num_of_channels_; - const uint8_t *pdata = image.data_.data(); - std::vector buffer(row_stride); - while (cinfo.next_scanline < cinfo.image_height) { - memcpy(buffer.data(), pdata, row_stride); - row_pointer[0] = buffer.data(); - jpeg_write_scanlines(&cinfo, row_pointer, 1); - pdata += row_stride; + try { + cinfo.err = jpeg_std_error(&jerr); + jerr.error_exit = jpeg_error_throw; + jpeg_create_compress(&cinfo); + jpeg_stdio_dest(&cinfo, file_out); + cinfo.image_width = image.width_; + cinfo.image_height = image.height_; + cinfo.input_components = image.num_of_channels_; + cinfo.in_color_space = + (cinfo.input_components == 1 ? JCS_GRAYSCALE : JCS_RGB); + jpeg_set_defaults(&cinfo); + jpeg_set_quality(&cinfo, quality, TRUE); + jpeg_start_compress(&cinfo, TRUE); + int row_stride = image.width_ * image.num_of_channels_; + const uint8_t *pdata = image.data_.data(); + std::vector buffer(row_stride); + while (cinfo.next_scanline < cinfo.image_height) { + memcpy(buffer.data(), pdata, row_stride); + row_pointer[0] = buffer.data(); + jpeg_write_scanlines(&cinfo, row_pointer, 1); + pdata += row_stride; + } + jpeg_finish_compress(&cinfo); + fclose(file_out); + jpeg_destroy_compress(&cinfo); + return true; + } catch (const std::runtime_error &err) { + fclose(file_out); + utility::LogWarning(err.what()); + return false; } - jpeg_finish_compress(&cinfo); - fclose(file_out); - jpeg_destroy_compress(&cinfo); - return true; } bool ReadJPGFromMemory(const unsigned char *image_data_ptr, @@ -141,48 +176,57 @@ bool ReadJPGFromMemory(const unsigned char *image_data_ptr, struct jpeg_error_mgr jerr; JSAMPARRAY buffer; - cinfo.err = jpeg_std_error(&jerr); - jpeg_create_decompress(&cinfo); - jpeg_mem_src(&cinfo, image_data_ptr, image_data_size); - jpeg_read_header(&cinfo, TRUE); - - // We only support two channel types: gray, and RGB. - int num_of_channels = 3; - int bytes_per_channel = 1; - switch (cinfo.jpeg_color_space) { - case JCS_RGB: - case JCS_YCbCr: - cinfo.out_color_space = JCS_RGB; - cinfo.out_color_components = 3; - num_of_channels = 3; - break; - case JCS_GRAYSCALE: - cinfo.jpeg_color_space = JCS_GRAYSCALE; - cinfo.out_color_components = 1; - num_of_channels = 1; - break; - case JCS_CMYK: - case JCS_YCCK: - default: - utility::LogWarning("Read JPG failed: color space not supported."); - jpeg_destroy_decompress(&cinfo); - return false; - } - jpeg_start_decompress(&cinfo); - image.Prepare(cinfo.output_width, cinfo.output_height, num_of_channels, - bytes_per_channel); - int row_stride = cinfo.output_width * cinfo.output_components; - buffer = (*cinfo.mem->alloc_sarray)((j_common_ptr)&cinfo, JPOOL_IMAGE, - row_stride, 1); - uint8_t *pdata = image.data_.data(); - while (cinfo.output_scanline < cinfo.output_height) { - jpeg_read_scanlines(&cinfo, buffer, 1); - memcpy(pdata, buffer[0], row_stride); - pdata += row_stride; + try { + cinfo.err = jpeg_std_error(&jerr); + jerr.error_exit = jpeg_error_throw; + jpeg_create_decompress(&cinfo); + jpeg_mem_src(&cinfo, image_data_ptr, image_data_size); + jpeg_read_header(&cinfo, TRUE); + + // We only support two channel types: gray, and RGB. + int num_of_channels = 3; + int bytes_per_channel = 1; + switch (cinfo.jpeg_color_space) { + case JCS_RGB: + case JCS_YCbCr: + cinfo.out_color_space = JCS_RGB; + cinfo.out_color_components = 3; + num_of_channels = 3; + break; + case JCS_GRAYSCALE: + cinfo.jpeg_color_space = JCS_GRAYSCALE; + cinfo.out_color_components = 1; + num_of_channels = 1; + break; + case JCS_CMYK: + case JCS_YCCK: + default: + utility::LogWarning( + "Read JPG failed: color space not supported."); + jpeg_destroy_decompress(&cinfo); + image.Clear(); + return false; + } + jpeg_start_decompress(&cinfo); + image.Prepare(cinfo.output_width, cinfo.output_height, num_of_channels, + bytes_per_channel); + int row_stride = cinfo.output_width * cinfo.output_components; + buffer = (*cinfo.mem->alloc_sarray)((j_common_ptr)&cinfo, JPOOL_IMAGE, + row_stride, 1); + uint8_t *pdata = image.data_.data(); + while (cinfo.output_scanline < cinfo.output_height) { + jpeg_read_scanlines(&cinfo, buffer, 1); + memcpy(pdata, buffer[0], row_stride); + pdata += row_stride; + } + jpeg_finish_decompress(&cinfo); + jpeg_destroy_decompress(&cinfo); + return true; + } catch (const std::runtime_error &err) { + image.Clear(); + utility::LogWarning(err.what()); + return false; } - jpeg_finish_decompress(&cinfo); - jpeg_destroy_decompress(&cinfo); - return true; } } // namespace io diff --git a/cpp/open3d/io/file_format/FilePNG.cpp b/cpp/open3d/io/file_format/FilePNG.cpp index 70832d8ee1e..55a7e410eda 100644 --- a/cpp/open3d/io/file_format/FilePNG.cpp +++ b/cpp/open3d/io/file_format/FilePNG.cpp @@ -106,6 +106,7 @@ bool ReadPNGFromMemory(const unsigned char *image_data_ptr, if (png_image_begin_read_from_memory(&pngimage, image_data_ptr, image_data_size) == 0) { utility::LogWarning("Read PNG failed: unable to parse header."); + image.Clear(); return false; } @@ -122,6 +123,7 @@ bool ReadPNGFromMemory(const unsigned char *image_data_ptr, if (png_image_finish_read(&pngimage, NULL, image.data_.data(), 0, NULL) == 0) { utility::LogWarning("PNG error: {}", pngimage.message); + image.Clear(); return false; } return true; diff --git a/cpp/open3d/t/io/ImageIO.cpp b/cpp/open3d/t/io/ImageIO.cpp index df1ca3625ce..d172ad16cb7 100644 --- a/cpp/open3d/t/io/ImageIO.cpp +++ b/cpp/open3d/t/io/ImageIO.cpp @@ -8,7 +8,9 @@ #include "open3d/t/io/ImageIO.h" #include +#include #include +#include #include #include @@ -23,14 +25,14 @@ namespace open3d { namespace t { namespace io { -static const std::unordered_map< - std::string, - std::function> - file_extension_to_image_read_function{ - {"png", ReadImageFromPNG}, - {"jpg", ReadImageFromJPG}, - {"jpeg", ReadImageFromJPG}, - }; +namespace { +using signature_decoder_t = + std::pair>; +static const std::array signature_decoder_list{ + {{"\x89\x50\x4e\x47\xd\xa\x1a\xa", ReadImageFromPNG}, + {"\xFF\xD8\xFF", ReadImageFromJPG}}}; +static constexpr uint8_t MAX_SIGNATURE_LEN = 8; static const std::unordered_map< std::string, @@ -40,6 +42,7 @@ static const std::unordered_map< {"jpg", WriteImageToJPG}, {"jpeg", WriteImageToJPG}, }; +} // namespace std::shared_ptr CreateImageFromFile( const std::string &filename) { @@ -49,21 +52,27 @@ std::shared_ptr CreateImageFromFile( } bool ReadImage(const std::string &filename, geometry::Image &image) { - std::string filename_ext = - utility::filesystem::GetFileExtensionInLowerCase(filename); - if (filename_ext.empty()) { - utility::LogWarning( - "Read geometry::Image failed: missing file extension."); - return false; - } - auto map_itr = file_extension_to_image_read_function.find(filename_ext); - if (map_itr == file_extension_to_image_read_function.end()) { - utility::LogWarning( - "Read geometry::Image failed: file extension {} unknown", - filename_ext); - return false; + std::string signature_buffer(MAX_SIGNATURE_LEN, 0); + std::ifstream file(filename, std::ios::binary); + file.read(&signature_buffer[0], MAX_SIGNATURE_LEN); + std::string err_msg; + if (!file) { + err_msg = "Read geometry::Image failed for file {}. I/O error."; + } else { + file.close(); + for (const auto &signature_decoder : signature_decoder_list) { + if (signature_buffer.compare(0, signature_decoder.first.size(), + signature_decoder.first) == 0) { + return signature_decoder.second(filename, image); + } + } + err_msg = + "Read geometry::Image failed for file {}. Unknown file " + "signature, only PNG and JPG are supported."; } - return map_itr->second(filename, image); + image.Clear(); + utility::LogWarning(err_msg.c_str(), filename); + return false; } bool WriteImage(const std::string &filename, @@ -99,7 +108,8 @@ DepthNoiseSimulator::DepthNoiseSimulator(const std::string &noise_model_path) { for (int i = 0; i < skip_first_n_lines; ++i) { if (!(line_buffer = file.ReadLine())) { utility::LogError( - "Read depth model failed: file {} is less than {} lines.", + "Read depth model failed: file {} is less than {} " + "lines.", noise_model_path, skip_first_n_lines); } } @@ -182,11 +192,11 @@ geometry::Image DepthNoiseSimulator::Simulate(const geometry::Image &im_src, geometry::kernel::TArrayIndexer dst_indexer(im_dst_tensor, 2); geometry::kernel::TArrayIndexer model_indexer(model_, 3); - // To match the original implementation, we try to keep the same variable - // names with reference to the original code. Compared to the original - // implementation, parallelization is done in im_dst_tensor per-pixel level, - // instead of per-image level. Check out the original code at: - // http://redwood-data.org/indoor/data/simdepth.py. + // To match the original implementation, we try to keep the same + // variable names with reference to the original code. Compared to the + // original implementation, parallelization is done in im_dst_tensor + // per-pixel level, instead of per-image level. Check out the original + // code at: http://redwood-data.org/indoor/data/simdepth.py. core::ParallelFor( core::Device("CPU:0"), width * height, [&] OPEN3D_DEVICE(int workload_idx) { diff --git a/cpp/open3d/t/io/file_format/FileJPG.cpp b/cpp/open3d/t/io/file_format/FileJPG.cpp index dc83fb3ad2e..e1830f5135f 100644 --- a/cpp/open3d/t/io/file_format/FileJPG.cpp +++ b/cpp/open3d/t/io/file_format/FileJPG.cpp @@ -19,6 +19,24 @@ namespace open3d { namespace t { namespace io { +namespace { + +/// Convert libjpeg error messages to std::runtime_error. This prevents +/// libjpeg from exit() in case of errors. +void jpeg_error_throw(j_common_ptr p_cinfo) { + if (p_cinfo->is_decompressor) + jpeg_destroy_decompress( + reinterpret_cast(p_cinfo)); + else + jpeg_destroy_compress( + reinterpret_cast(p_cinfo)); + char buffer[JMSG_LENGTH_MAX]; + (*p_cinfo->err->format_message)(p_cinfo, buffer); + throw std::runtime_error(buffer); +} + +} // namespace + bool ReadImageFromJPG(const std::string &filename, geometry::Image &image) { struct jpeg_decompress_struct cinfo; struct jpeg_error_mgr jerr; @@ -28,56 +46,67 @@ bool ReadImageFromJPG(const std::string &filename, geometry::Image &image) { if ((file_in = utility::filesystem::FOpen(filename, "rb")) == NULL) { utility::LogWarning("Read JPG failed: unable to open file: {}", filename); + image.Clear(); return false; } - cinfo.err = jpeg_std_error(&jerr); - jpeg_create_decompress(&cinfo); - jpeg_stdio_src(&cinfo, file_in); - jpeg_read_header(&cinfo, TRUE); - - // We only support two channel types: gray, and RGB. - int num_of_channels = 3; - switch (cinfo.jpeg_color_space) { - case JCS_RGB: - case JCS_YCbCr: - cinfo.out_color_space = JCS_RGB; - cinfo.out_color_components = 3; - num_of_channels = 3; - break; - case JCS_GRAYSCALE: - cinfo.jpeg_color_space = JCS_GRAYSCALE; - cinfo.out_color_components = 1; - num_of_channels = 1; - break; - case JCS_CMYK: - case JCS_YCCK: - default: - utility::LogWarning("Read JPG failed: color space not supported."); - jpeg_destroy_decompress(&cinfo); - fclose(file_in); - return false; - } - jpeg_start_decompress(&cinfo); - image.Clear(); - image.Reset(cinfo.output_height, cinfo.output_width, num_of_channels, - core::UInt8, image.GetDevice()); - - int row_stride = cinfo.output_width * cinfo.output_components; - buffer = (*cinfo.mem->alloc_sarray)((j_common_ptr)&cinfo, JPOOL_IMAGE, - row_stride, 1); - uint8_t *pdata = static_cast(image.GetDataPtr()); - - while (cinfo.output_scanline < cinfo.output_height) { - jpeg_read_scanlines(&cinfo, buffer, 1); - core::MemoryManager::MemcpyFromHost(pdata, image.GetDevice(), buffer[0], - row_stride * 1); - pdata += row_stride; + try { + cinfo.err = jpeg_std_error(&jerr); + jerr.error_exit = jpeg_error_throw; + jpeg_create_decompress(&cinfo); + jpeg_stdio_src(&cinfo, file_in); + jpeg_read_header(&cinfo, TRUE); + + // We only support two channel types: gray, and RGB. + int num_of_channels = 3; + switch (cinfo.jpeg_color_space) { + case JCS_RGB: + case JCS_YCbCr: + cinfo.out_color_space = JCS_RGB; + cinfo.out_color_components = 3; + num_of_channels = 3; + break; + case JCS_GRAYSCALE: + cinfo.jpeg_color_space = JCS_GRAYSCALE; + cinfo.out_color_components = 1; + num_of_channels = 1; + break; + case JCS_CMYK: + case JCS_YCCK: + default: + utility::LogWarning( + "Read JPG failed: color space not supported."); + jpeg_destroy_decompress(&cinfo); + fclose(file_in); + image.Clear(); + return false; + } + jpeg_start_decompress(&cinfo); + image.Clear(); + image.Reset(cinfo.output_height, cinfo.output_width, num_of_channels, + core::UInt8, image.GetDevice()); + + int row_stride = cinfo.output_width * cinfo.output_components; + buffer = (*cinfo.mem->alloc_sarray)((j_common_ptr)&cinfo, JPOOL_IMAGE, + row_stride, 1); + uint8_t *pdata = static_cast(image.GetDataPtr()); + + while (cinfo.output_scanline < cinfo.output_height) { + jpeg_read_scanlines(&cinfo, buffer, 1); + core::MemoryManager::MemcpyFromHost(pdata, image.GetDevice(), + buffer[0], row_stride * 1); + pdata += row_stride; + } + jpeg_finish_decompress(&cinfo); + jpeg_destroy_decompress(&cinfo); + fclose(file_in); + return true; + } catch (const std::runtime_error &err) { + fclose(file_in); + image.Clear(); + utility::LogWarning("libjpeg error: {}", err.what()); + return false; } - jpeg_finish_decompress(&cinfo); - jpeg_destroy_decompress(&cinfo); - fclose(file_in); - return true; } bool WriteImageToJPG(const std::string &filename, @@ -112,31 +141,38 @@ bool WriteImageToJPG(const std::string &filename, return false; } - cinfo.err = jpeg_std_error(&jerr); - jpeg_create_compress(&cinfo); - jpeg_stdio_dest(&cinfo, file_out); - cinfo.image_width = image.GetCols(); - cinfo.image_height = image.GetRows(); - cinfo.input_components = image.GetChannels(); - cinfo.in_color_space = - (cinfo.input_components == 1 ? JCS_GRAYSCALE : JCS_RGB); - jpeg_set_defaults(&cinfo); - jpeg_set_quality(&cinfo, quality, TRUE); - jpeg_start_compress(&cinfo, TRUE); - int row_stride = image.GetCols() * image.GetChannels(); - const uint8_t *pdata = static_cast(image.GetDataPtr()); - std::vector buffer(row_stride); - while (cinfo.next_scanline < cinfo.image_height) { - core::MemoryManager::MemcpyToHost(buffer.data(), pdata, - image.GetDevice(), row_stride * 1); - row_pointer[0] = buffer.data(); - jpeg_write_scanlines(&cinfo, row_pointer, 1); - pdata += row_stride; + try { + cinfo.err = jpeg_std_error(&jerr); + jerr.error_exit = jpeg_error_throw; + jpeg_create_compress(&cinfo); + jpeg_stdio_dest(&cinfo, file_out); + cinfo.image_width = image.GetCols(); + cinfo.image_height = image.GetRows(); + cinfo.input_components = image.GetChannels(); + cinfo.in_color_space = + (cinfo.input_components == 1 ? JCS_GRAYSCALE : JCS_RGB); + jpeg_set_defaults(&cinfo); + jpeg_set_quality(&cinfo, quality, TRUE); + jpeg_start_compress(&cinfo, TRUE); + int row_stride = image.GetCols() * image.GetChannels(); + const uint8_t *pdata = static_cast(image.GetDataPtr()); + std::vector buffer(row_stride); + while (cinfo.next_scanline < cinfo.image_height) { + core::MemoryManager::MemcpyToHost( + buffer.data(), pdata, image.GetDevice(), row_stride * 1); + row_pointer[0] = buffer.data(); + jpeg_write_scanlines(&cinfo, row_pointer, 1); + pdata += row_stride; + } + jpeg_finish_compress(&cinfo); + fclose(file_out); + jpeg_destroy_compress(&cinfo); + return true; + } catch (const std::runtime_error &err) { + fclose(file_out); + utility::LogWarning(err.what()); + return false; } - jpeg_finish_compress(&cinfo); - fclose(file_out); - jpeg_destroy_compress(&cinfo); - return true; } } // namespace io diff --git a/cpp/open3d/t/io/file_format/FilePNG.cpp b/cpp/open3d/t/io/file_format/FilePNG.cpp index 2497e2a71c8..8560bc8b431 100644 --- a/cpp/open3d/t/io/file_format/FilePNG.cpp +++ b/cpp/open3d/t/io/file_format/FilePNG.cpp @@ -41,6 +41,7 @@ bool ReadImageFromPNG(const std::string &filename, geometry::Image &image) { pngimage.version = PNG_IMAGE_VERSION; if (png_image_begin_read_from_file(&pngimage, filename.c_str()) == 0) { utility::LogWarning("Read PNG failed: unable to parse header."); + image.Clear(); return false; } @@ -64,6 +65,7 @@ bool ReadImageFromPNG(const std::string &filename, geometry::Image &image) { utility::LogWarning("Read PNG failed: unable to read file: {}", filename); utility::LogWarning("PNG error: {}", pngimage.message); + image.Clear(); return false; } return true; @@ -141,7 +143,7 @@ bool WriteImageToPNGInMemory(std::vector &buffer, buffer.resize(mem_bytes); if (png_image_write_to_memory(&pngimage, &buffer[0], &mem_bytes, 0, image.GetDataPtr(), 0, nullptr) == 0) { - utility::LogWarning("Unable to encode to encode to PNG in memory."); + utility::LogWarning("Unable to encode to PNG in memory."); return false; } return true; diff --git a/examples/python/visualization/demo_scene.py b/examples/python/visualization/demo_scene.py index cfb5d3d2393..b0662feadc7 100644 --- a/examples/python/visualization/demo_scene.py +++ b/examples/python/visualization/demo_scene.py @@ -6,33 +6,15 @@ # ---------------------------------------------------------------------------- """Demo scene demonstrating models, built-in shapes, and materials""" -import math import numpy as np -import os import open3d as o3d import open3d.visualization as vis -def convert_material_record(mat_record): - mat = vis.Material('defaultLit') - # Convert scalar parameters - mat.vector_properties['base_color'] = mat_record.base_color - mat.scalar_properties['metallic'] = mat_record.base_metallic - mat.scalar_properties['roughness'] = mat_record.base_roughness - mat.scalar_properties['reflectance'] = mat_record.base_reflectance - mat.texture_maps['albedo'] = o3d.t.geometry.Image.from_legacy( - mat_record.albedo_img) - mat.texture_maps['normal'] = o3d.t.geometry.Image.from_legacy( - mat_record.normal_img) - mat.texture_maps['ao_rough_metal'] = o3d.t.geometry.Image.from_legacy( - mat_record.ao_rough_metal_img) - return mat - - def create_scene(): - ''' - Creates the geometry and materials for the demo scene and returns a dictionary suitable for draw call - ''' + """Creates the geometry and materials for the demo scene and returns a + dictionary suitable for draw call + """ # Create some shapes for our scene a_cube = o3d.geometry.TriangleMesh.create_box(2, 4, @@ -47,7 +29,7 @@ def create_scene(): resolution=40, create_uv_map=True) a_sphere.compute_vertex_normals() - rotate_90 = o3d.geometry.get_rotation_matrix_from_xyz((-math.pi / 2, 0, 0)) + rotate_90 = o3d.geometry.get_rotation_matrix_from_xyz((-np.pi / 2, 0, 0)) a_sphere.rotate(rotate_90) a_sphere.translate((5, 2.4, 0)) a_sphere = o3d.t.geometry.TriangleMesh.from_legacy(a_sphere) @@ -68,17 +50,13 @@ def create_scene(): # Load an OBJ model for our scene helmet_data = o3d.data.FlightHelmetModel() helmet = o3d.io.read_triangle_model(helmet_data.path) - helmet_parts = [] - for m in helmet.meshes: - # m.mesh.paint_uniform_color((1.0, 0.75, 0.3)) - m.mesh.scale(10.0, (0.0, 0.0, 0.0)) - helmet_parts.append(m) + helmet_parts = o3d.t.geometry.TriangleMesh.from_triangle_mesh_model(helmet) # Create a ground plane ground_plane = o3d.geometry.TriangleMesh.create_box( 50.0, 0.1, 50.0, create_uv_map=True, map_texture_to_each_face=True) ground_plane.compute_triangle_normals() - rotate_180 = o3d.geometry.get_rotation_matrix_from_xyz((-math.pi, 0, 0)) + rotate_180 = o3d.geometry.get_rotation_matrix_from_xyz((-np.pi, 0, 0)) ground_plane.rotate(rotate_180) ground_plane.translate((-25.0, -0.1, -25.0)) ground_plane.paint_uniform_color((1, 1, 1)) @@ -157,12 +135,11 @@ def create_scene(): "geometry": a_sphere }] # Load the helmet - for part in helmet_parts: - name = part.mesh_name - tgeom = o3d.t.geometry.TriangleMesh.from_legacy(part.mesh) - tgeom.material = convert_material_record( - helmet.materials[part.material_idx]) - geoms.append({"name": name, "geometry": tgeom}) + for name, tmesh in helmet_parts.items(): + geoms.append({ + "name": name, + "geometry": tmesh.scale(10.0, (0.0, 0.0, 0.0)) + }) return geoms