Let’s face it: Pictures taken with a smartphone usually aren’t quite like Ansel Adams masterpieces. But with a little post-processing, some of them might still reveal their true beauty. A couple of Go libraries can help.

Almost all of the posts on AppliedGo.net so far are about applying Go to various problem domains and building a basic implementation from scratch. Today’s post is a bit different. I picked a goal - processing an image - and searched for Go libraries to help me with that job. These are the libraries I will be using here: artyom/smartcrop

anthonynsimon/bild

fogleman/primitive Update: artyom/smartcrop is replacing muesli/smartcrop that was used for the previous version of this article. artyom/smartcrop is a fork of muesli/smartcrop with no external dependencies and a simpler API. And last not least, the image package from the Go standard library. So let’s start coding! The inevitable Imports And Globals section

package main import ( "image" "image/jpeg" "github.com/anthonynsimon/bild/adjust" "github.com/anthonynsimon/bild/blend" "github.com/anthonynsimon/bild/effect" "github.com/anthonynsimon/bild/transform" "github.com/artyom/smartcrop" "github.com/fogleman/primitive/primitive" "github.com/pkg/errors" "fmt" "log" "math/rand" "os" "path" "runtime" "time" )

Loading and saving First, we want to load an image. Here is our test image. If I am not wrong, it shows a red kite. The image library provides a Decode function that can read JPG, GIF, and PNG data, provided that the appropriate sub-package has been loaded (see the import section). And while we are at it, let’s also define a function for saving an image.

func openImage (path string ) (image.Image, error ) { imgFile, err := os. Open (path) if err != nil { return nil , errors. Wrap (err, "Cannot open " + path) } img, err := jpeg. Decode (imgFile) if err != nil { return nil , errors. Wrap (err, "Decoding the image failed." ) } return img, nil } func saveImage (img image.Image, pname, fname string ) error { fpath := path. Join (pname, fname) f, err := os. Create (fpath) if err != nil { return errors. Wrap (err, "Cannot create file: " + fpath) } err = jpeg. Encode (f, img, & jpeg.Options{Quality: 85 }) if err != nil { return errors. Wrap (err, "Failed to encode the image as JPEG" ) } return nil }

Smartcrop The test image has quite some space around the Red Kite with nothing interesting to see. So let’s crop the image. But not manually; instead, let smartcrop do the job. smartcrop attempts to detect the most interesting part of an image. Note that Smartcrop can use face recognition for finding the right crop. Obviously, we cannot use this feature here on the bird picture, so we switch it off. Smartcrop does not crop the image itself, it only returns the suggested cropping rectangle. We can use the standard Image library for cropping the image. The Image type has no crop method, but the color types have a method called SubImage , like, for example, the RGBA type: func (p *RGBA) SubImage(r Rectangle) Image How can we access this method? We could type-assert the Image to the appropriate color type (like RGBA, CMYK, etc.) but Image 's properties give us no clue which color type our JPEG image has been decoded to. The solution is to type-assert Image to an interface that consists of just the SubImage method. Then we can call SubImage without knowing the exact color type.

type SubImager interface { SubImage (r image.Rectangle) image.Image } func crop (img image.Image, width, height int ) (image.Image, error ) { rect, err := smartcrop. Crop (img, width, height) if err != nil { return nil , errors. Wrap (err, "Smartcrop failed" ) } si, ok := (img).(SubImager) if !ok { return nil , errors. New ( "crop(): img does not support SubImage()" ) } subImg := si. SubImage (rect) return subImg, nil }

The result is not too bad! The algorithm found the interesting part of the image, although I would have put the bird a tad bit more towards the center. But hey, that’s an automated algorithm that is certainly not specialized for identifying birds, so the result is perfectly ok. bild Next, let’s try fixing the somewhat over-exposured foreground and grass. For this, I’ll use anthonynsimon/bild , a comprehensive image manipulation library. (If you wonder about the name, “Bild” is the German word for “picture” or “image”.) bild uses image.Image as image format, so we can reuse the img variable without having to save and re-open the image. bild is organized as sub-packages that group related operations. For example, the adjust package provides adjustments, the blend package provides image blending operations, and so on. Let’s try a few things for fun (each time starting from the unmodified image). As the colors seem a bit pale, let’s try increasing the saturation.

func saturate (img image.Image) image.Image { return adjust. Saturation (img, 0.5 ) }

Before: After: Already looks better, after just one simple adjustment! Next test: What happens if we multiply the image with itself?

func multiply (img image.Image) image.Image { return blend. Multiply (img, img) }

That’s interesting: Dark colors are darker, and so are the lighter colors, but not that much as the darker ones, and they also seem more intense. Try more effects for yourself! Especially, try to combine two or more effects to get new results. As a last test with bild , let’s sharpen the saturated image.

func sharpen (img image.Image) image.Image { return effect. UnsharpMask (img, 0.6 , 1.2 ) }

For better comparison, I zoomed in and put the before and after images side-by-side. primitive The next package is fogleman\primitive . Don’t be fooled by the name; this package is anything but primitive. The name has a meaning though: This package “reproduces” an image by applying geometric primitives like rectangles, ellipses, etc. to it. This package comes as a binary package; however, it is well structured and includes sub-packages, so after peeking into main.go we can integrate the algorithm in our code.

func primitivePicture (img image.Image) image.Image { img = transform. Resize (img, 256 , 256 , transform.Linear) rand. Seed (time. Now (). UTC (). UnixNano ()) bg := primitive. MakeColor (primitive. AverageImageColor (img)) model := primitive. NewModel (img, bg, 1024 , runtime. NumCPU ()) for i := 0 ; i < 100 ; i ++ { fmt. Print ( "." ) model. Step (primitive. ShapeType ( 5 ), 128 , 0 ) } return model.Context. Image () }

Here is the result: Niiice! If the first result is not satisfying, simply run the code again. The results will be different each time. Also try other mode values (replace the “5” in primitive.ShapeType(5) in the call to model.Step() above). Valid values are (from primitive 's help text): 0=combo 1=triangle 2=rect 3=ellipse 4=circle 5=rotatedrect 6=beziers 7=rotatedellipse 8=polygon Now the test image is not really suited for dramatic effects, so feel free to visit primitive 's GitHub repository to see a couple of awesome Primitive Pictures! Last not least, the main function connects all the code snippets.

func main () { img, err := openImage ( "original.jpg" ) if err != nil { log. Fatal (err) } img, err = crop (img, 1000 , 1000 ) if err != nil { log. Fatal (err) } err = saveImage (img, "." , "cropped.jpg" ) if err != nil { log. Fatal (err) } img, err = openImage ( "cropped.jpg" ) if err != nil { log. Fatal (err) } sat := saturate (img) err = saveImage (sat, "." , "saturated.jpg" ) if err != nil { log. Fatal (err) } mult := multiply (img) err = saveImage (mult, "." , "multiplied.jpg" ) if err != nil { log. Fatal (err) } shrp := sharpen (sat) err = saveImage (shrp, "." , "sharpened.jpg" ) if err != nil { log. Fatal (err) } pri := primitivePicture (sat) err = saveImage (pri, "." , "primitive.jpg" ) if err != nil { log. Fatal (err) } }