The Go Blog

Пакет image/draw в Go

Nigel Tao
29 сентября 2011

Введение

Пакет image/draw определяет только одну операцию: рисование исходного изображения на целевом изображении через необязательное маскирующее изображение. Эта одна операция удивительно универсальна и может выполнять множество частых задач по работе с изображениями элегантно и эффективно.

Композиция выполняется пиксель за пикселем в стиле библиотеки графики Plan 9 и расширения X Render. Модель основана на классической статье «Compositing Digital Images» Портера и Даффа, с дополнительным параметром маски: dst = (src IN mask) OP dst. Для полностью непрозрачной маски это сводится к оригинальной формуле Портера-Даффа: dst = src OP dst. В Go, nil маскируемое изображение эквивалентно бесконечно большому, полностью непрозрачному маскирующему изображению.

Статья Портера-Даффа описала 12 различных операторов композиции, но при использовании явной маски, на практике достаточно только двух из них: source-over-destination и source. В Go, эти операторы представлены константами Over и Src. Оператор Over выполняет естественное наложение исходного изображения поверх целевого изображения: изменения в целевом изображении минимальны там, где исходное (после применения маски) более прозрачно (то есть имеет меньшее значение альфа-канала). Оператор Src просто копирует исходное изображение (после применения маски), не учитывая исходное содержимое целевого изображения. Для полностью непрозрачных исходного и маскирующего изображений оба оператора дают одинаковый результат, но оператор Src обычно работает быстрее.

Геометрическое выравнивание

Композиция требует сопоставления пикселей целевого изображения с пикселями исходного и маски. Очевидно, что для этого требуются целевое, исходное и маскирующее изображения, а также оператор композиции, но также необходимо указать, какую часть каждого изображения использовать. Не каждое рисование должно записывать в полное целевое изображение: при обновлении анимированного изображения более эффективно рисовать только те части изображения, которые изменились. Не каждое рисование должно читать из всего исходного изображения: при использовании спрайта, который объединяет множество маленьких изображений в одно большое, нужно использовать только часть изображения. Не каждое рисование должно читать из всей маски: маскирующее изображение, которое собирает глифы шрифта, похоже на спрайт. Поэтому рисование также должно знать три прямоугольника, по одному для каждого изображения. Поскольку каждый прямоугольник имеет одинаковую ширину и высоту, достаточно передать целевой прямоугольник r и два точки sp и mp: прямоугольник исходного изображения равен r, сдвинутому так, чтобы r.Min в целевом изображении совпадало с sp в исходном изображении, аналогично для mp. Эффективный прямоугольник также обрезается по границам каждого изображения в их соответствующем координатном пространстве.

Функция Draw принимает семь аргументов, но явная маска и точка маски обычно не требуются, поэтому функция Draw принимает пять:

<code>// Draw вызывает DrawMask с nil маской.
func Draw(dst Image, r image.Rectangle, src image.Image, sp image.Point, op Op)
func DrawMask(dst Image, r image.Rectangle, src image.Image, sp image.Point,
mask image.Image, mp image.Point, op Op)
</code>

Целевое изображение должно быть изменяемым, поэтому пакет image/draw определяет draw.Image интерфейс, который имеет метод Set.

<code>type Image interface {
  image.Image
  Set(x, y int, c color.Color)
}
</code>

Заполнение прямоугольника

Чтобы заполнить прямоугольник сплошным цветом, используйте источник image.Uniform. Тип ColorImage переинтерпретирует Color как практически бесконечно большое Image того же цвета. Для тех, кто знаком с архитектурой библиотеки рисования Plan 9, в типах изображений на основе срезов Go нет необходимости в явном «бит повтора»; концепция учитывается в Uniform.

<code>// image.ZP — это нулевая точка — начало координат.
draw.Draw(dst, r, &image.Uniform{c}, image.ZP, draw.Src)
</code>

Чтобы инициализировать новое изображение полностью синим:

<code>m := image.NewRGBA(image.Rect(0, 0, 640, 480))
blue := color.RGBA{0, 0, 255, 255}
draw.Draw(m, m.Bounds(), &image.Uniform{blue}, image.ZP, draw.Src)
</code>

Чтобы сбросить изображение в прозрачный (или черный, если цветовая модель целевого изображения не поддерживает прозрачность), используйте image.Transparent, который является image.Uniform:

<code>draw.Draw(m, m.Bounds(), image.Transparent, image.ZP, draw.Src)
</code>

Копирование изображения

Чтобы скопировать из прямоугольника sr в исходном изображении в прямоугольник, начинающийся в точке dp в целевом изображении, преобразуйте исходный прямоугольник в пространство координат целевого изображения:

<code>r := image.Rectangle{dp, dp.Add(sr.Size())}
draw.Draw(dst, r, src, sr.Min, draw.Src)
</code>

Альтернативно:

<code>r := sr.Sub(sr.Min).Add(dp)
draw.Draw(dst, r, src, sr.Min, draw.Src)
</code>

Чтобы скопировать всё исходное изображение, используйте sr = src.Bounds().

Прокрутка изображения

Прокрутка изображения — это просто копирование изображения в него же, с разными прямоугольниками назначения и источника. Перекрывающиеся целевые и исходные изображения полностью допустимы, аналогично встроенной функции copy в Go, которая может обрабатывать перекрывающиеся исходные и целевые срезы. Чтобы прокрутить изображение m на 20 пикселей:

<code>b := m.Bounds()
p := image.Pt(0, 20)
// Обратите внимание, что несмотря на то, что вторым аргументом является b,
// эффективный прямоугольник меньше из-за обрезки.
draw.Draw(m, b, m, b.Min.Add(p), draw.Src)
dirtyRect := b.Intersect(image.Rect(b.Min.X, b.Max.Y-20, b.Max.X, b.Max.Y))
</code>

Преобразование изображения в RGBA

Результат декодирования формата изображения может не быть image.RGBA: декодирование GIF приводит к image.Paletted, декодирование JPEG приводит к ycbcr.YCbCr, а результат декодирования PNG зависит от данных изображения. Чтобы преобразовать любое изображение в image.RGBA:

<code>b := src.Bounds()
m := image.NewRGBA(image.Rect(0, 0, b.Dx(), b.Dy()))
draw.Draw(m, m.Bounds(), src, b.Min, draw.Src)
</code>

Рисование через маску

Чтобы нарисовать изображение через круглую маску с центром p и радиусом r:

<code>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-c.r, c.p.Y-c.r, c.p.X+c.r, c.p.Y+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}
}
draw.DrawMask(dst, dst.Bounds(), src, image.ZP, &circle{p, r}, image.ZP, draw.Over)
</code>

Рисование глифов шрифта

Чтобы нарисовать глиф шрифта синим цветом, начиная с точки p, рисуйте с источником image.ColorImage и image.Alpha mask. Для простоты мы не выполняем никакой субпиксельной позиции или рендеринга, или коррекции высоты шрифта над базовой линией.

<code>src := &image.Uniform{color.RGBA{0, 0, 255, 255}}
mask := theGlyphImageForAFont()
mr := theBoundsFor(glyphIndex)
draw.DrawMask(dst, mr.Sub(mr.Min).Add(p), src, image.ZP, mask, mr.Min, draw.Over)
</code>

Производительность

Реализация пакета image/draw демонстрирует, как предоставить функцию манипуляции с изображением, которая является как универсальной, так и эффективной для типичных случаев. Функция DrawMask принимает аргументы интерфейсных типов, но немедленно выполняет утверждения типа, что аргументы имеют конкретные типы структур, соответствующие типичным операциям, таким как рисование одного image.RGBA изображения на другом, или рисование image.Alpha маски (например, глифа шрифта) на image.RGBA изображении. Если утверждение типа проходит успешно, эта информация о типе используется для запуска специализированной реализации общего алгоритма. Если утверждение типа неудачно, резервный путь использует общие методы At и Set. Быстрые пути — это исключительно оптимизация производительности; результирующее целевое изображение будет одинаковым в любом случае. На практике, необходимо лишь небольшое количество специальных случаев, чтобы поддерживать типичные приложения.

Следующая статья: Изучите Go в браузере
Предыдущая статья: Пакет Go image
Индекс блога

GoRu.dev Golang на русском

На сайте представлена адаптированная под русский язык документация языка программирования Golang