Skip to content

Commit

Permalink
Merge pull request #157 from akash-akya/dev
Browse files Browse the repository at this point in the history
Add write_area_to_binary and get_pixel
  • Loading branch information
akash-akya committed Jul 26, 2024
2 parents eebfa29 + fc220c5 commit ccb6f75
Show file tree
Hide file tree
Showing 8 changed files with 273 additions and 34 deletions.
120 changes: 120 additions & 0 deletions c_src/vips_image.c
Original file line number Diff line number Diff line change
Expand Up @@ -875,3 +875,123 @@ ERL_NIF_TERM nif_image_write_to_binary(ErlNifEnv *env, int argc,
notify_consumed_timeslice(env, start, enif_monotonic_time(ERL_NIF_USEC));
return ret;
}

// Optimized version of fetching raw pixels for a region
ERL_NIF_TERM nif_image_write_area_to_binary(ErlNifEnv *env, int argc,
const ERL_NIF_TERM argv[]) {
ASSERT_ARGC(argc, 2);

VipsImage *image;
ErlNifTime start;
ERL_NIF_TERM list, head, ret;
void *bin;
size_t size;
guint list_length;
int params[6] = {0, 0, 0, 0, 0, 0};
int left, top, width, height, band_start, band_count;
VipsImage **t;

start = enif_monotonic_time(ERL_NIF_USEC);

if (!erl_term_to_g_object(env, argv[0], (GObject **)&image)) {
ret = make_error(env, "Failed to get VipsImage");
goto exit;
}

list = argv[1];

if (!enif_get_list_length(env, list, &list_length)) {
error("Failed to get list length");
ret = enif_make_badarg(env);
goto exit;
}

if (list_length != 6) {
error("Must pass 6 integer params");
ret = enif_make_badarg(env);
goto exit;
}

for (guint i = 0; i < 6; i++) {
if (!enif_get_list_cell(env, list, &head, &list)) {
ret = make_error(env, "Failed to get list entry");
goto exit;
}

if (!enif_get_int(env, head, &params[i])) {
ret = make_error(env, "Failed to get int");
goto exit;
}
}

left = params[0];
top = params[1];
width = params[2];
height = params[3];
band_start = params[4];
band_count = params[5];

if (left == -1)
left = 0;

if (top == -1)
top = 0;

if (width == -1)
width = vips_image_get_width(image);

if (height == -1)
height = vips_image_get_height(image);

if (band_start == -1)
band_start = 0;

if (band_count == -1)
band_count = vips_image_get_bands(image);

// vips operations checks boundary, this is just to get better error reporting
if (left + width > vips_image_get_width(image) ||
top + height > vips_image_get_height(image) || left < 0 || top < 0 ||
width <= 0 || height <= 0 ||
band_start + band_count > vips_image_get_bands(image) || band_start < 0 ||
band_count <= 0) {
error("Bad extract area, left: %d, top: %d, width: %d, height: %d, "
"band_start: %d, band_count: %d",
left, top, width, height, band_start, band_count);
vips_error_clear();
ret =
make_error(env, "Bad extract area. Ensure params are not out of bound");
goto exit;
}

t = (VipsImage **)vips_object_local_array((VipsObject *)image, 2);

if (vips_crop(image, &t[0], left, top, width, height, NULL) ||
vips_extract_band(t[0], &t[1], band_start, "n", band_count, NULL)) {
error("Failed to extract region and bands. error: %s", vips_error_buffer());
vips_error_clear();
ret = make_error(env, "Failed to extract region and bands");
goto exit;
}

bin = vips_image_write_to_memory(t[1], &size);

if (!bin) {
error("Failed to write extracted region to memory. error: %s",
vips_error_buffer());
vips_error_clear();
ret = make_error(env, "Failed to write extracted region to memory");
goto exit;
}

ret = make_ok(
env, enif_make_tuple5(env, to_binary_term(env, bin, size),
enif_make_int(env, vips_image_get_width(t[1])),
enif_make_int(env, vips_image_get_height(t[1])),
enif_make_int(env, vips_image_get_bands(t[1])),
enif_make_int(env, vips_image_get_format(t[1]))));

exit:
notify_consumed_timeslice(env, start, enif_monotonic_time(ERL_NIF_USEC));
return ret;
}
3 changes: 3 additions & 0 deletions c_src/vips_image.h
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,7 @@ ERL_NIF_TERM nif_image_new_from_binary(ErlNifEnv *env, int argc,

ERL_NIF_TERM nif_image_write_to_binary(ErlNifEnv *env, int argc,
const ERL_NIF_TERM argv[]);

ERL_NIF_TERM nif_image_write_area_to_binary(ErlNifEnv *env, int argc,
const ERL_NIF_TERM argv[]);
#endif
2 changes: 2 additions & 0 deletions c_src/vix.c
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ static ErlNifFunc nif_funcs[] = {
ERL_NIF_DIRTY_JOB_IO_BOUND},
{"nif_image_write_to_binary", 1, nif_image_write_to_binary,
ERL_NIF_DIRTY_JOB_CPU_BOUND},
{"nif_image_write_area_to_binary", 2, nif_image_write_area_to_binary,
ERL_NIF_DIRTY_JOB_CPU_BOUND},

/* VipsImage UNSAFE */
{"nif_image_update_metadata", 3, nif_image_update_metadata, 0},
Expand Down
3 changes: 3 additions & 0 deletions lib/vix/nif.ex
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ defmodule Vix.Nif do
def nif_image_write_to_binary(_vips_image),
do: :erlang.nif_error(:nif_library_not_loaded)

def nif_image_write_area_to_binary(_vips_image, _params_list),
do: :erlang.nif_error(:nif_library_not_loaded)

# VipsImage *UNSAFE*
def nif_image_update_metadata(_vips_image, _name, _value),
do: :erlang.nif_error(:nif_library_not_loaded)
Expand Down
144 changes: 111 additions & 33 deletions lib/vix/vips/image.ex
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
defmodule Vix.Vips.Image do
defstruct [:ref]

alias __MODULE__

@moduledoc """
Functions for reading and writing images as well as
accessing and updating image metadata.
Expand Down Expand Up @@ -99,6 +95,9 @@ defmodule Vix.Vips.Image do
%Vix.Vips.Image{ref: #Reference<0.2448791511.2685009949.153742>}
"""

defstruct [:ref]

alias __MODULE__
alias Vix.Nif
alias Vix.Type
alias Vix.Vips.MutableImage
Expand Down Expand Up @@ -152,7 +151,7 @@ defmodule Vix.Vips.Image do

# Extract band when the band number is positive or zero
def fetch(image, band) when is_integer(band) and band >= 0 do
case Vix.Vips.Operation.extract_band(image, band) do
case Operation.extract_band(image, band) do
{:ok, band} -> {:ok, band}
{:error, _reason} -> raise ArgumentError, "Invalid band requested. Found #{inspect(band)}"
end
Expand All @@ -179,9 +178,13 @@ defmodule Vix.Vips.Image do
with {:ok, args} <- normalize_access_args(args),
{:ok, left, width} <- validate_dimension(args[:width], width(image)),
{:ok, top, height} <- validate_dimension(args[:height], height(image)),
{:ok, first_band, bands} <- validate_dimension(args[:band], bands(image)),
{:ok, area} <- extract_area(image, left, top, width, height) do
extract_band(area, first_band, n: bands)
{:ok, first_band, bands} <- validate_dimension(args[:band], bands(image)) do
extracted_image =
image
|> extract_area!(left, top, width, height)
|> extract_band!(first_band, n: bands)

{:ok, extracted_image}
else
{:error, _} ->
raise ArgumentError, "Argument must be list of integers or ranges or keyword list"
Expand All @@ -190,30 +193,30 @@ defmodule Vix.Vips.Image do

@impl Access
def get_and_update(_image, _key, _fun) do
raise "get_and_update/3 for Vix.Vips.Image is not supported."
raise ArgumentError, "get_and_update/3 for Vix.Vips.Image is not supported."
end

@impl Access
def pop(_image, _band, _default \\ nil) do
raise "pop/3 for Vix.Vips.Image is not supported."
raise ArgumentError, "pop/3 for Vix.Vips.Image is not supported."
end

# Extract a range of bands
def fetch_range(image, %Range{first: first, last: last}) when first >= 0 and last >= first do
case Vix.Vips.Operation.extract_band(image, first, n: last - first + 1) do
defp fetch_range(image, %Range{first: first, last: last}) when first >= 0 and last >= first do
case Operation.extract_band(image, first, n: last - first + 1) do
{:ok, band} -> {:ok, band}
{:error, _reason} -> raise "Invalid band range #{inspect(first..last)}"
{:error, _reason} -> raise Error, "Invalid band range #{inspect(first..last)}"
end
end

def fetch_range(image, %Range{first: first, last: last}) when first >= 0 and last < 0 do
defp fetch_range(image, %Range{first: first, last: last}) when first >= 0 and last < 0 do
case bands(image) + last do
last when last >= 0 -> fetch(image, first..last)
_other -> raise ArgumentError, "Resolved invalid band range #{first..last}}"
end
end

def fetch_range(image, %Range{first: first, last: last}) when last < 0 and first < last do
defp fetch_range(image, %Range{first: first, last: last}) when last < 0 and first < last do
bands = bands(image)
last = bands + last

Expand All @@ -224,7 +227,7 @@ defmodule Vix.Vips.Image do
end
end

def fetch_range(_image, %Range{} = range) do
defp fetch_range(_image, %Range{} = range) do
raise ArgumentError, "Invalid range #{inspect(range)}"
end

Expand Down Expand Up @@ -281,17 +284,25 @@ defmodule Vix.Vips.Image do
Map.get(range, :step) == 1 || !Map.has_key?(range, :step)
end

defp extract_area(image, left, top, width, height) do
@spec extract_area!(
Image.t(),
non_neg_integer,
non_neg_integer,
non_neg_integer,
non_neg_integer
) :: Image.t() | no_return
defp extract_area!(image, left, top, width, height) do
case Operation.extract_area(image, left, top, width, height) do
{:ok, image} -> {:ok, image}
_other -> raise "Requested area could not be extracted"
{:ok, image} -> image
{:error, _} = _error -> raise Error, "Requested area could not be extracted"
end
end

defp extract_band(area, first_band, options) do
@spec extract_band!(Image.t(), non_neg_integer, keyword) :: Image.t() | no_return
defp extract_band!(area, first_band, options) do
case Operation.extract_band(area, first_band, options) do
{:ok, image} -> {:ok, image}
_other -> raise "Requested band(s) could not be extracted"
{:ok, image} -> image
{:error, _} = _error -> raise Error, "Requested band(s) could not be extracted"
end
end

Expand Down Expand Up @@ -380,7 +391,7 @@ defmodule Vix.Vips.Image do
@spec new_from_buffer(binary(), keyword()) :: {:ok, t()} | {:error, term()}
def new_from_buffer(bin, opts \\ []) do
with {:ok, loader} <- Vix.Vips.Foreign.find_load_buffer(bin),
{:ok, {ref, _optional}} <- Vix.Vips.Operation.Helper.operation_call(loader, [bin], opts) do
{:ok, {ref, _optional}} <- Operation.Helper.operation_call(loader, [bin], opts) do
{:ok, wrap_type(ref)}
end
end
Expand Down Expand Up @@ -438,9 +449,6 @@ defmodule Vix.Vips.Image do
@doc """
Create a new image from Enumerable.
> #### Caution {: .warning}
> This function is experimental and might cause crashes, use with caution
Returns an image which will lazily pull data from passed
Enumerable. `enum` should be stream of bytes of an encoded image
such as JPEG. This functions recognizes the image format and
Expand Down Expand Up @@ -512,9 +520,6 @@ defmodule Vix.Vips.Image do
@doc """
Creates a Stream from Image.
> #### Caution {: .warning}
> This function is experimental and might cause crashes, use with caution
Returns a Stream which will lazily pull data from passed image.
Useful when working with big images. Where you don't want to keep
Expand Down Expand Up @@ -759,8 +764,81 @@ defmodule Vix.Vips.Image do
binary blob. Such as height, width and bands.
"""
@spec write_to_binary(t()) :: {:ok, binary()} | {:error, term()}
def write_to_binary(%Image{ref: vips_image}) do
Nif.nif_image_write_to_binary(vips_image)
def write_to_binary(image) do
case write_area_to_binary(image) do
{:ok, %{binary: binary}} -> {:ok, binary}
error -> error
end
end

@doc """
Returns the pixel value for the passed position
Pixel value is a list of numbers. Size of the list depends on the
number of bands in the image and number type will depend on the
band format (see: `t:Vix.Vips.Operation.vips_band_format/0`).
For example for RGBA image with unsigned char band format the
return value will be a list of integer of size 4.
This function is similar to `Vix.Vips.Operation.getpoint/3`,
getpoint always returns the value as `float` but get_pixel returns
based on the image band format.
> #### Caution {: .warning}
> Loop through lot of pixels using `get_pixel` can be expensive.
> Use `extract_area` or Access syntax (slicing) instead
"""
@spec get_pixel(t(), x :: non_neg_integer, y :: non_neg_integer) ::
{:ok, [term]} | {:error, term()}
def get_pixel(image, x, y) do
unless x >= 0 and y >= 0 do
raise ArgumentError, "Pixel position must be non-negative"
end

case write_area_to_binary(image, left: x, top: y, width: 1, height: 1) do
{:ok, %{binary: binary, width: 1, height: 1, band_format: format}} ->
{:ok, binary_to_list(binary, format)}

{:error, _} = error ->
error
end
end

@doc """
Same as `get_pixel/3`. Returns the pixel value on success or raise the error.
"""
@spec get_pixel!(t(), x :: non_neg_integer, y :: non_neg_integer) :: [term] | no_return
def get_pixel!(image, x, y) do
case get_pixel(image, x, y) do
{:ok, list} -> list
{:error, reason} when is_binary(reason) -> raise Error, reason
{:error, reason} -> raise Error, inspect(reason)
end
end

@spec write_area_to_binary(t(), params :: keyword) :: {:ok, binary()} | {:error, term()}
defp write_area_to_binary(%Image{ref: vips_image}, params \\ []) do
params =
Enum.map(~w(left top width height band_start band_count)a, fn key ->
params[key] || -1
end)

case Nif.nif_image_write_area_to_binary(vips_image, params) do
{:ok, {binary, width, height, bands, band_format}} ->
{:ok,
%{
binary: binary,
width: width,
height: height,
bands: bands,
band_format: Vix.Vips.Enum.VipsBandFormat.to_erl_term(band_format)
}}

{:error, _} = error ->
error
end
end

@doc """
Expand Down Expand Up @@ -1209,8 +1287,8 @@ defmodule Vix.Vips.Image do
end

if Kernel.macro_exported?(Logger, :warning, 1) do
def log_warn(msg), do: Logger.warning(msg)
defp log_warn(msg), do: Logger.warning(msg)
else
def log_warn(msg), do: Logger.warn(msg)
defp log_warn(msg), do: Logger.warn(msg)
end
end
Loading

0 comments on commit ccb6f75

Please sign in to comment.