├── containers ├── image_container.go ├── image_itself_container.go └── file_image_container.go ├── README.md ├── .gitignore ├── operations ├── lightest.go ├── mode_test.go ├── lightest_test.go └── mode.go └── main.go /containers/image_container.go: -------------------------------------------------------------------------------- 1 | package containers 2 | 3 | import ( 4 | "image" 5 | ) 6 | 7 | type ImageContainer interface { 8 | GetImage() image.Image 9 | } 10 | -------------------------------------------------------------------------------- /containers/image_itself_container.go: -------------------------------------------------------------------------------- 1 | package containers 2 | 3 | import ( 4 | "image" 5 | ) 6 | 7 | type ImageItselfContainer struct { 8 | Image image.Image 9 | } 10 | 11 | func (i ImageItselfContainer) GetImage() image.Image { 12 | return i.Image 13 | } 14 | -------------------------------------------------------------------------------- /containers/file_image_container.go: -------------------------------------------------------------------------------- 1 | package containers 2 | 3 | import ( 4 | "image" 5 | "os" 6 | ) 7 | 8 | type FileImageContainer struct { 9 | Filename string 10 | } 11 | 12 | func (f FileImageContainer) GetImage() image.Image { 13 | file, _ := os.Open(f.Filename) 14 | defer file.Close() 15 | image, _, _ := image.Decode(file) 16 | return image 17 | } 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # blzimg 2 | 3 | A command line utility written in Go to do some image operations. 4 | 5 | ## Why not Photoshop, The GIMP or ImageMagick? 6 | 7 | I know that these tools do what I want to do here and much more. But I wanted to understand some 8 | algorithms and learn some Go. 9 | 10 | ## Using 11 | 12 | Up to this moment, the only operation in blzimg is the *lightest* operation. This operation reads 13 | every pixel from a list of images and, for a position (x,y), the final image will have the lightest 14 | pixel at that position. 15 | 16 | The command is 17 | 18 | `blzimg --output final.jpg lightest image1.jpg image2.jpg image3.jpg` 19 | 20 | ### More information about *lightest* 21 | 22 | Please read [this post](http://wp.me/pMrQd-7H). 23 | 24 | ## Limitations 25 | 26 | Until now, blzimg is working only with images in JPEG format. 27 | 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io 2 | 3 | ### SublimeText ### 4 | # cache files for sublime text 5 | *.tmlanguage.cache 6 | *.tmPreferences.cache 7 | *.stTheme.cache 8 | 9 | # workspace files are user-specific 10 | *.sublime-workspace 11 | 12 | # project files should be checked into the repository, unless a significant 13 | # proportion of contributors will probably not be using SublimeText 14 | # *.sublime-project 15 | 16 | # sftp configuration file 17 | sftp-config.json 18 | 19 | 20 | ### Vim ### 21 | [._]*.s[a-w][a-z] 22 | [._]s[a-w][a-z] 23 | *.un~ 24 | Session.vim 25 | .netrwhist 26 | *~ 27 | 28 | 29 | ### Linux ### 30 | *~ 31 | 32 | # KDE directory preferences 33 | .directory 34 | 35 | # Linux trash folder which might appear on any partition or disk 36 | .Trash-* 37 | 38 | 39 | ### OSX ### 40 | .DS_Store 41 | .AppleDouble 42 | .LSOverride 43 | 44 | # Icon must end with two \r 45 | Icon 46 | 47 | 48 | # Thumbnails 49 | ._* 50 | 51 | # Files that might appear in the root of a volume 52 | .DocumentRevisions-V100 53 | .fseventsd 54 | .Spotlight-V100 55 | .TemporaryItems 56 | .Trashes 57 | .VolumeIcon.icns 58 | 59 | # Directories potentially created on remote AFP share 60 | .AppleDB 61 | .AppleDesktop 62 | Network Trash Folder 63 | Temporary Items 64 | .apdisk 65 | 66 | 67 | ### Go ### 68 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 69 | *.o 70 | *.a 71 | *.so 72 | 73 | # Folders 74 | _obj 75 | _test 76 | 77 | # Architecture specific extensions/prefixes 78 | *.[568vq] 79 | [568vq].out 80 | 81 | *.cgo1.go 82 | *.cgo2.c 83 | _cgo_defun.c 84 | _cgo_gotypes.go 85 | _cgo_export.* 86 | 87 | _testmain.go 88 | 89 | *.exe 90 | *.test 91 | *.prof 92 | blzimg 93 | -------------------------------------------------------------------------------- /operations/lightest.go: -------------------------------------------------------------------------------- 1 | package operations 2 | 3 | import ( 4 | "errors" 5 | "image" 6 | "image/color" 7 | "image/draw" 8 | 9 | "github.com/esdrasbeleza/blzimg/containers" 10 | ) 11 | 12 | type LightestOperation struct{} 13 | 14 | func (c LightestOperation) lightest(color1, color2 color.Color) color.Color { 15 | if c.luminance(color1) > c.luminance(color2) { 16 | return color1 17 | } else { 18 | return color2 19 | } 20 | } 21 | 22 | func (c LightestOperation) Result(images []containers.ImageContainer) (image.Image, error) { 23 | if len(images) == 0 { 24 | return nil, nil 25 | } else if len(images) == 1 { 26 | return images[0].GetImage(), nil 27 | } 28 | 29 | firstImage := images[0].GetImage() 30 | bounds := firstImage.Bounds() 31 | lightest := image.NewRGBA(image.Rect(0, 0, bounds.Dx(), bounds.Dy())) 32 | draw.Draw(lightest, bounds, firstImage, bounds.Min, draw.Src) 33 | 34 | for _, currentImageContainer := range images[1:] { 35 | currentImage := currentImageContainer.GetImage() 36 | if currentImage.Bounds() != bounds { 37 | return nil, errors.New("The images have different size!") 38 | } 39 | 40 | c.getLightestImageBetweenTwo(lightest, currentImage) 41 | } 42 | 43 | return lightest, nil 44 | } 45 | 46 | func (c LightestOperation) getLightestImageBetweenTwo(current *image.RGBA, other image.Image) { 47 | for i := current.Bounds().Min.X; i < current.Bounds().Max.X; i++ { 48 | for j := current.Bounds().Min.Y; j < current.Bounds().Max.Y; j++ { 49 | currentLightestImagePixel := current.At(i, j) 50 | otherImagePixel := other.At(i, j) 51 | 52 | lightestColor := c.lightest(currentLightestImagePixel, otherImagePixel) 53 | if currentLightestImagePixel != lightestColor { 54 | current.Set(i, j, lightestColor) 55 | } 56 | } 57 | } 58 | } 59 | 60 | // http://stackoverflow.com/questions/596216/formula-to-determine-brightness-of-rgb-color 61 | func (c LightestOperation) luminance(someColor color.Color) uint32 { 62 | r, g, b, _ := someColor.RGBA() 63 | return uint32(0.2126*float32(r) + 0.7152*float32(g) + 0.0722*float32(b)) 64 | } 65 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "image/jpeg" 6 | "os" 7 | 8 | "github.com/codegangsta/cli" 9 | "github.com/esdrasbeleza/blzimg/containers" 10 | "github.com/esdrasbeleza/blzimg/operations" 11 | ) 12 | 13 | func main() { 14 | app := cli.NewApp() 15 | app.Name = "blzimg" 16 | app.Usage = "Execute some operations on images" 17 | app.Version = "0.1" 18 | 19 | app.Authors = []cli.Author{cli.Author{"Esdras Beleza", "esdras@esdrasbeleza.com"}} 20 | 21 | app.Flags = []cli.Flag{ 22 | cli.StringFlag{ 23 | Name: "output", 24 | Value: "final.jpg", 25 | Usage: "Output file", 26 | }, 27 | } 28 | 29 | app.Commands = []cli.Command{ 30 | { 31 | Name: "lightest", 32 | Aliases: []string{"l"}, 33 | Usage: "Merge the lightest pixels of some images in a single one", 34 | Action: func(c *cli.Context) { 35 | filenames := c.Args() 36 | output := c.GlobalString("output") 37 | 38 | fmt.Printf("Processing images...") 39 | fileContainers := make([]containers.ImageContainer, len(filenames)) 40 | for index, filename := range filenames { 41 | fileContainers[index] = containers.FileImageContainer{filename} 42 | } 43 | 44 | operation := operations.LightestOperation{} 45 | finalImage, _ := operation.Result(fileContainers) 46 | finalFile, _ := os.Create(output) 47 | defer finalFile.Close() 48 | jpeg.Encode(finalFile, finalImage, &jpeg.Options{jpeg.DefaultQuality}) 49 | fmt.Printf(" done.\n") 50 | }, 51 | }, 52 | { 53 | Name: "mode", 54 | Aliases: []string{"m"}, 55 | Usage: "Merge the lightest pixels of some images in a single one", 56 | Action: func(c *cli.Context) { 57 | filenames := c.Args() 58 | output := c.GlobalString("output") 59 | 60 | fmt.Printf("Processing images...") 61 | fileContainers := make([]containers.ImageContainer, len(filenames)) 62 | for index, filename := range filenames { 63 | fileContainers[index] = containers.FileImageContainer{filename} 64 | } 65 | 66 | operation := operations.ModeOperation{} 67 | finalImage, _ := operation.Result(fileContainers) 68 | finalFile, _ := os.Create(output) 69 | defer finalFile.Close() 70 | jpeg.Encode(finalFile, finalImage, &jpeg.Options{jpeg.DefaultQuality}) 71 | fmt.Printf(" done.\n") 72 | }, 73 | }, 74 | } 75 | 76 | app.Run(os.Args) 77 | } 78 | -------------------------------------------------------------------------------- /operations/mode_test.go: -------------------------------------------------------------------------------- 1 | package operations 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | "image/draw" 7 | "testing" 8 | 9 | "github.com/esdrasbeleza/blzimg/containers" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestIfWeGetTheRightModeColorBetweenSomeOfThem(t *testing.T) { 14 | operation := ModeOperation{} 15 | 16 | pixel1 := color.RGBA{10, 100, 180, 0} 17 | pixel2 := color.RGBA{10, 120, 190, 0} 18 | pixel3 := color.RGBA{20, 120, 220, 0} 19 | pixel4 := color.RGBA{33, 130, 220, 0} 20 | 21 | result := color.RGBA{10, 120, 220, 0} 22 | 23 | assert.Equal(t, result, operation.mode([]color.Color{pixel1, pixel2, pixel3, pixel4}), "The mode color is (10, 120, 220)") 24 | } 25 | 26 | func TestIfWeGetAnImageMadeWithTheModePixelsIfWeMergeSomeImages(t *testing.T) { 27 | var ( 28 | operation = ModeOperation{} 29 | 30 | black = color.RGBA{0, 0, 0, 0} 31 | white = color.RGBA{255, 255, 255, 0} 32 | ) 33 | 34 | /* 35 | * wbb 36 | * wbb 37 | * wbb 38 | */ 39 | image1 := image.NewRGBA(image.Rect(0, 0, 3, 3)) 40 | draw.Draw(image1, image1.Bounds(), &image.Uniform{black}, image.ZP, draw.Src) 41 | image1.Set(0, 0, white) 42 | image1.Set(0, 1, white) 43 | image1.Set(0, 2, white) 44 | 45 | /* 46 | * bwb 47 | * bwb 48 | * bwb 49 | */ 50 | image2 := image.NewRGBA(image.Rect(0, 0, 3, 3)) 51 | draw.Draw(image2, image2.Bounds(), &image.Uniform{black}, image.ZP, draw.Src) 52 | image2.Set(1, 0, white) 53 | image2.Set(1, 1, white) 54 | image2.Set(1, 2, white) 55 | 56 | /* 57 | * bbw 58 | * bbw 59 | * bbw 60 | */ 61 | image3 := image.NewRGBA(image.Rect(0, 0, 3, 3)) 62 | draw.Draw(image3, image3.Bounds(), &image.Uniform{black}, image.ZP, draw.Src) 63 | image3.Set(2, 0, white) 64 | image3.Set(2, 1, white) 65 | image3.Set(2, 2, white) 66 | 67 | var ( 68 | imageContainer1 = containers.ImageItselfContainer{image1} 69 | imageContainer2 = containers.ImageItselfContainer{image2} 70 | imageContainer3 = containers.ImageItselfContainer{image3} 71 | 72 | mergedImage, _ = operation.Result([]containers.ImageContainer{imageContainer1, imageContainer2, imageContainer3}) 73 | bounds = mergedImage.Bounds().Canon() 74 | ) 75 | 76 | for i := bounds.Min.X; i < bounds.Max.X; i++ { 77 | for j := bounds.Min.Y; j < bounds.Max.Y; j++ { 78 | assert.Equal(t, black, mergedImage.At(i, j), "Pixel must be black!") 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /operations/lightest_test.go: -------------------------------------------------------------------------------- 1 | package operations 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | "image/draw" 7 | "testing" 8 | 9 | "github.com/esdrasbeleza/blzimg/containers" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestIfWeGetTheRightLightestColorBetweenSomeOfThem(t *testing.T) { 14 | operation := LightestOperation{} 15 | 16 | pixel1 := color.RGBA{128, 22, 33, 0} 17 | pixel2 := color.RGBA{255, 255, 255, 0} 18 | 19 | assert.Equal(t, pixel2, operation.lightest(pixel1, pixel2), "The lightest color is (128, 128, 128)") 20 | } 21 | 22 | func TestIfWeGetAImageMadeWithTheLightestPixelsIfWeMergeSomeImages(t *testing.T) { 23 | operation := LightestOperation{} 24 | 25 | black := color.RGBA{0, 0, 0, 0} 26 | white := color.RGBA{255, 255, 255, 0} 27 | 28 | /* 29 | * wbb 30 | * wbb 31 | * wbb 32 | */ 33 | image1 := image.NewRGBA(image.Rect(0, 0, 3, 3)) 34 | draw.Draw(image1, image1.Bounds(), &image.Uniform{black}, image.ZP, draw.Src) 35 | image1.Set(0, 0, white) 36 | image1.Set(0, 1, white) 37 | image1.Set(0, 2, white) 38 | 39 | /* 40 | * bwb 41 | * bwb 42 | * bwb 43 | */ 44 | image2 := image.NewRGBA(image.Rect(0, 0, 3, 3)) 45 | draw.Draw(image2, image2.Bounds(), &image.Uniform{black}, image.ZP, draw.Src) 46 | image2.Set(1, 0, white) 47 | image2.Set(1, 1, white) 48 | image2.Set(1, 2, white) 49 | 50 | /* 51 | * bbw 52 | * bbw 53 | * bbw 54 | */ 55 | image3 := image.NewRGBA(image.Rect(0, 0, 3, 3)) 56 | draw.Draw(image3, image3.Bounds(), &image.Uniform{black}, image.ZP, draw.Src) 57 | image3.Set(2, 0, white) 58 | image3.Set(2, 1, white) 59 | image3.Set(2, 2, white) 60 | 61 | imageContainer1 := containers.ImageItselfContainer{image1} 62 | imageContainer2 := containers.ImageItselfContainer{image2} 63 | imageContainer3 := containers.ImageItselfContainer{image3} 64 | 65 | mergedImage, _ := operation.Result([]containers.ImageContainer{imageContainer1, imageContainer2, imageContainer3}) 66 | bounds := mergedImage.Bounds().Canon() 67 | for i := bounds.Min.X; i < bounds.Max.X; i++ { 68 | for j := bounds.Min.Y; j < bounds.Max.Y; j++ { 69 | assert.Equal(t, white, mergedImage.At(i, j), "Pixel must be white!") 70 | } 71 | } 72 | } 73 | 74 | func ShouldNotWorkWithAnEmptyArray(t *testing.T) { 75 | operation := LightestOperation{} 76 | image, error := operation.Result([]containers.ImageContainer{}) 77 | assert.Nil(t, image, "Image must be nil") 78 | assert.Nil(t, error, "Error must be nil") 79 | } 80 | 81 | func ShouldWorkWithOneImageOnly(t *testing.T) { 82 | operation := LightestOperation{} 83 | 84 | image1 := image.NewRGBA(image.Rect(0, 0, 3, 3)) 85 | draw.Draw(image1, image1.Bounds(), &image.Uniform{color.RGBA{0, 0, 0, 0}}, image.ZP, draw.Src) 86 | imageContainer1 := containers.ImageItselfContainer{image1} 87 | 88 | image, _ := operation.Result([]containers.ImageContainer{imageContainer1}) 89 | assert.Equal(t, image, image1, "Images must be the same") 90 | } 91 | -------------------------------------------------------------------------------- /operations/mode.go: -------------------------------------------------------------------------------- 1 | package operations 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "image" 7 | "image/color" 8 | "image/draw" 9 | "sort" 10 | 11 | "github.com/esdrasbeleza/blzimg/containers" 12 | ) 13 | 14 | type Channel struct { 15 | Value uint8 16 | Count int 17 | } 18 | 19 | type ByFreq []Channel 20 | 21 | func (c ByFreq) Len() int { 22 | return len(c) 23 | } 24 | 25 | func (c ByFreq) Swap(i, j int) { 26 | c[i], c[j] = c[j], c[i] 27 | } 28 | 29 | func (a ByFreq) Less(i, j int) bool { 30 | return a[i].Count < a[j].Count 31 | } 32 | 33 | type ModeOperation struct{} 34 | 35 | func (m ModeOperation) mode(colors []color.Color) color.Color { 36 | var ( 37 | rMap = make(map[uint32]int) 38 | gMap = make(map[uint32]int) 39 | bMap = make(map[uint32]int) 40 | ) 41 | 42 | for _, currentColor := range colors { 43 | r, g, b, _ := currentColor.RGBA() 44 | rMap[r]++ 45 | gMap[g]++ 46 | bMap[b]++ 47 | } 48 | 49 | var ( 50 | rMode = modeForMap(rMap) 51 | gMode = modeForMap(gMap) 52 | bMode = modeForMap(bMap) 53 | ) 54 | 55 | return color.RGBA{rMode, gMode, bMode, 0} 56 | } 57 | 58 | func modeForMap(cMap map[uint32]int) uint8 { 59 | size := len(cMap) 60 | count := make([]Channel, size) 61 | i := 0 62 | 63 | for k, v := range cMap { 64 | count[i] = Channel{ 65 | Value: uint8(k), 66 | Count: v, 67 | } 68 | i++ 69 | } 70 | 71 | sort.Sort(ByFreq(count)) 72 | modeForMap := count[size-1].Value 73 | 74 | return modeForMap 75 | } 76 | 77 | func (c ModeOperation) Result(images []containers.ImageContainer) (image.Image, error) { 78 | imageCount := len(images) 79 | if imageCount == 0 { 80 | return nil, nil 81 | } else if imageCount == 1 { 82 | return images[0].GetImage(), nil 83 | } 84 | 85 | firstImage := images[0].GetImage() 86 | bounds := firstImage.Bounds() 87 | mode := image.NewRGBA(image.Rect(0, 0, bounds.Dx(), bounds.Dy())) 88 | draw.Draw(mode, bounds, firstImage, bounds.Min, draw.Src) 89 | 90 | pixelTable := NewMemoryDataTable(imageCount, bounds.Dx(), bounds.Dy()) 91 | 92 | // Store data from all images -- Danger: O(n^3) 93 | imgCount := 0 94 | for _, currentImageContainer := range images { 95 | imgCount++ 96 | fmt.Printf("\nReading image %d...\n", imgCount) 97 | 98 | currentImage := currentImageContainer.GetImage() 99 | if currentImage.Bounds() != bounds { 100 | return nil, errors.New("The images have different size!") 101 | } 102 | 103 | for i := currentImage.Bounds().Min.X; i < currentImage.Bounds().Max.X; i++ { 104 | for j := currentImage.Bounds().Min.Y; j < currentImage.Bounds().Max.Y; j++ { 105 | currentColor := currentImage.At(i, j) 106 | pixelTable.StoreData(i, j, currentColor) 107 | } 108 | } 109 | } 110 | 111 | fmt.Println("Generating output image...") 112 | for i := bounds.Min.X; i < bounds.Max.X; i++ { 113 | for j := bounds.Min.Y; j < bounds.Max.Y; j++ { 114 | currentPixelData := pixelTable.GetData(i, j) 115 | result := c.mode(currentPixelData.Colors) 116 | mode.Set(i, j, result) 117 | } 118 | } 119 | 120 | return mode, nil 121 | } 122 | 123 | type PixelData struct { 124 | Colors []color.Color 125 | currentLength uint 126 | } 127 | 128 | type PixelDataTable interface { 129 | StoreData(x, y int, pointColor color.Color) error 130 | GetData(x, y int) *PixelData 131 | } 132 | 133 | type MemoryDataTable struct { 134 | length int 135 | dataTable map[image.Point]*PixelData 136 | } 137 | 138 | func NewMemoryDataTable(imageCount, width, height int) *MemoryDataTable { 139 | table := &MemoryDataTable{length: imageCount} 140 | table.dataTable = make(map[image.Point]*PixelData) 141 | return table 142 | } 143 | 144 | func (ds *MemoryDataTable) StoreData(x, y int, pointColor color.Color) error { 145 | point := image.Pt(x, y) 146 | pixelData := ds.dataTable[point] 147 | 148 | if pixelData == nil { 149 | pixelData = &PixelData{} 150 | pixelData.Colors = make([]color.Color, ds.length) 151 | pixelData.currentLength = 0 152 | 153 | ds.dataTable[point] = pixelData 154 | } 155 | 156 | pixelData.Colors[pixelData.currentLength] = pointColor 157 | pixelData.currentLength++ 158 | 159 | return nil 160 | } 161 | 162 | func (ds *MemoryDataTable) GetData(x, y int) *PixelData { 163 | point := image.Pt(x, y) 164 | return ds.dataTable[point] 165 | } 166 | --------------------------------------------------------------------------------