Scrimage
Scrimage is a Scala image library for manipulating and processing of images. The aim of the this library is to provide a quick and easy way to do the kinds of image operations that most people need, such as scaling, rotating, converting between formats and applying filters. It is not intended to provide functionality that might be required by a more "serious" image application - such as face recognition or movement tracking.
A typical use case for this library would be creating thumbnails of images uploaded by users in a web app, or resizing a set of images to have a consistent size, or optimizing PNG uploads by users to apply maximum compression, or applying a grayscale filter in a print application.
Scrimage has a consistent, idiomatic scala, and mostly immutable API that builds on the java.awt.Image methods. I say mostly immutable because for some operations creating a copy of the underlying image would prove expensive (think an in place filter on a 8000 x 6000 image where you do not care about keeping the original in memory). For these kinds of operations Scrimage supports a MutableImage instance where operations that can be performed in place mutate the original instead of returning a copy.
API
Operation | Description | Example |
---|---|---|
resize | Resizes the canvas to the given dimensions. This does not scale the image but simply changes the dimensions of the canvas on which the image is sitting. Specifying a larger size will pad the image with a background color and specifying a smaller size will crop the image. | image.resizeTo(500,800) for an absolute resize or image.resize(0.5) for a percentage resize |
scale | Scales the image to given dimensions. This operation will change both the canvas and the image. This is what most people think of when they want a "resize" operation. | image.scaleTo(x,y) to specify dimensions or image.scale(percentage) for a percentage scale |
pad | Resizes the canvas by adding a number of pixels around the image in a given color. | image.pad(20, Color.Black) would add 20 pixels of black on each edge, increasing both canvas width and height by 40 pixels. |
fit | Resizes the canvas to the given dimensions and scales the original image so that it is the maximum possible size inside the canvas while maintaining aspect ratio. This operation is useful if you want a group of images to all have the same canvas dimensions while maintaining the original aspect ratios. Think thumbnails on a site like amazon where they are padded with white background. |
Given a 800x600 image then image.fit(200,200,Color.White) would result in a new image of 200x200 (as specified in the call) where the original image is now 200,150 as that is the largest it can be scaled to without overflowing the canvas bounds. The additional 'spare' height of 50 pixels would be set to the given background color, white in this case. |
cover | Resizes the canvas to the given dimensions and scales the original image so that it is the minimum size needed to cover the new dimensions without leaving any background visible. This operation is useful if you want to generate an avatar/thumbnail style image from a larger image where having no background is more important than cropping part of the image. Think a facebook style profile thumbnail. |
Given a 64x64 image then image.cover(128,96) would result in a new image of 128x96 where the original image is now 128,128 as that is the smallest it can be scaled to without leaving any visible background. 32 pixels of the height is lost as that is "off canvas" |
bound | Scales the image maintaining the aspect ratio so that it fits inside the given dimensions. There is no padding included so the resultant image might not have the exact dimensions specified, but it will be as large as possible without overflowing the bounds (hence the name). This is useful when you want to ensure images do not exceed a certain size, but you don't any padding. |
Given a 800x600 image then image.bound(200,200) would scale the image to 200x150 as that is the largest it can be without overflowing the 200x200 bounds whilst maintaining the same aspect ratio. |
copy | Creates a new clone of this image with a new pixel buffer. Any operations on the copy do not write back to the original. | image.copy |
empty | Creates a new image but without initializing the data buffer to any specific values. | image.empty on an existing instance to use the same dimensions or Image.empty(x,y) to create a new image with the given dimensions |
filled | Creates a new image and initializes the data buffer to the given color. | image.filled(Color.Red) |
rotate left | Rotates the image anti clockwise. Results in width and height being flipped. | image.rotateLeft |
rotate right | Rotates the image clockwise. Results in width and height being flipped. | image.rotateRight |
flip x | Flips the image horizontally. Left becomes right. | image.flipX |
flip y | Flips the image vertically. Top becomes bottom. | image.flipY |
filter | Returns a new image with the given filter applied. See the filters section for examples of the filters available. Filters can be chained and are applied in sequence. | image.filter(BlurFilter) or image.filter(GaussianBlur(5)).filter(SepiaFilter) Most filters can be created from companion objects with sensible default values. |
Quick Examples
Reading an image, scaling it to 50% using the Bicubic method, and writing out as PNG
val in = ... // input stream
val out = ... // output stream
Image(in).scale(0.5, Bicubic).write(out) // Png is default
Reading an image from a java File, applying a blur filter, then flipping it on the horizontal axis, then writing out as a Jpeg
val inFile = ... // input File
val outFile = ... // output File
Image(inFile).filter(BlurFilter).flipX.write(outFile, Format.Jpeg) // specified Jpeg
Padding an image with a 20 pixel border around the edges in red
val in = ... // input stream
val out = ... // output stream
Image(in).pad(20, Color.Red)
Enlarging the canvas of an image without scaling the image (resize method changes canvas size, scale method scales image)
val in = ... // input stream
val out = ... // output stream
Image(in).resize(600,400)
Scaling an image to a specific size using a fast non-smoothed scale
val in = ... // input stream
val out = ... // output stream
Image(in).scale(300, 200, FastScale)
Writing out a heavily compressed Jpeg thumbnail
val in = ... // input stream
val out = ... // output stream
Image(in).fit(180,120).writer(Format.JPEG).withCompression(0.5).write(out)
Printing the sizes and ratio of the image
val in = ... // input stream
val out = ... // output stream
val image = Image(in)
println(s"Width: ${image.width} Height: ${image.height} Ratio: ${image.ratio}")
Converting a byte array in JPEG to a byte array in PNG
val in : Array[Byte] = ... // array of bytes in JPEG say
val out = Image(in).write // default is PNG
val out2 = Image(in).write(Format.PNG) // to be explicit about the output format
Coverting an input stream to a maximum compressed PNG
val in : InputStream = ... // some input stream
val out : OutputStream = ... // some output stream
val compressed = Image(in).writer(Format.PNG).withMaxCompression.write(out)
Input / Output
Scrimage supports loading and saving of images in the common web formats (currently png, jpeg, gif, tiff). In addition it extends jav'sa image.io support by giving you an easy way to compress / optimize / interlace the images when saving.
To load an image simply use the Image apply methods on an input stream, file, filepath (String) or a byte array. The format does not matter as the underlying reader will determine that. Eg,
val in = ... // a handle to an input stream
val image = Image(in)
To save a method, Scrimage provides an ImageWriter for each format it supports. An ImageWriter supports saving to a File, filepath (String), byte array, or OutputStream. The quickest way to use an ImageWriter is to call write() on an image, which will get a handle to an ImageWriter with the default configuration and use it for you. Eg,
val image = ... // some image
image.write(new File("/home/sam/spaghetti.png"))
If you want to override the configuration for a writer then you will need to get a handle to the writer itself using the writer() method which returns an ImageWriter instance. From here you can then configure it before writing. A common example would be optimising a PNG to use compression (uses a modified version of PngTastic behind the scenes). Eg,
val image = ... // some image
image.writer(Format.PNG).withCompression(9).write(new File("/home/sam/compressed_spahgetti.png"))
Note the writers are immutable and are created per image.
Async
In version 1.1.0 support for asynchronous operations was added. This is achieved using the AsyncImage class. First, get an instance of AsyncImage from an Image or other source:
val in = ... // input stream
val a = AsyncImage(in)
Then any operations that act on that image return a Future[Image] instead of a standard Image. They will operate on the scala.concurrent implicit execution context.
... given an async image
val filtered = a.filter(VintageFilter) // filtered has type Future[Image]
A more complicated example would be to load all images instead a directory, apply a grayscale filter, and then re-save them out as optimized PNGs.
val dir = new File("/home/sam/images")
dir.listFiles().foreach(file => AsyncImage(file).filter(GrayscaleFilter).onSuccess {
case image => image.writer(Format.PNG).withMaxCompression.write(file)
})
Benchmarks
Some noddy benchmarks comparing the speed of rescaling an image. I've compared the basic getScaledInstance method in java.awt.Image with ImgScalr and Scrimage. ImgScalr delegates to awt.Graphics2D for its rendering. Scrimage adapts the methods implemented by Morten Nobel.
The code is inside src/test/scala/com/sksamuel/scrimage/ScalingBenchmark.scala.
The results are for 100 runs of a resize to a fixed width / height.
Library | Fast | High Quality (Method) |
---|---|---|
java.awt.Image.getScaledInstance | 11006ms | 17134ms (Area Averaging) |
ImgScalr | 57ms | 5018ms (ImgScalr.Quality) |
Scrimage | 113ms | 2730ms (Bicubic) |
As you can see, ImgScalr is the fastest for a simple rescale, but Scrimage is much faster than the rest for a high quality scale.
Including Scrimage in your project
Scrimage is available on maven central. There are two dependencies. One is the core library, and one is the image filters. They are split because the image filters is a large jar, and most people just want the basic resize/scale/load/save functionality. Only include the filters dependency if you need the image filters, otherwise just the core one is needed.
Maven:
<dependency>
<groupId>com.sksamuel.scrimage</groupId>
<artifactId>scrimage-core_2.10</artifactId>
<version>1.3.5</version>
</dependency>
<dependency>
<groupId>com.sksamuel.scrimage</groupId>
<artifactId>scrimage-filters_2.10</artifactId>
<version>1.3.5</version>
</dependency>
If using SBT then you want:
libraryDependencies += "com.sksamuel.scrimage" % "scrimage-core_2.10" % "1.3.5"
libraryDependencies += "com.sksamuel.scrimage" % "scrimage-filters_2.10" % "1.3.5"
Filters
Scrimage comes with a wide array (or Iterable ;) of filters. Most of these filters I have not written myself, but rather collected from other open source imaging libraries (for compliance with licenses and / or attribution - see file headers), and either re-written them in Scala, wrapped them in Scala, fixed bugs or improved them.
Some filters have options which can be set when creating the filters. All filters are immutable. Most filters have sensible default options as default parameters.
Click on the small images to see an enlarged example.
License
This software is licensed under the Apache 2 license, quoted below.
Copyright 2013 Stephen Samuel
Licensed under the Apache License, Version 2.0 (the "License"); you may not
use this file except in compliance with the License. You may obtain a copy of
the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations under
the License.