Commit f7e943a7 authored by Baptiste Esteban's avatar Baptiste Esteban
Browse files

Add runtime conversion between ndbuffer_image and numpy array

parent 5e76699d
......@@ -23,6 +23,10 @@ endif ()
find_package(FreeImage REQUIRED)
set(PYLENE_PYTHON YES CACHE BOOL "Set to NO to disable building python bindings")
if (PYLENE_PYTHON)
find_package(pybind11)
endif(PYLENE_PYTHON)
# CONFIGURE COMPILER LAUNCHERS
......
......@@ -93,6 +93,24 @@ target_sources(Pylene PRIVATE
src/morpho/filters2d.cpp
)
if (pybind11_FOUND AND PYLENE_PYTHON)
message(STATUS "Building python bindings")
# To make Pylene linkable to the pylena python module
set_target_properties(Pylene PROPERTIES POSITION_INDEPENDENT_CODE TRUE)
# Pylena python module
pybind11_add_module(pylena)
target_link_libraries(pylena PRIVATE Pylene::Pylene ImagePath)
target_sources(pylena PRIVATE src/python/image_cast.cpp
src/python/io.cpp
src/python/module.cpp
src/python/numpy_format.cpp
src/python/utils.cpp)
target_compile_features(pylena PUBLIC cxx_std_20)
elseif(NOT pybind11_FOUND AND PYLENE_PYTHON)
message(STATUS "Pybind11 missing. Skipping python bindings build...")
endif()
# Compiler configurations
target_compile_features(Pylene PUBLIC cxx_std_20)
......
......@@ -243,6 +243,11 @@ namespace mln
const details::ndbuffer_image_info_t* __info() const { return this; }
const axis_info_t& __axes(int i) const { return m_axes[i]; }
std::byte* __buf() const { return m_buffer; }
public:
/// Access to the shared_ptr holding the data
// SHOULD NOT BE USED EXCEPT FOR PYTHON BINDINGS
std::shared_ptr<internal::ndbuffer_image_data>& __data();
};
......
#pragma once
#include <mln/core/image/ndbuffer_image.hpp>
#include <pybind11/numpy.h>
#include <pybind11/pybind11.h>
namespace pln
{
mln::ndbuffer_image from_numpy(pybind11::array arr);
pybind11::array to_numpy(mln::ndbuffer_image);
}
namespace pybind11::detail
{
template <>
struct type_caster<mln::ndbuffer_image>
{
PYBIND11_TYPE_CASTER(mln::ndbuffer_image, _("numpy.ndarray"));
bool load(handle h, bool)
{
pybind11::array arr = reinterpret_borrow<pybind11::array>(h);
value = pln::from_numpy(arr);
return true;
}
static handle cast(mln::ndbuffer_image img, return_value_policy, handle)
{
return pln::to_numpy(img).inc_ref();
}
};
}
\ No newline at end of file
#pragma once
#include <mln/python/image_cast.hpp>
#include <string>
namespace pln
{
void imsave(const std::string& filename, const mln::ndbuffer_image& img);
void def_io_module(pybind11::module_& m);
}
\ No newline at end of file
#pragma once
#include <mln/core/image_format.hpp>
#include <pybind11/pybind11.h>
namespace pln
{
/* mln -> numpy sample type */
std::string get_sample_type(mln::sample_type_id);
/* numpy -> mln sample type */
mln::sample_type_id get_sample_type(const std::string&);
}
\ No newline at end of file
#pragma once
#include <mln/python/image_cast.hpp>
namespace pln
{
void def_utils_module(pybind11::module_& m);
}
\ No newline at end of file
......@@ -624,5 +624,10 @@ namespace mln
return out;
}
std::shared_ptr<internal::ndbuffer_image_data>& __ndbuffer_image<void, -1>::__data()
{
return m_data;
}
} // namespace mln
#include <mln/core/image/private/ndbuffer_image_data.hpp>
#include <mln/python/image_cast.hpp>
#include <mln/python/numpy_format.hpp>
#include <vector>
namespace pln
{
mln::ndbuffer_image from_numpy(pybind11::array arr)
{
auto base = arr.base();
const auto info = arr.request();
const auto type = get_sample_type(info.format);
const bool is_rgb8 = info.ndim == 3 && info.shape[2] == 3 && type == mln::sample_type_id::UINT8;
const auto pdim = info.ndim - (is_rgb8 ? 1 : 0);
std::vector<int> size(pdim);
std::vector<std::ptrdiff_t> strides(pdim);
for (auto d = 0; d < pdim; d++)
{
size[d] = info.shape[pdim - d - 1];
strides[d] = info.strides[pdim - d - 1];
}
const auto sample_type = is_rgb8 ? mln::sample_type_id::RGB8 : type;
auto res = mln::ndbuffer_image::from_buffer(reinterpret_cast<std::byte*>(info.ptr),
sample_type,
pdim,
size.data(),
strides.data());
if (base && pybind11::isinstance<mln::internal::ndbuffer_image_data>(base))
res.__data() = pybind11::cast<std::shared_ptr<mln::internal::ndbuffer_image_data>>(base);
return res;
}
pybind11::array to_numpy(mln::ndbuffer_image img)
{
auto data = pybind11::handle();
if (img.__data()) data = pybind11::cast(img.__data()).inc_ref();
/* For the moment, restrict RGB8 image to 2D image */
const bool is_rgb8 = img.pdim() == 2 && img.sample_type() == mln::sample_type_id::RGB8;
const auto ndim = img.pdim() + (is_rgb8 ? 1 : 0);
std::vector<std::size_t> strides(ndim, 1);
std::vector<std::size_t> shapes(ndim, 3);
for (auto d = 0; d < img.pdim(); d++)
{
strides[d] = img.byte_stride(img.pdim() - d - 1);
shapes[d] = img.size(img.pdim() - d - 1);
}
return pybind11::array(pybind11::buffer_info(img.buffer(),
mln::get_sample_type_id_traits(img.sample_type()).size(),
get_sample_type(img.sample_type()),
ndim,
shapes,
strides)
, data /* To keep data alive until the end of the life of the numpy array*/);
}
}
\ No newline at end of file
#include <mln/io/imread.hpp>
#include <mln/io/private/freeimage_plugin.hpp>
#include <mln/io/private/io.hpp>
#include <mln/python/io.hpp>
namespace pln
{
void imsave(const std::string& filename, const mln::ndbuffer_image& img)
{
mln::io::internal::freeimage_writer_plugin p;
mln::io::internal::save(img, &p, filename.c_str());
}
void def_io_module(pybind11::module_& m)
{
auto io_m = m.def_submodule("io");
io_m.def("imread", [](const std::string& filename) { return mln::io::imread(filename); });
io_m.def("imsave", &imsave);
}
}
\ No newline at end of file
#include <mln/core/image/private/ndbuffer_image_data.hpp>
#include <mln/python/image_cast.hpp>
#include <mln/python/io.hpp>
#include <mln/python/utils.hpp>
#include <pybind11/pybind11.h>
#include <memory>
namespace pln
{
namespace py = pybind11;
PYBIND11_MODULE(pylena, m)
{
py::class_<mln::internal::ndbuffer_image_data, std::shared_ptr<mln::internal::ndbuffer_image_data>>(m, "ndbuffer_image_data");
def_io_module(m);
def_utils_module(m);
}
}
\ No newline at end of file
#include <mln/python/numpy_format.hpp>
namespace pln
{
namespace details
{
template <mln::sample_type_id t>
using numpy_desc = pybind11::format_descriptor<typename mln::sample_type_id_traits<t>::type>;
}
std::string get_sample_type(mln::sample_type_id type)
{
switch (type)
{
case mln::sample_type_id::INT8: return details::numpy_desc<mln::sample_type_id::INT8>::format();
case mln::sample_type_id::INT16: return details::numpy_desc<mln::sample_type_id::INT16>::format();
case mln::sample_type_id::INT32: return details::numpy_desc<mln::sample_type_id::INT32>::format();
case mln::sample_type_id::INT64: return details::numpy_desc<mln::sample_type_id::INT64>::format();
case mln::sample_type_id::UINT8: return details::numpy_desc<mln::sample_type_id::UINT8>::format();
case mln::sample_type_id::UINT16: return details::numpy_desc<mln::sample_type_id::UINT16>::format();
case mln::sample_type_id::UINT32: return details::numpy_desc<mln::sample_type_id::UINT32>::format();
case mln::sample_type_id::UINT64: return details::numpy_desc<mln::sample_type_id::UINT64>::format();
case mln::sample_type_id::FLOAT: return details::numpy_desc<mln::sample_type_id::FLOAT>::format();
case mln::sample_type_id::DOUBLE: return details::numpy_desc<mln::sample_type_id::DOUBLE>::format();
case mln::sample_type_id::BOOL: return details::numpy_desc<mln::sample_type_id::BOOL>::format();
case mln::sample_type_id::RGB8: return details::numpy_desc<mln::sample_type_id::UINT8>::format();
default: return std::string("");
}
}
mln::sample_type_id get_sample_type(const std::string& s)
{
if (s.size() > 1)
return mln::sample_type_id::OTHER;
char type = s[0];
switch (type)
{
case pybind11::format_descriptor<std::int8_t>::value[0]: return mln::sample_type_id::INT8;
case pybind11::format_descriptor<std::int16_t>::value[0]: return mln::sample_type_id::INT16;
case pybind11::format_descriptor<std::int32_t>::value[0]: return mln::sample_type_id::INT32;
case 'l':
case pybind11::format_descriptor<std::int64_t>::value[0]: return mln::sample_type_id::INT64;
case pybind11::format_descriptor<std::uint8_t>::value[0]: return mln::sample_type_id::UINT8;
case pybind11::format_descriptor<std::uint16_t>::value[0]: return mln::sample_type_id::UINT16;
case pybind11::format_descriptor<std::uint32_t>::value[0]: return mln::sample_type_id::UINT32;
case 'L':
case pybind11::format_descriptor<std::uint64_t>::value[0]: return mln::sample_type_id::UINT64;
case pybind11::format_descriptor<float>::value[0]: return mln::sample_type_id::FLOAT;
case pybind11::format_descriptor<double>::value[0]: return mln::sample_type_id::DOUBLE;
case pybind11::format_descriptor<bool>::value[0]: return mln::sample_type_id::BOOL;
default: return mln::sample_type_id::OTHER;
}
}
}
\ No newline at end of file
#include <fixtures/ImagePath/image_path.hpp>
#include <mln/python/utils.hpp>
namespace pln
{
void def_utils_module(pybind11::module_& m)
{
auto utils_m = m.def_submodule("utils");
utils_m.def("get_image_filename", [](const std::string& name){ return fixtures::ImagePath::concat_with_filename(name); });
}
}
\ No newline at end of file
......@@ -43,3 +43,6 @@ add_subdirectory(morpho)
add_subdirectory(labeling)
add_subdirectory(contrib)
add_subdirectory(bp)
if (pybind11_FOUND AND PYLENE_PYTHON)
add_subdirectory(python)
endif()
\ No newline at end of file
function(add_python_test filename)
file(COPY ${filename} DESTINATION ${CMAKE_CURRENT_BINARY_DIR})
endfunction(add_python_test)
# PYTHON FILES TO MOVE HERE
add_python_test(test_pylena_numpy.py)
add_test(NAME test_python
COMMAND ${PYTHON_EXECUTABLE} -m unittest discover
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/tests/python)
\ No newline at end of file
import unittest
import numpy as np
from imageio import imread
import sys
sys.path.append("../../pylene")
import pylena as pln
class TestNumpyImage(unittest.TestCase):
def test_lena_grayscale(self):
img = pln.io.imread(pln.utils.get_image_filename("lena.pgm"))
ref = imread(pln.utils.get_image_filename("lena.pgm"))
self.assertTrue(np.all(img == ref))
def test_lena_color(self):
img = pln.io.imread(pln.utils.get_image_filename("lena.ppm"))
ref = imread(pln.utils.get_image_filename("lena.ppm"))
self.assertTrue(np.all(img == ref))
if __name__ == "__main__":
unittest.main()
\ No newline at end of file
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment