54.Go 图像

图像标准库为处理图像提供了基础。

image包提供了:
image.Image接口描述位图图像
一种最常见的表示内存中图像的方式的实现,例如Image.RGBA
向image.RegisterFormat注册图像格式解码器(例如PNG,JPEG等)的方法。

标准库提供了:

  • GIF编码和解码
  • PNG编码和解码
  • JPEG编码和解码

其他库还有:

  • BMP 编码和解码
  • TIFF 编码和解码
  • WEBP 解码
  • vp8 解码
  • vp8l 解码.

image.Image接口是最小的:

type Image interface {
        // ColorModel returns the Image's color model.
        ColorModel() color.Model
        // Bounds returns the domain for which At can return non-zero color.
        // The bounds do not necessarily contain the point (0, 0).
        Bounds() Rectangle
        // At returns the color of the pixel at (x, y).
        // At(Bounds().Min.X, Bounds().Min.Y) returns the upper-left pixel of the grid.
        // At(Bounds().Max.X-1, Bounds().Max.Y-1) returns the lower-right one.
        At(x, y int) color.Color
}

这是从URL解码PNG图像并使用Bounds()方法打印图像大小的示例:

func showImageSize(uri string) {
    resp, err := http.Get(uri)
    if err != nil {
        log.Fatalf("http.Get('%s') failed with %s\n", uri, err)
    }
    defer resp.Body.Close()
    if resp.StatusCode >= 400 {
        log.Fatalf("http.Get() failed with '%s'\n", resp.Status)
    }
    img, err := png.Decode(resp.Body)
    if err != nil {
        log.Fatalf("png.Decode() failed with '%s'\n", err)
    }
    size := img.Bounds().Size()
    fmt.Printf("Image '%s'\n", uri)
    fmt.Printf("  size: %dx%d\n", size.X, size.Y)
    fmt.Printf("  format in memory: '%T'\n", img)
}

输出:

Image 'https://www.programming-books.io/covers/Go.png'
size: 595x842
format in memory: '*image.NRGBA'

基本概念

图像代表像素的矩形网格。 在image包中,像素表示为image/color包中定义的颜色之一。 图像的二维几何图形表示为image.Rectangle,image.Point表示网格上的位置。

图像和二维几何

[图片上传失败...(image-7c7e93-1635385188344)]

上图说明了包中图像的基本概念。尺寸为15x14像素的图像具有从左上角开始的矩形边界(例如,上图中的坐标(-3,-4)),并且其轴从右到下增加到右下角(例如,坐标( 图中的12、10)。 请注意,边界不一定始于或包含点(0,0)。

图像相关类型

在Go中,图像始终实现以下image.Image接口

type Image interface {
    // ColorModel returns the Image's color model.
    ColorModel() color.Model
    // Bounds returns the domain for which At can return non-zero color.
    // The bounds do not necessarily contain the point (0, 0).
    Bounds() Rectangle
    // At returns the color of the pixel at (x, y).
    // At(Bounds().Min.X, Bounds().Min.Y) returns the upper-left pixel of the grid.
    // At(Bounds().Max.X-1, Bounds().Max.Y-1) returns the lower-right one.
    At(x, y int) color.Color
}

其中color.Color接口定义为:

type Color interface {
    // RGBA returns the alpha-premultiplied red, green, blue and alpha values
    // for the color. Each value ranges within [0, 0xffff], but is represented
    // by a uint32 so that multiplying by a blend factor up to 0xffff will not
    // overflow.
    //
    // An alpha-premultiplied color component c has been scaled by alpha (a),
    // so has valid values 0 <= c <= a.
    RGBA() (r, g, b, a uint32)
}

color.Model接口如下:

type Model interface {
    Convert(c Color) Color
}

访问图像尺寸和像素

假设我们将图像存储为变量img,则可以通过以下方式获取尺寸和图像像素:

// Image bounds and dimension
b := img.Bounds()
width, height := b.Dx(), b.Dy()
// do something with dimension ...

// Corner co-ordinates
top := b.Min.Y
left := b.Min.X
bottom := b.Max.Y
right := b.Max.X

// Accessing pixel. The (x,y) position must be
// started from (left, top) position not (0,0)
for y := top; y < bottom; y++ {
    for x := left; x < right; x++ {
        cl := img.At(x, y)
        r, g, b, a := cl.RGBA()
        // do something with r,g,b,a color component
    }
}

请注意,每个R,G,B,A组件的值都在0-65535(0x0000-0xffff)之间,而不是0-255。

加载和保存图像

在内存中,图像可以看作是像素(颜色)的矩阵。但是当将图像存储在永久存储器中时,可以按原始(RAW格式)位图或其他具有特定压缩算法的图像格式进行存储,以节省存储空间,例如 PNG,JPEG,GIF等。加载特定格式的图像时,必须使用相应的算法将图像解码为image.Image。

image.Decode函数声明为
func Decode(r io.Reader) (Image, string, error)

提供此特定用途。 为了能够处理各种图像格式,在调用image.Decode函数之前,必须通过定义为image.RegisterFormat函数的解码器进行注册。

func RegisterFormat(name, magic string,
    decode func(io.Reader) (Image, error), decodeConfig func(io.Reader) (Config, error))

目前,image包支持三种文件格式:JPEG,GIF和PNG。 要注册解码器,请添加以下内容:
import _ "image/jpeg" //register JPEG decoder
到应用程序的主程序包。

如要在代码中的某个位置(在主程序包中不是必需的)加载JPEG图像,请使用以下代码段:

f, err := os.Open("inputimage.jpg")
if err != nil {
    log.Fatalf("os.Open() failed with %s\n", err)
}
defer f.Close()

img, fmtName, err := image.Decode(f)
if err != nil {
    log.Fatalf("image.Decode() failed with %s\n", err)
}

// `fmtName` contains the name used during format registration
// Work with `img` ...

保存png图片

为了将图像保存为特定格式,必须明确导入相应的编码器,即
import "image/png" //needed to use png encoder

然后可以使用以下代码段保存图像:

f, err := os.Create("outimage.png")
if err != nil {
    log.Fatalf("os.Create() failed with %s\n", err)
}
defer f.Close()

// Encode to `PNG` with `DefaultCompression` level
// then save to file
err = png.Encode(f, img)
if err != nil {
    log.Fatalf("png.Encode() failed with %s\n", err)
}

如果您要指定默认压缩级别以外的其他压缩级别,请创建一个编码器,例如:

enc := png.Encoder{
    CompressionLevel: png.BestSpeed,
}
err := enc.Encode(f, img)

保存JPEG图片

要保存为jpeg格式,请使用以下命令:

import "image/jpeg"

// Somewhere in the same package
f, err := os.Create("outimage.jpg")
if err != nil {
    log.Fatalf("os.Create() failed with %s\n", err)
}
defer f.Close()

// Specify the quality, between 0-100
// Higher is better
opt := jpeg.Options{
    Quality: 90,
}
err = jpeg.Encode(f, img, &opt)
if err != nil {
    log.Fatalf("jpeg.Encode() failed with %s\n", err)
}

保存GIF图片

要将图像保存到GIF文件,请使用以下代码段:

import "image/gif"

// Samewhere in the same package
f, err := os.Create("outimage.gif")
if err != nil {
    log.Fatalf("os.Create() failed with %s\n", err)
}
defer f.Close()

opt := gif.Options {
    NumColors: 256,
    // Add more parameters as needed
}

err = gif.Encode(f, img, &opt)
if err != nil {
    log.Fatalf("gif.Encode() failed with %s\n", err)
}

裁剪图像

除image.Uniform外,大多数具有SubImage(r Rectangle)Image方法的图像包中的图像类型。 基于这一事实,我们可以实现如下功能来裁剪任意图像

func CropImage(img image.Image, cropRect image.Rectangle) (cropImg image.Image, newImg bool) {
    //Interface for asserting whether `img`
    //implements SubImage or not.
    //This can be defined globally.
    type CropableImage interface {
        image.Image
        SubImage(r image.Rectangle) image.Image
    }

    if p, ok := img.(CropableImage); ok {
        // Call SubImage. This should be fast,
        // since SubImage (usually) shares underlying pixel.
        cropImg = p.SubImage(cropRect)
    } else if cropRect = cropRect.Intersect(img.Bounds()); !cropRect.Empty() {
        // If `img` does not implement `SubImage`,
        // copy (and silently convert) the image portion to RGBA image.
        rgbaImg := image.NewRGBA(cropRect)
        for y := cropRect.Min.Y; y < cropRect.Max.Y; y++ {
            for x := cropRect.Min.X; x < cropRect.Max.X; x++ {
                rgbaImg.Set(x, y, img.At(x, y))
            }
        }
        cropImg = rgbaImg
        newImg = true
    } else {
        // Return an empty RGBA image
        cropImg = &image.RGBA{}
        newImg = true
    }

    return cropImg, newImg
}

请注意,裁剪后的图像可能会与原始图像共享其基础像素。 在这种情况下,对裁剪图像的任何修改都会影响原始图像。

将彩色图像转换为灰度

一些数字图像处理算法,例如边缘检测,图像强度所携带的信息(即灰度值)就足够了。 使用颜色信息(R,G,B通道)可能会提供更好的结果,但是会增加算法的复杂性。 因此,在这种情况下,我们需要在应用这种算法之前将彩色图像转换为灰度图像。

以下代码是将任意图像转换为8位灰度图像的示例。 使用net/http软件包从远程位置检索图像,将其转换为灰度,最后保存为PNG图像:

package main

import (
    "image"
    "log"
    "net/http"
    "os"

    _ "image/jpeg"
    "image/png"
)

func main() {
    // Load image from remote through http
    // The Go gopher was designed by Renee French. (http://reneefrench.blogspot.com/)
    // Images are available under the Creative Commons 3.0 Attributions license.
    resp, err := http.Get("http://golang.org/doc/gopher/fiveyears.jpg")
    if err != nil {
        log.Fatal(err)
    }
    defer resp.Body.Close()

    // Decode image to JPEG
    img, _, err := image.Decode(resp.Body)
    if err != nil {
        log.Fatal(err)
    }
    log.Printf("Image type: %T", img)

    // Convert image to grayscale
    grayImg := image.NewGray(img.Bounds())
    for y := img.Bounds().Min.Y; y < img.Bounds().Max.Y; y++ {
        for x := img.Bounds().Min.X; x < img.Bounds().Max.X; x++ {
            grayImg.Set(x, y, img.At(x, y))
        }
    }

    // Working with grayscale image, e.g. convert to png
    f, err := os.Create("fiveyears_gray.png")
    if err != nil {
        log.Fatal(err)
    }
    defer f.Close()

    if err := png.Encode(f, grayImg); err != nil {
        log.Fatal(err)
    }
}

通过Set(x,y int,c color.Color)分配像素时发生颜色转换,这在image.go中实现为

func (p *Gray) Set(x, y int, c color.Color) {
    if !(Point{x, y}.In(p.Rect)) {
        return
    }

    i := p.PixOffset(x, y)
    p.Pix[i] = color.GrayModel.Convert(c).(color.Gray).Y
}

其中,color.GrayModel在color.go中定义为:

func grayModel(c Color) Color {
    if _, ok := c.(Gray); ok {
        return c
    }
    r, g, b, _ := c.RGBA()

    // These coefficients (the fractions 0.299, 0.587 and 0.114) are the same
    // as those given by the JFIF specification and used by func RGBToYCbCr in
    // ycbcr.go.
    //
    // Note that 19595 + 38470 + 7471 equals 65536.
    //
    // The 24 is 16 + 8. The 16 is the same as used in RGBToYCbCr. The 8 is
    // because the return value is 8 bit color, not 16 bit color.
    y := (19595*r + 38470*g + 7471*b + 1<<15) >> 24

    return Gray{uint8(y)}
}

基于上述事实,强度Y通过以下公式计算:
Luminance: Y = 0.299R + 0.587G + 0.114B

如果我们想应用不同的公式/算法将颜色转换为强度,例如:
Mean: Y = (R + G + B) / 3 Luma: Y = 0.2126R + 0.7152G + 0.0722B Luster: Y = (min(R, G, B) + max(R, G, B))/2

然后,可以使用以下片段。

// Convert image to grayscale
grayImg := image.NewGray(img.Bounds())
for y := img.Bounds().Min.Y; y < img.Bounds().Max.Y; y++ {
    for x := img.Bounds().Min.X; x < img.Bounds().Max.X; x++ {
        R, G, B, _ := img.At(x, y).RGBA()
        //Luma: Y = 0.2126*R + 0.7152*G + 0.0722*B
        Y := (0.2126*float64(R) + 0.7152*float64(G) + 0.0722*float64(B)) * (255.0 / 65535)
        grayPix := color.Gray{uint8(Y)}
        grayImg.Set(x, y, grayPix)
    }
}

上面的计算是通过浮点乘法完成的,虽然肯定不是最有效的方法,但是足以证明这一点。 另一点是,当使用color.Gray作为第三个参数调用Set(x,y int,c color.Color)时,颜色模型将不会执行颜色转换,就像在以前的grayModel函数中可以看到的那样。

调整图像大小

有几个库可以在Go中调整图像大小。 软件包golang.org/x/image/draw是以下选项之一:

func resize(src image.Image, dstSize image.Point) *image.RGBA {
    srcRect := src.Bounds()
    dstRect := image.Rectangle{
        Min: image.Point{0, 0},
        Max: dstSize,
    }
    dst := image.NewRGBA(dstRect)
    draw.CatmullRom.Scale(dst, dstRect, src, srcRect, draw.Over, nil)
    return dst
}

要点:

我们返回image.RGBA而不是image.Image,因为Go的最佳实践是接受接口作为函数的参数,但返回具体类型
图像边界不必从(0,0)开始,因此对于源图像,我们要求边界矩形。我们自己创建目标图像,所以我们创建的边界以(0,0)开始
缩放接口非常通用,因此有些复杂。包绘制是其核心,它是一个合成引擎,它允许使用特定的操作来合成(绘制)位图图像。 draw.Over是一种操作,可以选择使用遮罩在一个图像上绘制另一个图像。
resizing是作为Scaler接口实现的,它接收目标图像,目标图像内的矩形,源图像,源图像内的矩形并将源矩形绘制为目标矩形。当目标矩形的尺寸与源矩形的尺寸不同时,会发生大小调整
该软件包实现了4种不同的缩放算法:
最近的邻居-快速,低质量
近似双线性-速度较慢,质量更好
双线性-甚至更慢,高质量
catmull-rom-最慢,最高质量
draw.CatmullRom是实现catmull-rom算法的实例的全局变量。其他算法可以作为draw.NearestNeighbor,draw.ApproxBiLinear和draw.BiLinear包全局变量使用
放在一起:要将源图像调整为给定大小,我们创建所需大小的目标图像,然后使用全局缩放器之一的Scale方法将源图像绘制到目标中,而调整大小是使用scaler的副作用。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343

推荐阅读更多精彩内容