Skip to content

Commit

Permalink
remove pointers from error diffusion dithering routine
Browse files Browse the repository at this point in the history
Also: update test CI, add benchmark

For #13
  • Loading branch information
makew0rld committed Dec 18, 2023
1 parent 6488762 commit eedcc4d
Show file tree
Hide file tree
Showing 4 changed files with 43 additions and 9 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
strategy:
fail-fast: false
matrix:
go-version: ["1.13", "1.14", "1.15", "1.16"]
go-version: ["1.20", "1.21", "tip"]
steps:
- name: Install Go
uses: actions/setup-go@v2
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
### Changed
- Increased error diffusion dithering speed by ~50%
- Reduced error diffusion dithering memory usage by ~70%

### Fixed
- Docs: the input does still need to be converted to grayscale for grayscale palettes, actually (#7)

Expand Down
18 changes: 10 additions & 8 deletions dither.go
Original file line number Diff line number Diff line change
Expand Up @@ -292,27 +292,29 @@ func (d *Ditherer) Dither(src image.Image) image.Image {

// Store linear values here instead of converting back and forth and storing
// sRGB values inside the image.
// Pointers are used to differentiate between a zero value and an unset value
lins := make([][][3]*uint16, b.Dy())
lins := make([][][3]uint16, b.Dy())
for i := 0; i < len(lins); i++ {
lins[i] = make([][3]*uint16, b.Dx())
lins[i] = make([][3]uint16, b.Dx())
}

// Setters and getters for that linear storage
linearSet := func(x, y int, r, g, b uint16) {
lins[y][x] = [3]*uint16{&r, &g, &b}
lins[y][x] = [3]uint16{r, g, b}
}
linearAt := func(x, y int) (uint16, uint16, uint16) {
c := lins[y][x]
if c[0] == nil {
// This pixel hasn't been linearized yet
return c[0], c[1], c[2]
}

// Pre-fill that 2D-array with the linearized image pixels
for y := b.Min.Y; y < b.Max.Y; y++ {
for x := b.Min.X; x < b.Max.X; x++ {
r, g, b, _ := unpremultAndLinearize(img.At(x, y))
linearSet(x, y, r, g, b)
return r, g, b
}
return *c[0], *c[1], *c[2]
}

// Now do the actual dithering
for y := b.Min.Y; y < b.Max.Y; y++ {
for x := b.Min.X; x < b.Max.X; x++ {

Expand Down
28 changes: 28 additions & 0 deletions dither_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,34 @@ func TestErrorDiffusionColor(t *testing.T) {
ditherAndCompareImage(peppers, "edm_peppers_atkinson_red-green-yellow-black.png", d, t)
}

func BenchmarkErrorDiffusionColor(b *testing.B) {
f, err := os.Open(peppers)
if err != nil {
b.Fatal(err)
}
defer f.Close()

img, _, err := image.Decode(f)
if err != nil {
b.Fatal(err)
}

palette := []color.Color{
color.Black,
color.White,
color.RGBA{255, 0, 0, 255}, // Red
color.RGBA{0, 255, 0, 255}, // Green
color.RGBA{0, 0, 255, 255}, // Blue
}
d := NewDitherer(palette)
d.Matrix = FloydSteinberg

b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = d.Dither(img)
}
}

func TestSubset(t *testing.T) {
assert.Equal(t, true, subset([]color.Color{color.Black}, blackWhite))
assert.Equal(t, false, subset(blackWhite, []color.Color{color.Black}))
Expand Down

0 comments on commit eedcc4d

Please sign in to comment.