1

I have written a program which converts RGBA image to Grayscale sequentially. I'm now trying to convert it so it runs in parallel.

I kind of understand how I need to be doing this but I'm struggling to get started.

Here is what I have so far.

   package main

import (
"image"
"image/color"
"image/jpeg"
"log"
"os"
)

var lum float64


type ImageSet interface {
Set(x, y int, c color.Color)
}

func rgbtogray(r uint32, g uint32, b uint32)  float64{
    lum = 0.299*float64(r) + 0.587*float64(g) + 0.114*float64(b)
    return lum
}


func main() {
file, err := os.Open("flower.jpg")
if err != nil {
    log.Fatal(err)
}
defer file.Close()

img, err := jpeg.Decode(file)
if err != nil {
    log.Fatal(os.Stderr, "%s: %v\n", "flower.jpg", err)
}

channel1 := make(chan float64)
channel2 := make(chan float64)


b := img.Bounds()
imgSet := image.NewRGBA(b)

halfImage := b.Max.X/2
fullImage := b.Max.X

for y := 0; y < b.Max.Y; y++ {
      go func() {
        for x := 0; x < halfImage; x++ {
          oldPixel := img.At(x, y)
          r, g, b, _ := oldPixel.RGBA()
        channel1 <- rgbtogray(r, g, b)
        pixel := color.Gray{uint8(lum / 256)}
        imgSet.Set(x, y, pixel)
      }
      }()

      go func() {
        for x := halfImage; x< fullImage; x++ {
          oldPixel := img.At(x, y)
          r, g, b, _ := oldPixel.RGBA()
        channel2 <- rgbtogray(r, g, b)
        pixel := color.Gray{uint8(lum / 256)}
        imgSet.Set(x, y, pixel)
      }
      }()


}

    outFile, err := os.Create("changed.jpg")
    if err != nil {
      log.Fatal(err)
    }
    defer outFile.Close()
    jpeg.Encode(outFile, imgSet, nil)

}

This runs but just returns a black image. I know the way I'm going about it is wrong but I'm not 100% what route I need to be taking.

My idea is to split the image down the middle, have one channel work on the pixels on the left and one channel work on the pixels on the right. Before moving down to the next y coordinate and so on.

I've tried moving all of the code in my go functions into my rgbatogray function but I was getting multiple errors to do with passing through variables etc. Would it be best to create another function which deals with the splitting etc as I think I calling my go functions should just look something like:

go func() {
      channel1 <- rgbtogray(r, g, b)
}()
go func() {
      channel2 <- rgbtogray(r, g, b)
}()

I'm unsure what steps I should be taking next on this so any tips and help greatly appreciated.

Scott Stensland
  • 26,870
  • 12
  • 93
  • 104
benjano
  • 369
  • 1
  • 5
  • 17
  • 1
    start by running you program with the race detector. To start, you're sharing a global `lum` variable across multiple goroutines, you're closing closing over the the `y` value form the for loop in the goroutines, you're not reading from either channel, so the sends can never complete (not sure where they would go), you're not waiting for any goroutines to finish before you attempt to write the file, you calling Set with the global `lum` which could be any value at any time, and you're going to have data races with them all trying to call `imgSet.Set`. – JimB Mar 02 '17 at 18:47
  • Hi, so the first step is making it so lum is a local variable? – benjano Mar 02 '17 at 18:52
  • I tried moving the for loops for x into my rgbtogray function but variables were messing me up. Is this the right approach to take? – benjano Mar 02 '17 at 18:52
  • 1
    There is no need for channels here: Just logically split the image and have one goroutine work on the left and the other goroutine on the right half of the image. – Volker Mar 02 '17 at 20:22
  • @benjano: you are likely going to have to operate directly on the pixel data rather than go though the `At` and `Set`methods in order to pass the race detector. The good new is that bypassing the interface abstractions will likely be as much of a performance gain as the added parallelism. – JimB Mar 02 '17 at 20:33
  • BTW, I'm not sure if this is simply an exercise or not, but if you're only dealing with jpegs, the pixel data is going to be YCbCr, so you already have the luminance calculated for you. Simply setting all the chrominance data to 128 will have the same effect, except much much faster. – JimB Mar 03 '17 at 00:14

2 Answers2

2

Here's an implementation of @JimB's suggestion in comments. It exploits the fact of JPEG images being in YCbCr to process the image setting inplace Cb and Cr components to 128 using one goroutine for each of them.

func set(wg *sync.WaitGroup, a []uint8, v uint8) {
    for i := 0; i < len(a); i++ {
        a[i] = v
    }
    wg.Done()
}

func gray(i *image.YCbCr) {
    var wg sync.WaitGroup
    wg.Add(2)
    go set(&wg, i.Cb, 128)
    go set(&wg, i.Cr, 128)
    wg.Wait()
}

func main() {
    i, err := jpeg.Decode(os.Stdin)
    if err != nil {
        log.Fatalf("decoding image: %v", err)
    }
    gray(i.(*image.YCbCr))
    err = jpeg.Encode(os.Stdout, i, nil)
    if err != nil {
        log.Fatalf("encoding image: %v", err)
    }
}

It turned out pretty simple.

Of course it could be extended to create more goroutines (possibly one per available core) assigning slices of Cb & Cr arrays to each for processing.

Pablo Lalloni
  • 2,615
  • 19
  • 20
  • Hi, thanks for the answer, I'm trying to get it to run now, I'm trying to get it to work still using my os.Open("flower.jpg") code as this is how I understand images are loaded in. Will this code work with that method? – benjano Mar 03 '17 at 13:08
  • @benjano, yes, just use the open file instead of os.Stdin in the call to jpeg.Decode(). – Pablo Lalloni Mar 03 '17 at 13:14
  • Hi, just so I understand how this works a little better, the two go routines working in parallel are go set(&wg, i.Cb, 128. This doesn't split the image in half i.e. down the middle. It works instead by one function working on Chroma Blue and one function working on Chroma Red? Have I understood this correctly – benjano Mar 03 '17 at 13:23
  • @benjano you might like to check my [recent answer](http://stackoverflow.com/a/42600281/301051) on a similar problem which implements more generically image partitioning and concurrent processing using a finite set of worker goroutines. – Pablo Lalloni Mar 04 '17 at 19:27
  • Hi, thanks for letting me know. I've implemented some of the ideas with in my code and got it working. Just so I fully understand it, is this working in parallel? Is it splitting the image into 4 vertical splits and working on each one of these at a time before reconstructing the image? – benjano Mar 06 '17 at 10:54
  • It is concurrent, and it could be parallel (depending on the available hardware and golang runtime configuration). And yes, the splitVert partitioner splits the image vertically in segments which are then processed concurrently by a number of processor goroutines. The image is not reconstructed since it is never really divided, instead I pass each processor the full image and a Rectangle limiting the image part it should work on. – Pablo Lalloni Mar 06 '17 at 15:23
0

How about creating a pipeline of pixels read from the image, converted to gray and finally set to a new image, where all steps run concurrently and each step could be internally parallelized?

Then Go's runtime will parallelize the tasks using all available cores.

This implementation works:

package main

import (
    "image"
    "image/color"
    "image/jpeg"
    "log"
    "os"
    "sync"
)

type Setter interface {
    Set(x, y int, c color.Color)
}

type Pixel struct {
    X, Y int
    C    color.Color
}

func sendPixels(in image.Image, out chan Pixel) {
    b := in.Bounds()
    for x := 0; x < b.Max.X; x++ {
        for y := 0; y < b.Max.Y; y++ {
            out <- Pixel{x, y, in.At(x, y)}
        }
    }
    close(out)
}

func convertToGray(in chan Pixel, out chan Pixel) {
    var wg sync.WaitGroup
    for p := range in {
        wg.Add(1)
        go func(p Pixel) {
            r, g, b, _ := p.C.RGBA()
            l := 0.299*float64(r) + 0.587*float64(g) + 0.114*float64(b)
            out <- Pixel{p.X, p.Y, color.Gray{uint8(l / 256)}}
            wg.Done()
        }(p)
    }
    wg.Wait()
    close(out)
}

func buildImage(in chan Pixel, out Setter, done chan int) {
    for p := range in {
        out.Set(p.X, p.Y, p.C)
    }
    close(done)
}

func main() {
    i, err := jpeg.Decode(os.Stdin)
    if err != nil {
        log.Fatalf("decoding image: %v", err)
    }

    a := make(chan Pixel, 1000)
    go sendPixels(i, a)

    b := make(chan Pixel, 1000)
    go convertToGray(a, b)

    c := image.NewRGBA(i.Bounds())
    d := make(chan int)
    go buildImage(b, c, d)

    <-d

    err = jpeg.Encode(os.Stdout, c, nil)
    if err != nil {
        log.Fatalf("encoding image: %v", err)
    }
}

Which can be tested running:

go run main.go <in.jpg >out.jpg
Pablo Lalloni
  • 2,615
  • 19
  • 20
  • while this technically works, it's going to be many orders of magnitude slower and possibly use huge amounts of memory. – JimB Mar 02 '17 at 22:39
  • @JimB WRT to performance: I thought this implementation as a proof of concept to be refined by batching pixels to be processed down the pipeline. WRT memory usage: what are you referring to? – Pablo Lalloni Mar 02 '17 at 23:22
  • This launches a goroutine for every pixel, with only the number of pixels as a limit to the number of concurrent goroutines. What was some primitive math operations on 3 values is now a 2k stack plus waitgroup and channel synchronization. I just ran this code with no modoifications on a relatively small image, and it took 32 seconds to do what should have taken a couple milliseconds. – JimB Mar 02 '17 at 23:34
  • Oh, as for performance, yes, batching pixels and/or only having a set number of workers consuming them would help significantly. Though it's still a lot of synchronization for such small operations. I doubt this pipeline could be improved past N workers simply taking separates slices of the raw pixel data and modifying it in place. – JimB Mar 02 '17 at 23:46
  • @JimB you're, as always, right. I've added another answer showing the implementation you suggested in comments above. – Pablo Lalloni Mar 03 '17 at 12:25