Fork me on GitHub

Image processing in pure Go.
No OpenCV. No libvips.

The scikit-image vocabulary over an RGBA raster โ€” blur, edges, morphology, geometry, colour โ€” with CGO_ENABLED=0, so it cross-compiles to a static binary and runs in a browser tab. Image processing in most languages means binding a C library; this is one portable module, accelerated by go-asmgen SIMD. BSD-3-Clause.

8 modules6 architectures, tests run100% coverage, gated
main.go
// The vocabulary, over any image.Image.
m := images.GaussianBlur(src, 2.0)
e := images.Canny(m, 0.1, 0.3)
b := images.Otsu(images.Grayscale(src))

// A stream that stopped early still says what it got.
img, rows, err := png.DecodePartial(r)
the first `rows` are complete; the rest is not

// And a Photoshop file is a picture like any other.
import _ "github.com/go-images/psd"
m, _, err = image.Decode(f)
One organisation, 8 modules, every one of them pure Goand the suite runs on 6 architectures, big-endian included
Why it exists

Because the alternative is a C library.

OpenCV and libvips are excellent, and they are C: a cgo build, a system package, a cross-compilation problem, and nothing at all in a browser. go-images is one module that builds for every target Go builds for โ€” and where the work is separable and hot, it is faster than scikit-image rather than merely portable.

๐Ÿงฉ

A vocabulary, not a framework

The names are scikit-image's, so somebody porting a pipeline reads one list and recognises it: gaussian_blur, sobel, canny, erode, dilate, resize, otsu.

โšก

SIMD where it pays

Kernels generated by go-asmgen for amd64 (SSE2), arm64 (NEON) and s390x (vector). The portable scalar core is what the other targets run, and the benchmark pages say which is which rather than quoting the best number.

๐Ÿ“‰

Honest where it loses

It beats scikit-image on box blur and Sobel โ€” the separable hot operations โ€” and the documentation names the operations where it does not, because a benchmark page that only shows wins is an advertisement.

๐ŸŒ

And in a browser

CGO_ENABLED=0 means GOOS=js works, which is the one thing no binding to a C library can offer: the pictures never leave the machine.

The organisation

One library, three codec forks, two readers.

Each is its own module with its own release line. The three forks of Go's own codecs carry their changes in a NOTICE and pass Go's suite unaltered; a reader registers itself with image.Decode, so a caller imports it for its side effect and never names it again.

images ready

The library: the scikit-image vocabulary in pure Go. Filters (GaussianBlur, BoxBlur, Median, Sharpen), edges (Sobel, Prewitt, Scharr, Laplacian, Canny), morphology (Erode, Dilate), geometry (Resize, Rotate, Crop, Flip) and colour (Grayscale, RGBToHSV, Otsu). SIMD kernels from go-asmgen on amd64 (SSE2), arm64 (NEON) and s390x (vector), and it beats scikit-image on the separable hot operations โ€” box blur and Sobel โ€” with pages that say where it does not.

repository ยท pkg.go.dev

depth ready

One picture in, two eyes out. Depth guessed from cues in the picture itself (Cues), softened (Soften), and both eyes synthesised (Views) โ€” for a headset or a 3D display. โ›” It takes any depth map, so a real network can be substituted: this is the whole answer only where none is available, including in a browser. One pass over the source columns per row and no global sort โ€” 6.3 ms for a 4K frame on sixteen cores, 46.9 ms on a telephone โ€” checked against a GPU implementation of the same rule, byte for byte.

repository ยท pkg.go.dev

psd ready

Photoshop files (.psd, .psb) for image.Decode. โ›” The flattened composite, deliberately: reading layers means re-implementing a compositor, and the composite is what Photoshop itself shows. Bitmap, Grayscale, Indexed, RGB, CMYK, Duotone and Lab; 1, 8 and 16 bits a channel; raw and RLE. โ›” CMYK is stored inverted (255 is no ink) and alpha is straight โ€” the two readings that otherwise come out as a photographic negative and as every edge pixel too bright. Judged against sips, which is ImageIO and has nothing to do with this code: exact on 2 073 600 pixels of a real file.

repository ยท pkg.go.dev

png ready

A fork of Go’s image/png, under the same licence, with every change stated in its NOTICE and Go’s own suite passing unaltered. What differs: DecodePartial says what a stream that stopped early did decode โ€” the first rows, and the error that stopped it โ€” because a picture still arriving is not the same thing as a picture that failed.

repository ยท pkg.go.dev

jpeg ready

A fork of Go’s image/jpeg. What differs: a four-component JPEG’s chroma is upsampled the way libjpeg does โ€” reading between the samples โ€” rather than by repeating each one, which is what made CMYK scans come back softer here than everywhere else.

repository ยท pkg.go.dev

jpeg2000 ready

A pure-Go JPEG 2000 codec (.j2k, .jp2, decode and encode), forked from ajroetker/go-jpeg2000 with every change stated in its NOTICE. โ›” A file that says CMYK decodes to an *image.CMYK: read as red, green and blue it lost the black plate, 255 levels off poppler on every pixel, and it is now at most 2 over 2 440 044 samples. Tiled encodes now land where the decoder and OpenJPEG read them. The lossless colour transform and the inverse 5/3 wavelet are plain loops, because the SIMD path took an allocating fallback on amd64. A lossless 1080p decode there went from 638 ms and 14.55 million allocations to 269 ms and 93 thousand, and arm64 is not slower. โ›” Not yet under the organisation’s 100% coverage gate: it is tested on eight architectures, but its coverage is not gated.

repository ยท pkg.go.dev

gif ready

A fork of Go’s image/gif, with the same DecodePartial as png: the first frame at its full declared size, the number of pixel rows that are complete counting from the top, and the error that stopped the decode.

repository ยท pkg.go.dev

webp ready

WebP encoding in pure Go: the RIFF container, a lossless VP8L encoder (the pixels out bit for bit as they went in) and a lossy VP8 one with a quality from 0 to 100. On a rasterised page of text the lossless form is half the size of PNG; on a photograph the lossy form is 26ร— smaller than the lossless one.

repository ยท pkg.go.dev

The bar

100% of statements, and a judge that is not us.

๐Ÿ’ฏ

Coverage is a gate, not a goal

Every repository here fails its own CI below 100% of statements, error branches included. โ›” And the gate reads the profile rather than the percentage: total: 99.95% prints as 100.0%.

๐Ÿ”ฌ

Validated against scikit-image

The library's filters are compared against the thing it borrows its vocabulary from, which is the only way a claim about a blur means anything: a filter checked against its own output is checked against its author.

โš–๏ธ

And against ImageIO

The Photoshop reader is judged by sips โ€” macOS's own decoder, nothing to do with this code โ€” pixel for pixel: exact on 2 073 600 pixels of a real file. โ›” The first comparison blamed us for the instrument: At().RGBA() premultiplies, and the reference was already premultiplied, so it was done twice.

๐Ÿงช

Mutation-tested

Deleting a guard and running the suite is the only way to know a test holds it. The PSD reader's sweep found a clamp nothing held โ€” and the colour it protects is one Lab holds and sRGB cannot, which Go converts to a byte in an implementation-dependent way: 0 on one architecture, 251 on another.

Where it runs

6 architectures, and the tests run on every one.

Cross-compiling proves a program builds. It says nothing about what it does when a byte order, a word size or a vector unit differs โ€” and that is exactly where an image kernel lives: amd64, arm64, riscv64, loong64, ppc64le and s390x.

๐Ÿ”„

s390x is big-endian and has a vector unit

So it is the one target where a byte-order assumption and a SIMD kernel can hide each other. Both are exercised: the scalar core and the vector kernel have to agree there as well as on amd64.

๐Ÿ“

32-bit targets are built, and that matters here

A picture's size is a number in a file: 300000 ร— 300000 overflows an int on a 32-bit build, and a bounds check that overflows waves the bomb through. The PSD reader multiplies as int64 before comparing, for that reason.

๐Ÿงฎ

The scalar core is not a fallback

It is the reference. Every SIMD kernel is checked against it rather than against a recorded output, so an architecture with no kernel is slower and never different.

๐Ÿ“š

Documentation of its own

The docs site is built with MkDocs and versioned with mike: the API reference, the roadmap, and the benchmark pages against scikit-image.

Pictures in, pictures out, with no C anywhere.

And where a picture becomes a document, go-pdfkit takes it from here โ€” a PSD, a scan or a photograph onto a PDF page, in the same binary.

Read the source on GitHub โ†’