Skip to content

Commit

Permalink
Backport some PRs to release/15.x (#7132)
Browse files Browse the repository at this point in the history
* Require Python 3.8+ in CMake build (#7117)
* Add range-checking to Buffer objects in Python (#7128)
* Fix Python buffer handling (#7125)
* add 'reverse_axes' options to Buffer conversions (#7127)
  • Loading branch information
steven-johnson authored Oct 28, 2022
1 parent efb46cd commit 23af11d
Show file tree
Hide file tree
Showing 16 changed files with 333 additions and 154 deletions.
3 changes: 3 additions & 0 deletions python_bindings/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ set(PYBIND11_VER 2.6.2 CACHE STRING "The pybind11 version to use (or download)")
# part, so only requesting Module avoids failures when Embed is not
# available, as is the case in the manylinux Docker images.
find_package(Python3 REQUIRED COMPONENTS Interpreter Development.Module)
if (Python3_VERSION VERSION_LESS "3.8")
message(FATAL_ERROR "Halide requires Python v3.8 or later, but found ${Python3_VERSION}.")
endif ()

if (PYBIND11_USE_FETCHCONTENT)
include(FetchContent)
Expand Down
1 change: 1 addition & 0 deletions python_bindings/src/halide/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ list(TRANSFORM native_sources PREPEND "halide_/")
set(python_sources
__init__.py
_generator_helpers.py
imageio.py
)

# It is technically still possible for a user to override the LIBRARY_OUTPUT_DIRECTORY by setting
Expand Down
90 changes: 57 additions & 33 deletions python_bindings/src/halide/halide_/PyBuffer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,22 @@ std::string type_to_format_descriptor(const Type &type) {
return std::string();
}

void check_out_of_bounds(Buffer<> &buf, const std::vector<int> &pos) {
const int d = buf.dimensions();
if ((size_t)pos.size() != (size_t)d) {
throw py::value_error("Incorrect number of dimensions.");
}
for (int i = 0; i < d; i++) {
const auto &dim = buf.dim(i);
if (pos[i] < dim.min() || pos[i] > dim.max()) {
// Try to mimic the wording of similar errors in NumPy
std::ostringstream o;
o << "index " << pos[i] << " is out of bounds for axis " << i << " with min=" << dim.min() << ", extent=" << dim.extent();
throw py::index_error(o.str());
}
}
}

} // namespace

Type format_descriptor_to_type(const std::string &fd) {
Expand Down Expand Up @@ -196,10 +212,7 @@ Type format_descriptor_to_type(const std::string &fd) {
}

py::object buffer_getitem_operator(Buffer<> &buf, const std::vector<int> &pos) {
if ((size_t)pos.size() != (size_t)buf.dimensions()) {
throw py::value_error("Incorrect number of dimensions.");
}
// TODO: add bounds checking?
check_out_of_bounds(buf, pos);

#define HANDLE_BUFFER_TYPE(TYPE) \
if (buf.type() == type_of<TYPE>()) \
Expand Down Expand Up @@ -229,10 +242,8 @@ py::object buffer_getitem_operator(Buffer<> &buf, const std::vector<int> &pos) {
namespace {

py::object buffer_setitem_operator(Buffer<> &buf, const std::vector<int> &pos, const py::object &value) {
if ((size_t)pos.size() != (size_t)buf.dimensions()) {
throw py::value_error("Incorrect number of dimensions.");
}
// TODO: add bounds checking?
check_out_of_bounds(buf, pos);

#define HANDLE_BUFFER_TYPE(TYPE) \
if (buf.type() == type_of<TYPE>()) \
return py::cast(buf.as<TYPE>()(pos.data()) = value_cast<TYPE>(value));
Expand Down Expand Up @@ -264,8 +275,8 @@ py::object buffer_setitem_operator(Buffer<> &buf, const std::vector<int> &pos, c
class PyBuffer : public Buffer<> {
py::buffer_info info;

PyBuffer(py::buffer_info &&info, const std::string &name)
: Buffer<>(pybufferinfo_to_halidebuffer(info), name),
PyBuffer(py::buffer_info &&info, const std::string &name, bool reverse_axes)
: Buffer<>(pybufferinfo_to_halidebuffer(info, reverse_axes), name),
info(std::move(info)) {
}

Expand All @@ -278,8 +289,8 @@ class PyBuffer : public Buffer<> {
: Buffer<>(b), info() {
}

PyBuffer(const py::buffer &buffer, const std::string &name)
: PyBuffer(buffer.request(/*writable*/ true), name) {
PyBuffer(const py::buffer &buffer, const std::string &name, bool reverse_axes)
: PyBuffer(buffer.request(/*writable*/ true), name, reverse_axes) {
// Default to setting host-dirty on any PyBuffer we create from an existing py::buffer;
// this allows (e.g.) code like
//
Expand All @@ -299,6 +310,30 @@ class PyBuffer : public Buffer<> {
~PyBuffer() override = default;
};

py::buffer_info to_buffer_info(Buffer<> &b, bool reverse_axes = true) {
if (b.data() == nullptr) {
throw py::value_error("Cannot convert a Buffer<> with null host ptr to a Python buffer.");
}

const int d = b.dimensions();
const int bytes = b.type().bytes();
std::vector<Py_ssize_t> shape(d), strides(d);
for (int i = 0; i < d; i++) {
const int dst_axis = reverse_axes ? (d - i - 1) : i;
shape[dst_axis] = (Py_ssize_t)b.raw_buffer()->dim[i].extent;
strides[dst_axis] = (Py_ssize_t)b.raw_buffer()->dim[i].stride * (Py_ssize_t)bytes;
}

return py::buffer_info(
b.data(), // Pointer to buffer
bytes, // Size of one scalar
type_to_format_descriptor(b.type()), // Python struct-style format descriptor
d, // Number of dimensions
shape, // Buffer dimensions
strides // Strides (in bytes) for each index
);
}

} // namespace

void define_buffer(py::module &m) {
Expand All @@ -317,31 +352,12 @@ void define_buffer(py::module &m) {
// Note that this allows us to convert a Buffer<> to any buffer-like object in Python;
// most notably, we can convert to an ndarray by calling numpy.array()
.def_buffer([](Buffer<> &b) -> py::buffer_info {
if (b.data() == nullptr) {
throw py::value_error("Cannot convert a Buffer<> with null host ptr to a Python buffer.");
}

const int d = b.dimensions();
const int bytes = b.type().bytes();
std::vector<Py_ssize_t> shape, strides;
for (int i = 0; i < d; i++) {
shape.push_back((Py_ssize_t)b.raw_buffer()->dim[i].extent);
strides.push_back((Py_ssize_t)(b.raw_buffer()->dim[i].stride * bytes));
}

return py::buffer_info(
b.data(), // Pointer to buffer
bytes, // Size of one scalar
type_to_format_descriptor(b.type()), // Python struct-style format descriptor
d, // Number of dimensions
shape, // Buffer dimensions
strides // Strides (in bytes) for each index
);
return to_buffer_info(b, /*reverse_axes*/ true);
})

// This allows us to use any buffer-like python entity to create a Buffer<>
// (most notably, an ndarray)
.def(py::init_alias<py::buffer, const std::string &>(), py::arg("buffer"), py::arg("name") = "")
.def(py::init_alias<py::buffer, const std::string &, bool>(), py::arg("buffer"), py::arg("name") = "", py::arg("reverse_axes") = true)
.def(py::init_alias<>())
.def(py::init_alias<const Buffer<> &>())
.def(py::init([](Type type, const std::vector<int> &sizes, const std::string &name) -> Buffer<> {
Expand Down Expand Up @@ -404,6 +420,14 @@ void define_buffer(py::module &m) {

.def("copy", &Buffer<>::copy)
.def("copy_from", &Buffer<>::copy_from<void, Buffer<>::AnyDims>)
.def("reverse_axes", [](Buffer<> &b) -> Buffer<> {
const int d = b.dimensions();
std::vector<int> order;
for (int i = 0; i < b.dimensions(); i++) {
order.push_back(d - i - 1);
}
return b.transposed(order);
})

.def("add_dimension", (void(Buffer<>::*)()) & Buffer<>::add_dimension)

Expand Down
12 changes: 8 additions & 4 deletions python_bindings/src/halide/halide_/PyBuffer.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,28 @@ py::object buffer_getitem_operator(Buffer<> &buf, const std::vector<int> &pos);
template<typename T = void,
int Dims = AnyDims,
int InClassDimStorage = (Dims == AnyDims ? 4 : std::max(Dims, 1))>
Halide::Runtime::Buffer<T, Dims, InClassDimStorage> pybufferinfo_to_halidebuffer(const py::buffer_info &info) {
Halide::Runtime::Buffer<T, Dims, InClassDimStorage> pybufferinfo_to_halidebuffer(const py::buffer_info &info, bool reverse_axes) {
const Type t = format_descriptor_to_type(info.format);
halide_dimension_t *dims = (halide_dimension_t *)alloca(info.ndim * sizeof(halide_dimension_t));
_halide_user_assert(dims);
for (int i = 0; i < info.ndim; i++) {
if (INT_MAX < info.shape[i] || INT_MAX < (info.strides[i] / t.bytes())) {
throw py::value_error("Out of range dimensions in buffer conversion.");
}
dims[i] = {0, (int32_t)info.shape[i], (int32_t)(info.strides[i] / t.bytes())};
// Halide's default indexing convention is col-major (the most rapidly varying index comes first);
// Numpy's default is row-major (most rapidly varying comes last).
// We usually want to reverse the order so that most-varying comes first.
const int dst_axis = reverse_axes ? (info.ndim - i - 1) : i;
dims[dst_axis] = {0, (int32_t)info.shape[i], (int32_t)(info.strides[i] / t.bytes())};
}
return Halide::Runtime::Buffer<T, Dims, InClassDimStorage>(t, info.ptr, (int)info.ndim, dims);
}

template<typename T = void,
int Dims = AnyDims,
int InClassDimStorage = (Dims == AnyDims ? 4 : std::max(Dims, 1))>
Halide::Runtime::Buffer<T, Dims, InClassDimStorage> pybuffer_to_halidebuffer(const py::buffer &pyb, bool writable) {
return pybufferinfo_to_halidebuffer(pyb.request(writable));
Halide::Runtime::Buffer<T, Dims, InClassDimStorage> pybuffer_to_halidebuffer(const py::buffer &pyb, bool writable, bool reverse_axes) {
return pybufferinfo_to_halidebuffer(pyb.request(writable), reverse_axes);
}

} // namespace PythonBindings
Expand Down
5 changes: 4 additions & 1 deletion python_bindings/src/halide/halide_/PyCallable.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,10 @@ class PyCallable {
argv[slot] = b.raw_buffer();
} else {
const bool writable = c_arg.is_output();
buffers.buffers[slot] = pybuffer_to_halidebuffer<void, AnyDims, MaxFastDimensions>(cast_to<py::buffer>(value), writable);
const bool reverse_axes = true;
buffers.buffers[slot] =
pybuffer_to_halidebuffer<void, AnyDims, MaxFastDimensions>(
cast_to<py::buffer>(value), writable, reverse_axes);
argv[slot] = buffers.buffers[slot].raw_buffer();
}
cci[slot] = Callable::make_buffer_qcci();
Expand Down
73 changes: 73 additions & 0 deletions python_bindings/src/halide/imageio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
try:
import imageio.v2 as imageio
except:
import imageio
import numpy


def is_interleaved(im):
"""If the given ndarray is 3-dimensional and appears to have an interleaved
layout, return True. Otherwise, return False."""

# Assume that 'interleaved' will only apply to channels <= 4
return im.ndim == 3 and im.strides[2] == 1 and im.shape[2] in [1, 2, 3, 4]


def _as_interleaved(im):
"""If the given ndarray is 3-dimensional and appears to be planar layout,
return a view that is in interleaved form, leaving the input unchanged.
Otherwise, return the image ndarray unchanged.
Note that this call must be used with care, as the returnee may or may
not be a copy."""
if im.ndim == 3 and not is_interleaved(im):
return numpy.moveaxis(im, 0, 2)
else:
return im


def _as_planar(im):
"""If the given ndarray is 3-dimensional and appears to be interleaved
layout, return a view that is in planar form, leaving the input
unchanged. Otherwise, return the image ndarray unchanged.
Note that this call must be used with care, as the returnee may or may
not be a copy."""
if is_interleaved(im):
return numpy.moveaxis(im, 2, 0)
else:
return im


def copy_to_interleaved(im):
"""If the given ndarray is 3-dimensional and appears to be planar
layout, return a copy that is in interleaved form. Otherwise, return
an unchanged copy of the input. Note that this call will always return
a copy, leaving the input unchanged."""
return _as_interleaved(im).copy()


def copy_to_planar(im):
"""If the given ndarray is 3-dimensional and appears to be interleaved
layout, return a copy that is in planar form. Otherwise, return
an unchanged copy of the input. Note that this call will always return
a copy, leaving the input unchanged."""
return _as_planar(im).copy()


def imread(uri, format=None, **kwargs):
"""halide.imageio.imread is a thin wrapper around imagio.imread,
except that for 3-dimensional images that appear to be interleaved,
the result is converted to a planar layout before returning."""
return copy_to_planar(imageio.imread(uri, format, **kwargs))


def imwrite(uri, im, format=None, **kwargs):
"""halide.imageio.imwrite is a thin wrapper around imagio.imwrite,
except that for 3-dimensional images that appear to be planar,
a temporary interleaved copy of the input is made, which is used for
writing."""

# We can use _as_interleaved() here to save a possible copy; since the
# caller will never see the possibly-a-copy value, there should be no
# risk of possibly-different behavior between cases that need converting
# and cases that don't.
imageio.imwrite(uri, _as_interleaved(im), format, **kwargs)
8 changes: 3 additions & 5 deletions python_bindings/test/apps/bilateral_grid_shell.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from bilateral_grid_Adams2019 import bilateral_grid_Adams2019
from bilateral_grid_Li2018 import bilateral_grid_Li2018
from bilateral_grid_Mullapudi2016 import bilateral_grid_Mullapudi2016
import imageio
import halide.imageio
import numpy as np
import os
import sys
Expand All @@ -24,9 +24,7 @@ def main():
r_sigma = float(sys.argv[3])
timing_iterations = int(sys.argv[4])

assert os.path.exists(input_path), "Could not find %s" % input_path

input_buf_u8 = imageio.imread(input_path)
input_buf_u8 = halide.imageio.imread(input_path)
assert input_buf_u8.dtype == np.uint8
# Convert to float32
input_buf = input_buf_u8.astype(np.float32)
Expand Down Expand Up @@ -54,7 +52,7 @@ def main():

output_buf *= 255.0
output_buf_u8 = output_buf.astype(np.uint8)
imageio.imsave(output_path, output_buf_u8)
halide.imageio.imwrite(output_path, output_buf_u8)

print("Success!")
sys.exit(0)
Expand Down
20 changes: 7 additions & 13 deletions python_bindings/test/apps/blur.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import halide as hl

import numpy as np
import imageio
import halide.imageio
import os.path

# Return the directory to look in for test images:
Expand Down Expand Up @@ -44,13 +44,10 @@ def get_blur(input):

def get_input_data():
image_path = os.path.join(apps_images_dir(), "rgb.png")
assert os.path.exists(image_path), \
"Could not find %s" % image_path
rgb_data = imageio.imread(image_path)
print("rgb_data", type(rgb_data), rgb_data.shape, rgb_data.dtype)
rgb_data = halide.imageio.imread(image_path)

grey_data = np.mean(rgb_data, axis=2, dtype=np.float32).astype(rgb_data.dtype)
input_data = np.copy(grey_data, order="F")
grey_data = np.mean(rgb_data, axis=0, dtype=np.float32).astype(rgb_data.dtype)
input_data = np.copy(grey_data)

return input_data

Expand All @@ -65,20 +62,17 @@ def main():
input_image = hl.Buffer(input_data)
input.set(input_image)

output_data = np.empty(input_data.shape, dtype=input_data.dtype, order="F")
output_data = np.empty(input_data.shape, dtype=input_data.dtype)
output_image = hl.Buffer(output_data)

print("input_image", input_image)
print("output_image", output_image)

# do the actual computation
blur.realize(output_image)

# save results
input_path = os.path.join(apps_output_dir(), "blur_input.png")
output_path = os.path.join(apps_output_dir(), "blur_result.png")
imageio.imsave(input_path, input_data)
imageio.imsave(output_path, output_data)
halide.imageio.imwrite(input_path, input_data)
halide.imageio.imwrite(output_path, output_data)
print("\nblur realized on output image.",
"Result saved at", output_path,
"( input data copy at", input_path, ")")
Expand Down
Loading

0 comments on commit 23af11d

Please sign in to comment.