add imaging

This commit is contained in:
Jack
2015-09-23 20:00:36 -07:00
parent f43d355078
commit dd94a3df5d
9 changed files with 1984 additions and 0 deletions

View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2012-2014 Grigory Dryapak
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,160 @@
# Imaging
Package imaging provides basic image manipulation functions (resize, rotate, flip, crop, etc.).
This package is based on the standard Go image package and works best along with it.
Image manipulation functions provided by the package take any image type
that implements `image.Image` interface as an input, and return a new image of
`*image.NRGBA` type (32bit RGBA colors, not premultiplied by alpha).
## Installation
Imaging requires Go version 1.2 or greater.
go get -u github.com/disintegration/imaging
## Documentation
http://godoc.org/github.com/disintegration/imaging
## Usage examples
A few usage examples can be found below. See the documentation for the full list of supported functions.
### Image resizing
```go
// resize srcImage to size = 128x128px using the Lanczos filter
dstImage128 := imaging.Resize(srcImage, 128, 128, imaging.Lanczos)
// resize srcImage to width = 800px preserving the aspect ratio
dstImage800 := imaging.Resize(srcImage, 800, 0, imaging.Lanczos)
// scale down srcImage to fit the 800x600px bounding box
dstImageFit := imaging.Fit(srcImage, 800, 600, imaging.Lanczos)
// resize and crop the srcImage to make a 100x100px thumbnail
dstImageThumb := imaging.Thumbnail(srcImage, 100, 100, imaging.Lanczos)
```
Imaging supports image resizing using various resampling filters. The most notable ones:
- `NearestNeighbor` - Fastest resampling filter, no antialiasing.
- `Box` - Simple and fast averaging filter appropriate for downscaling. When upscaling it's similar to NearestNeighbor.
- `Linear` - Bilinear filter, smooth and reasonably fast.
- `MitchellNetravali` - А smooth bicubic filter.
- `CatmullRom` - A sharp bicubic filter.
- `Gaussian` - Blurring filter that uses gaussian function, useful for noise removal.
- `Lanczos` - High-quality resampling filter for photographic images yielding sharp results, but it's slower than cubic filters.
The full list of supported filters: NearestNeighbor, Box, Linear, Hermite, MitchellNetravali, CatmullRom, BSpline, Gaussian, Lanczos, Hann, Hamming, Blackman, Bartlett, Welch, Cosine. Custom filters can be created using ResampleFilter struct.
**Resampling filters comparison**
Original image. Will be resized from 512x512px to 128x128px.
![srcImage](http://disintegration.github.io/imaging/in_lena_bw_512.png)
Filter | Resize result
---|---
`imaging.NearestNeighbor` | ![dstImage](http://disintegration.github.io/imaging/out_resize_down_nearest.png)
`imaging.Box` | ![dstImage](http://disintegration.github.io/imaging/out_resize_down_box.png)
`imaging.Linear` | ![dstImage](http://disintegration.github.io/imaging/out_resize_down_linear.png)
`imaging.MitchellNetravali` | ![dstImage](http://disintegration.github.io/imaging/out_resize_down_mitchell.png)
`imaging.CatmullRom` | ![dstImage](http://disintegration.github.io/imaging/out_resize_down_catrom.png)
`imaging.Gaussian` | ![dstImage](http://disintegration.github.io/imaging/out_resize_down_gaussian.png)
`imaging.Lanczos` | ![dstImage](http://disintegration.github.io/imaging/out_resize_down_lanczos.png)
### Gaussian Blur
```go
dstImage := imaging.Blur(srcImage, 0.5)
```
Sigma parameter allows to control the strength of the blurring effect.
Original image | Sigma = 0.5 | Sigma = 1.5
---|---|---
![srcImage](http://disintegration.github.io/imaging/in_lena_bw_128.png) | ![dstImage](http://disintegration.github.io/imaging/out_blur_0.5.png) | ![dstImage](http://disintegration.github.io/imaging/out_blur_1.5.png)
### Sharpening
```go
dstImage := imaging.Sharpen(srcImage, 0.5)
```
Uses gaussian function internally. Sigma parameter allows to control the strength of the sharpening effect.
Original image | Sigma = 0.5 | Sigma = 1.5
---|---|---
![srcImage](http://disintegration.github.io/imaging/in_lena_bw_128.png) | ![dstImage](http://disintegration.github.io/imaging/out_sharpen_0.5.png) | ![dstImage](http://disintegration.github.io/imaging/out_sharpen_1.5.png)
### Gamma correction
```go
dstImage := imaging.AdjustGamma(srcImage, 0.75)
```
Original image | Gamma = 0.75 | Gamma = 1.25
---|---|---
![srcImage](http://disintegration.github.io/imaging/in_lena_bw_128.png) | ![dstImage](http://disintegration.github.io/imaging/out_gamma_0.75.png) | ![dstImage](http://disintegration.github.io/imaging/out_gamma_1.25.png)
### Contrast adjustment
```go
dstImage := imaging.AdjustContrast(srcImage, 20)
```
Original image | Contrast = 20 | Contrast = -20
---|---|---
![srcImage](http://disintegration.github.io/imaging/in_lena_bw_128.png) | ![dstImage](http://disintegration.github.io/imaging/out_contrast_p20.png) | ![dstImage](http://disintegration.github.io/imaging/out_contrast_m20.png)
### Brightness adjustment
```go
dstImage := imaging.AdjustBrightness(srcImage, 20)
```
Original image | Brightness = 20 | Brightness = -20
---|---|---
![srcImage](http://disintegration.github.io/imaging/in_lena_bw_128.png) | ![dstImage](http://disintegration.github.io/imaging/out_brightness_p20.png) | ![dstImage](http://disintegration.github.io/imaging/out_brightness_m20.png)
### Complete code example
Here is the code example that loads several images, makes thumbnails of them
and combines them together side-by-side.
```go
package main
import (
"image"
"image/color"
"github.com/disintegration/imaging"
)
func main() {
// input files
files := []string{"01.jpg", "02.jpg", "03.jpg"}
// load images and make 100x100 thumbnails of them
var thumbnails []image.Image
for _, file := range files {
img, err := imaging.Open(file)
if err != nil {
panic(err)
}
thumb := imaging.Thumbnail(img, 100, 100, imaging.CatmullRom)
thumbnails = append(thumbnails, thumb)
}
// create a new blank image
dst := imaging.New(100*len(thumbnails), 100, color.NRGBA{0, 0, 0, 0})
// paste thumbnails into the new image side by side
for i, thumb := range thumbnails {
dst = imaging.Paste(dst, thumb, image.Pt(i*100, 0))
}
// save the combined image to file
err := imaging.Save(dst, "dst.jpg")
if err != nil {
panic(err)
}
}
```

View File

@@ -0,0 +1,200 @@
package imaging
import (
"image"
"image/color"
"math"
)
// AdjustFunc applies the fn function to each pixel of the img image and returns the adjusted image.
//
// Example:
//
// dstImage = imaging.AdjustFunc(
// srcImage,
// func(c color.NRGBA) color.NRGBA {
// // shift the red channel by 16
// r := int(c.R) + 16
// if r > 255 {
// r = 255
// }
// return color.NRGBA{uint8(r), c.G, c.B, c.A}
// }
// )
//
func AdjustFunc(img image.Image, fn func(c color.NRGBA) color.NRGBA) *image.NRGBA {
src := toNRGBA(img)
width := src.Bounds().Max.X
height := src.Bounds().Max.Y
dst := image.NewNRGBA(image.Rect(0, 0, width, height))
parallel(height, func(partStart, partEnd int) {
for y := partStart; y < partEnd; y++ {
for x := 0; x < width; x++ {
i := y*src.Stride + x*4
j := y*dst.Stride + x*4
r := src.Pix[i+0]
g := src.Pix[i+1]
b := src.Pix[i+2]
a := src.Pix[i+3]
c := fn(color.NRGBA{r, g, b, a})
dst.Pix[j+0] = c.R
dst.Pix[j+1] = c.G
dst.Pix[j+2] = c.B
dst.Pix[j+3] = c.A
}
}
})
return dst
}
// AdjustGamma performs a gamma correction on the image and returns the adjusted image.
// Gamma parameter must be positive. Gamma = 1.0 gives the original image.
// Gamma less than 1.0 darkens the image and gamma greater than 1.0 lightens it.
//
// Example:
//
// dstImage = imaging.AdjustGamma(srcImage, 0.7)
//
func AdjustGamma(img image.Image, gamma float64) *image.NRGBA {
e := 1.0 / math.Max(gamma, 0.0001)
lut := make([]uint8, 256)
for i := 0; i < 256; i++ {
lut[i] = clamp(math.Pow(float64(i)/255.0, e) * 255.0)
}
fn := func(c color.NRGBA) color.NRGBA {
return color.NRGBA{lut[c.R], lut[c.G], lut[c.B], c.A}
}
return AdjustFunc(img, fn)
}
func sigmoid(a, b, x float64) float64 {
return 1 / (1 + math.Exp(b*(a-x)))
}
// AdjustSigmoid changes the contrast of the image using a sigmoidal function and returns the adjusted image.
// It's a non-linear contrast change useful for photo adjustments as it preserves highlight and shadow detail.
// The midpoint parameter is the midpoint of contrast that must be between 0 and 1, typically 0.5.
// The factor parameter indicates how much to increase or decrease the contrast, typically in range (-10, 10).
// If the factor parameter is positive the image contrast is increased otherwise the contrast is decreased.
//
// Examples:
//
// dstImage = imaging.AdjustSigmoid(srcImage, 0.5, 3.0) // increase the contrast
// dstImage = imaging.AdjustSigmoid(srcImage, 0.5, -3.0) // decrease the contrast
//
func AdjustSigmoid(img image.Image, midpoint, factor float64) *image.NRGBA {
if factor == 0 {
return Clone(img)
}
lut := make([]uint8, 256)
a := math.Min(math.Max(midpoint, 0.0), 1.0)
b := math.Abs(factor)
sig0 := sigmoid(a, b, 0)
sig1 := sigmoid(a, b, 1)
e := 1.0e-6
if factor > 0 {
for i := 0; i < 256; i++ {
x := float64(i) / 255.0
sigX := sigmoid(a, b, x)
f := (sigX - sig0) / (sig1 - sig0)
lut[i] = clamp(f * 255.0)
}
} else {
for i := 0; i < 256; i++ {
x := float64(i) / 255.0
arg := math.Min(math.Max((sig1-sig0)*x+sig0, e), 1.0-e)
f := a - math.Log(1.0/arg-1.0)/b
lut[i] = clamp(f * 255.0)
}
}
fn := func(c color.NRGBA) color.NRGBA {
return color.NRGBA{lut[c.R], lut[c.G], lut[c.B], c.A}
}
return AdjustFunc(img, fn)
}
// AdjustContrast changes the contrast of the image using the percentage parameter and returns the adjusted image.
// The percentage must be in range (-100, 100). The percentage = 0 gives the original image.
// The percentage = -100 gives solid grey image.
//
// Examples:
//
// dstImage = imaging.AdjustContrast(srcImage, -10) // decrease image contrast by 10%
// dstImage = imaging.AdjustContrast(srcImage, 20) // increase image contrast by 20%
//
func AdjustContrast(img image.Image, percentage float64) *image.NRGBA {
percentage = math.Min(math.Max(percentage, -100.0), 100.0)
lut := make([]uint8, 256)
v := (100.0 + percentage) / 100.0
for i := 0; i < 256; i++ {
if 0 <= v && v <= 1 {
lut[i] = clamp((0.5 + (float64(i)/255.0-0.5)*v) * 255.0)
} else if 1 < v && v < 2 {
lut[i] = clamp((0.5 + (float64(i)/255.0-0.5)*(1/(2.0-v))) * 255.0)
} else {
lut[i] = uint8(float64(i)/255.0+0.5) * 255
}
}
fn := func(c color.NRGBA) color.NRGBA {
return color.NRGBA{lut[c.R], lut[c.G], lut[c.B], c.A}
}
return AdjustFunc(img, fn)
}
// AdjustBrightness changes the brightness of the image using the percentage parameter and returns the adjusted image.
// The percentage must be in range (-100, 100). The percentage = 0 gives the original image.
// The percentage = -100 gives solid black image. The percentage = 100 gives solid white image.
//
// Examples:
//
// dstImage = imaging.AdjustBrightness(srcImage, -15) // decrease image brightness by 15%
// dstImage = imaging.AdjustBrightness(srcImage, 10) // increase image brightness by 10%
//
func AdjustBrightness(img image.Image, percentage float64) *image.NRGBA {
percentage = math.Min(math.Max(percentage, -100.0), 100.0)
lut := make([]uint8, 256)
shift := 255.0 * percentage / 100.0
for i := 0; i < 256; i++ {
lut[i] = clamp(float64(i) + shift)
}
fn := func(c color.NRGBA) color.NRGBA {
return color.NRGBA{lut[c.R], lut[c.G], lut[c.B], c.A}
}
return AdjustFunc(img, fn)
}
// Grayscale produces grayscale version of the image.
func Grayscale(img image.Image) *image.NRGBA {
fn := func(c color.NRGBA) color.NRGBA {
f := 0.299*float64(c.R) + 0.587*float64(c.G) + 0.114*float64(c.B)
y := uint8(f + 0.5)
return color.NRGBA{y, y, y, c.A}
}
return AdjustFunc(img, fn)
}
// Invert produces inverted (negated) version of the image.
func Invert(img image.Image) *image.NRGBA {
fn := func(c color.NRGBA) color.NRGBA {
return color.NRGBA{255 - c.R, 255 - c.G, 255 - c.B, c.A}
}
return AdjustFunc(img, fn)
}

View File

@@ -0,0 +1,187 @@
package imaging
import (
"image"
"math"
)
func gaussianBlurKernel(x, sigma float64) float64 {
return math.Exp(-(x*x)/(2*sigma*sigma)) / (sigma * math.Sqrt(2*math.Pi))
}
// Blur produces a blurred version of the image using a Gaussian function.
// Sigma parameter must be positive and indicates how much the image will be blurred.
//
// Usage example:
//
// dstImage := imaging.Blur(srcImage, 3.5)
//
func Blur(img image.Image, sigma float64) *image.NRGBA {
if sigma <= 0 {
// sigma parameter must be positive!
return Clone(img)
}
src := toNRGBA(img)
radius := int(math.Ceil(sigma * 3.0))
kernel := make([]float64, radius+1)
for i := 0; i <= radius; i++ {
kernel[i] = gaussianBlurKernel(float64(i), sigma)
}
var dst *image.NRGBA
dst = blurHorizontal(src, kernel)
dst = blurVertical(dst, kernel)
return dst
}
func blurHorizontal(src *image.NRGBA, kernel []float64) *image.NRGBA {
radius := len(kernel) - 1
width := src.Bounds().Max.X
height := src.Bounds().Max.Y
dst := image.NewNRGBA(image.Rect(0, 0, width, height))
parallel(width, func(partStart, partEnd int) {
for x := partStart; x < partEnd; x++ {
start := x - radius
if start < 0 {
start = 0
}
end := x + radius
if end > width-1 {
end = width - 1
}
weightSum := 0.0
for ix := start; ix <= end; ix++ {
weightSum += kernel[absint(x-ix)]
}
for y := 0; y < height; y++ {
r, g, b, a := 0.0, 0.0, 0.0, 0.0
for ix := start; ix <= end; ix++ {
weight := kernel[absint(x-ix)]
i := y*src.Stride + ix*4
r += float64(src.Pix[i+0]) * weight
g += float64(src.Pix[i+1]) * weight
b += float64(src.Pix[i+2]) * weight
a += float64(src.Pix[i+3]) * weight
}
r = math.Min(math.Max(r/weightSum, 0.0), 255.0)
g = math.Min(math.Max(g/weightSum, 0.0), 255.0)
b = math.Min(math.Max(b/weightSum, 0.0), 255.0)
a = math.Min(math.Max(a/weightSum, 0.0), 255.0)
j := y*dst.Stride + x*4
dst.Pix[j+0] = uint8(r + 0.5)
dst.Pix[j+1] = uint8(g + 0.5)
dst.Pix[j+2] = uint8(b + 0.5)
dst.Pix[j+3] = uint8(a + 0.5)
}
}
})
return dst
}
func blurVertical(src *image.NRGBA, kernel []float64) *image.NRGBA {
radius := len(kernel) - 1
width := src.Bounds().Max.X
height := src.Bounds().Max.Y
dst := image.NewNRGBA(image.Rect(0, 0, width, height))
parallel(height, func(partStart, partEnd int) {
for y := partStart; y < partEnd; y++ {
start := y - radius
if start < 0 {
start = 0
}
end := y + radius
if end > height-1 {
end = height - 1
}
weightSum := 0.0
for iy := start; iy <= end; iy++ {
weightSum += kernel[absint(y-iy)]
}
for x := 0; x < width; x++ {
r, g, b, a := 0.0, 0.0, 0.0, 0.0
for iy := start; iy <= end; iy++ {
weight := kernel[absint(y-iy)]
i := iy*src.Stride + x*4
r += float64(src.Pix[i+0]) * weight
g += float64(src.Pix[i+1]) * weight
b += float64(src.Pix[i+2]) * weight
a += float64(src.Pix[i+3]) * weight
}
r = math.Min(math.Max(r/weightSum, 0.0), 255.0)
g = math.Min(math.Max(g/weightSum, 0.0), 255.0)
b = math.Min(math.Max(b/weightSum, 0.0), 255.0)
a = math.Min(math.Max(a/weightSum, 0.0), 255.0)
j := y*dst.Stride + x*4
dst.Pix[j+0] = uint8(r + 0.5)
dst.Pix[j+1] = uint8(g + 0.5)
dst.Pix[j+2] = uint8(b + 0.5)
dst.Pix[j+3] = uint8(a + 0.5)
}
}
})
return dst
}
// Sharpen produces a sharpened version of the image.
// Sigma parameter must be positive and indicates how much the image will be sharpened.
//
// Usage example:
//
// dstImage := imaging.Sharpen(srcImage, 3.5)
//
func Sharpen(img image.Image, sigma float64) *image.NRGBA {
if sigma <= 0 {
// sigma parameter must be positive!
return Clone(img)
}
src := toNRGBA(img)
blurred := Blur(img, sigma)
width := src.Bounds().Max.X
height := src.Bounds().Max.Y
dst := image.NewNRGBA(image.Rect(0, 0, width, height))
parallel(height, func(partStart, partEnd int) {
for y := partStart; y < partEnd; y++ {
for x := 0; x < width; x++ {
i := y*src.Stride + x*4
for j := 0; j < 4; j++ {
k := i + j
val := int(src.Pix[k]) + (int(src.Pix[k]) - int(blurred.Pix[k]))
if val < 0 {
val = 0
} else if val > 255 {
val = 255
}
dst.Pix[k] = uint8(val)
}
}
}
})
return dst
}

View File

@@ -0,0 +1,392 @@
/*
Package imaging provides basic image manipulation functions (resize, rotate, flip, crop, etc.).
This package is based on the standard Go image package and works best along with it.
Image manipulation functions provided by the package take any image type
that implements `image.Image` interface as an input, and return a new image of
`*image.NRGBA` type (32bit RGBA colors, not premultiplied by alpha).
*/
package imaging
import (
"errors"
"image"
"image/color"
"image/gif"
"image/jpeg"
"image/png"
"io"
"os"
"path/filepath"
"strings"
"golang.org/x/image/bmp"
"golang.org/x/image/tiff"
)
type Format int
const (
JPEG Format = iota
PNG
GIF
TIFF
BMP
)
func (f Format) String() string {
switch f {
case JPEG:
return "JPEG"
case PNG:
return "PNG"
case GIF:
return "GIF"
case TIFF:
return "TIFF"
case BMP:
return "BMP"
default:
return "Unsupported"
}
}
var (
ErrUnsupportedFormat = errors.New("imaging: unsupported image format")
)
// Decode reads an image from r.
func Decode(r io.Reader) (image.Image, error) {
img, _, err := image.Decode(r)
if err != nil {
return nil, err
}
return toNRGBA(img), nil
}
// Open loads an image from file
func Open(filename string) (image.Image, error) {
file, err := os.Open(filename)
if err != nil {
return nil, err
}
defer file.Close()
img, err := Decode(file)
return img, err
}
// Encode writes the image img to w in the specified format (JPEG, PNG, GIF, TIFF or BMP).
func Encode(w io.Writer, img image.Image, format Format) error {
var err error
switch format {
case JPEG:
var rgba *image.RGBA
if nrgba, ok := img.(*image.NRGBA); ok {
if nrgba.Opaque() {
rgba = &image.RGBA{
Pix: nrgba.Pix,
Stride: nrgba.Stride,
Rect: nrgba.Rect,
}
}
}
if rgba != nil {
err = jpeg.Encode(w, rgba, &jpeg.Options{Quality: 95})
} else {
err = jpeg.Encode(w, img, &jpeg.Options{Quality: 95})
}
case PNG:
err = png.Encode(w, img)
case GIF:
err = gif.Encode(w, img, &gif.Options{NumColors: 256})
case TIFF:
err = tiff.Encode(w, img, &tiff.Options{Compression: tiff.Deflate, Predictor: true})
case BMP:
err = bmp.Encode(w, img)
default:
err = ErrUnsupportedFormat
}
return err
}
// Save saves the image to file with the specified filename.
// The format is determined from the filename extension: "jpg" (or "jpeg"), "png", "gif", "tif" (or "tiff") and "bmp" are supported.
func Save(img image.Image, filename string) (err error) {
formats := map[string]Format{
".jpg": JPEG,
".jpeg": JPEG,
".png": PNG,
".tif": TIFF,
".tiff": TIFF,
".bmp": BMP,
".gif": GIF,
}
ext := strings.ToLower(filepath.Ext(filename))
f, ok := formats[ext]
if !ok {
return ErrUnsupportedFormat
}
file, err := os.Create(filename)
if err != nil {
return err
}
defer file.Close()
return Encode(file, img, f)
}
// New creates a new image with the specified width and height, and fills it with the specified color.
func New(width, height int, fillColor color.Color) *image.NRGBA {
if width <= 0 || height <= 0 {
return &image.NRGBA{}
}
dst := image.NewNRGBA(image.Rect(0, 0, width, height))
c := color.NRGBAModel.Convert(fillColor).(color.NRGBA)
if c.R == 0 && c.G == 0 && c.B == 0 && c.A == 0 {
return dst
}
cs := []uint8{c.R, c.G, c.B, c.A}
// fill the first row
for x := 0; x < width; x++ {
copy(dst.Pix[x*4:(x+1)*4], cs)
}
// copy the first row to other rows
for y := 1; y < height; y++ {
copy(dst.Pix[y*dst.Stride:y*dst.Stride+width*4], dst.Pix[0:width*4])
}
return dst
}
// Clone returns a copy of the given image.
func Clone(img image.Image) *image.NRGBA {
srcBounds := img.Bounds()
srcMinX := srcBounds.Min.X
srcMinY := srcBounds.Min.Y
dstBounds := srcBounds.Sub(srcBounds.Min)
dstW := dstBounds.Dx()
dstH := dstBounds.Dy()
dst := image.NewNRGBA(dstBounds)
switch src := img.(type) {
case *image.NRGBA:
rowSize := srcBounds.Dx() * 4
parallel(dstH, func(partStart, partEnd int) {
for dstY := partStart; dstY < partEnd; dstY++ {
di := dst.PixOffset(0, dstY)
si := src.PixOffset(srcMinX, srcMinY+dstY)
copy(dst.Pix[di:di+rowSize], src.Pix[si:si+rowSize])
}
})
case *image.NRGBA64:
parallel(dstH, func(partStart, partEnd int) {
for dstY := partStart; dstY < partEnd; dstY++ {
di := dst.PixOffset(0, dstY)
si := src.PixOffset(srcMinX, srcMinY+dstY)
for dstX := 0; dstX < dstW; dstX++ {
dst.Pix[di+0] = src.Pix[si+0]
dst.Pix[di+1] = src.Pix[si+2]
dst.Pix[di+2] = src.Pix[si+4]
dst.Pix[di+3] = src.Pix[si+6]
di += 4
si += 8
}
}
})
case *image.RGBA:
parallel(dstH, func(partStart, partEnd int) {
for dstY := partStart; dstY < partEnd; dstY++ {
di := dst.PixOffset(0, dstY)
si := src.PixOffset(srcMinX, srcMinY+dstY)
for dstX := 0; dstX < dstW; dstX++ {
a := src.Pix[si+3]
dst.Pix[di+3] = a
switch a {
case 0:
dst.Pix[di+0] = 0
dst.Pix[di+1] = 0
dst.Pix[di+2] = 0
case 0xff:
dst.Pix[di+0] = src.Pix[si+0]
dst.Pix[di+1] = src.Pix[si+1]
dst.Pix[di+2] = src.Pix[si+2]
default:
dst.Pix[di+0] = uint8(uint16(src.Pix[si+0]) * 0xff / uint16(a))
dst.Pix[di+1] = uint8(uint16(src.Pix[si+1]) * 0xff / uint16(a))
dst.Pix[di+2] = uint8(uint16(src.Pix[si+2]) * 0xff / uint16(a))
}
di += 4
si += 4
}
}
})
case *image.RGBA64:
parallel(dstH, func(partStart, partEnd int) {
for dstY := partStart; dstY < partEnd; dstY++ {
di := dst.PixOffset(0, dstY)
si := src.PixOffset(srcMinX, srcMinY+dstY)
for dstX := 0; dstX < dstW; dstX++ {
a := src.Pix[si+6]
dst.Pix[di+3] = a
switch a {
case 0:
dst.Pix[di+0] = 0
dst.Pix[di+1] = 0
dst.Pix[di+2] = 0
case 0xff:
dst.Pix[di+0] = src.Pix[si+0]
dst.Pix[di+1] = src.Pix[si+2]
dst.Pix[di+2] = src.Pix[si+4]
default:
dst.Pix[di+0] = uint8(uint16(src.Pix[si+0]) * 0xff / uint16(a))
dst.Pix[di+1] = uint8(uint16(src.Pix[si+2]) * 0xff / uint16(a))
dst.Pix[di+2] = uint8(uint16(src.Pix[si+4]) * 0xff / uint16(a))
}
di += 4
si += 8
}
}
})
case *image.Gray:
parallel(dstH, func(partStart, partEnd int) {
for dstY := partStart; dstY < partEnd; dstY++ {
di := dst.PixOffset(0, dstY)
si := src.PixOffset(srcMinX, srcMinY+dstY)
for dstX := 0; dstX < dstW; dstX++ {
c := src.Pix[si]
dst.Pix[di+0] = c
dst.Pix[di+1] = c
dst.Pix[di+2] = c
dst.Pix[di+3] = 0xff
di += 4
si += 1
}
}
})
case *image.Gray16:
parallel(dstH, func(partStart, partEnd int) {
for dstY := partStart; dstY < partEnd; dstY++ {
di := dst.PixOffset(0, dstY)
si := src.PixOffset(srcMinX, srcMinY+dstY)
for dstX := 0; dstX < dstW; dstX++ {
c := src.Pix[si]
dst.Pix[di+0] = c
dst.Pix[di+1] = c
dst.Pix[di+2] = c
dst.Pix[di+3] = 0xff
di += 4
si += 2
}
}
})
case *image.YCbCr:
parallel(dstH, func(partStart, partEnd int) {
for dstY := partStart; dstY < partEnd; dstY++ {
di := dst.PixOffset(0, dstY)
for dstX := 0; dstX < dstW; dstX++ {
srcX := srcMinX + dstX
srcY := srcMinY + dstY
siy := src.YOffset(srcX, srcY)
sic := src.COffset(srcX, srcY)
r, g, b := color.YCbCrToRGB(src.Y[siy], src.Cb[sic], src.Cr[sic])
dst.Pix[di+0] = r
dst.Pix[di+1] = g
dst.Pix[di+2] = b
dst.Pix[di+3] = 0xff
di += 4
}
}
})
case *image.Paletted:
plen := len(src.Palette)
pnew := make([]color.NRGBA, plen)
for i := 0; i < plen; i++ {
pnew[i] = color.NRGBAModel.Convert(src.Palette[i]).(color.NRGBA)
}
parallel(dstH, func(partStart, partEnd int) {
for dstY := partStart; dstY < partEnd; dstY++ {
di := dst.PixOffset(0, dstY)
si := src.PixOffset(srcMinX, srcMinY+dstY)
for dstX := 0; dstX < dstW; dstX++ {
c := pnew[src.Pix[si]]
dst.Pix[di+0] = c.R
dst.Pix[di+1] = c.G
dst.Pix[di+2] = c.B
dst.Pix[di+3] = c.A
di += 4
si += 1
}
}
})
default:
parallel(dstH, func(partStart, partEnd int) {
for dstY := partStart; dstY < partEnd; dstY++ {
di := dst.PixOffset(0, dstY)
for dstX := 0; dstX < dstW; dstX++ {
c := color.NRGBAModel.Convert(img.At(srcMinX+dstX, srcMinY+dstY)).(color.NRGBA)
dst.Pix[di+0] = c.R
dst.Pix[di+1] = c.G
dst.Pix[di+2] = c.B
dst.Pix[di+3] = c.A
di += 4
}
}
})
}
return dst
}
// This function used internally to convert any image type to NRGBA if needed.
func toNRGBA(img image.Image) *image.NRGBA {
srcBounds := img.Bounds()
if srcBounds.Min.X == 0 && srcBounds.Min.Y == 0 {
if src0, ok := img.(*image.NRGBA); ok {
return src0
}
}
return Clone(img)
}

View File

@@ -0,0 +1,564 @@
package imaging
import (
"image"
"math"
)
type iwpair struct {
i int
w int32
}
type pweights struct {
iwpairs []iwpair
wsum int32
}
func precomputeWeights(dstSize, srcSize int, filter ResampleFilter) []pweights {
du := float64(srcSize) / float64(dstSize)
scale := du
if scale < 1.0 {
scale = 1.0
}
ru := math.Ceil(scale * filter.Support)
out := make([]pweights, dstSize)
for v := 0; v < dstSize; v++ {
fu := (float64(v)+0.5)*du - 0.5
startu := int(math.Ceil(fu - ru))
if startu < 0 {
startu = 0
}
endu := int(math.Floor(fu + ru))
if endu > srcSize-1 {
endu = srcSize - 1
}
wsum := int32(0)
for u := startu; u <= endu; u++ {
w := int32(0xff * filter.Kernel((float64(u)-fu)/scale))
if w != 0 {
wsum += w
out[v].iwpairs = append(out[v].iwpairs, iwpair{u, w})
}
}
out[v].wsum = wsum
}
return out
}
// Resize resizes the image to the specified width and height using the specified resampling
// filter and returns the transformed image. If one of width or height is 0, the image aspect
// ratio is preserved.
//
// Supported resample filters: NearestNeighbor, Box, Linear, Hermite, MitchellNetravali,
// CatmullRom, BSpline, Gaussian, Lanczos, Hann, Hamming, Blackman, Bartlett, Welch, Cosine.
//
// Usage example:
//
// dstImage := imaging.Resize(srcImage, 800, 600, imaging.Lanczos)
//
func Resize(img image.Image, width, height int, filter ResampleFilter) *image.NRGBA {
dstW, dstH := width, height
if dstW < 0 || dstH < 0 {
return &image.NRGBA{}
}
if dstW == 0 && dstH == 0 {
return &image.NRGBA{}
}
src := toNRGBA(img)
srcW := src.Bounds().Max.X
srcH := src.Bounds().Max.Y
if srcW <= 0 || srcH <= 0 {
return &image.NRGBA{}
}
// if new width or height is 0 then preserve aspect ratio, minimum 1px
if dstW == 0 {
tmpW := float64(dstH) * float64(srcW) / float64(srcH)
dstW = int(math.Max(1.0, math.Floor(tmpW+0.5)))
}
if dstH == 0 {
tmpH := float64(dstW) * float64(srcH) / float64(srcW)
dstH = int(math.Max(1.0, math.Floor(tmpH+0.5)))
}
var dst *image.NRGBA
if filter.Support <= 0.0 {
// nearest-neighbor special case
dst = resizeNearest(src, dstW, dstH)
} else {
// two-pass resize
if srcW != dstW {
dst = resizeHorizontal(src, dstW, filter)
} else {
dst = src
}
if srcH != dstH {
dst = resizeVertical(dst, dstH, filter)
}
}
return dst
}
func resizeHorizontal(src *image.NRGBA, width int, filter ResampleFilter) *image.NRGBA {
srcBounds := src.Bounds()
srcW := srcBounds.Max.X
srcH := srcBounds.Max.Y
dstW := width
dstH := srcH
dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH))
weights := precomputeWeights(dstW, srcW, filter)
parallel(dstH, func(partStart, partEnd int) {
for dstY := partStart; dstY < partEnd; dstY++ {
for dstX := 0; dstX < dstW; dstX++ {
var c [4]int32
for _, iw := range weights[dstX].iwpairs {
i := dstY*src.Stride + iw.i*4
c[0] += int32(src.Pix[i+0]) * iw.w
c[1] += int32(src.Pix[i+1]) * iw.w
c[2] += int32(src.Pix[i+2]) * iw.w
c[3] += int32(src.Pix[i+3]) * iw.w
}
j := dstY*dst.Stride + dstX*4
sum := weights[dstX].wsum
dst.Pix[j+0] = clampint32(int32(float32(c[0])/float32(sum) + 0.5))
dst.Pix[j+1] = clampint32(int32(float32(c[1])/float32(sum) + 0.5))
dst.Pix[j+2] = clampint32(int32(float32(c[2])/float32(sum) + 0.5))
dst.Pix[j+3] = clampint32(int32(float32(c[3])/float32(sum) + 0.5))
}
}
})
return dst
}
func resizeVertical(src *image.NRGBA, height int, filter ResampleFilter) *image.NRGBA {
srcBounds := src.Bounds()
srcW := srcBounds.Max.X
srcH := srcBounds.Max.Y
dstW := srcW
dstH := height
dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH))
weights := precomputeWeights(dstH, srcH, filter)
parallel(dstW, func(partStart, partEnd int) {
for dstX := partStart; dstX < partEnd; dstX++ {
for dstY := 0; dstY < dstH; dstY++ {
var c [4]int32
for _, iw := range weights[dstY].iwpairs {
i := iw.i*src.Stride + dstX*4
c[0] += int32(src.Pix[i+0]) * iw.w
c[1] += int32(src.Pix[i+1]) * iw.w
c[2] += int32(src.Pix[i+2]) * iw.w
c[3] += int32(src.Pix[i+3]) * iw.w
}
j := dstY*dst.Stride + dstX*4
sum := weights[dstY].wsum
dst.Pix[j+0] = clampint32(int32(float32(c[0])/float32(sum) + 0.5))
dst.Pix[j+1] = clampint32(int32(float32(c[1])/float32(sum) + 0.5))
dst.Pix[j+2] = clampint32(int32(float32(c[2])/float32(sum) + 0.5))
dst.Pix[j+3] = clampint32(int32(float32(c[3])/float32(sum) + 0.5))
}
}
})
return dst
}
// fast nearest-neighbor resize, no filtering
func resizeNearest(src *image.NRGBA, width, height int) *image.NRGBA {
dstW, dstH := width, height
srcBounds := src.Bounds()
srcW := srcBounds.Max.X
srcH := srcBounds.Max.Y
dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH))
dx := float64(srcW) / float64(dstW)
dy := float64(srcH) / float64(dstH)
parallel(dstH, func(partStart, partEnd int) {
for dstY := partStart; dstY < partEnd; dstY++ {
fy := (float64(dstY)+0.5)*dy - 0.5
for dstX := 0; dstX < dstW; dstX++ {
fx := (float64(dstX)+0.5)*dx - 0.5
srcX := int(math.Min(math.Max(math.Floor(fx+0.5), 0.0), float64(srcW)))
srcY := int(math.Min(math.Max(math.Floor(fy+0.5), 0.0), float64(srcH)))
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
}
// Fit scales down the image using the specified resample filter to fit the specified
// maximum width and height and returns the transformed image.
//
// Supported resample filters: NearestNeighbor, Box, Linear, Hermite, MitchellNetravali,
// CatmullRom, BSpline, Gaussian, Lanczos, Hann, Hamming, Blackman, Bartlett, Welch, Cosine.
//
// Usage example:
//
// dstImage := imaging.Fit(srcImage, 800, 600, imaging.Lanczos)
//
func Fit(img image.Image, width, height int, filter ResampleFilter) *image.NRGBA {
maxW, maxH := width, height
if maxW <= 0 || maxH <= 0 {
return &image.NRGBA{}
}
srcBounds := img.Bounds()
srcW := srcBounds.Dx()
srcH := srcBounds.Dy()
if srcW <= 0 || srcH <= 0 {
return &image.NRGBA{}
}
if srcW <= maxW && srcH <= maxH {
return Clone(img)
}
srcAspectRatio := float64(srcW) / float64(srcH)
maxAspectRatio := float64(maxW) / float64(maxH)
var newW, newH int
if srcAspectRatio > maxAspectRatio {
newW = maxW
newH = int(float64(newW) / srcAspectRatio)
} else {
newH = maxH
newW = int(float64(newH) * srcAspectRatio)
}
return Resize(img, newW, newH, filter)
}
// Thumbnail scales the image up or down using the specified resample filter, crops it
// to the specified width and hight and returns the transformed image.
//
// Supported resample filters: NearestNeighbor, Box, Linear, Hermite, MitchellNetravali,
// CatmullRom, BSpline, Gaussian, Lanczos, Hann, Hamming, Blackman, Bartlett, Welch, Cosine.
//
// Usage example:
//
// dstImage := imaging.Thumbnail(srcImage, 100, 100, imaging.Lanczos)
//
func Thumbnail(img image.Image, width, height int, filter ResampleFilter) *image.NRGBA {
thumbW, thumbH := width, height
if thumbW <= 0 || thumbH <= 0 {
return &image.NRGBA{}
}
srcBounds := img.Bounds()
srcW := srcBounds.Dx()
srcH := srcBounds.Dy()
if srcW <= 0 || srcH <= 0 {
return &image.NRGBA{}
}
srcAspectRatio := float64(srcW) / float64(srcH)
thumbAspectRatio := float64(thumbW) / float64(thumbH)
var tmp image.Image
if srcAspectRatio > thumbAspectRatio {
tmp = Resize(img, 0, thumbH, filter)
} else {
tmp = Resize(img, thumbW, 0, filter)
}
return CropCenter(tmp, thumbW, thumbH)
}
// Resample filter struct. It can be used to make custom filters.
//
// Supported resample filters: NearestNeighbor, Box, Linear, Hermite, MitchellNetravali,
// CatmullRom, BSpline, Gaussian, Lanczos, Hann, Hamming, Blackman, Bartlett, Welch, Cosine.
//
// General filter recommendations:
//
// - Lanczos
// Probably the best resampling filter for photographic images yielding sharp results,
// but it's slower than cubic filters (see below).
//
// - CatmullRom
// A sharp cubic filter. It's a good filter for both upscaling and downscaling if sharp results are needed.
//
// - MitchellNetravali
// A high quality cubic filter that produces smoother results with less ringing than CatmullRom.
//
// - BSpline
// A good filter if a very smooth output is needed.
//
// - Linear
// Bilinear interpolation filter, produces reasonably good, smooth output. It's faster than cubic filters.
//
// - Box
// Simple and fast resampling filter appropriate for downscaling.
// When upscaling it's similar to NearestNeighbor.
//
// - NearestNeighbor
// Fastest resample filter, no antialiasing at all. Rarely used.
//
type ResampleFilter struct {
Support float64
Kernel func(float64) float64
}
// Nearest-neighbor filter, no anti-aliasing.
var NearestNeighbor ResampleFilter
// Box filter (averaging pixels).
var Box ResampleFilter
// Linear filter.
var Linear ResampleFilter
// Hermite cubic spline filter (BC-spline; B=0; C=0).
var Hermite ResampleFilter
// Mitchell-Netravali cubic filter (BC-spline; B=1/3; C=1/3).
var MitchellNetravali ResampleFilter
// Catmull-Rom - sharp cubic filter (BC-spline; B=0; C=0.5).
var CatmullRom ResampleFilter
// Cubic B-spline - smooth cubic filter (BC-spline; B=1; C=0).
var BSpline ResampleFilter
// Gaussian Blurring Filter.
var Gaussian ResampleFilter
// Bartlett-windowed sinc filter (3 lobes).
var Bartlett ResampleFilter
// Lanczos filter (3 lobes).
var Lanczos ResampleFilter
// Hann-windowed sinc filter (3 lobes).
var Hann ResampleFilter
// Hamming-windowed sinc filter (3 lobes).
var Hamming ResampleFilter
// Blackman-windowed sinc filter (3 lobes).
var Blackman ResampleFilter
// Welch-windowed sinc filter (parabolic window, 3 lobes).
var Welch ResampleFilter
// Cosine-windowed sinc filter (3 lobes).
var Cosine ResampleFilter
func bcspline(x, b, c float64) float64 {
x = math.Abs(x)
if x < 1.0 {
return ((12-9*b-6*c)*x*x*x + (-18+12*b+6*c)*x*x + (6 - 2*b)) / 6
}
if x < 2.0 {
return ((-b-6*c)*x*x*x + (6*b+30*c)*x*x + (-12*b-48*c)*x + (8*b + 24*c)) / 6
}
return 0
}
func sinc(x float64) float64 {
if x == 0 {
return 1
}
return math.Sin(math.Pi*x) / (math.Pi * x)
}
func init() {
NearestNeighbor = ResampleFilter{
Support: 0.0, // special case - not applying the filter
}
Box = ResampleFilter{
Support: 0.5,
Kernel: func(x float64) float64 {
x = math.Abs(x)
if x <= 0.5 {
return 1.0
}
return 0
},
}
Linear = ResampleFilter{
Support: 1.0,
Kernel: func(x float64) float64 {
x = math.Abs(x)
if x < 1.0 {
return 1.0 - x
}
return 0
},
}
Hermite = ResampleFilter{
Support: 1.0,
Kernel: func(x float64) float64 {
x = math.Abs(x)
if x < 1.0 {
return bcspline(x, 0.0, 0.0)
}
return 0
},
}
MitchellNetravali = ResampleFilter{
Support: 2.0,
Kernel: func(x float64) float64 {
x = math.Abs(x)
if x < 2.0 {
return bcspline(x, 1.0/3.0, 1.0/3.0)
}
return 0
},
}
CatmullRom = ResampleFilter{
Support: 2.0,
Kernel: func(x float64) float64 {
x = math.Abs(x)
if x < 2.0 {
return bcspline(x, 0.0, 0.5)
}
return 0
},
}
BSpline = ResampleFilter{
Support: 2.0,
Kernel: func(x float64) float64 {
x = math.Abs(x)
if x < 2.0 {
return bcspline(x, 1.0, 0.0)
}
return 0
},
}
Gaussian = ResampleFilter{
Support: 2.0,
Kernel: func(x float64) float64 {
x = math.Abs(x)
if x < 2.0 {
return math.Exp(-2 * x * x)
}
return 0
},
}
Bartlett = ResampleFilter{
Support: 3.0,
Kernel: func(x float64) float64 {
x = math.Abs(x)
if x < 3.0 {
return sinc(x) * (3.0 - x) / 3.0
}
return 0
},
}
Lanczos = ResampleFilter{
Support: 3.0,
Kernel: func(x float64) float64 {
x = math.Abs(x)
if x < 3.0 {
return sinc(x) * sinc(x/3.0)
}
return 0
},
}
Hann = ResampleFilter{
Support: 3.0,
Kernel: func(x float64) float64 {
x = math.Abs(x)
if x < 3.0 {
return sinc(x) * (0.5 + 0.5*math.Cos(math.Pi*x/3.0))
}
return 0
},
}
Hamming = ResampleFilter{
Support: 3.0,
Kernel: func(x float64) float64 {
x = math.Abs(x)
if x < 3.0 {
return sinc(x) * (0.54 + 0.46*math.Cos(math.Pi*x/3.0))
}
return 0
},
}
Blackman = ResampleFilter{
Support: 3.0,
Kernel: func(x float64) float64 {
x = math.Abs(x)
if x < 3.0 {
return sinc(x) * (0.42 - 0.5*math.Cos(math.Pi*x/3.0+math.Pi) + 0.08*math.Cos(2.0*math.Pi*x/3.0))
}
return 0
},
}
Welch = ResampleFilter{
Support: 3.0,
Kernel: func(x float64) float64 {
x = math.Abs(x)
if x < 3.0 {
return sinc(x) * (1.0 - (x * x / 9.0))
}
return 0
},
}
Cosine = ResampleFilter{
Support: 3.0,
Kernel: func(x float64) float64 {
x = math.Abs(x)
if x < 3.0 {
return sinc(x) * math.Cos((math.Pi/2.0)*(x/3.0))
}
return 0
},
}
}

View File

@@ -0,0 +1,182 @@
package imaging
import (
"image"
"math"
)
// Anchor is the anchor point for image alignment.
type Anchor int
const (
Center Anchor = iota
TopLeft
Top
TopRight
Left
Right
BottomLeft
Bottom
BottomRight
)
func anchorPt(b image.Rectangle, w, h int, anchor Anchor) image.Point {
var x, y int
switch anchor {
case TopLeft:
x = b.Min.X
y = b.Min.Y
case Top:
x = b.Min.X + (b.Dx()-w)/2
y = b.Min.Y
case TopRight:
x = b.Max.X - w
y = b.Min.Y
case Left:
x = b.Min.X
y = b.Min.Y + (b.Dy()-h)/2
case Right:
x = b.Max.X - w
y = b.Min.Y + (b.Dy()-h)/2
case BottomLeft:
x = b.Min.X
y = b.Max.Y - h
case Bottom:
x = b.Min.X + (b.Dx()-w)/2
y = b.Max.Y - h
case BottomRight:
x = b.Max.X - w
y = b.Max.Y - h
default:
x = b.Min.X + (b.Dx()-w)/2
y = b.Min.Y + (b.Dy()-h)/2
}
return image.Pt(x, y)
}
// Crop cuts out a rectangular region with the specified bounds
// from the image and returns the cropped image.
func Crop(img image.Image, rect image.Rectangle) *image.NRGBA {
src := toNRGBA(img)
srcRect := rect.Sub(img.Bounds().Min)
sub := src.SubImage(srcRect)
return Clone(sub) // New image Bounds().Min point will be (0, 0)
}
// CropAnchor cuts out a rectangular region with the specified size
// from the image using the specified anchor point and returns the cropped image.
func CropAnchor(img image.Image, width, height int, anchor Anchor) *image.NRGBA {
srcBounds := img.Bounds()
pt := anchorPt(srcBounds, width, height, anchor)
r := image.Rect(0, 0, width, height).Add(pt)
b := srcBounds.Intersect(r)
return Crop(img, b)
}
// CropCenter cuts out a rectangular region with the specified size
// from the center of the image and returns the cropped image.
func CropCenter(img image.Image, width, height int) *image.NRGBA {
return CropAnchor(img, width, height, Center)
}
// Paste pastes the img image to the background image at the specified position and returns the combined image.
func Paste(background, img image.Image, pos image.Point) *image.NRGBA {
src := toNRGBA(img)
dst := Clone(background) // cloned image bounds start at (0, 0)
startPt := pos.Sub(background.Bounds().Min) // so we should translate start point
endPt := startPt.Add(src.Bounds().Size())
pasteBounds := image.Rectangle{startPt, endPt}
if dst.Bounds().Overlaps(pasteBounds) {
intersectBounds := dst.Bounds().Intersect(pasteBounds)
rowSize := intersectBounds.Dx() * 4
numRows := intersectBounds.Dy()
srcStartX := intersectBounds.Min.X - pasteBounds.Min.X
srcStartY := intersectBounds.Min.Y - pasteBounds.Min.Y
i0 := dst.PixOffset(intersectBounds.Min.X, intersectBounds.Min.Y)
j0 := src.PixOffset(srcStartX, srcStartY)
di := dst.Stride
dj := src.Stride
for row := 0; row < numRows; row++ {
copy(dst.Pix[i0:i0+rowSize], src.Pix[j0:j0+rowSize])
i0 += di
j0 += dj
}
}
return dst
}
// PasteCenter pastes the img image to the center of the background image and returns the combined image.
func PasteCenter(background, img image.Image) *image.NRGBA {
bgBounds := background.Bounds()
bgW := bgBounds.Dx()
bgH := bgBounds.Dy()
bgMinX := bgBounds.Min.X
bgMinY := bgBounds.Min.Y
centerX := bgMinX + bgW/2
centerY := bgMinY + bgH/2
x0 := centerX - img.Bounds().Dx()/2
y0 := centerY - img.Bounds().Dy()/2
return Paste(background, img, image.Pt(x0, y0))
}
// Overlay draws the img image over the background image at given position
// and returns the combined image. Opacity parameter is the opacity of the img
// image layer, used to compose the images, it must be from 0.0 to 1.0.
//
// Usage examples:
//
// // draw the sprite over the background at position (50, 50)
// dstImage := imaging.Overlay(backgroundImage, spriteImage, image.Pt(50, 50), 1.0)
//
// // blend two opaque images of the same size
// dstImage := imaging.Overlay(imageOne, imageTwo, image.Pt(0, 0), 0.5)
//
func Overlay(background, img image.Image, pos image.Point, opacity float64) *image.NRGBA {
opacity = math.Min(math.Max(opacity, 0.0), 1.0) // check: 0.0 <= opacity <= 1.0
src := toNRGBA(img)
dst := Clone(background) // cloned image bounds start at (0, 0)
startPt := pos.Sub(background.Bounds().Min) // so we should translate start point
endPt := startPt.Add(src.Bounds().Size())
pasteBounds := image.Rectangle{startPt, endPt}
if dst.Bounds().Overlaps(pasteBounds) {
intersectBounds := dst.Bounds().Intersect(pasteBounds)
for y := intersectBounds.Min.Y; y < intersectBounds.Max.Y; y++ {
for x := intersectBounds.Min.X; x < intersectBounds.Max.X; x++ {
i := y*dst.Stride + x*4
srcX := x - pasteBounds.Min.X
srcY := y - pasteBounds.Min.Y
j := srcY*src.Stride + srcX*4
a1 := float64(dst.Pix[i+3])
a2 := float64(src.Pix[j+3])
coef2 := opacity * a2 / 255.0
coef1 := (1 - coef2) * a1 / 255.0
coefSum := coef1 + coef2
coef1 /= coefSum
coef2 /= coefSum
dst.Pix[i+0] = uint8(float64(dst.Pix[i+0])*coef1 + float64(src.Pix[j+0])*coef2)
dst.Pix[i+1] = uint8(float64(dst.Pix[i+1])*coef1 + float64(src.Pix[j+1])*coef2)
dst.Pix[i+2] = uint8(float64(dst.Pix[i+2])*coef1 + float64(src.Pix[j+2])*coef2)
dst.Pix[i+3] = uint8(math.Min(a1+a2*opacity*(255.0-a1)/255.0, 255.0))
}
}
}
return dst
}

View File

@@ -0,0 +1,201 @@
package imaging
import (
"image"
)
// Rotate90 rotates the image 90 degrees counterclockwise and returns the transformed image.
func Rotate90(img image.Image) *image.NRGBA {
src := toNRGBA(img)
srcW := src.Bounds().Max.X
srcH := src.Bounds().Max.Y
dstW := srcH
dstH := srcW
dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH))
parallel(dstH, func(partStart, partEnd int) {
for dstY := partStart; dstY < partEnd; dstY++ {
for dstX := 0; dstX < dstW; dstX++ {
srcX := dstH - 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
}
// Rotate180 rotates the image 180 degrees counterclockwise and returns the transformed image.
func Rotate180(img image.Image) *image.NRGBA {
src := toNRGBA(img)
srcW := src.Bounds().Max.X
srcH := src.Bounds().Max.Y
dstW := srcW
dstH := srcH
dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH))
parallel(dstH, func(partStart, partEnd int) {
for dstY := partStart; dstY < partEnd; dstY++ {
for dstX := 0; dstX < dstW; dstX++ {
srcX := dstW - dstX - 1
srcY := dstH - dstY - 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
}
// Rotate270 rotates the image 270 degrees counterclockwise and returns the transformed image.
func Rotate270(img image.Image) *image.NRGBA {
src := toNRGBA(img)
srcW := src.Bounds().Max.X
srcH := src.Bounds().Max.Y
dstW := srcH
dstH := srcW
dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH))
parallel(dstH, func(partStart, partEnd int) {
for dstY := partStart; dstY < partEnd; dstY++ {
for dstX := 0; dstX < dstW; dstX++ {
srcX := dstY
srcY := dstW - 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
}
// FlipH flips the image horizontally (from left to right) and returns the transformed image.
func FlipH(img image.Image) *image.NRGBA {
src := toNRGBA(img)
srcW := src.Bounds().Max.X
srcH := src.Bounds().Max.Y
dstW := srcW
dstH := srcH
dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH))
parallel(dstH, func(partStart, partEnd int) {
for dstY := partStart; dstY < partEnd; dstY++ {
for dstX := 0; dstX < dstW; dstX++ {
srcX := dstW - dstX - 1
srcY := dstY
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
}
// FlipV flips the image vertically (from top to bottom) and returns the transformed image.
func FlipV(img image.Image) *image.NRGBA {
src := toNRGBA(img)
srcW := src.Bounds().Max.X
srcH := src.Bounds().Max.Y
dstW := srcW
dstH := srcH
dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH))
parallel(dstH, func(partStart, partEnd int) {
for dstY := partStart; dstY < partEnd; dstY++ {
for dstX := 0; dstX < dstW; dstX++ {
srcX := dstX
srcY := dstH - dstY - 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
}
// Transpose flips the image horizontally and rotates 90 degrees counter-clockwise.
func Transpose(img image.Image) *image.NRGBA {
src := toNRGBA(img)
srcW := src.Bounds().Max.X
srcH := src.Bounds().Max.Y
dstW := srcH
dstH := srcW
dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH))
parallel(dstH, func(partStart, partEnd int) {
for dstY := partStart; dstY < partEnd; dstY++ {
for dstX := 0; dstX < dstW; dstX++ {
srcX := dstY
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
}
// Transverse flips the image vertically and rotates 90 degrees counter-clockwise.
func Transverse(img image.Image) *image.NRGBA {
src := toNRGBA(img)
srcW := src.Bounds().Max.X
srcH := src.Bounds().Max.Y
dstW := srcH
dstH := srcW
dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH))
parallel(dstH, func(partStart, partEnd int) {
for dstY := partStart; dstY < partEnd; dstY++ {
for dstX := 0; dstX < dstW; dstX++ {
srcX := dstH - dstY - 1
srcY := dstW - 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
}

View File

@@ -0,0 +1,77 @@
package imaging
import (
"math"
"runtime"
"sync"
"sync/atomic"
)
var parallelizationEnabled = true
// if GOMAXPROCS = 1: no goroutines used
// if GOMAXPROCS > 1: spawn N=GOMAXPROCS workers in separate goroutines
func parallel(dataSize int, fn func(partStart, partEnd int)) {
numGoroutines := 1
partSize := dataSize
if parallelizationEnabled {
numProcs := runtime.GOMAXPROCS(0)
if numProcs > 1 {
numGoroutines = numProcs
partSize = dataSize / (numGoroutines * 10)
if partSize < 1 {
partSize = 1
}
}
}
if numGoroutines == 1 {
fn(0, dataSize)
} else {
var wg sync.WaitGroup
wg.Add(numGoroutines)
idx := uint64(0)
for p := 0; p < numGoroutines; p++ {
go func() {
defer wg.Done()
for {
partStart := int(atomic.AddUint64(&idx, uint64(partSize))) - partSize
if partStart >= dataSize {
break
}
partEnd := partStart + partSize
if partEnd > dataSize {
partEnd = dataSize
}
fn(partStart, partEnd)
}
}()
}
wg.Wait()
}
}
func absint(i int) int {
if i < 0 {
return -i
}
return i
}
// clamp & round float64 to uint8 (0..255)
func clamp(v float64) uint8 {
return uint8(math.Min(math.Max(v, 0.0), 255.0) + 0.5)
}
// clamp int32 to uint8 (0..255)
func clampint32(v int32) uint8 {
if v < 0 {
return 0
} else if v > 255 {
return 255
}
return uint8(v)
}