Golang: 4 points Image transformation using GoCV / OpenCV
Tags: go, datascience
Published: 2020-07-16 03:15:49

#### In this post, I will cover the following:
- What is 4-points OpenCV image transformation?
- How to implement 4-points OpenCV image transformation in Golang using Gocv? 
- Demonstrate usage of OpenCV getPerspectiveTransform function in Golang.
- Demonstrate how to do computer vision and image processing in Golang
- Demonstrate how to do numerical processing in Golang using [gonum](https://www.gonum.org/)


#### Prerequisite
- Basic knowledge of Golang.
- [Golang](https://golang.org/) & your favourite IDE installed on your machine.
- OpenCV v4.3.0 or above & pkgconfig installed in your machine. On mac you can run the following command to install: `brew install opencv` & `brew install pkgconfig`
- [GoCV git repository](https://github.com/hybridgroup/gocv) cloned to your local computer in GOPATH directory. Follow the instructions to install & clone gocv from its [official website](https://gocv.io/getting-started/)

---

#### What is OpenCV?
OpenCV also called Open source computer vision library is a popular open-source software library with more than 2500 algorithms for computer vision and machine learning problems. These algorithms are used widely to detect faces or moving objects, to generate high-resolution 3D images etc. You can read more about OpenCV on their [official website](https://opencv.org/about/)

Luckily, there are various libraries and packages available in different languages to access OpenCV library algorithms, so you don't need to implement them from scratch. `GoCV` is the implementation of OpenCV in Golang.

#### What is 4 point OpenCV image transformation?
4-point image transformation is a process to straighten an image by selecting four points (corners) in an Image. In the cover image, the green highlighted four-points are used to transform the image. OpenCV's getPerspectiveTransform() is the function that helps to achieve the image transformation.


---

#### Enough theory, let's code
>Most of the code provided below has decent comments to understand what's happening. The complete code can also be found at [Github](https://github.com/gkumarau/golang-four-point-image-transformation).

>Feel free to use the below code as-is or customise for personal/commercial use at your own risk. For any feedback or question, reach out to me on [Linkedin](https://www.linkedin.com/in/gkumarau/) or [Twitter](https://twitter.com/gkumarau).

```
package main

import (
	"gocv.io/x/gocv"
	"gonum.org/v1/gonum/floats"
	"gonum.org/v1/gonum/mat"
	"image"
	"math"
)

func main() {
	// Create a new gocv window to show the original image.
	original_window := gocv.NewWindow("Original")

	// Use IMRead function to read the image in colored form
	// Change the image location here to your custom image.
	original_image := gocv.IMRead("samples/original.png", gocv.IMReadColor)

	// Show the original image in the gocv window.
	// Press Return/ Enter to proceed next.
	for {
		original_window.IMShow(original_image)
		if original_window.WaitKey(1) >= 0 {
			break
		}
	}

	// Create a 4 x 2 matrix which has 4 rows and two columns
	// Each row represents the coordinates of a point.
	// Column (0, 1) represents the X, Y coordinate of a point.
	// We need to pass 4 points i.e 4 corners of image) or
	// as I call it, 4 points of interest to the below Matrix.
	// Important, the order of rows doesn't matter. We will
	// calculate which point is Top-left, top-right, bottom-left,
	// bottom-right automatically.
	pts := mat.NewDense(4, 2, []float64{
		69, 145,
		97, 735,
		938, 723,
		971, 170,
	})
	// Obviously, the above 4-points are hard-coded here.
	// Check my other article where I explain how to find the
	// four points automatically. It's a bit complex process
	// but fun to learn.

	// Create a new gocv window to show the transformed image.
	tranformed_window := gocv.NewWindow("Transformed")

	// Call our custom function FourPointTransform() to
	// transform the image. This function expects the
	// original gocv image and points of interest matrix.
	transformed_image := FourPointTransform(original_image, pts)

	// Show the original image in the gocv window.
	// Press Return/ Enter to proceed next.
	for {
		tranformed_window.IMShow(transformed_image)
		if tranformed_window.WaitKey(1) >= 0 {
			break
		}
	}
}

```

```

// Our custom function to perform 4-point transformation.
func FourPointTransform(img gocv.Mat, pts *mat.Dense) gocv.Mat{

	// We need to order the points so that we can find top-right,
	// top-left, bottom-right, bottom-left points.
	// The function orderPoints() is custom written and code is
	// provided in the same article.
	rect := orderPoints(pts)
	tl := rect.RawRowView(0)
	tr := rect.RawRowView(1)
	br := rect.RawRowView(2)
	bl := rect.RawRowView(3)

	// compute the width of the new image, which will be the
	// maximum distance between bottom-right and bottom-left
	// x-coordiates or the top-right and top-left x-coordinates
	widthA := math.Sqrt(math.Pow((br[0] - bl[0]), 2) + math.Pow((br[1] - bl[1]), 2))
	widthB := math.Sqrt(math.Pow((tr[0] - tl[0]), 2) + math.Pow((tr[1] - tl[1]), 2))
	maxWidth := int(math.Max(widthA, widthB))

	// compute the height of the new image, which will be the
	// maximum distance between the top-right and bottom-right
	// y-coordinates or the top-left and bottom-left y-coordinates
	heightA := math.Sqrt(math.Pow((tr[0] - br[0]),2) + math.Pow((tr[1] - br[1]), 2))
	heightB := math.Sqrt(math.Pow((tl[0] - bl[0]), 2) + math.Pow((tl[1] - bl[1]), 2))
	maxHeight := int(math.Max(heightA, heightB))

	// now that we have the dimensions of the new image, construct
	// the set of destination points to obtain a "birds eye view",
	// (i.e. top-down view) of the image, again specifying points
	// in the top-left, top-right, bottom-right, and bottom-left order
	dst := mat.NewDense(4, 2, []float64{
		0, 0,
		(float64(maxWidth) - 1), 0,
		(float64(maxWidth) - 1), (float64(maxHeight) - 1),
		0, (float64(maxHeight) - 1),
	})

	// Call the gocv's GetPerspectiveTransform() function and
	// WarpPerspective() function which does the magic of transforming
	// the image and writing it to destination.
	M := gocv.GetPerspectiveTransform(convertDenseToImagePoint(rect), convertDenseToImagePoint(dst))
	gocv.WarpPerspective(img, &img, M, image.Point{X: maxWidth, Y: maxHeight})

	// convertDenseToImagePoint() function is custom written, it converts
	// gonum matrix (*mat.Dense) -> []image.Point
	// This is very important as at this moment, gocv doesn't support
	// *mat.Dense directly and I did a lot of search and couldn't find
	// any easy solution except writing a convertor.

	return img
}

func convertDenseToImagePoint(pts *mat.Dense) []image.Point {
	var sd []image.Point

	r, c := pts.Dims()
	if (c !=2 ) {
		return sd
	}
	for i := 0; i < r; i++ {
		row := pts.RowView(i)
		sd = append(sd, image.Point{
			X: int(row.AtVec(0)),
			Y: int(row.AtVec(1)),
		})
	}
	return sd
}

```


```
func orderPoints(pts *mat.Dense) *mat.Dense{
	// initialzie a list of coordinates that will be ordered
	// such that the first entry in the list is the top-left,
	// the second entry is the top-right, the third is the
	// bottom-right, and the fourth is the bottom-left

	rect := mat.NewDense(4, 2, nil)

	// the top-left point will have the smallest sum, whereas
	// the bottom-right point will have the largest sum
	sumMinIndex, sumMaxIndex := findMinMaxSumIndex(*pts)
	rect.SetRow(0, pts.RawRowView(sumMinIndex))
	rect.SetRow(2, pts.RawRowView(sumMaxIndex))

	// now, compute the difference between the points, the
	// top-right point will have the smallest difference,
	// whereas the bottom-left will have the largest difference
	diffMinIndex, diffMaxIndex := findMinMaxDiffIndex(*pts)
	rect.SetRow(1, pts.RawRowView(diffMinIndex))
	rect.SetRow(3, pts.RawRowView(diffMaxIndex))

	// return the ordered coordinates
	return rect
}
```

```
func findMinMaxSumIndex(pts mat.Dense) (int, int){
	r, c := pts.Dims()

	maxIndex := 0
	maxValue := 0.0
	minIndex := 0
	minValue := 0.0

	for i := 0; i < r; i++ {
		row := pts.RowView(i)
		sum := 0.0
		for j := 0; j < c; j++ {
			sum += row.AtVec(j)
		}

		if (i == 0 ) {
			maxValue = sum
			minValue = sum
		}

		//Find max value and index
		if (sum > maxValue) {
			maxValue = sum
			maxIndex = i
		}
		//Find min value and index
		if (sum < minValue) {
			minValue = sum
			minIndex = i
		}
	}
	return minIndex, maxIndex
}

func findMinMaxDiffIndex(pts mat.Dense) (int, int){
	r, c := pts.Dims()

	maxIndex := 0
	maxValue := 0.0
	minIndex := 0
	minValue := 0.0

	for i := 0; i < r; i++ {
		row := pts.RowView(i)
		diff := row.AtVec(c - 1) //Do check c is not Zero or 1
		for j := c - 2; j >= 0; j-- {
			diff -= row.AtVec(j)
		}

		if (i == 0 ) {
			maxValue = diff
			minValue = diff
		}

		//Find max value and index
		if (diff > maxValue) {
			maxValue = diff
			maxIndex = i
		}
		//Find min value and index
		if (diff < minValue) {
			minValue = diff
			minIndex = i
		}
	}
	return minIndex, maxIndex
}
```