Skip to content

Claude bugreport: ImageToMatRGBA produces a BGRA mat despite its name; roundtrip via ToImage silently changes alpha semantics — propose deprecation + honestly-named replacements #1375

Description

@marrasen

I've been working with the help of Claude on a couple of projects that uses gocv, and today it found a bug. I understand that using AI to send issues are a sensitive subject right now, so I want to be honest and clear about that, and that this issue is written by AI (Claude Fable 5).

But it has also written a small proof, so I'll paste it. If "AI bug reports" are unwanted, I'll gladly delete this and investigate on my own at a later time.

Here we go:

Environment: gocv 0.42.0–0.43.0 (behavior dates back to #234), OpenCV 4.12/4.13, all platforms.

Two related surprises in the RGBA conversion helpers, both invisible in the common alpha=255 case:

1. ImageToMatRGBA returns an R/B-swapped mat. For *image.RGBA input it copies Pix (RGBA byte order) into a CV8UC4 mat and then applies CvtColor(ColorBGRAToRGBA). Since the bytes are already RGBA, the conversion swaps channels 0/2, so the returned mat actually holds BGRA-ordered content. The doc comment says the Mat "represents RGBA image", so any color-sensitive OpenCV call on it (channel means, FillPoly scalars, IMEncode) operates on swapped channels.

2. Mat.ToImage on CV8UC4 returns non-premultiplied image.NRGBA, so a roundtrip changes color semantics. ImageToMatRGBA consumes premultiplied image.RGBA; ToImage produces image.NRGBA with the same bytes. Byte-for-byte the roundtrip is lossless, but Go's color model differs: At().RGBA() premultiplies for NRGBA, so reported colors differ wherever alpha ≠ 255.

src := image.NewRGBA(image.Rect(0, 0, 1, 1))
src.SetRGBA(0, 0, color.RGBA{R: 182, G: 38, B: 78, A: 53})

mat, _ := gocv.ImageToMatRGBA(src)
defer mat.Close()

// (1) mat bytes are B,G,R,A — channel 0 is 78, not 182
data, _ := mat.DataPtrUint8()
fmt.Println(data[0]) // 78 (blue), despite "RGBA" in the name

// (2) roundtrip reports different colors
back, _ := mat.ToImage()
r1, _, _, _ := src.At(0, 0).RGBA()  // 182 * 0x101
r2, _, _, _ := back.At(0, 0).RGBA() // ≈ 182 * 53 / 255 * 0x101 — premultiplied on read
fmt.Println(r1>>8, r2>>8) // 182 37

Proposed fix: new honestly-named functions, deprecate the misleading one.

Changing the existing behavior in place would silently break every caller that already compensates, so instead:

  1. Add ImageToMatBGRA(img image.Image) (Mat, error) — exactly the current behavior of ImageToMatRGBA, but named for what it produces: a mat in OpenCV's native BGRA channel order, ready for color-aware OpenCV APIs. Implementation is a rename.

  2. Add ImageToMatRGBAExact(img *image.RGBA) (Mat, error) (or similar) — byte-exact copy of Pix into a CV8UC4 mat with no CvtColor, documented as "channel order is RGBA; OpenCV color-aware APIs will see swapped channels; use for byte-level pipelines (masking, copying, encoding via Go)".

  3. Add Mat.ToImageRGBA() (*image.RGBA, error) — byte-exact inverse of (2): no conversion, returns premultiplied-typed image.RGBA with the mat's bytes, making ImageToMatRGBAExact → OpenCV mask/copy ops → ToImageRGBA a true identity for kept pixels.

  4. Mark ImageToMatRGBA with a Go deprecation notice pointing at the replacements:

    // Deprecated: despite its name, the returned Mat is in BGRA channel
    // order. Use ImageToMatBGRA (same behavior, honest name) for OpenCV
    // color operations, or ImageToMatRGBAExact for byte-exact RGBA data.

    The // Deprecated: convention means staticcheck (SA1019), gopls, and IDEs warn existing users automatically — which is the point: everyone currently calling it is either compensating on purpose or silently wrong, and the warning sorts the two groups.

  5. ToImage itself should not be deprecated (it's correct for 8UC1/8UC3 and widely used) — but its doc comment should state that CV8UC4 mats come back as non-premultiplied image.NRGBA, so an image.RGBA → Mat → image roundtrip preserves bytes but not color.Color values when alpha < 255.

If the new-API route is unwanted, the minimum fix is the documentation in (4)/(5). Happy to send a PR for either variant.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions