Skip to content
This repository has been archived by the owner on Jan 24, 2024. It is now read-only.

Commit

Permalink
Improvements to concurrency
Browse files Browse the repository at this point in the history
Reduce the overhead caused by using a huge amount of goroutines to compute histograms. 

The concurrent algorithms will now divide the `Rectangle` that makes up the image in up to `runtime.NumProc()` parts to be analyzed concurrently. This gives slight speed improvements and decreases memory consumption.
  • Loading branch information
AlessandroPomponio authored Feb 27, 2020
2 parents 77fd536 + 756956b commit bff3922
Show file tree
Hide file tree
Showing 5 changed files with 212 additions and 149 deletions.
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,11 @@ func main() {

Benchmarks can be found in the `histogram` package and are run on the `beach_medium.jpg` image (1280x1917).

Here are the results on system with `Windows 10 (1909), i7 4770, 16GB RAM, Go 1.13.8`.

```
BenchmarkWith32Bins-8 5 244802280 ns/op
BenchmarkWith32BinsConcurrent-8 20 53650080 ns/op
BenchmarkWith64Bins-8 5 250596880 ns/op
BenchmarkWith64BinsConcurrent-8 20 54100025 ns/op
BenchmarkWith32Bins-8 5 235398880 ns/op
BenchmarkWith32BinsConcurrent-8 21 50714243 ns/op
BenchmarkWith64Bins-8 5 246097460 ns/op
BenchmarkWith64BinsConcurrent-8 21 51881000 ns/op
```
94 changes: 27 additions & 67 deletions histogram/32_bins.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
package histogram

import (
"github.com/AlessandroPomponio/hsv/conversion"
"image"
"math"

"github.com/AlessandroPomponio/hsv/conversion"
"runtime"
)

const (
Expand Down Expand Up @@ -80,33 +80,17 @@ func With32Bins(img image.Image, roundType int) []float64 {
func With32BinsConcurrent(img image.Image, roundType int) []float64 {

bins := make([]float64, 32)
binChannel := make(chan []float64)

xBound := img.Bounds().Dx()
yBound := img.Bounds().Dy()

var routineAmount int

if xBound >= yBound {

routineAmount = xBound

for x := 0; x < xBound; x++ {
go calculate32BinsForColumn(x, yBound, img, binChannel)
}

} else {

routineAmount = yBound

for y := 0; y < yBound; y++ {
go calculate32BinsForRow(xBound, y, img, binChannel)
}
cpuAmt := runtime.NumCPU()
binChannel := make(chan []float64, cpuAmt/2)

// Split image into NumCPU sub-images to help speed up the computation.
rectangles := splitInto(cpuAmt, img.Bounds())
for _, rectangle := range rectangles {
go calculate32BinsForRectangle(rectangle, img, binChannel)
}

// Gather the results from all goroutines and sum them.
for i := 0; i < routineAmount; i++ {
for i := 0; i < len(rectangles); i++ {

currentBins := <-binChannel

Expand All @@ -116,61 +100,37 @@ func With32BinsConcurrent(img image.Image, roundType int) []float64 {

}

return normalize32BinsHistogram(roundType, xBound, yBound, bins)
return normalize32BinsHistogram(roundType, img.Bounds().Dx(), img.Bounds().Dy(), bins)

}

func calculate32BinsForColumn(x, yBound int, img image.Image, outputChan chan []float64) {

verticalBins := make([]float64, 32)

for y := 0; y < yBound; y++ {

h, s, _ := conversion.RGBAToHSV(img.At(x, y).RGBA())

// hueBin in [0,7].
// Try to map hue in equally-sized
// levels by dividing it for 360/7.
hueBin := int(h / 51.42857142857143)
func calculate32BinsForRectangle(rectangle image.Rectangle, img image.Image, outputChan chan []float64) {

// saturationBin in [0,3]
// Try to map saturation in equally-sized
// levels by dividing it for 100/3.
saturationBin := int(s / 33.33333333333333)

index := 4*hueBin + saturationBin
verticalBins[index]++

}

outputChan <- verticalBins

}

func calculate32BinsForRow(xBound, y int, img image.Image, outputChan chan []float64) {
bins := make([]float64, 32)
for x := rectangle.Min.X; x <= rectangle.Max.X; x++ {

horizontalBins := make([]float64, 32)
for y := rectangle.Min.Y; y <= rectangle.Max.Y; y++ {

for x := 0; x < xBound; x++ {
h, s, _ := conversion.RGBAToHSV(img.At(x, y).RGBA())

h, s, _ := conversion.RGBAToHSV(img.At(x, y).RGBA())
// hueBin in [0,7].
// Try to map hue in equally-sized
// levels by dividing it for 360/7.
hueBin := int(h / 51.42857142857143)

// hueBin in [0,7].
// Try to map hue in equally-sized
// levels by dividing it for 360/7.
hueBin := int(h / 51.42857142857143)
// saturationBin in [0,3]
// Try to map saturation in equally-sized
// levels by dividing it for 100/3.
saturationBin := int(s / 33.33333333333333)

// saturationBin in [0,3]
// Try to map saturation in equally-sized
// levels by dividing it for 100/3.
saturationBin := int(s / 33.33333333333333)
index := 4*hueBin + saturationBin
bins[index]++

index := 4*hueBin + saturationBin
horizontalBins[index]++
}

}

outputChan <- horizontalBins
outputChan <- bins

}

Expand Down
109 changes: 31 additions & 78 deletions histogram/64_bins.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package histogram
import (
"image"
"math"
"runtime"

"github.com/AlessandroPomponio/hsv/conversion"
)
Expand Down Expand Up @@ -75,33 +76,17 @@ func With64Bins(img image.Image, roundType int) []float64 {
func With64BinsConcurrent(img image.Image, roundType int) []float64 {

bins := make([]float64, 64)
binChannel := make(chan []float64)

xBound := img.Bounds().Dx()
yBound := img.Bounds().Dy()

var routineAmount int

if xBound >= yBound {

routineAmount = xBound

for x := 0; x < xBound; x++ {
go calculate64BinsForColumn(x, yBound, img, binChannel)
}

} else {

routineAmount = yBound

for y := 0; y < yBound; y++ {
go calculate64BinsForRow(xBound, y, img, binChannel)
}
cpuAmt := runtime.NumCPU()
binChannel := make(chan []float64, cpuAmt/2)

// Split image into NumCPU sub-images to help speed up the computation.
rectangles := splitInto(cpuAmt, img.Bounds())
for _, rectangle := range rectangles {
go calculate64BinsForRectangle(rectangle, img, binChannel)
}

// Gather the results from all goroutines and sum them.
for i := 0; i < routineAmount; i++ {
for i := 0; i < len(rectangles); i++ {

currentBins := <-binChannel

Expand All @@ -111,74 +96,42 @@ func With64BinsConcurrent(img image.Image, roundType int) []float64 {

}

return normalize64BinsHistogram(roundType, xBound, yBound, bins)
return normalize64BinsHistogram(roundType, img.Bounds().Dx(), img.Bounds().Dy(), bins)

}

func calculate64BinsForColumn(x, yBound int, img image.Image, outputChan chan []float64) {

verticalBins := make([]float64, 64)

for y := 0; y < yBound; y++ {

h, s, v := conversion.RGBAToHSV(img.At(x, y).RGBA())

// hueBin in [0,7].
// Try to map hue in equally-sized
// levels by dividing it for 360/7.
hueBin := int(h / 51.42857142857143)

// saturationBin in [0,3]
// Try to map saturation in equally-sized
// levels by dividing it for 100/3.
saturationBin := int(s / 33.33333333333333)

// valueBin in [0,1]
// Try to map value in equally-sized
// levels by dividing it for a value
// that's just above 50.
valueBin := int(v / 50.0000000000001)

index := 4*hueBin + saturationBin + 32*valueBin
verticalBins[index]++

}
func calculate64BinsForRectangle(rectangle image.Rectangle, img image.Image, outputChan chan []float64) {

outputChan <- verticalBins

}

func calculate64BinsForRow(xBound, y int, img image.Image, outputChan chan []float64) {

horizontalBins := make([]float64, 64)
bins := make([]float64, 64)
for x := rectangle.Min.X; x <= rectangle.Max.X; x++ {

for x := 0; x < xBound; x++ {
for y := rectangle.Min.Y; y <= rectangle.Max.Y; y++ {

h, s, v := conversion.RGBAToHSV(img.At(x, y).RGBA())
h, s, v := conversion.RGBAToHSV(img.At(x, y).RGBA())

// hueBin in [0,7].
// Try to map hue in equally-sized
// levels by dividing it for 360/7.
hueBin := int(h / 51.42857142857143)
// hueBin in [0,7].
// Try to map hue in equally-sized
// levels by dividing it for 360/7.
hueBin := int(h / 51.42857142857143)

// saturationBin in [0,3]
// Try to map saturation in equally-sized
// levels by dividing it for 100/3.
saturationBin := int(s / 33.333333333)
// saturationBin in [0,3]
// Try to map saturation in equally-sized
// levels by dividing it for 100/3.
saturationBin := int(s / 33.333333333)

// valueBin in [0,1]
// Try to map value in equally-sized
// levels by dividing it for a value
// that's just above 50.
valueBin := int(v / 50.0000000000001)
// valueBin in [0,1]
// Try to map value in equally-sized
// levels by dividing it for a value
// that's just above 50.
valueBin := int(v / 50.0000000000001)

index := 4*hueBin + saturationBin + 32*valueBin
horizontalBins[index]++
index := 4*hueBin + saturationBin + 32*valueBin
bins[index]++

}
}

outputChan <- horizontalBins

outputChan <- bins
}

// normalize64BinsHistogram normalizes 64-bin histograms by the amount of pixels in the image.
Expand Down
58 changes: 58 additions & 0 deletions histogram/rectangles.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright 2019 Alessandro Pomponio. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package histogram

import (
"image"
)

// splitInto splits a rectangle in up to amount parts.
func splitInto(amount int, rectangle image.Rectangle) []image.Rectangle {

// Avoid infinite recursion in case amount ends up being odd
if amount < 4 {
return split(rectangle)
}

// Check if it's worth to stop the recursion
xBound := rectangle.Dx()
yBound := rectangle.Dy()
if xBound < 400 && yBound < 400 {
return split(rectangle)
}

rects := split(rectangle)
return append(splitInto(amount/2, rects[0]), splitInto(amount/2, rects[1])...)

}

// split splits a Rectangle in two, horizontally.
func split(r image.Rectangle) []image.Rectangle {

return []image.Rectangle{
{

Min: image.Point{
X: r.Min.X,
Y: r.Min.Y,
},
Max: image.Point{
X: ((r.Max.X + r.Min.X) / 2) - 1,
Y: r.Max.Y,
},
},
{
Min: image.Point{
X: (r.Max.X + r.Min.X) / 2,
Y: r.Min.Y,
},
Max: image.Point{
X: r.Max.X,
Y: r.Max.Y,
},
},
}

}
Loading

0 comments on commit bff3922

Please sign in to comment.