Image processing in Go part 1

Amer Zildzic
Ministry of Programming — Technology
11 min readJan 21, 2020

When it comes to image processing, Python, Java or C# are the choices of most developers. However, other languages has their own weapons in this battle. Recently I stumbled upon few Go packages for image processing. I was quite impressed how easy is to apply some image transformations.

The problem

This article will deal with one of the most frequent problems in image processing — making the collage of images. Every single person wanted to have this at one point in their life — you had 5,6 or 10 photos from your beautiful holiday or image set of your favourite pet, and you wanted to print it all together, print it out and hang it on the wall.

We’ll build an image collage using script, which accepts the following arguments:

  • shape in which we’ll draw images on background (rectangle or circle)
  • the number of rows in which pictures are shown in the output image,
  • array of images to show in collage

We’ll execute the script like this:

go run src/github.com/user/imagecollager/imagecollager.go Rectangle 2 image1.jpeg image2.jpeg image3.jpg image4.jpeg image5.jpg image6.jpeg

Resizing and showing image with/without mask a.k.a draw package magic

To combine a few images (let’s say each 1000 x 1000 px), we would probably need to resize each image, using resize package as following:

resizedImg := resize.Resize(width, height, innerImg, resize.Lanczos3)

As simple as that. The last parameter of Resize function is interpolation function. For the sake of clarity, interpolation comes from words inter (in, between) and pollos (dot or point). Each calculation of a new point and its value that is based on two or more other points is called interpolation. In this specific example. we choose interpolation function which will be used for calculating color (R, G, B components) of each point in our new (resized) image based on the points in the original image. Supported interpolations are: NearestNeighbor, Billinear, Bicubic, MitchellNetravali, Lancoz2 and Lancoz3. We won’t go further into details and differences between them since it is not the topic of this article.

Now, when we have resized the images, we need to paste them to a new, resulting image. Let’s create a new function for that task:

func (bgImg *MyImage) drawRaw(innerImg image.Image, sp image.Point, width uint, height uint) {  resizedImg := resize.Resize(width, height, innerImg,  resize.Lanczos3)  w := int(Width(resizedImg))
h := int(Height(resizedImg))
draw.Draw(bgImg, image.Rectangle{sp, image.Point{sp.X + w, sp.Y + h}}, resizedImg, image.ZP, draw.Src)}

Drawing is implemented as a method of class MyImage (we’ll show it’s structure below) and it manipulates that same image, using the following parameters:

  • innerImage — an image which should be drawn in the background image
  • sp — a starting point, a point on the background image where a top left corner of the inner image should fit
  • width — the width of the inner image when pasted to the background
  • height — the height of the inner image when pasted to the background

To paste the inner image to the background image at a desired position, we just need to use draw.Draw function, which is a simple, straightforward job.

Here are the definitions of functions Height and Weight:

func Width(i image.Image) int {
return i.Bounds().Max.X — i.Bounds().Min.X
}
func Height(i image.Image) int {
return i.Bounds().Max.Y — i.Bounds().Min.Y
}

Image class

Package image defines Image interface, with these 3 methods:

type Image interface {
ColorModel() color.Model
Bounds() Rectangle
At(x, y int) color.Color
}

You may wonder why we didn’t work with this class directly. First reason is Image is interface, so we can’t define drawRaw and other methods on it. Second, we will use imview package to show images in this way:

imview.Show(output)

Show method accepts class image.RGBA as its argument, so we need image.Image somehow converted to RGB. We could use type assertion, but in my opinion its better to use the new type. Let’s define our image:

type MyImage struct {
value *image.RGBA
}
func (i *MyImage) Set(x, y int, c color.Color) {
i.value.Set(x, y, c)
}
func (i *MyImage) ColorModel() color.Model {
return i.value.ColorModel()
}
func (i *MyImage) Bounds() image.Rectangle {
return i.value.Bounds()
}
func (i *MyImage) At(x, y int) color.Color {
return i.value.At(x, y)
}

It has member value of type image.RGBA and implements image.Image interface by implementing all its methods. This way, we can easily use it when image.Image is expected and we can show it using imview.Show.

Finally, let’s test our drawRaw function on this image:

fimg, _ := os.Open("dog.jpg")
defer fimg.Close()
img, _, _ := image.Decode(fimg)
output := MyImage{image.NewRGBA(image.Rectangle{image.ZP, image.Point{400, 400}})}output.drawRaw(img, image.Point{100, 100}, 180, 150)imview.Show(output.value)

Drawing in circle

We can do the same thing, with having a dog inside a circle, not a rectangle. Just use DrawMask function from draw package:

func DrawMask(dst Image, r image.Rectangle, src image.Image, sp image.Point, mask image.Image, mp image.Point, op Op)

It accepts these parameters:

  • r — rectangle inside which the image is showed. The top left bottom of the rectangle is equal to sp parameter, or the starting point of image src
  • mask — the image through which src will be drawn on dst (background image)
  • mp — a mapping point, we’ll use zero point (0, 0)
  • op — can be Over (draw over the rectangle in the background) or Src (draw beside the background)

Here is our solution for drawing an image in the circle in the background

func (bgImg *MyImage) drawInCircle(innerImg image.Image, sp image.Point, width uint, height uint, diameter int) {  resizedImg := resize.Resize(width, height, innerImg,    resize.Lanczos3)  r := diameter
if r > Width(resizedImg) {
r = int(Width(resizedImg))
}
if r > Height(resizedImg) {
r = int(Height(resizedImg))
}
mask := &Circle{image.Point{Width(resizedImg) / 2, Height(resizedImg) / 2}, r / 2} draw.DrawMask(bgImg, image.Rectangle{sp, image.Point{sp.X + Width(resizedImg), sp.Y + Height(resizedImg)}}, resizedImg, image.ZP, mask, image.ZP, draw.Over)
}

We first resize the inner image to desired width and height and then recalculate diameter. If the specified diameter is bigger than height or width, it is scaled. Mask is created as Circle class:

type Circle struct {
p image.Point
r int
}
func (c *Circle) ColorModel() color.Model {
return color.AlphaModel
}
func (c *Circle) Bounds() image.Rectangle {
return image.Rect(c.p.X-int(c.r), c.p.Y-int(c.r), c.p.X+int(c.r), c.p.Y+int(c.r))
}
func (c *Circle) At(x, y int) color.Color {
xx, yy, rr := float64(x-c.p.X)+0.5, float64(y-c.p.Y)+0.5, float64(c.r)
if xx*xx+yy*yy < rr*rr {
return color.Alpha{255}
}
return color.Alpha{0}
}

Circle implements Image interface. We defined Bounds and At for Circle using this logic: if pixel is in the circle region, restricted by a diameter, show its value, otherwise, show a black point.

Let’s test it:

output := MyImage{image.NewRGBA(image.Rectangle{image.ZP, image.Point{400, 400}})}fimg, _ := os.Open("dog.jpg")
defer fimg.Close()
img, _, _ := image.Decode(fimg)
output.drawInCircle(img, image.Point{100, 100}, 180, 180, 150)
imview.Show(output.value)

Nice!

Putting it all together

Now we have a way to paste a single inner image on the background picture. We need a way to take the array of images and draw each of them onto the background. Here is a function we’ll use to achieve that:

func makeImageCollage(desiredWidth int, desiredHeight int, numberOfRows int, shape ImageShape, images ...image.Image) *MyImage 

It accepts the width and height of the resulting image, the number of rows we want to use and the shape (rectangle or circle) in which we want to draw each image on the background.

There are 2 main issues in the process:

  • How to scale images properly? One image can be portrait, another landscape. One image can be much bigger than the other. We can handle this in different ways. I prefer to have all images in approximately the same size.
  • How to calculate each inner image position? Based on the number of images and the number of rows, we need to calculate the row and the column where we’ll place each image on the background. Then, based on the row and the column, calculate a starting point for each image in the background.

First, we need to sort images by height, since we want to have a minimum gap between images. If the first image is 1000px and next 400px, than after scaling, we’ll have a lot of empty space under the second image if they are shown in the same row. That’s where sorting by height comes into place:

sort.Slice(images, func(i, j int) bool {
return Height(images[i]) > Height(images[j])
})

Now, let’s create a two-dimensional array of images, based on the number of rows and input images:

currentIndex := 0
maxNumberOfColumns := 0
for idx := 0; idx < numberOfRows; idx++ {
columnsInRow := numberOfColumns
if len(images)%numberOfRows > 0 && (numberOfRows-idx)*numberOfColumns < len(images)-currentIndex {
columnsInRow++
}
if columnsInRow > maxNumberOfColumns {
maxNumberOfColumns = columnsInRow
}
imagesMatrix[idx] = images[currentIndex : currentIndex+columnsInRow] currentIndex += columnsInRow
}

Pretty self-explanatory. If we have 5 images to show in 2 rows, the first row will contain three images and the second two.

Now, we need to calculate the total size of the resulting image and the size of each inner image. Let’s say we gave six images as input to the script and said we want to show them in two rows inside 1000px x 1000px resulting image. That means each row will have three images inside. However, the images are in landscape format, so width is bigger than height. Thus, width will be much greater than height.

type Size struct {
width uint
height uint
}
const (
RectangleShape ImageShape = "Rectangle"
CircleShape ImageShape = "Circle"
CircleDiameter = 0.8
)
sort.Slice(images, func(i, j int) bool {
return Height(images[i]) > Height(images[j])
})
numberOfColumns := len(images) / numberOfRows
imagesMatrix := make([][]image.Image, numberOfRows)
currentIndex := 0
maxNumberOfColumns := 0
for idx := 0; idx < numberOfRows; idx++ {
columnsInRow := numberOfColumns
if len(images)%numberOfRows > 0 && (numberOfRows-idx)*numberOfColumns < len(images)-currentIndex {
columnsInRow++
}
if columnsInRow > maxNumberOfColumns {
maxNumberOfColumns = columnsInRow
}
imagesMatrix[idx] = images[currentIndex : currentIndex+columnsInRow]
currentIndex += columnsInRow
}
maxWidth := uint(0)
imagesSize := make([][]Size, numberOfRows)
for row := 0; row < numberOfRows; row++ {
imagesSize[row] = make([]Size, len(imagesMatrix[row]))
calculatedWidth := math.Floor(float64(desiredWidth) / float64(len(imagesMatrix[row]))) rowWidth := uint(0)
rowHeight := uint(0)
for col := 0; col < len(imagesMatrix[row]); col++ {
originalWidth := float64(Width(imagesMatrix[row][col]))
originalHeight := float64(Height(imagesMatrix[row][col]))
resizeFactor := calculatedWidth / originalWidth
w := uint(originalWidth * resizeFactor)
h := uint(originalHeight * resizeFactor)
imagesSize[row][col] = Size{w, h}
if shape == RectangleShape {
rowWidth += w
} else {
rowWidth += uint(math.Min(float64(w), float64(h)) * CircleDiameter)
}
rowHeight += h
}
if rowWidth > maxWidth {
maxWidth = rowWidth
}
}
maxHeight := uint(0)for col := 0; col < maxNumberOfColumns; col++ {
colHeight := uint(0)
for row := 0; row < numberOfRows; row++ {
if len(imagesSize[row]) > col {
if shape == RectangleShape {
colHeight += imagesSize[row][col].height
} else {
colHeight += uint(math.Min(float64(imagesSize[row][col].height), float64(imagesSize[row][col].width)) * CircleDiameter)
}
}
}
if colHeight > maxHeight {
maxHeight = colHeight
}
}

We’ll show each image inside a row in the same width. If there are three images in a row, each of them takes around 33% of the total row width. Based on the width and the number of images in a row, we calculate the resize factor for each image. The same factor will be used for scaling by both width and height. Also, we calculate the maximal row width and the maximal column height (since each row and each column may not contain the same number of images). That will be the size of new, output image.

Now, let’s draw all images on the background:

padding := 1if shape == CircleShape {
padding = 20
}
rectangleEnd := image.Point{int(maxWidth) + (maxNumberOfColumns-1)*padding + 2*padding, int(maxHeight) + (numberOfRows-1)*padding + 2*padding}output := MyImage{image.NewRGBA(image.Rectangle{image.ZP, rectangleEnd})}sp_x, sp_y := 0, 0for row := 0; row < numberOfRows; row++ {
rowHeight := uint(0)
calculatedWidth := math.Floor(float64(desiredWidth) / float64(len(imagesMatrix[row])))for col := 0; col < len(imagesMatrix[row]); col++ {
originalWidth := float64(Width(imagesMatrix[row][col]))
originalHeight := float64(Height(imagesMatrix[row][col]))
resizeFactor := calculatedWidth / originalWidth
w := uint(originalWidth * resizeFactor)
h := uint(originalHeight * resizeFactor)
if col == 0 {
sp_x = padding
}
if row == 0 {
sp_y = padding
}
sp := image.Point{sp_x, sp_y} if shape == RectangleShape {
output.drawRaw(imagesMatrix[row][col], sp, w, h)
} else {
w = uint(math.Min(float64(w), float64(h)) * CircleDiameter)
h = w
output.drawInCircle(imagesMatrix[row][col], sp, w, h, int(w))
}

sp_x += int(w) + padding
if h > rowHeight {
rowHeight = h
}
}
sp_x = 0
sp_y += int(rowHeight) + padding
}
return &output

First, we define padding — the same value for the outer and the inner padding (between images). If a shape is circle, we want to show it more centered, since images corners will be rounded and images will be smaller overall. Therefore, we choose a bigger padding.

We create a new image (called the background image in this text), which will contain an image collage.

Then we loop through the images matrix and draw each image onto the background. As already said, the resize factor is calculated based on the width, and images are scaled by the width and height using the same factor. If the images are shown in the circle, we will take min(width, height) as the base for a diameter. Imagine this scenario. Some of the images is in the landscape mode and we took the width as a base. That way, a diameter of the circle would be bigger than the image height and we’ll have a blank space at the bottom and the top of the image. That’s why we need to take min (width, height) as base for a diameter. That’s it about creating an image collage.

Putting it to action — main function

func main() {
if len(os.Args) < 3 {
log.Fatal("No shape or number of rows defined")
} else {
imageShape := ImageShape(os.Args[1])
numberOfRows, errNr := strconv.Atoi(os.Args[2])
if errNr == nil && (imageShape == RectangleShape || imageShape == CircleShape) {
images := make([]image.Image, len(os.Args)-3)
for i := 3; i < len(os.Args); i++ {
fimg, _ := os.Open(os.Args[i])
defer fimg.Close()
img, _, _ := image.Decode(fimg)
images[i-3] = img
}
output := makeImageCollage(800, 800, numberOfRows, imageShape, images...) imview.Show(output.value) } else {
log.Fatal("No shape or number of rows defined")
}
}

Here we simply read images from the file system, call function makeImageCollage and show the result.

Of course, this same task could be done in many different ways. Full code can be found here: https://github.com/amerpersonal/imagecollager/blob/master/imagecollager.go

Let’s test it:

go run src/github.com/user/imagecollager/imagecollager.go Rectangle 2 nature1.jpeg nature2.jpeg nature3.jpg nature4.jpeg nature5.jpg nature6.jpeg
image collage — rectangle

Nice! Now let’s draw it in circle shape.

go run src/github.com/user/imagecollager/imagecollager.go Circle 2 nature1.jpeg nature2.jpeg nature3.jpg nature4.jpeg nature5.jpg nature6.jpeg
image collage — circle

Conclusion

We only scratched a surface on tools and possibilities for image processing in Go. We can do much more, like face and shape recognition, combining images, steganography (hiding text into images) and else. In the parts that follow, we will see how to employ go routines and channels to parallelise process and how to create a small web app to upload images and make collage.

Until then, have fun, write some good code and enjoy!

--

--

Amer Zildzic
Ministry of Programming — Technology

Software Engineer @ Pennylane , Master Degree in Electrical Engineering , Ruby on Rails, Scala, GoLang, Backend development