images

package module
v0.0.0-...-ea366b4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 2, 2026 License: BSD-3-Clause Imports: 9 Imported by: 0

README

go-images/images

images — go-images

Docs License Go Status

A pure-Go (no cgo) image-processing library in the style of scikit-image, built entirely on the Go standard library's image, image/color, image/png and image/jpeg packages.

Every operation is a pure function: it takes an image and returns a freshly allocated *image.RGBA, never mutating its input.

Why not just use a C library?

The established image stacks all need a native C dependency: ruby-vips wraps libvips, RMagick wraps ImageMagick, and the cgo-backed Go wrappers pull in the same shared libraries. That makes cross-compilation and embedding awkward. go-images/images is CGO=0: it builds and runs identically on every Go target with no system packages, which makes it trivially cross-compilable and embeddable (for example inside go-embedded-ruby).

Pure-Go image libraries already exist (bild, disintegration/imaging); the goal here is a clean, correct, fully test-covered core whose hot pixel kernels run SIMD inner loops generated by go-asmgen (amd64 SSE2, arm64 NEON, s390x z/vector; scalar + multicore on loong64 / ppc64le / riscv64) plus multicore tiling, across the six supported 64-bit targets (amd64, arm64, riscv64, loong64, ppc64le, s390x). See docs/plan-images.md for the roadmap and an honest comparison.

API

import "github.com/go-images/images"

Conversion and I/O:

  • images.ToRGBA(img image.Image) *image.RGBA
  • images.Load(path string) (*image.RGBA, error)
  • images.Save(path string, img image.Image) error — format by extension (.png, .jpg, .jpeg)
  • images.Decode(r io.Reader) (*image.RGBA, error) — format auto-detected
  • images.Encode(w io.Writer, img image.Image, format images.Format) errorimages.PNG, images.JPEG

Operations (each returns a new *image.RGBA):

Point & colour:

  • images.Grayscale(img) — luminance-weighted (Rec. 601)
  • images.Invert(img)
  • images.AdjustBrightness(img, delta) — clamped to [0, 255]
  • images.AdjustContrast(img, factor) — about mid-point 128, clamped
  • images.RGBToHSV(img) / images.HSVToRGB(img) — byte-encoded, round-trip-stable
  • images.OtsuThreshold(img) — Otsu level (matches skimage.filters.threshold_otsu)
  • images.Threshold(img, t) / images.Otsu(img) — binarise on luminance

Filters:

  • images.Convolve(img, images.Kernel{...}) — arbitrary odd kernel, clamp-to-edge
  • images.GaussianBlur(img, sigma) — separable Gaussian
  • images.BoxBlur(img, radius) — separable running-sum mean (matches scipy.ndimage.uniform_filter)
  • images.Median(img, radius) — square median (matches scipy.ndimage.median_filter, mode="nearest")
  • images.UnsharpMask(img, radius, amount) — sharpen via src + amount*(src − blur) (matches skimage.filters.unsharp_mask)
  • images.Sharpen(img)UnsharpMask(img, 1.0, 1.0)

Edges:

  • images.Sobel(img) — gradient-magnitude edge map (classic integer kernels, on luminance)
  • images.SobelX(img) / images.SobelY(img) — directional Sobel responses (mid-grey = zero gradient)
  • images.Prewitt(img) / images.Scharr(img) / images.SobelMag(img) — normalised gradient magnitude (match skimage.filters.{prewitt,scharr,sobel})
  • images.Laplacian(img) — discrete Laplacian (matches skimage.filters.laplace, ksize 3)
  • images.Canny(img, sigma, low, high) — binary Canny edge map (Gaussian → Sobel → bilinear NMS → hysteresis)

Morphology (grayscale square element, also binary on 0/255 images):

  • images.Erode(img, r) / images.Dilate(img, r) — local min / max
  • images.Open(img, r) / images.Close(img, r) — erode→dilate / dilate→erode

Geometry:

  • images.Resize(img, w, h, mode)images.NearestNeighbor or images.Bilinear
  • images.FlipHorizontal(img) / images.FlipVertical(img)numpy.fliplr / flipud
  • images.Rotate90(img) / images.Rotate180(img) / images.Rotate270(img)numpy.rot90
  • images.Crop(img, image.Rect(x0, y0, x1, y1))
Example
src, err := images.Load("in.png")
if err != nil {
    log.Fatal(err)
}
gray := images.Grayscale(src)
blurred, err := images.GaussianBlur(gray, 2.0)
if err != nil {
    log.Fatal(err)
}
if err := images.Save("out.png", blurred); err != nil {
    log.Fatal(err)
}

Performance

A rigorous parity benchmark against scikit-image 0.26 / scipy 1.18 and OpenCV 4.13 lives in BENCHMARKS.md (reproducible harness in benchmarks/). Headline, single-thread, core-for-core on an Apple M4 Max:

  • Wins vs scikit-image: box blur 1.8–2.6× (O(1) running-window sum), RGB→HSV ~4.8× (fused pass), flip ~2×, and Gaussian 1.0–1.3× — the former Gaussian loss is now closed by the SIMD axpy separable convolution.
  • Morphology — now at scikit-image parity. The O(radius) fold was replaced with the O(1) van Herk / Gil-Werman running min/max, so erode/dilate are flat in radius and reach parity → 1.02× of scikit-image at 4096² single-thread (≈0.8× at small radius is a pure constant factor → SIMD).
  • Gaps vs scikit-image: Sobel at ~0.78× — it still recomputes luminance and the gradient magnitude per pixel; the fix (cached luminance plane, vectorised magnitude) is in BENCHMARKS.md.
  • Multicore: with all cores go-images is faster than single-threaded scikit-image on every op (morphology now 4.8–7.8×), but OpenCV's O(1)+SIMD morphology is still far ahead — cores don't replace SIMD.

Box blur and grayscale morphology match SciPy bit-for-bit; Gaussian within one LSB; every SIMD kernel is validated against its scalar oracle. See BENCHMARKS.md for the full tables, methodology and action items, and docs/perf.md for the historical SIMD notes.

License

BSD-3-Clause. See LICENSE.

Documentation

Overview

Package images is a pure-Go (cgo-free) image-processing library in the style of scikit-image, built on the Go standard library's image, image/color, image/png and image/jpeg packages.

Unlike the established options in the Ruby world — ruby-vips (libvips) and RMagick (ImageMagick) — and unlike cgo-backed Go wrappers, this library has no native C dependency: it is CGO=0 and builds and runs identically on every Go target. That makes it trivially cross-compilable and embeddable (for example inside go-embedded-ruby), at the cost — for now — of the hand-tuned SIMD that libvips/ImageMagick rely on. A later phase will close that gap with SIMD kernels generated by go-asmgen across the six supported 64-bit targets. See docs/plan-images.md for the roadmap and an honest comparison with the existing pure-Go libraries (bild, disintegration/imaging).

Operations are pure functions: each takes an image and returns a freshly allocated *image.RGBA, never mutating its input. Use ToRGBA to convert an arbitrary image.Image to the *image.RGBA the operations consume, and the I/O helpers (Load, Save, Decode, Encode) for PNG and JPEG.

Phase 0 implements Grayscale, Invert, Resize (nearest-neighbour and bilinear), Convolve (arbitrary odd-sized kernels with edge clamping), GaussianBlur (separable), AdjustBrightness and AdjustContrast.

Phase 1 adds edge detection — Sobel (gradient magnitude) with SobelX/SobelY directional responses, the normalised Prewitt, Scharr and SobelMag operators and the Laplacian (matching skimage.filters.{prewitt,scharr,sobel,laplace}), and the full Canny detector (Gaussian, Sobel, bilinear non-maximum suppression, hysteresis); the filters BoxBlur (separable running-sum mean, matching scipy.ndimage.uniform_filter), Median (square median, matching scipy.ndimage.median_filter), and UnsharpMask/Sharpen (matching skimage.filters.unsharp_mask); the geometric transforms FlipHorizontal, FlipVertical, Rotate90/Rotate180/Rotate270 (numpy.fliplr/flipud/rot90) and Crop; the colour conversions RGBToHSV/HSVToRGB; and thresholding via OtsuThreshold/Threshold/Otsu (matching skimage.filters.threshold_otsu).

Phase 2 adds grayscale morphology over a square structuring element: Erode and Dilate (per-channel local min/max, matching scipy.ndimage.grey_erosion and grey_dilation) and the derived Open and Close. On binary 0/255 images these reduce to ordinary binary morphology.

docs/perf.md reports honest go-images-vs-scikit-image/SciPy benchmarks and correctness checks for the hot operations.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func AdjustBrightness

func AdjustBrightness(img image.Image, delta float64) *image.RGBA

AdjustBrightness returns a copy of img with delta added to the R, G and B channels, clamped to [0, 255]. Alpha is preserved.

func AdjustContrast

func AdjustContrast(img image.Image, factor float64) *image.RGBA

AdjustContrast returns a copy of img with the R, G and B channels scaled about the mid-point (128) by factor, clamped to [0, 255]. A factor of 1 leaves the image unchanged; values above 1 increase contrast, values in [0, 1) reduce it. Alpha is preserved.

func BoxBlur

func BoxBlur(img image.Image, radius int) (*image.RGBA, error)

BoxBlur returns img blurred by a square averaging filter of the given radius: every output pixel is the mean of the (2*radius+1) by (2*radius+1) source neighbourhood centred on it, computed independently per R, G and B channel (alpha preserved). Borders use clamp-to-edge addressing, matching scipy.ndimage.uniform_filter with mode="nearest" and size 2*radius+1. The filter is separable and evaluated with a running window sum, so its cost is independent of the radius. It returns an error if radius is not positive.

func Canny

func Canny(img image.Image, sigma, low, high float64) (*image.RGBA, error)

Canny returns the binary Canny edge map of img: white (255,255,255) edges on an opaque black background. It implements the classic Canny pipeline, matching the algorithm of skimage.feature.canny:

  1. smooth the Rec. 601 luminance with a Gaussian of standard deviation sigma (clamp-to-edge borders);
  2. estimate gradients with the Sobel operator; the edge strength is the gradient norm;
  3. thin to 1-pixel ridges by non-maximum suppression with bilinear interpolation along the gradient direction;
  4. link edges by hysteresis: keep every ridge pixel with magnitude >= high, plus every ridge pixel with magnitude >= low that is 8-connected to a kept one.

low and high are absolute thresholds on the Sobel gradient magnitude (the smoothed luminance is in [0,255], so the magnitudes are on that scale). It returns an error if sigma is not positive, if either threshold is negative, or if high < low.

func Close

func Close(img image.Image, radius int) (*image.RGBA, error)

Close returns the morphological closing of img (dilation followed by erosion). Closing fills small dark features smaller than the structuring element. It returns an error if radius is not positive.

func Convolve

func Convolve(img image.Image, k Kernel) (*image.RGBA, error)

Convolve returns img convolved with k, using clamp-to-edge addressing at the borders. The R, G and B channels are convolved and clamped to [0, 255]; alpha is preserved. It returns an error if k has non-positive or even dimensions, or if the length of k.Weights does not match Width*Height.

func Crop

func Crop(img image.Image, r image.Rectangle) (*image.RGBA, error)

Crop returns the rectangular region r of img as a new image anchored at the origin. r is interpreted in the coordinate system of img converted to RGBA (origin at the top-left). It returns an error if r is empty or extends outside the image bounds.

func Decode

func Decode(r io.Reader) (*image.RGBA, error)

Decode reads an image from r, auto-detecting the format among the registered decoders (PNG and JPEG). It returns the decoded image converted to *image.RGBA.

func Dilate

func Dilate(img image.Image, radius int) (*image.RGBA, error)

Dilate returns the grayscale morphological dilation of img: the per-channel local maximum over a square structuring element. See Erode for borders, alpha and the radius rule.

func Encode

func Encode(w io.Writer, img image.Image, format Format) error

Encode writes img to w in the given format.

func Erode

func Erode(img image.Image, radius int) (*image.RGBA, error)

Erode returns the grayscale morphological erosion of img with a square structuring element of the given radius: every output channel is the minimum of the (2*radius+1) by (2*radius+1) source neighbourhood. Borders use clamp-to-edge addressing; alpha is preserved. The operation is separable (matching a square footprint in scipy.ndimage.grey_erosion). On a binary image (0/255) this is ordinary binary erosion. It returns an error if radius is not positive.

func FlipHorizontal

func FlipHorizontal(img image.Image) *image.RGBA

FlipHorizontal returns a copy of img mirrored left-to-right (column x of the width-w source becomes column w-1-x). It matches numpy.fliplr.

func FlipVertical

func FlipVertical(img image.Image) *image.RGBA

FlipVertical returns a copy of img mirrored top-to-bottom (row y of the height-h source becomes row h-1-y). It matches numpy.flipud.

func GaussianBlur

func GaussianBlur(img image.Image, sigma float64) (*image.RGBA, error)

GaussianBlur returns img blurred by a Gaussian of standard deviation sigma, implemented as a separable convolution with clamp-to-edge borders. It returns an error if sigma is not positive.

func Grayscale

func Grayscale(img image.Image) *image.RGBA

Grayscale returns a copy of img with every pixel replaced by its luminance-weighted gray value (Rec. 601 coefficients). Alpha is preserved.

func HSVToRGB

func HSVToRGB(img image.Image) *image.RGBA

HSVToRGB inverts RGBToHSV: it interprets each pixel's first three channels as byte-encoded H, S, V and returns the corresponding R, G, B. Alpha is preserved.

func Invert

func Invert(img image.Image) *image.RGBA

Invert returns a copy of img with the R, G and B channels negated. Alpha is preserved.

func Laplacian

func Laplacian(img image.Image) *image.RGBA

Laplacian returns the discrete Laplacian edge map of img, matching skimage.filters.laplace with ksize=3 (the kernel [0,-1,0; -1,4,-1; 0,-1,0] applied to the luminance plane). The signed second-derivative response is offset by 128 so a flat region is mid-grey, written to R, G and B and clamped to [0,255]; alpha is preserved and borders use clamp-to-edge addressing. Being a second-derivative operator it highlights intensity curvature (lines, spots, zero-crossings) rather than step edges.

func Load

func Load(path string) (*image.RGBA, error)

Load reads and decodes the image at path, returning it as *image.RGBA. The format is auto-detected from the file contents.

func Median

func Median(img image.Image, radius int) (*image.RGBA, error)

Median returns img filtered by a square median of the given radius: every output channel is the median of the (2*radius+1) by (2*radius+1) source neighbourhood, computed independently per R, G and B (alpha preserved), with clamp-to-edge addressing. It matches scipy.ndimage.median_filter with a (2*radius+1)-square footprint and mode="nearest". The median is robust to outliers, so it removes salt-and-pepper noise while preserving edges far better than a linear blur. It returns an error if radius is not positive.

func Open

func Open(img image.Image, radius int) (*image.RGBA, error)

Open returns the morphological opening of img (erosion followed by dilation with the same square structuring element). Opening removes small bright features smaller than the element while preserving overall shape. It returns an error if radius is not positive.

func Otsu

func Otsu(img image.Image) *image.RGBA

Otsu is a convenience wrapper that thresholds img at the level chosen by Otsu's method (equivalent to Threshold(img, OtsuThreshold(img))).

func OtsuThreshold

func OtsuThreshold(img image.Image) uint8

OtsuThreshold returns the gray level in [0, 255] computed by Otsu's method on img's Rec. 601 luminance histogram: the level that maximises the between-class variance of the two pixel populations split at it. It matches the value returned by skimage.filters.threshold_otsu on a 256-bin histogram. Pass the result to Threshold (foreground = luminance strictly greater than it).

func Prewitt

func Prewitt(img image.Image) *image.RGBA

Prewitt returns the Prewitt gradient-magnitude edge map of img. The operator is the separable 3x3 Prewitt kernel applied to each pixel's Rec. 601 luminance; the magnitude sqrt((gx^2+gy^2)/2) is written to the R, G and B channels as a grayscale edge image (alpha preserved). It mirrors skimage.filters.prewitt: the directional kernels are normalised so each axis kernel's absolute weights sum to one, and clamp-to-edge addressing reproduces skimage's default reflect border for a 3-tap kernel.

func RGBToHSV

func RGBToHSV(img image.Image) *image.RGBA

RGBToHSV returns a copy of img with each pixel's R, G, B replaced by a byte-encoded H, S, V triple: H is the hue mapped from [0,360) to [0,255], S and V are mapped from [0,1] to [0,255]. Alpha is preserved. HSVToRGB inverts the mapping (within rounding). The encoding keeps the result inside the same RGBA-backed representation the rest of the pipeline uses.

func Resize

func Resize(img image.Image, w, h int, mode ResizeMode) (*image.RGBA, error)

Resize returns img scaled to w by h pixels using the given mode. It returns an error if w or h is not positive.

func Rotate90

func Rotate90(img image.Image) *image.RGBA

Rotate90 returns img rotated 90 degrees counter-clockwise (matching numpy.rot90 with k=1). A w-by-h image becomes h-by-w.

func Rotate180

func Rotate180(img image.Image) *image.RGBA

Rotate180 returns img rotated 180 degrees (numpy.rot90 with k=2). The dimensions are unchanged.

func Rotate270

func Rotate270(img image.Image) *image.RGBA

Rotate270 returns img rotated 90 degrees clockwise, i.e. 270 degrees counter-clockwise (numpy.rot90 with k=3). A w-by-h image becomes h-by-w.

func Save

func Save(path string, img image.Image) error

Save encodes img and writes it to path, choosing the format from the file extension: ".png" for PNG and ".jpg" or ".jpeg" for JPEG (case-insensitive). It returns an error for any other extension.

func Scharr

func Scharr(img image.Image) *image.RGBA

Scharr returns the Scharr gradient-magnitude edge map of img, matching skimage.filters.scharr. The Scharr smoothing triple (0.1875, 0.625, 0.1875) gives the best rotational symmetry of the Sobel/Prewitt/Scharr family. See Prewitt for the luminance, magnitude and border conventions.

func Sharpen

func Sharpen(img image.Image) *image.RGBA

Sharpen returns a sharpened copy of img with sensible defaults: an unsharp mask with radius 1.0 and amount 1.0, i.e. it adds back the full single-pixel- scale detail layer. For finer control over the scale or strength use UnsharpMask directly.

func Sobel

func Sobel(img image.Image) *image.RGBA

Sobel returns the Sobel gradient-magnitude edge map of img. The operator is applied to each pixel's Rec. 601 luminance with clamp-to-edge borders; the magnitude is clamped to [0, 255] and written to the R, G and B channels, producing a grayscale edge image. Alpha is preserved. Strong intensity transitions appear bright, flat regions dark.

func SobelMag

func SobelMag(img image.Image) *image.RGBA

SobelMag returns the normalised Sobel gradient-magnitude edge map of img using the scikit-image convention (sqrt((gx^2+gy^2)/2) on luminance scaled to [0,1]), matching skimage.filters.sobel. It differs from Sobel, which uses the classic integer kernels and the unnormalised magnitude sqrt(gx^2+gy^2); SobelMag shares the one definition used by Prewitt and Scharr so the edge family is directly comparable to scikit-image.

func SobelX

func SobelX(img image.Image) *image.RGBA

SobelX returns the horizontal Sobel response of img: an estimate of the left-to-right intensity derivative of each pixel's luminance. The signed response is scaled and offset so a zero gradient is mid-grey (128), a rising edge brighter and a falling edge darker, clamped to [0, 255] and written to R, G and B. Alpha is preserved; borders use clamp-to-edge addressing.

func SobelY

func SobelY(img image.Image) *image.RGBA

SobelY returns the vertical Sobel response of img: an estimate of the top-to-bottom intensity derivative of each pixel's luminance, with the same scaling, offset and addressing conventions as SobelX. Alpha is preserved.

func Threshold

func Threshold(img image.Image, t uint8) *image.RGBA

Threshold returns a binary image: every pixel of img whose Rec. 601 luminance is strictly greater than t becomes white, every other pixel black. Alpha is preserved. Combine with OtsuThreshold for an automatically chosen level.

func ToRGBA

func ToRGBA(img image.Image) *image.RGBA

ToRGBA returns img as an *image.RGBA. If img is already an *image.RGBA whose bounds start at the origin, it is returned unchanged; otherwise the pixels are copied (and, when necessary, colour-converted) into a freshly allocated origin-anchored *image.RGBA of the same dimensions.

func UnsharpMask

func UnsharpMask(img image.Image, radius, amount float64) (*image.RGBA, error)

UnsharpMask returns a sharpened copy of img using the unsharp-masking technique: dst = clamp(src + amount*(src - blurred)), where blurred is the Gaussian blur of img with standard deviation radius. The R, G and B channels are processed independently and alpha is preserved. It matches skimage.filters.unsharp_mask applied per channel.

radius controls the scale of the detail recovered (the Gaussian sigma) and must be positive; amount scales how strongly that detail is added back: 0 leaves the image unchanged, typical sharpening uses values around 0.5–2, and negative values soften. It returns an error if radius is not positive.

Types

type Format

type Format int

Format identifies an encodable image format.

const (
	// PNG is the lossless PNG format.
	PNG Format = iota
	// JPEG is the lossy JPEG format, encoded at the package's default quality.
	JPEG
)

type Kernel

type Kernel struct {
	Width   int
	Height  int
	Weights []float64
}

Kernel is a 2-D convolution kernel: Weights is a row-major slice of length Width*Height, and both Width and Height must be odd.

type ResizeMode

type ResizeMode int

ResizeMode selects the interpolation used by Resize.

const (
	// NearestNeighbor selects the source pixel nearest to each destination
	// pixel. It is fast and exact for integer scale factors but blocky.
	NearestNeighbor ResizeMode = iota
	// Bilinear linearly interpolates the four nearest source pixels. It is
	// smoother than nearest-neighbour at the cost of more arithmetic.
	Bilinear
)

Directories

Path Synopsis
internal
kernels
Package kernels holds the per-pixel inner loops used by the public image operations.
Package kernels holds the per-pixel inner loops used by the public image operations.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL