Image - an image processing library based upon Vix

I tend to agree with you. There is room for both approaches:

  1. As you say, the pipeline approach is the easiest to reason about and the most idiomatic
  2. The composition list is the most efficient since it results in a single NIF call. However the actual compositing of the image is the dominant time factor
  3. The composition list approach makes it much easier to build a composition programmatically - since its just a list.

Both function signatures are now merged into master and will be release this weekend. Then ā€¦ back to image streaming (which I think is a ā€œbig dealā€) for many apps.

10 Likes

I love this. Excellent library!

Iā€™ve just published image version 0.2.0. It has the following enhancements:

Enhancements

  • Adds Image.Text to support generating an image from a string. See Image.Text.text/2.

  • Adds Image.Shape to support generating images from shapes. In this release there is a generalised support for polygons and stars. See Image.Shape.polygon/2 and Image.Shape.star/2.

  • Improves Image.compose/3 to support composing with image lists, relative image placement and more.

It was a fun and very intense few days to get this update ready for release, even though it wasnā€™t what I had planned. Now ā€¦ back to image streaming.

As always, comments, suggestions, bug reports very welcome.

Thanks again to @akash-akya for the fabulous Vix without which none of this would be possible.

10 Likes

This looks really neat. Iā€™ll be sure to give it a try when I add image processing to Keila :+1:

What an awesome work you did here, I just love it :slight_smile:

Just perfect! Iā€™ll be needing a library like this soon. Couple of well-intentioned comments:

  • Ready-to-use Livebook for interactive experimenting would be awesome,
  • Not really discoverable on Hex :face_with_diagonal_mouth:,
  • The logo in the docs could use some love, especially since this is an image processing library :wink:.
1 Like

I completely agree with all of this. Contributions would be most welcome :slight_smile:

Next steps:

  1. Vix and eVision integration so Image can do object detection. This also means integration with Nx will be supported.
  2. Finish up image streaming. Be able to stream an image from S3, process it in parallel while also streaming it to a front end. Game changer.
  3. Docs, Tutorials, Livebooks, ā€¦ really important topic.

And as for the logo ā€¦ totally agree, it sucks. Very open to suggestions and contributions for something that reflects the intent and passion to make this a great lib.

BTW, the description of the package is:

An approachable image processing library based upon Vix and libvips that is NIF-based, fast, multi-threaded, pipelined and has a low memory footprint.

Which I thought hit all the buzzwords for discoverability. I guess not!

6 Likes

Thanks to some fabulous work by @akash-akya in the new Vix version 0.11.0, Image version 0.4.0 is now released with a focus on image streaming.

Changelog

  • Adds support for opening streaming images. This allows images to be streamed from File.stream!/3 or from any t:Enumerable.t/0 including those created from ExAws.S3 by ExAws.stream!/2.

  • Adds support writing streaming images. This allows images to be streamed as an enumerable which can then be consumed by Plug.Conn.chunk/2, by ExAws.S3.upload/3, File.stream/3 or any other function that processes stream resources. See the test/stream_image_test.exs file for examples.

  • Adds a :memory option to Image.write/3. Instead of a path name or stream, use :memory if youā€™d like to return a binary form of an image in its formatted type. Note that this will run the image transformation pipeline resulting in the entire final image being loaded into memory. Therefore this option should be used sparingly since most use cases do not require this option. It is primarily added to facilitate passing images to other libraries in the Elixir ecosystem.

Use case

One of the benefits of libvips, and therefore Vix, is that transformations are built in pipelines. Its actually quite a lot like Elixir pipelines in nature (albeit the implementation is wildly different).

This means we can support streaming an image into Image.open/2, apply a pipeline of transformations and stream the image out with Image.write/3 or Image.stream/2. These operations all happen concurrently: streaming the image into the pipeline, the pipeline itself, and the streaming out again. Very memory efficient, very Elixir friendly, very easy to apply in an application.

Example

Imagine an example where we want to stream an image from S3, apply some transformations, and stream the image to an HTTP client. Hereā€™s the code:

ExAws.S3.download_file("images", "Hong-Kong-2015-07-1998.jpg", :memory)
|> ExAws.stream!()
|> Image.open!()
|> Image.resize!(200)
|> Image.write(conn, suffix: ".jpg")

Here conn is the conn of a Plug/Phoenix request. All the machinery of streaming is abstracted away. Some additional headers would need to be set for the MIME type and filename but apart from that there is nothing to do.

Another approach makes the stream more explicit but ultimately is the same example as that above:

ExAws.S3.download_file("images", "Hong-Kong-2015-07-1998.jpg", :memory)
|> ExAws.stream!()
|> Image.open!()
|> Image.resize!(200)
|> Image.stream!(suffix: ".jpg")
|> Enum.reduce_while(conn, fn (chunk, conn) ->
  case Plug.Conn.chunk(conn, chunk) do
    {:ok, conn} ->
      {:cont, conn}
    {:error, :closed} ->
      {:halt, conn}
  end
end)

Next steps

The next release will focus on interoperability with eVision and hopefully also Nx. This will enable interaction with the very cool axon and axon_onnx.

Feedback and suggestions

After the next release, primary development will be user-driven. Open an issue, start a discussion or comment here with what youā€™d like to see.

15 Likes

Turns out that Nx integration was really straight forward due primarily to the excellent work of @akash-akya and the Nx team. Introducing Image version 0.5.0 with the following changelog entry:

Enhancements

  • Adds Image.to_nx/1 to convert an image to an Nx tensor.

  • Adds Image.from_nx/1 to convert an Nx tensor into an image.

The conversation take care of ensuring type compatibility and ensuring the correct axes in the right order for Image.

When calling Image.to_nx/1, there is no data copying, just passing a reference to a heap binary so the movement is fast and garbage collection takes care of cleaning up.

15 Likes

Image version 0.6.0 has just been published. The changelog reads:

Bug fixes

  • Donā€™t attempt to compile code that refers to Plug.t if Plug is not configured. Thanks to @kwando. Closes #7.

Enhancements

  • Adds Image.dshash/1 to return a 512-bit image hash.

  • Adds Image.hamming_distance/2 to compare the similarity of two images or two image hashes.

  • Adds Image.radial_gradient/3 to great a radial gradient image. This could be used to composite over another image to add a vignette.

  • Adds Image.Math logical functions boolean_and/2, boolean_or/2, boolean_xor/2, boolean_lshift/2 and boolean_rshift/2. Also includes the ! versions.

  • Add operator use Image.Math overloads for image &&& (and), ||| (or), <<< (lshift) and >>> (rshift).

11 Likes

Image version 0.9.0 has just been published.

The major feature is the beginning of integration with the fabulous eVision (OpenCV) by @cocoa. The integration doesnā€™t copy any data to/from the libvips NIF and the openCV NIF so performance and resource utilisation should be quite good.

In this initial experimental release, eVision is an optional dependency. Only one function is exposed which decodes QRcodes. See Image.QRcode.decode/1.

The full changelog is:

Enhancements

  • Image.open/2 supports opening .gif and .tif images from raw binary data in addition to the previous .jpeg, .png and .webp.

  • Add Image.shape/1

  • Add Image.interpretation/1

  • Add Image.type/1

  • Add initial support for eVision. In this release the function Image.QRcode.decode/1 is provided to decode images that contain a QRcode. See the eVision repository for installation instructions since this library is not yet on hex.pm.

  • Removed most dialyzer warnings. The remaining warnings require an update to Vix (a PR has been submitted).

12 Likes

Image version 0.13.0 has been released. Lots of community encouragement and some fun challenges from @GenericJam that motivated the addition of chroma keying and meme generation.

Meme Generation

Generates a meme image from a base image and some text. For example:

iex> Image.open!("./test/support/images/meme.jpg", access: :random) 
..|> Image.meme!("One simply cannot", text: "Enjoy image processing without libvips") 
..|> Image.preview()

image

Chroma Keying

This is the process of eliminating a background color or color range from an image to produce an image mask that can be overlayed onto another image. For example:

iex> foreground =  Image.open!("./test/support/images/chroma_key/greenscreen.jpg", access: :random)
...> |> Image.preview()

image

iex> foreground
...> |> Image.chroma_key! 
...> |> Image.preview()

image

Now we can compose this image over another image:

iex> background = Image.open!("./test/support/images/chroma_key/background.jpg", access: :random)
...> Image.preview()

image

iex> Image.compose!(background, foreground) 
...> |> Image.preview()

image

Image Previews

In the examples above you can see the use of Image.preview/1 (and its short form Image.p/1). This function emits an image preview to iTerm2 terminal windows. Its useful to import it into your .iex.exs file like this:

% cat .iex.exs
import_if_available(Image, only: [p: 1, preview: 1])

QRcode encoding

Leveraging the fabulous eVision library by @cocoa (now on hex.pm!) there is both QRcode encoding and decoding. Encoding is new in this release of Image. For example:

iex> {:ok, qrcode} = Image.QRcode.encode("eVision, OpenCV, vix and libvips make for a great image processing experience for Elixir", size: 100)
iex> Image.preview(qrcode)

image

Full Changelog for Image 0.13.0

Breaking change

  • Image.resize/3 renamed to Image.thumbnail/3

Bug fixes

  • Fix Image.open/2 when opening JPEG images from a binary. Seems JPEG files agree on the <<0xff, 0xd8, 0xff>> header for the first three bytes. But not completely consistent with the following bytes.

  • Fix options for Image.Draw functions which are expected to be a map after validation (but were a keyword list).

Enhancements

  • Add Image.chroma_key/2 and Image.chroma_mask/2.

  • Add Image.meme/3 and Image.meme!/3.

  • Add Image.QRcode.encode/1 and Image.QRcode.decode/1.

  • Add Image.blur/2.

  • Add Image.feather/2.

  • Add Image.new/2 that creates a new image of the same shape as the provided image.

  • Add Image.resize/3.

  • Add Image.split_bands/1.

  • Add Image.if_then_else/3.

  • Add Image.preview/1 (and Image.p/1 that delegates to it) to preview an image inline in an iTerm2 terminal windows.

  • Add Image.split_bands/1 to return a list single band images, one for each band in the original image.

11 Likes

This is awesome!!!

I recently added Imgproxy + MinIO + Varnish.

Can this library replace what Imgproxy does or it is to be used alongside.

i.e. Imgproxy will be accessible directly using signed URL. And this library will be used for special use cases like Overlays, Meme Generation, streaming images, Greenscreen magic, etc.

P.S. I just love how much control this gives us. Iā€™m just confused how to replace Imgproxy with this. :sweat_smile:

Thats the next library on my list (hmmmm, too many libraries on my list). Iā€™m tentatively calling it Imagine and it will provide image proxy, streaming, caching, use client hints for resizing and include all of the capabilities of Image (like overlays, composition, chroma keying, memes, etc etc etc).

All of the basic functionality is now in Image for all the things you mentioned. Including streaming images from S3, processing them and streaming them out through Phoenix - all in parallel. It will be very cool I think. But its a couple of months away - the design is in my head but not in a repo. Think a little bit like Thumbor but idiomatic for Elixir.

5 Likes

Iā€™m going to use image library definitely. Post image creation will become super easy, so will memes.

Take inspiration from Imgproxy as well.

As itā€™s fastest of the lot:

1 Like

Very happy to do so. Iā€™ll post when the repo is up - feedback and suggestions will be very welcome.

My reference to Thumbor wasnā€™t about architecture or performance, just basic problem domain and capability.

1 Like

One thing I am clear on: configure it from hex and youā€™re good to go. No static configuration, no setup process (other than install libvips). Be up and running in 30 seconds. And after that a great developer experience and designer experience.

3 Likes

This looks like a very promising library for the project Iā€™m currently working on. In one of my previous projects I didnā€™t want to use Mogrify as itā€™s not really performant, instead, I used sharp (node library) which uses libvips to process images in combination with rambo (hex package) which allowed me to run node commands from elixir app.

I will give Image a try to see how it performs in real life, but just to get a better understanding, what would be the pros of using it compared to the sharp+rambo combo?

I think these would be the main differences. I canā€™t really say if they are pros or cons for you. I can say though that libvips is amazing (but since you use Sharp you already know that), @akash-akya wrote vix which is the NIF binding to libvips and its really well done.

Things to consider

  • Image is a wrapper around vix/libvips which is written as a NIF so its as fast an integration as you can make.

  • The image binary data is never copied between Elixir-land and the native C-based NIF world. Big performance win. (Ok, two exceptions: (1) when specifically asking for the binary to be copied to an Elixir binary and (2) when streaming an image to a Phoenix conn)

  • Image data can be shared with eVision with zero-copy sharing of imaging data. And can also be shared with Nx.

  • Using NIFs introduces a reliability and security risk since that code falls outside the control of the BEAM. However in all my testing Iā€™ve never had a segfault.

  • John Cupitt who maintains libvips is very active and supportive of both Image and Vix development (as he is with Sharp)

  • The functional orientation, immutability (with a small escape hatch), multi-threaded execution, small memory footprint and pipelined execution really fits well into the Elixir/BEAM operating model.

  • Image (because of Vix) supports image streaming both as a source and a sink. So you can stream an image from S3, put it in a transformation pipeline and stream it to a Phoenix conn almost seamlessly and with good performance. The data is only copied from the NIF to the Phoenix conn on demand.

Overall I would expect you would see lower latency using a NIF-based solution that a ports based one and you should find the threading model and pipeline process improving memory and CPU utilisation. This is speculation but Vix (the NIF to libvips) uses dirty threads to ensure its a good neighbour to the BEAM at the same time it can use BEAM scheduling and because of the way its memory is managed, very unlikely to have leaks or phantom processes.

Hope that helps, happy to answer any other questions or unknowns. I probably sound like a fanboy but John Cupitt (libvips) and @akash-akya (Vix) have really done an amazing job and Image just makes it more idiomatic for Elixir developers.

4 Likes

Roadmap to Version 1.0

It is and has always been my intention that this library is largely user driven for new requirements. There are few additional capabilities I will add before 1.0 but after that I see it as ā€œuser drivenā€. Itā€™s built on the amazing Vix and eVision so Image should only concentrate on those functions which simplify complex transforms, makes common usage more idiomatic for Elixir developers or improve over all developer experience.

I think it is quite close. Iā€™m expect to release 1.0 before end of 2022. The additional functions I will definitely implement before 1.0 are:

  • Canny and Sobel edge detection

  • Object detection and tagging (based on eVision)

  • A complete set of free-standing live books as tutorials

  • Possibly (not committed) blob detection

Community feedback

Comments, suggestions very welcome. It was @GenericJam that motivated implementation of Image.meme/3 and Image.chroma_key/2 and it was a blast doing those. I learned a lot. Other ideas are very welcome.

4 Likes