SIGN IN SIGN UP
esimov / caire UNCLAIMED

Content aware image resize library

0 0 1 Go
2018-01-11 17:14:52 +02:00
package caire
import (
"image"
2018-01-15 16:23:41 +02:00
_ "image/png"
2018-01-11 17:14:52 +02:00
"math"
2018-01-16 12:33:03 +02:00
"image/color"
2018-01-15 15:19:13 +02:00
"github.com/pkg/errors"
2018-01-11 17:14:52 +02:00
)
2018-01-15 09:59:53 +02:00
// SeamCarver is an interface that Carver uses to implement the Resize function.
// It takes an image and the output as parameters and returns the resized image
// and the error, if exists.
type SeamCarver interface {
2018-01-15 16:23:41 +02:00
Resize(*image.NRGBA) (image.Image, error)
2018-01-11 17:14:52 +02:00
}
2018-01-15 09:59:53 +02:00
// Seam struct containing the pixel coordinate values.
2018-01-13 15:11:54 +02:00
type Seam struct {
2018-01-11 17:14:52 +02:00
X int
Y int
}
2018-01-15 09:59:53 +02:00
// NewCarver returns an initialized Carver structure.
2018-01-16 12:33:03 +02:00
func NewCarver(width, height, threshold, blur, rw, rh int, perc bool) *Carver {
2018-01-15 09:59:53 +02:00
return &Carver{
width,
height,
make([]float64, width*height),
threshold,
blur,
rw, rh,
perc,
}
}
// Get energy pixel value.
func (c *Carver) get(x, y int) float64 {
2018-01-15 10:06:26 +02:00
px := x + y*c.Width
2018-01-15 09:59:53 +02:00
return c.Points[px]
2018-01-11 17:14:52 +02:00
}
2018-01-15 09:59:53 +02:00
// Set energy pixel value.
func (c *Carver) set(x, y int, px float64) {
2018-01-15 10:06:26 +02:00
idx := x + y*c.Width
2018-01-15 09:59:53 +02:00
c.Points[idx] = px
2018-01-11 17:14:52 +02:00
}
// Compute the minimum energy level based on the following logic:
// - traverse the image from the second row to the last row
// and compute the cumulative minimum energy M for all possible
// connected seams for each entry (i, j).
//
// - the minimum energy level is calculated by summing up the current pixel value
// with the minimum pixel value of the neighboring pixels from the previous row.
2018-01-15 09:59:53 +02:00
func (c *Carver) computeSeams(img *image.NRGBA) []float64 {
2018-01-13 15:11:54 +02:00
var src *image.NRGBA
bounds := img.Bounds()
iw, ih := bounds.Dx(), bounds.Dy()
2018-01-15 09:59:53 +02:00
sobel := SobelFilter(Grayscale(img), float64(c.SobelThreshold))
2018-01-14 14:49:48 +02:00
2018-01-15 09:59:53 +02:00
if c.BlurRadius > 0 {
src = Stackblur(sobel, uint32(iw), uint32(ih), uint32(c.BlurRadius))
2018-01-13 15:11:54 +02:00
} else {
src = sobel
}
2018-01-15 09:59:53 +02:00
for x := 0; x < c.Width; x++ {
for y := 0; y < c.Height; y++ {
2018-01-14 14:49:48 +02:00
r, _, _, a := src.At(x, y).RGBA()
2018-01-15 10:06:26 +02:00
c.set(x, y, float64(r)/float64(a))
2018-01-11 17:14:52 +02:00
}
}
2018-01-15 09:59:53 +02:00
// Compute the minimum energy level and set the resulting value into carver table.
for x := 0; x < c.Width; x++ {
for y := 1; y < c.Height; y++ {
2018-01-13 15:11:54 +02:00
var left, middle, right float64
left, right = math.MaxFloat64, math.MaxFloat64
2018-01-11 17:14:52 +02:00
2018-01-13 15:11:54 +02:00
// Do not compute edge cases: pixels are far left.
if x > 0 {
2018-01-15 09:59:53 +02:00
left = c.get(x-1, y-1)
2018-01-13 15:11:54 +02:00
}
2018-01-15 09:59:53 +02:00
middle = c.get(x, y-1)
2018-01-13 15:11:54 +02:00
// Do not compute edge cases: pixels are far right.
2018-01-15 09:59:53 +02:00
if x < c.Width-1 {
right = c.get(x+1, y-1)
2018-01-13 15:11:54 +02:00
}
// Obtain the minimum pixel value
min := math.Min(math.Min(left, middle), right)
2018-01-15 10:06:26 +02:00
c.set(x, y, c.get(x, y)+min)
2018-01-11 17:14:52 +02:00
}
}
2018-01-15 09:59:53 +02:00
return c.Points
2018-01-11 17:14:52 +02:00
}
// Find the lowest vertical energy seam.
2018-01-15 09:59:53 +02:00
func (c *Carver) findLowestEnergySeams() []Seam {
2018-01-11 17:14:52 +02:00
// Find the lowest cost seam from the energy matrix starting from the last row.
var min float64 = math.MaxFloat64
var px int
2018-01-13 15:11:54 +02:00
seams := make([]Seam, 0)
2018-01-11 17:14:52 +02:00
2018-01-14 14:49:48 +02:00
// Find the pixel on the last row with the minimum cumulative energy and use this as the starting pixel
2018-01-15 09:59:53 +02:00
for x := 0; x < c.Width; x++ {
seam := c.get(x, c.Height-1)
2018-01-14 14:49:48 +02:00
if seam < min && seam > 0 {
2018-01-11 17:14:52 +02:00
min = seam
px = x
}
}
2018-01-15 10:06:26 +02:00
seams = append(seams, Seam{X: px, Y: c.Height - 1})
2018-01-13 15:11:54 +02:00
var left, middle, right float64
2018-01-11 17:14:52 +02:00
// Walk up in the matrix table,
// check the immediate three top pixel seam level and
// add add the one which has the lowest cumulative energy.
2018-01-15 10:06:26 +02:00
for y := c.Height - 2; y >= 0; y-- {
2018-01-13 15:11:54 +02:00
left, right = math.MaxFloat64, math.MaxFloat64
2018-01-15 09:59:53 +02:00
middle = c.get(px, y)
2018-01-11 17:14:52 +02:00
// Leftmost seam, no child to the left
if px == 0 {
2018-01-15 09:59:53 +02:00
right = c.get(px+1, y)
middle = c.get(px, y)
2018-01-13 15:11:54 +02:00
if right < middle {
2018-01-11 17:14:52 +02:00
px += 1
}
2018-01-15 10:06:26 +02:00
// Rightmost seam, no child to the right
2018-01-15 09:59:53 +02:00
} else if px == c.Width-1 {
left = c.get(px-1, y)
middle = c.get(px, y)
2018-01-13 15:11:54 +02:00
if left < middle {
2018-01-11 17:14:52 +02:00
px -= 1
}
} else {
2018-01-15 09:59:53 +02:00
left = c.get(px-1, y)
middle = c.get(px, y)
right = c.get(px+1, y)
2018-01-13 15:11:54 +02:00
min := math.Min(math.Min(left, middle), right)
2018-01-11 17:14:52 +02:00
if min == left {
px -= 1
} else if min == right {
px += 1
}
}
2018-01-13 15:11:54 +02:00
seams = append(seams, Seam{X: px, Y: y})
2018-01-11 17:14:52 +02:00
}
return seams
}
2018-01-15 09:59:53 +02:00
// Remove image pixels based on energy seams level.
func (c *Carver) removeSeam(img *image.NRGBA, seams []Seam) *image.NRGBA {
2018-01-13 15:11:54 +02:00
bounds := img.Bounds()
2018-01-14 14:49:48 +02:00
dst := image.NewNRGBA(image.Rect(0, 0, bounds.Dx()-1, bounds.Dy()))
2018-01-13 15:11:54 +02:00
for _, seam := range seams {
y := seam.Y
for x := 0; x < bounds.Max.X; x++ {
if seam.X == x {
2018-01-16 12:33:03 +02:00
//continue
dst.Set(x-1, y, color.RGBA{255, 0, 0, 255})
2018-01-13 15:11:54 +02:00
} else if seam.X < x {
dst.Set(x-1, y, img.At(x, y))
} else {
dst.Set(x, y, img.At(x, y))
}
}
}
return dst
}
2018-01-15 16:23:41 +02:00
// Rotate image by 90 degree counter clockwise
2018-01-15 15:19:13 +02:00
func (c *Carver) rotateImage90(src *image.NRGBA) *image.NRGBA {
b := src.Bounds()
dst := image.NewNRGBA(image.Rect(0, 0, b.Max.Y, b.Max.X))
for dstY := 0; dstY < b.Max.X; dstY++ {
for dstX := 0; dstX < b.Max.Y; dstX++ {
srcX := b.Max.X - dstY - 1
srcY := dstX
srcOff := srcY*src.Stride + srcX*4
dstOff := dstY*dst.Stride + dstX*4
copy(dst.Pix[dstOff:dstOff+4], src.Pix[srcOff:srcOff+4])
}
}
return dst
}
2018-01-15 16:23:41 +02:00
// Rotate image by 270 degree counter clockwise
2018-01-15 15:19:13 +02:00
func (c *Carver) rotateImage270(src *image.NRGBA) *image.NRGBA {
b := src.Bounds()
dst := image.NewNRGBA(image.Rect(0, 0, b.Max.Y, b.Max.X))
for dstY := 0; dstY < b.Max.X; dstY++ {
for dstX := 0; dstX < b.Max.Y; dstX++ {
srcX := dstY
srcY := b.Max.Y - dstX - 1
srcOff := srcY*src.Stride + srcX*4
dstOff := dstY*dst.Stride + dstX*4
copy(dst.Pix[dstOff:dstOff+4], src.Pix[srcOff:srcOff+4])
}
}
return dst
}
2018-01-15 09:59:53 +02:00
// Resize is the main function taking the source image and encoding the rescaled image into the output file.
2018-01-15 16:23:41 +02:00
func (c *Carver) Resize(img *image.NRGBA) (image.Image, error) {
2018-01-15 15:19:13 +02:00
width, height := img.Bounds().Max.X, img.Bounds().Max.Y
carver := NewCarver(width, height, c.SobelThreshold, c.BlurRadius, c.NewWidth, c.NewHeight, c.Percentage)
2018-01-15 09:59:53 +02:00
resize := func() {
2018-01-15 15:19:13 +02:00
carver.computeSeams(img)
2018-01-15 09:59:53 +02:00
seams := carver.findLowestEnergySeams()
2018-01-15 15:19:13 +02:00
img = carver.removeSeam(img, seams)
2018-01-15 09:59:53 +02:00
}
2018-01-15 15:19:13 +02:00
2018-01-16 12:33:03 +02:00
if carver.Percentage {
2018-01-15 09:59:53 +02:00
// Calculate new sizes based on provided percentage.
2018-01-16 12:33:03 +02:00
nw := carver.Width - int(float64(carver.Width) - (float64(carver.NewWidth)/100 * float64(carver.Width)))
nh := carver.Height - int(float64(carver.Height) - (float64(carver.NewHeight)/100 * float64(carver.Height)))
2018-01-15 15:19:13 +02:00
// Resize image horizontally
2018-01-15 09:59:53 +02:00
for x := 0; x < nw; x++ {
resize()
}
2018-01-15 15:19:13 +02:00
// Resize image vertically
img = c.rotateImage90(img)
2018-01-15 18:15:13 +02:00
// Needs to update the slice width & height because of image rotation.
carver.Width = img.Bounds().Dx()
carver.Height = img.Bounds().Dy()
2018-01-15 09:59:53 +02:00
for y := 0; y < nh; y++ {
resize()
}
2018-01-15 15:19:13 +02:00
img = c.rotateImage270(img)
} else if carver.NewWidth > 0 || carver.NewHeight > 0 {
if carver.NewWidth > 0 {
if carver.NewWidth > carver.Width {
err := errors.New("new width should be less than image width.")
return nil, err
}
2018-01-15 09:59:53 +02:00
for x := 0; x < carver.NewWidth; x++ {
resize()
}
2018-01-15 15:19:13 +02:00
}
if carver.NewHeight > 0 {
if carver.NewHeight > carver.Height {
err := errors.New("new height should be less than image height.")
return nil, err
}
img = c.rotateImage90(img)
2018-01-15 18:15:13 +02:00
2018-01-16 12:33:03 +02:00
// Needs to update the slice width & height because of image rotation
// otherwise the new image will be cut off.
2018-01-15 18:15:13 +02:00
carver.Width = img.Bounds().Dx()
carver.Height = img.Bounds().Dy()
2018-01-15 09:59:53 +02:00
for y := 0; y < carver.NewHeight; y++ {
resize()
}
2018-01-15 15:19:13 +02:00
img = c.rotateImage270(img)
2018-01-13 15:11:54 +02:00
}
}
2018-01-15 16:23:41 +02:00
return img, nil
2018-01-15 18:15:13 +02:00
}