├── .github
└── workflows
│ └── go.yml
├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
├── README_EN.md
├── go.mod
├── input.png
├── main.go
├── output.png
├── pack.sh
├── smalloutput.png
└── test
└── the witcher3
├── wp1854626-the-witcher-3-wallpapers.jpg
├── wp1854627-the-witcher-3-wallpapers.png
├── wp1854628-the-witcher-3-wallpapers.jpg
├── wp1854629-the-witcher-3-wallpapers.png
├── wp1854635-the-witcher-3-wallpapers.jpg
├── wp1854636-the-witcher-3-wallpapers.jpg
├── wp1854637-the-witcher-3-wallpapers.png
├── wp1854638-the-witcher-3-wallpapers.jpg
├── wp1854640-the-witcher-3-wallpapers.jpg
├── wp1854641-the-witcher-3-wallpapers.jpg
├── wp1854642-the-witcher-3-wallpapers.jpg
├── wp1854643-the-witcher-3-wallpapers.jpg
├── wp1854644-the-witcher-3-wallpapers.jpg
├── wp1854670-the-witcher-3-wallpapers.png
├── wp1854671-the-witcher-3-wallpapers.jpg
├── wp1854672-the-witcher-3-wallpapers.jpg
├── wp1854673-the-witcher-3-wallpapers.jpg
├── wp1854716-the-witcher-3-wallpapers.jpg
├── wp1854723-the-witcher-3-wallpapers.jpg
├── wp1854746-the-witcher-3-wallpapers.jpg
├── wp1854776-the-witcher-3-wallpapers.png
├── wp1854777-the-witcher-3-wallpapers.jpg
├── wp1854784-the-witcher-3-wallpapers.jpg
├── wp1854785-the-witcher-3-wallpapers.jpg
├── wp1854787-the-witcher-3-wallpapers.jpg
├── wp1854844-the-witcher-3-wallpapers.jpg
├── wp1854853-the-witcher-3-wallpapers.jpg
├── wp1854864-the-witcher-3-wallpapers.png
├── wp1854865-the-witcher-3-wallpapers.jpg
├── wp1854866-the-witcher-3-wallpapers.jpg
├── wp1854868-the-witcher-3-wallpapers.jpg
├── wp1854869-the-witcher-3-wallpapers.jpg
├── wp1854872-the-witcher-3-wallpapers.jpg
├── wp1854874-the-witcher-3-wallpapers.jpg
├── wp1854875-the-witcher-3-wallpapers.jpg
├── wp1854876-the-witcher-3-wallpapers.jpg
├── wp1854885-the-witcher-3-wallpapers.jpg
├── wp1854887-the-witcher-3-wallpapers.jpg
├── wp1854896-the-witcher-3-wallpapers.jpg
├── wp1854900-the-witcher-3-wallpapers.png
├── wp1854903-the-witcher-3-wallpapers.jpg
├── wp1854908-the-witcher-3-wallpapers.jpg
├── wp1854910-the-witcher-3-wallpapers.jpg
├── wp1854911-the-witcher-3-wallpapers.jpg
├── wp1854914-the-witcher-3-wallpapers.jpg
├── wp1854915-the-witcher-3-wallpapers.png
├── wp582338-the-witcher-3-wallpapers.jpg
└── wp775029-the-witcher-3-wallpapers.jpg
/.github/workflows/go.yml:
--------------------------------------------------------------------------------
1 | # This workflow will build a golang project
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go
3 |
4 | name: Go
5 |
6 | on:
7 | push:
8 | branches: [ "master" ]
9 | pull_request:
10 | branches: [ "master" ]
11 |
12 | jobs:
13 |
14 | build:
15 | runs-on: ubuntu-latest
16 | steps:
17 | - uses: actions/checkout@v3
18 |
19 | - name: Set up Go
20 | uses: actions/setup-go@v3
21 | with:
22 | go-version: 1.19
23 |
24 | - name: Build
25 | run: |
26 | go mod tidy
27 | go build -v ./...
28 |
29 | - name: Test
30 | run: go test -v ./...
31 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Binaries for programs and plugins
2 | *.exe
3 | *.exe~
4 | *.dll
5 | *.so
6 | *.dylib
7 |
8 | # Test binary, built with `go test -c`
9 | *.test
10 |
11 | # Output of the go coverage tool, specifically when used with LiteIDE
12 | *.out
13 |
14 | # Dependency directories (remove the comment below to include it)
15 | # vendor/
16 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang AS build-env
2 |
3 | RUN GO111MODULE=off go get -u github.com/esrrhs/go-mosaic
4 | RUN GO111MODULE=off go get -u github.com/esrrhs/go-mosaic/...
5 | RUN GO111MODULE=off go install github.com/esrrhs/go-mosaic
6 |
7 | FROM debian
8 | COPY --from=build-env /go/bin/go-mosaic .
9 | WORKDIR ./
10 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 zhao xin
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # go-mosaic
2 |
3 | [
](https://github.com/esrrhs/go-mosaic)
4 | [
](https://github.com/esrrhs/go-mosaic)
5 | [](https://goreportcard.com/report/github.com/esrrhs/go-mosaic)
6 | [
](https://github.com/esrrhs/go-mosaic/releases)
7 | [
](https://github.com/esrrhs/go-mosaic/releases)
8 | [
](https://hub.docker.com/repository/docker/esrrhs/go-mosaic)
9 | [
](https://github.com/esrrhs/go-mosaic/actions)
10 |
11 | go-mosaic是一个制作相片马赛克的工具。相片馬賽克,或稱蒙太奇照片、蒙太奇拼貼,是一種影像處理的藝術技巧,利用這個方式做出來的圖片,近看時是由許多張小照片合在一起的,但遠看時,每張照片透過光影和色彩的微調,組成了一張大圖的基本像素,就叫做相片馬賽克技巧
12 |
13 | [README_EN](./README_EN.md)
14 |
15 | # 特性
16 | * 专为海量图片设计,可支持数百万张图片
17 | * 内建缓存数据库,图片删除、更改自动从缓存剔除
18 | * 多核构建,加载、计算、替换均为并发
19 |
20 | # 使用
21 | * 克隆项目,编译,或者下载[release](https://github.com/esrrhs/go-mosaic/releases)
22 | * 执行命令,等待完成
23 | ```
24 | go-mosaic.exe -src input.png -target output.jpg -lib ./test
25 | ```
26 | * 其中./test为图片文件夹,用来组成最终图片的元素。input.png为目标图片,用来生成最终的大图output.jpg。素材图片越多,生成越精确
27 | * 更多参数,参考help
28 | ```
29 | Usage of D:\project\go-mosaic\test.exe:
30 | -checkhash
31 | check database pic hash (default true)
32 | -database string
33 | cache datbase (default "./database.bin")
34 | -lib string
35 | image lib path
36 | -libname string
37 | image lib name in database (default "default")
38 | -maxsize int
39 | pic max size in GB (default 4)
40 | -pixelsize int
41 | pic scale size per one pixel (default 64)
42 | -scalealg string
43 | pic scale function NearestNeighbor/ApproxBiLinear/BiLinear/CatmullRom (default "CatmullRom")
44 | -src string
45 | src image path
46 | -srcsize int
47 | src image auto scale pixel size (default 128)
48 | -target string
49 | target image path
50 | -worker int
51 | worker thread num (default 12)
52 | ```
53 |
54 | # 示例
55 | 
56 | 
57 |
58 |
59 |
--------------------------------------------------------------------------------
/README_EN.md:
--------------------------------------------------------------------------------
1 | # go-mosaic
2 |
3 | [
](https://github.com/esrrhs/go-mosaic)
4 | [
](https://github.com/esrrhs/go-mosaic)
5 | [](https://goreportcard.com/report/github.com/esrrhs/go-mosaic)
6 | [
](https://github.com/esrrhs/go-mosaic/releases)
7 | [
](https://github.com/esrrhs/go-mosaic/releases)
8 | [
](https://hub.docker.com/repository/docker/esrrhs/go-mosaic)
9 | [
](https://github.com/esrrhs/go-mosaic/actions)
10 |
11 | go-mosaic is a tool for making photo mosaics. Photo mosaics, or montage photos, montage collages, are an art technique of image processing. Pictures made in this way are composed of many small photos when viewed close, but when viewed from a distance, each photo Through the fine-tuning of light and shadow and color, the basic pixels of a large picture are called the photo mosaic technique
12 |
13 | # Features
14 | * Designed for massive pictures, can support millions of pictures
15 | * Built-in cache database, pictures are deleted and changed automatically from cache
16 | * Multi-core construction, loading, calculation and replacement are all concurrent
17 |
18 | # Use
19 | * Clone the project, compile, or download [release](https://github.com/esrrhs/go-mosaic/releases)
20 | * Execute commands and wait for completion
21 | ```
22 | go-mosaic.exe -src input.png -target output.jpg -lib ./test
23 | ```
24 | * Among them, ./test is the picture folder, which is used to form the elements of the final picture. input.png is the target image, which is used to generate the final large image output.jpg. The more material pictures, the more accurate the generation
25 | * For more parameters, refer to help
26 | ```
27 | Usage of D:\project\go-mosaic\test.exe:
28 | -checkhash
29 | check database pic hash (default true)
30 | -database string
31 | cache datbase (default "./database.bin")
32 | -lib string
33 | image lib path
34 | -libname string
35 | image lib name in database (default "default")
36 | -maxsize int
37 | pic max size in GB (default 4)
38 | -pixelsize int
39 | pic scale size per one pixel (default 64)
40 | -scalealg string
41 | pic scale function NearestNeighbor/ApproxBiLinear/BiLinear/CatmullRom (default "CatmullRom")
42 | -src string
43 | src image path
44 | -srcsize int
45 | src image auto scale pixel size (default 128)
46 | -target string
47 | target image path
48 | -worker int
49 | worker thread num (default 12)
50 | ```
51 |
52 | # Example
53 | 
54 | 
55 |
56 |
57 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/esrrhs/go-mosaic
2 |
3 | go 1.19
4 |
5 | require (
6 | github.com/boltdb/bolt v1.3.1
7 | github.com/esrrhs/gohome v0.0.0-20230318054539-1132b09f7684
8 | golang.org/x/image v0.18.0
9 | )
10 |
11 | require (
12 | github.com/OneOfOne/xxhash v1.2.8 // indirect
13 | github.com/google/uuid v1.3.0 // indirect
14 | golang.org/x/sys v0.5.0 // indirect
15 | google.golang.org/protobuf v1.33.0 // indirect
16 | )
17 |
--------------------------------------------------------------------------------
/input.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/input.png
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "encoding/gob"
6 | "errors"
7 | "flag"
8 | "fmt"
9 | "github.com/boltdb/bolt"
10 | "github.com/esrrhs/gohome/common"
11 | "github.com/esrrhs/gohome/loggo"
12 | "github.com/esrrhs/gohome/threadpool"
13 | "golang.org/x/image/draw"
14 | "image"
15 | "image/color"
16 | _ "image/gif"
17 | "image/jpeg"
18 | _ "image/jpeg"
19 | "image/png"
20 | _ "image/png"
21 | "io/ioutil"
22 | "math"
23 | "os"
24 | "path/filepath"
25 | "strconv"
26 | "strings"
27 | "sync"
28 | "sync/atomic"
29 | "time"
30 | )
31 |
32 | func main() {
33 |
34 | defer common.CrashLog()
35 |
36 | src := flag.String("src", "", "src image path")
37 | target := flag.String("target", "", "target image path")
38 | lib := flag.String("lib", "", "image lib path")
39 | worker := flag.Int("worker", 12, "worker thread num")
40 | database := flag.String("database", "./database.bin", "cache datbase")
41 | pixelsize := flag.Int("pixelsize", 64, "pic scale size per one pixel")
42 | scalealg := flag.String("scalealg", "CatmullRom", "pic scale function NearestNeighbor/ApproxBiLinear/BiLinear/CatmullRom")
43 | checkhash := flag.Bool("checkhash", true, "check database pic hash")
44 | maxsize := flag.Int("maxsize", 4, "pic max size in GB")
45 | libname := flag.String("libname", "default", "image lib name in database")
46 | srcsize := flag.Int("srcsize", 128, "src image auto scale pixel size")
47 |
48 | flag.Parse()
49 |
50 | if *src == "" || *target == "" || *lib == "" {
51 | fmt.Println("need src target lib")
52 | flag.Usage()
53 | return
54 | }
55 | if getScaler(*scalealg) == nil {
56 | fmt.Println("scalealg type error")
57 | flag.Usage()
58 | return
59 | }
60 | if !strings.HasSuffix(strings.ToLower(*target), ".png") &&
61 | !strings.HasSuffix(strings.ToLower(*target), ".jpg") {
62 | fmt.Println("target type error, png/jpg")
63 | flag.Usage()
64 | return
65 | }
66 |
67 | level := loggo.LEVEL_INFO
68 | loggo.Ini(loggo.Config{
69 | Level: level,
70 | Prefix: "mosaic",
71 | MaxDay: 3,
72 | })
73 | loggo.Info("start...")
74 |
75 | loggo.Info("src %s", *src)
76 | loggo.Info("target %s", *target)
77 | loggo.Info("lib %s", *lib)
78 |
79 | err, srcimg, cachemap := parse_src(*src, *scalealg, *srcsize)
80 | if err != nil {
81 | return
82 | }
83 | err = load_lib(*lib, *worker, *database, *pixelsize, *scalealg, *checkhash, *libname)
84 | if err != nil {
85 | return
86 | }
87 | err = gen_target(srcimg, *target, *worker, *database, *pixelsize, *maxsize, *scalealg, *libname, cachemap)
88 | if err != nil {
89 | return
90 | }
91 | }
92 |
93 | type CacheInfo struct {
94 | num int
95 | img []image.Image
96 | lock sync.Mutex
97 | }
98 |
99 | func parse_src(src string, scalealg string, srcsize int) (error, image.Image, *sync.Map) {
100 | loggo.Info("parse_src %s", src)
101 |
102 | reader, err := os.Open(src)
103 | if err != nil {
104 | loggo.Error("parse_src Open fail %s %s", src, err)
105 | return err, nil, nil
106 | }
107 | defer reader.Close()
108 |
109 | fi, err := reader.Stat()
110 | if err != nil {
111 | loggo.Error("parse_src Stat fail %s %s", src, err)
112 | return err, nil, nil
113 | }
114 | filesize := fi.Size()
115 |
116 | img, _, err := image.Decode(reader)
117 | if err != nil {
118 | loggo.Error("parse_src Decode image fail %s %s", src, err)
119 | return err, nil, nil
120 | }
121 |
122 | scale := getScaler(scalealg)
123 |
124 | lenx := img.Bounds().Dx()
125 | leny := img.Bounds().Dy()
126 | len := common.MaxOfInt(lenx, leny)
127 | if len > srcsize {
128 | newlenx := lenx * srcsize / len
129 | newleny := leny * srcsize / len
130 | rect := image.Rectangle{image.Point{0, 0}, image.Point{newlenx, newleny}}
131 | dst := image.NewRGBA(rect)
132 | scale.Scale(dst, rect, img, img.Bounds(), draw.Over, nil)
133 | img = dst
134 | }
135 |
136 | bounds := img.Bounds()
137 |
138 | startx := bounds.Min.X
139 | starty := bounds.Min.Y
140 | endx := bounds.Max.X
141 | endy := bounds.Max.Y
142 |
143 | pixelnum := make(map[string]int)
144 | for y := starty; y < endy; y++ {
145 | for x := startx; x < endx; x++ {
146 | r, g, b, _ := img.At(x, y).RGBA()
147 | r, g, b = r>>8, g>>8, b>>8
148 |
149 | pixelnum[make_string(uint8(r), uint8(g), uint8(b))]++
150 | }
151 | }
152 |
153 | var cachemap sync.Map
154 | top := 0
155 | num := 0
156 | for {
157 | maxpixel := ""
158 | maxpixelnum := 0
159 | for k, v := range pixelnum {
160 | if v > maxpixelnum {
161 | maxpixelnum = v
162 | maxpixel = k
163 | }
164 | }
165 | if maxpixelnum >= 16 {
166 | cachemap.Store(maxpixel, &CacheInfo{num: maxpixelnum})
167 | num++
168 | } else {
169 | break
170 | }
171 | if maxpixelnum > top {
172 | top = maxpixelnum
173 | }
174 | pixelnum[maxpixel] = 0
175 | }
176 |
177 | loggo.Info("parse_src cache top pixel num=%d max=%d", num, top)
178 | for i := 2; i <= top; i++ {
179 | cachemap.Range(func(key, value interface{}) bool {
180 | ci := value.(*CacheInfo)
181 | if ci.num == i {
182 | loggo.Info("parse_src cache top pixel [%s]=%d", key, i)
183 | }
184 | return true
185 | })
186 | }
187 |
188 | loggo.Info("parse_src ok %s %d %d*%d", src, filesize, img.Bounds().Dx(), img.Bounds().Dy())
189 | return nil, img, &cachemap
190 | }
191 |
192 | func getScaler(scalealg string) draw.Scaler {
193 | var scale draw.Scaler
194 | if scalealg == "NearestNeighbor" {
195 | scale = draw.NearestNeighbor
196 | } else if scalealg == "ApproxBiLinear" {
197 | scale = draw.ApproxBiLinear
198 | } else if scalealg == "BiLinear" {
199 | scale = draw.BiLinear
200 | } else if scalealg == "CatmullRom" {
201 | scale = draw.CatmullRom
202 | }
203 | return scale
204 | }
205 |
206 | type FileInfo struct {
207 | Filename string
208 | R uint8
209 | G uint8
210 | B uint8
211 | Hash string
212 | }
213 |
214 | type CalFileInfo struct {
215 | fi FileInfo
216 | ok bool
217 | done bool
218 | }
219 |
220 | type ColorData struct {
221 | file int
222 | r uint8
223 | g uint8
224 | b uint8
225 | }
226 |
227 | func load_lib(lib string, workernum int, database string, pixelsize int, scalealg string, checkhash bool, libname string) error {
228 | loggo.Info("load_lib %s", lib)
229 |
230 | loggo.Info("load_lib start ini database")
231 | var colordata []ColorData
232 | for i := 0; i <= 255; i++ {
233 | for j := 0; j <= 255; j++ {
234 | for z := 0; z <= 255; z++ {
235 | colordata = append(colordata, ColorData{})
236 | }
237 | }
238 | }
239 |
240 | for i := 0; i <= 255; i++ {
241 | for j := 0; j <= 255; j++ {
242 | for z := 0; z <= 255; z++ {
243 | k := make_key(uint8(i), uint8(j), uint8(z))
244 | colordata[k].r, colordata[k].g, colordata[k].b = uint8(i), uint8(j), uint8(z)
245 | }
246 | }
247 | }
248 |
249 | loggo.Info("load_lib ini database ok")
250 |
251 | loggo.Info("load_lib start load database")
252 |
253 | db, err := bolt.Open(database, 0600, nil)
254 | if err != nil {
255 | loggo.Error("load_lib Open database fail %s %s", database, err)
256 | return err
257 | }
258 | defer db.Close()
259 |
260 | bucket_name := "FileInfo" + libname + strconv.Itoa(pixelsize)
261 |
262 | dbtotal := 0
263 | db.Update(func(tx *bolt.Tx) error {
264 | _, err := tx.CreateBucketIfNotExists([]byte(bucket_name))
265 | if err != nil {
266 | loggo.Error("load_lib Open database CreateBucketIfNotExists fail %s %s %s", database, bucket_name, err)
267 | os.Exit(1)
268 | }
269 | b := tx.Bucket([]byte(bucket_name))
270 | b.ForEach(func(k, v []byte) error {
271 | dbtotal++
272 | return nil
273 | })
274 | return nil
275 | })
276 |
277 | lastload := time.Now()
278 | beginload := time.Now()
279 | var doneload int32
280 | var loading int32
281 | var doneloadsize int64
282 | var lock sync.Mutex
283 | db.Update(func(tx *bolt.Tx) error {
284 | b := tx.Bucket([]byte(bucket_name))
285 |
286 | need_del := make([]string, 0)
287 |
288 | type LoadFileInfo struct {
289 | k, v []byte
290 | }
291 |
292 | tp := threadpool.NewThreadPool(workernum, 16, func(in interface{}) {
293 | defer atomic.AddInt32(&doneload, 1)
294 | defer atomic.AddInt32(&loading, -1)
295 |
296 | lf := in.(LoadFileInfo)
297 |
298 | var b bytes.Buffer
299 | b.Write(lf.v)
300 |
301 | dec := gob.NewDecoder(&b)
302 | var fi FileInfo
303 | err = dec.Decode(&fi)
304 | if err != nil {
305 | loggo.Error("load_lib Open database Decode fail %s %s %s", database, string(lf.k), err)
306 | lock.Lock()
307 | defer lock.Unlock()
308 | need_del = append(need_del, string(lf.k))
309 | return
310 | }
311 |
312 | osfi, err := os.Stat(fi.Filename)
313 | if err != nil && os.IsNotExist(err) {
314 | loggo.Error("load_lib Open Filename IsNotExist, need delete %s %s %s", database, fi.Filename, err)
315 | lock.Lock()
316 | defer lock.Unlock()
317 | need_del = append(need_del, string(lf.k))
318 | return
319 | }
320 |
321 | defer atomic.AddInt64(&doneloadsize, osfi.Size())
322 |
323 | if checkhash {
324 | reader, err := os.Open(fi.Filename)
325 | if err != nil {
326 | loggo.Error("load_lib Open fail %s %s %s", database, fi.Filename, err)
327 | return
328 | }
329 | defer reader.Close()
330 |
331 | bytes, err := ioutil.ReadAll(reader)
332 | if err != nil {
333 | loggo.Error("load_lib ReadAll fail %s %s %s", database, fi.Filename, err)
334 | return
335 | }
336 |
337 | hashstr := common.GetXXHashString(string(bytes))
338 |
339 | if hashstr != fi.Hash {
340 | loggo.Error("load_lib hash diff need delete %s %s %s %s", database, fi.Filename, hashstr, fi.Hash)
341 | lock.Lock()
342 | defer lock.Unlock()
343 | need_del = append(need_del, string(lf.k))
344 | return
345 | }
346 | }
347 | })
348 |
349 | b.ForEach(func(k, v []byte) error {
350 |
351 | for {
352 | ret := tp.AddJobTimeout(int(common.RandInt()), LoadFileInfo{k, v}, 10)
353 | if ret {
354 | atomic.AddInt32(&loading, 1)
355 | break
356 | }
357 | }
358 |
359 | if time.Now().Sub(lastload) >= time.Second {
360 | lastload = time.Now()
361 | speed := float64(doneload) / float64(int(time.Now().Sub(beginload))/int(time.Second))
362 | left := ""
363 | if speed > 0 {
364 | left = time.Duration(int64(float64(dbtotal-int(doneload))/speed) * int64(time.Second)).String()
365 | }
366 | donesizem := doneloadsize / 1024 / 1024
367 | dataspeed := int(donesizem) / (int(time.Now().Sub(beginload)) / int(time.Second))
368 | loggo.Info("load speed=%.2f/s percent=%d%% time=%s thead=%d progress=%d/%d data=%dM dataspeed=%dM/s", speed, int(doneload)*100/dbtotal, left,
369 | loading, doneload, dbtotal, donesizem, dataspeed)
370 | }
371 |
372 | return nil
373 | })
374 |
375 | for loading != 0 {
376 | time.Sleep(time.Millisecond * 10)
377 | }
378 |
379 | tp.Stop()
380 |
381 | for _, k := range need_del {
382 | b.Delete([]byte(k))
383 | }
384 |
385 | return nil
386 | })
387 |
388 | loggo.Info("load_lib load database ok")
389 |
390 | loggo.Info("load_lib start get image file list")
391 | imagefilelist := make([]CalFileInfo, 0)
392 | cached := 0
393 | filepath.Walk(lib, func(path string, f os.FileInfo, err error) error {
394 |
395 | if f == nil || f.IsDir() {
396 | return nil
397 | }
398 |
399 | if !strings.HasSuffix(strings.ToLower(f.Name()), ".jpeg") &&
400 | !strings.HasSuffix(strings.ToLower(f.Name()), ".jpg") &&
401 | !strings.HasSuffix(strings.ToLower(f.Name()), ".png") &&
402 | !strings.HasSuffix(strings.ToLower(f.Name()), ".gif") {
403 | return nil
404 | }
405 |
406 | abspath, err := filepath.Abs(path)
407 | if err != nil {
408 | loggo.Error("load_lib get Abs fail %s %s %s", database, path, err)
409 | return nil
410 | }
411 |
412 | db.View(func(tx *bolt.Tx) error {
413 | b := tx.Bucket([]byte(bucket_name))
414 | v := b.Get([]byte(abspath))
415 | if v == nil {
416 | imagefilelist = append(imagefilelist, CalFileInfo{fi: FileInfo{abspath, 0, 0, 0, ""}})
417 | } else {
418 | cached++
419 | }
420 | return nil
421 | })
422 |
423 | return nil
424 | })
425 |
426 | loggo.Info("load_lib get image file list ok %d cache %d", len(imagefilelist), cached)
427 |
428 | loggo.Info("load_lib start calc image avg color %d", len(imagefilelist))
429 | var worker int32
430 | begin := time.Now()
431 | last := time.Now()
432 | var done int32
433 | var donesize int64
434 |
435 | atomic.AddInt32(&worker, 1)
436 | var save_inter int
437 | go save_to_database(&worker, &imagefilelist, db, &save_inter, bucket_name)
438 |
439 | scale := getScaler(scalealg)
440 |
441 | tp := threadpool.NewThreadPool(workernum, 16, func(in interface{}) {
442 | i := in.(int)
443 | calc_avg_color(&imagefilelist[i], &worker, &done, &donesize, scale, pixelsize)
444 | })
445 |
446 | i := 0
447 | for worker != 0 {
448 | if i < len(imagefilelist) {
449 | ret := tp.AddJobTimeout(int(common.RandInt()), i, 10)
450 | if ret {
451 | atomic.AddInt32(&worker, 1)
452 | i++
453 | }
454 | } else {
455 | time.Sleep(time.Millisecond * 10)
456 | }
457 | if time.Now().Sub(last) >= time.Second {
458 | last = time.Now()
459 | speed := float64(done) / float64(int(time.Now().Sub(begin))/int(time.Second))
460 | left := ""
461 | if speed > 0 {
462 | left = time.Duration(int64(float64(len(imagefilelist)-int(done))/speed) * int64(time.Second)).String()
463 | }
464 | donesizem := donesize / 1024 / 1024
465 | dataspeed := int(donesizem) / (int(time.Now().Sub(begin)) / int(time.Second))
466 | loggo.Info("calc speed=%.2f/s percent=%d%% time=%s thead=%d progress=%d/%d saved=%d data=%dM dataspeed=%dM/s", speed, int(done)*100/len(imagefilelist),
467 | left, int(worker), int(done), len(imagefilelist), save_inter, donesizem, dataspeed)
468 | }
469 | }
470 | tp.Stop()
471 |
472 | loggo.Info("load_lib calc image avg color ok %d %d", len(imagefilelist), done)
473 |
474 | loggo.Info("load_lib start save image avg color")
475 |
476 | maxcolornum := 0
477 | totalnum := 0
478 | db.View(func(tx *bolt.Tx) error {
479 | b := tx.Bucket([]byte(bucket_name))
480 |
481 | b.ForEach(func(k, v []byte) error {
482 |
483 | var b bytes.Buffer
484 | b.Write(v)
485 |
486 | dec := gob.NewDecoder(&b)
487 | var fi FileInfo
488 | err = dec.Decode(&fi)
489 | if err != nil {
490 | loggo.Error("load_lib Open database Decode fail %s %s %s", database, string(k), err)
491 | return nil
492 | }
493 |
494 | key := make_key(fi.R, fi.G, fi.B)
495 | colordata[key].file++
496 | if colordata[key].file > maxcolornum {
497 | maxcolornum = colordata[key].file
498 | }
499 | totalnum++
500 |
501 | return nil
502 | })
503 |
504 | return nil
505 | })
506 |
507 | loggo.Info("load_lib save image avg color ok total %d max %d", totalnum, maxcolornum)
508 |
509 | if totalnum <= 0 {
510 | loggo.Error("load_lib no pic in lib %s", database)
511 | return errors.New("no pic")
512 | }
513 |
514 | tmpcolornum := make(map[int]int)
515 | tmpcolorone := make(map[int]ColorData)
516 | colorgourp := []struct {
517 | name string
518 | c color.RGBA
519 | num int
520 | }{
521 | {"Black", common.Black, 0},
522 | {"White", common.White, 0},
523 | {"Red", common.Red, 0},
524 | {"Lime", common.Lime, 0},
525 | {"Blue", common.Blue, 0},
526 | {"Yellow", common.Yellow, 0},
527 | {"Cyan", common.Cyan, 0},
528 | {"Magenta", common.Magenta, 0},
529 | {"Silver", common.Silver, 0},
530 | {"Gray", common.Gray, 0},
531 | {"Maroon", common.Maroon, 0},
532 | {"Olive", common.Olive, 0},
533 | {"Green", common.Green, 0},
534 | {"Purple", common.Purple, 0},
535 | {"Teal", common.Teal, 0},
536 | {"Navy", common.Navy, 0},
537 | }
538 |
539 | for _, data := range colordata {
540 | tmpcolornum[data.file]++
541 | tmpcolorone[data.file] = data
542 |
543 | if data.file > 0 {
544 | min := 0
545 | mindistance := math.MaxFloat64
546 | for index, cg := range colorgourp {
547 | diff := common.ColorDistance(color.RGBA{data.r, data.g, data.b, 0}, cg.c)
548 | if diff < mindistance {
549 | min = index
550 | mindistance = diff
551 | }
552 | }
553 |
554 | colorgourp[min].num += data.file
555 | }
556 | }
557 |
558 | for i := 0; i <= maxcolornum; i++ {
559 | str := ""
560 | if tmpcolornum[i] == 1 {
561 | str = make_string(tmpcolorone[i].r, tmpcolorone[i].g, tmpcolorone[i].b)
562 | }
563 | loggo.Info("load_lib avg color num distribution %d = %d %s", i, tmpcolornum[i], str)
564 | }
565 |
566 | maxcolorgroupnum := 0
567 | maxcolorgroupindex := 0
568 | for index, cg := range colorgourp {
569 | loggo.Info("load_lib avg color color distribution %s = %d", cg.name, cg.num)
570 | if cg.num > maxcolorgroupnum {
571 | maxcolorgroupnum = cg.num
572 | maxcolorgroupindex = index
573 | }
574 | }
575 | loggo.Info("load_lib avg color color max %s %d", colorgourp[maxcolorgroupindex].name, colorgourp[maxcolorgroupindex].num)
576 |
577 | return nil
578 | }
579 |
580 | func make_key(r uint8, g uint8, b uint8) int {
581 | return int(r)*256*256 + int(g)*256 + int(b)
582 | }
583 |
584 | func make_string(r uint8, g uint8, b uint8) string {
585 | return "r " + strconv.Itoa(int(r)) + " g " + strconv.Itoa(int(g)) + " b " + strconv.Itoa(int(b))
586 | }
587 |
588 | func calc_img(src image.Image, filename string, scaler draw.Scaler, pixelsize int) (image.Image, error) {
589 |
590 | bounds := src.Bounds()
591 |
592 | len := common.MinOfInt(bounds.Dx(), bounds.Dy())
593 | startx := bounds.Min.X + (bounds.Dx()-len)/2
594 | starty := bounds.Min.Y + (bounds.Dy()-len)/2
595 | endx := common.MinOfInt(startx+len, bounds.Max.X)
596 | endy := common.MinOfInt(starty+len, bounds.Max.Y)
597 |
598 | if startx != bounds.Min.X || starty != bounds.Min.Y || endx != bounds.Max.X || endy != bounds.Max.Y {
599 | dst := image.NewRGBA(image.Rectangle{image.Point{0, 0}, image.Point{len, len}})
600 | draw.Copy(dst, image.Point{0, 0}, src, image.Rectangle{image.Point{startx, starty}, image.Point{endx, endy}}, draw.Over, nil)
601 | src = dst
602 | }
603 |
604 | bounds = src.Bounds()
605 | if bounds.Dx() != bounds.Dy() {
606 | loggo.Error("calc_img cult image fail %s %d %d", filename, bounds.Dx(), bounds.Dy())
607 | return nil, errors.New("bounds error")
608 | }
609 |
610 | len = common.MinOfInt(bounds.Dx(), bounds.Dy())
611 | if len < pixelsize {
612 | loggo.Error("calc_img image too small %s %d %d", filename, len, pixelsize)
613 | return nil, errors.New("too small")
614 | }
615 |
616 | if len > pixelsize {
617 | rect := image.Rectangle{image.Point{0, 0}, image.Point{pixelsize, pixelsize}}
618 | dst := image.NewRGBA(rect)
619 | scaler.Scale(dst, rect, src, src.Bounds(), draw.Over, nil)
620 | src = dst
621 | }
622 |
623 | return src, nil
624 | }
625 |
626 | func calc_avg_color(cfi *CalFileInfo, worker *int32, done *int32, donesize *int64, scaler draw.Scaler, pixelsize int) {
627 | defer common.CrashLog()
628 | defer atomic.AddInt32(worker, -1)
629 | defer atomic.AddInt32(done, 1)
630 | defer func() {
631 | cfi.done = true
632 | }()
633 |
634 | reader, err := os.Open(cfi.fi.Filename)
635 | if err != nil {
636 | loggo.Error("calc_avg_color Open fail %s %s", cfi.fi.Filename, err)
637 | return
638 | }
639 | defer reader.Close()
640 |
641 | fi, err := reader.Stat()
642 | if err != nil {
643 | loggo.Error("calc_avg_color Stat fail %s %s", cfi.fi.Filename, err)
644 | return
645 | }
646 | filesize := fi.Size()
647 | defer atomic.AddInt64(donesize, filesize)
648 |
649 | img, _, err := image.Decode(reader)
650 | if err != nil {
651 | loggo.Error("calc_avg_color Decode image fail %s %s", cfi.fi.Filename, err)
652 | return
653 | }
654 |
655 | img, err = calc_img(img, cfi.fi.Filename, scaler, pixelsize)
656 | if err != nil {
657 | loggo.Error("calc_avg_color calc_img image fail %s %s", cfi.fi.Filename, err)
658 | return
659 | }
660 |
661 | bounds := img.Bounds()
662 |
663 | var sumR, sumG, sumB, count float64
664 |
665 | for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
666 | for x := bounds.Min.X; x < bounds.Max.X; x++ {
667 | r, g, b, _ := img.At(x, y).RGBA()
668 | r, g, b = r>>8, g>>8, b>>8
669 |
670 | sumR += float64(r)
671 | sumG += float64(g)
672 | sumB += float64(b)
673 |
674 | count += 1
675 | }
676 | }
677 |
678 | readerhash, err := os.Open(cfi.fi.Filename)
679 | if err != nil {
680 | loggo.Error("calc_avg_color Open fail %s %s", cfi.fi.Filename, err)
681 | return
682 | }
683 | defer readerhash.Close()
684 |
685 | b, err := ioutil.ReadAll(readerhash)
686 | if err != nil {
687 | loggo.Error("calc_avg_color ReadAll fail %s %d", cfi.fi.Filename, pixelsize)
688 | return
689 | }
690 |
691 | cfi.fi.R = uint8(sumR / count)
692 | cfi.fi.G = uint8(sumG / count)
693 | cfi.fi.B = uint8(sumB / count)
694 | cfi.fi.Hash = common.GetXXHashString(string(b))
695 | cfi.ok = true
696 |
697 | return
698 | }
699 |
700 | func save_to_database(worker *int32, imagefilelist *[]CalFileInfo, db *bolt.DB, save_inter *int, bucket_name string) {
701 | defer common.CrashLog()
702 | defer atomic.AddInt32(worker, -1)
703 |
704 | i := 0
705 | for {
706 | if i >= len(*imagefilelist) {
707 | return
708 | }
709 |
710 | cfi := (*imagefilelist)[i]
711 | if cfi.done {
712 | i++
713 |
714 | if cfi.ok {
715 | var b bytes.Buffer
716 |
717 | enc := gob.NewEncoder(&b)
718 | err := enc.Encode(&cfi.fi)
719 | if err != nil {
720 | loggo.Error("calc_avg_color Encode FileInfo fail %s %s", cfi.fi.Filename, err)
721 | return
722 | }
723 |
724 | k := []byte(cfi.fi.Filename)
725 | v := b.Bytes()
726 |
727 | db.Update(func(tx *bolt.Tx) error {
728 | b := tx.Bucket([]byte(bucket_name))
729 | err := b.Put(k, v)
730 | return err
731 | })
732 | }
733 |
734 | *save_inter = i
735 | } else {
736 | time.Sleep(time.Millisecond * 10)
737 | }
738 | }
739 | }
740 |
741 | func gen_target(srcimg image.Image, target string, workernum int, database string, pixelsize int, maxsize int, scalealg string, libname string, cachemap *sync.Map) error {
742 | loggo.Info("gen_target %s", target)
743 |
744 | db, err := bolt.Open(database, 0600, nil)
745 | if err != nil {
746 | loggo.Error("gen_target Open database fail %s %s", database, err)
747 | return err
748 | }
749 | defer db.Close()
750 |
751 | bucket_name := "FileInfo" + libname + strconv.Itoa(pixelsize)
752 |
753 | bounds := srcimg.Bounds()
754 |
755 | startx := bounds.Min.X
756 | starty := bounds.Min.Y
757 | endx := bounds.Max.X
758 | endy := bounds.Max.Y
759 |
760 | last := time.Now()
761 | begin := time.Now()
762 | total := bounds.Dx() * bounds.Dy()
763 | var done int32
764 | var doing int32
765 | var cached int32
766 |
767 | lenx := bounds.Dx() * pixelsize
768 | leny := bounds.Dy() * pixelsize
769 |
770 | outputfilesize := lenx * leny * 4 / 1024 / 1024 / 1024
771 | if outputfilesize > maxsize {
772 | loggo.Error("gen_target too big %s %dG than %dG", target, outputfilesize, maxsize)
773 | return errors.New("too big")
774 | }
775 |
776 | loggo.Info("gen_target start gen pixel %s %dG max %dG", target, outputfilesize, maxsize)
777 |
778 | dst := image.NewRGBA(image.Rectangle{image.Point{0, 0}, image.Point{lenx, leny}})
779 |
780 | type GenInfo struct {
781 | x int
782 | y int
783 | c color.RGBA
784 | }
785 |
786 | tp := threadpool.NewThreadPool(workernum, 16, func(in interface{}) {
787 | defer atomic.AddInt32(&done, 1)
788 | defer atomic.AddInt32(&doing, -1)
789 | gi := in.(GenInfo)
790 | gen_target_pixel(gi.c, gi.x, gi.y, dst, db, bucket_name, pixelsize, scalealg, cachemap, &cached)
791 | })
792 |
793 | for y := starty; y < endy; y++ {
794 | for x := startx; x < endx; x++ {
795 | r, g, b, _ := srcimg.At(x, y).RGBA()
796 | r, g, b = r>>8, g>>8, b>>8
797 |
798 | for {
799 | ret := tp.AddJobTimeout(int(common.RandInt()), GenInfo{x: x, y: y, c: color.RGBA{uint8(r), uint8(g), uint8(b), 0}}, 10)
800 | if ret {
801 | atomic.AddInt32(&doing, 1)
802 | break
803 | }
804 | }
805 |
806 | if time.Now().Sub(last) >= time.Second {
807 | last = time.Now()
808 | speed := float64(done) / float64(int(time.Now().Sub(begin))/int(time.Second))
809 | left := ""
810 | if speed > 0 {
811 | left = time.Duration(int64(float64(total-int(done))/speed) * int64(time.Second)).String()
812 | }
813 | loggo.Info("gen speed=%.2f/s percent=%d%% time=%s thead=%d progress=%d/%d cached=%d cached-percent=%d%%", speed, int(done)*100/total,
814 | left, int(doing), int(done), total, cached, int(cached)*100/total)
815 | }
816 | }
817 | }
818 |
819 | for doing != 0 {
820 | time.Sleep(time.Millisecond * 10)
821 | }
822 |
823 | tp.Stop()
824 |
825 | loggo.Info("gen_target gen pixel ok %s", target)
826 |
827 | loggo.Info("gen_target start write file %s", target)
828 | dstFile, err := os.Create(target)
829 | if err != nil {
830 | loggo.Error("gen_target Create fail %s %s", target, err)
831 | return err
832 | }
833 | defer dstFile.Close()
834 |
835 | if strings.HasSuffix(strings.ToLower(target), ".png") {
836 | err = png.Encode(dstFile, dst)
837 | } else if strings.HasSuffix(strings.ToLower(target), ".jpg") {
838 | err = jpeg.Encode(dstFile, dst, &jpeg.Options{Quality: 100})
839 | }
840 | if err != nil {
841 | loggo.Error("gen_target Encode fail %s %s", target, err)
842 | return err
843 | }
844 |
845 | loggo.Info("gen_target write file ok %s", target)
846 |
847 | return nil
848 | }
849 |
850 | func gen_target_pixel(src color.RGBA, x int, y int, dst *image.RGBA, db *bolt.DB, bucket_name string, pixelsize int, scalealg string, cachemap *sync.Map, cached *int32) {
851 |
852 | var minimgs []image.Image
853 |
854 | key := make_string(src.R, src.G, src.B)
855 | v, ok := cachemap.Load(key)
856 | if ok {
857 | ci := v.(*CacheInfo)
858 | minimgs = ci.img
859 | }
860 |
861 | if len(minimgs) <= 0 {
862 | if ok {
863 | ci := v.(*CacheInfo)
864 | ci.lock.Lock()
865 | }
866 |
867 | if len(minimgs) <= 0 {
868 |
869 | mindiff := math.MaxFloat64
870 | var mindiffnames []string
871 | var minfi FileInfo
872 |
873 | db.View(func(tx *bolt.Tx) error {
874 | b := tx.Bucket([]byte(bucket_name))
875 | b.ForEach(func(k, v []byte) error {
876 |
877 | var b bytes.Buffer
878 | b.Write(v)
879 |
880 | dec := gob.NewDecoder(&b)
881 | var fi FileInfo
882 | err := dec.Decode(&fi)
883 | if err != nil {
884 | loggo.Error("gen_target_pixel database Decode fail %s %s", string(k), err)
885 | os.Exit(1)
886 | }
887 |
888 | if minfi.R == fi.R && minfi.G == fi.G && minfi.B == fi.B {
889 | mindiffnames = append(mindiffnames, fi.Filename)
890 | return nil
891 | }
892 |
893 | tmp := color.RGBA{fi.R, fi.G, fi.B, 0}
894 | diff := common.ColorDistance(src, tmp)
895 | if diff < mindiff {
896 | mindiff = diff
897 | mindiffnames = mindiffnames[:0]
898 | mindiffnames = append(mindiffnames, fi.Filename)
899 | minfi = fi
900 | }
901 |
902 | return nil
903 | })
904 | return nil
905 | })
906 |
907 | for _, mindiffname := range mindiffnames {
908 | reader, err := os.Open(mindiffname)
909 | if err != nil {
910 | loggo.Error("gen_target_pixel Open fail %s %s", mindiffname, err)
911 | os.Exit(1)
912 | }
913 | defer reader.Close()
914 |
915 | minimg, _, err := image.Decode(reader)
916 | if err != nil {
917 | loggo.Error("gen_target_pixel Decode fail %s %s", mindiffname, err)
918 | return
919 | }
920 |
921 | scale := getScaler(scalealg)
922 |
923 | minimg, err = calc_img(minimg, mindiffname, scale, pixelsize)
924 | if err != nil {
925 | loggo.Error("gen_target_pixel calc_img image fail %s %s", mindiffname, err)
926 | return
927 | }
928 |
929 | minimgs = append(minimgs, minimg)
930 | }
931 |
932 | v, ok := cachemap.Load(key)
933 | if ok {
934 | ci := v.(*CacheInfo)
935 | ci.img = minimgs
936 | }
937 | } else {
938 | atomic.AddInt32(cached, 1)
939 | }
940 |
941 | if ok {
942 | ci := v.(*CacheInfo)
943 | ci.lock.Unlock()
944 | }
945 | } else {
946 | atomic.AddInt32(cached, 1)
947 | }
948 |
949 | var minimg image.Image
950 | minimg = minimgs[common.RandInt31n(len(minimgs))]
951 |
952 | if common.RandInt()%2 == 0 {
953 | flippedImg := image.NewRGBA(minimg.Bounds())
954 | for j := 0; j < minimg.Bounds().Dy(); j++ {
955 | for i := 0; i < minimg.Bounds().Dx(); i++ {
956 | flippedImg.Set((minimg.Bounds().Dx()-1)-i, j, minimg.At(i, j))
957 | }
958 | }
959 | minimg = flippedImg
960 | }
961 |
962 | draw.Copy(dst, image.Point{x * pixelsize, y * pixelsize}, minimg, minimg.Bounds(), draw.Over, nil)
963 | }
964 |
--------------------------------------------------------------------------------
/output.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/output.png
--------------------------------------------------------------------------------
/pack.sh:
--------------------------------------------------------------------------------
1 | #! /bin/bash
2 | #set -x
3 | NAME="go-mosaic"
4 |
5 | export GO111MODULE=on
6 |
7 | #go tool dist list
8 | build_list=$(go tool dist list)
9 |
10 | rm pack -rf
11 | rm pack.zip -f
12 | mkdir pack
13 |
14 | for line in $build_list; do
15 | os=$(echo "$line" | awk -F"/" '{print $1}')
16 | arch=$(echo "$line" | awk -F"/" '{print $2}')
17 | echo "os="$os" arch="$arch" start build"
18 | if [ $os == "android" ] || [ $os == "ios" ] || [ $os == "plan9" ] || [ $arch == "ppc64" ] || [ $arch == "wasm" ] || [ $arch == "mips" ] ||
19 | [ $arch == "mips64" ] || [ $arch == "mips64le" ] || [ $arch == "mipsle" ] || [ $arch == "riscv64" ]; then
20 | continue
21 | fi
22 | CGO_ENABLED=0 GOOS=$os GOARCH=$arch go build -ldflags="-s -w"
23 | if [ $? -ne 0 ]; then
24 | echo "os="$os" arch="$arch" build fail"
25 | exit 1
26 | fi
27 | if [ $os = "windows" ]; then
28 | zip ${NAME}_"${os}"_"${arch}"".zip" $NAME".exe"
29 | if [ $? -ne 0 ]; then
30 | echo "os="$os" arch="$arch" zip fail"
31 | exit 1
32 | fi
33 | mv ${NAME}_"${os}"_"${arch}"".zip" pack/
34 | rm $NAME".exe" -f
35 | else
36 | zip ${NAME}_"${os}"_"${arch}"".zip" $NAME
37 | if [ $? -ne 0 ]; then
38 | echo "os="$os" arch="$arch" zip fail"
39 | exit 1
40 | fi
41 | mv ${NAME}_"${os}"_"${arch}"".zip" pack/
42 | rm $NAME -f
43 | fi
44 | echo "os="$os" arch="$arch" done build"
45 | done
46 |
47 | zip pack.zip pack/ -r
48 |
49 | echo "all done"
50 |
--------------------------------------------------------------------------------
/smalloutput.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/smalloutput.png
--------------------------------------------------------------------------------
/test/the witcher3/wp1854626-the-witcher-3-wallpapers.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854626-the-witcher-3-wallpapers.jpg
--------------------------------------------------------------------------------
/test/the witcher3/wp1854627-the-witcher-3-wallpapers.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854627-the-witcher-3-wallpapers.png
--------------------------------------------------------------------------------
/test/the witcher3/wp1854628-the-witcher-3-wallpapers.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854628-the-witcher-3-wallpapers.jpg
--------------------------------------------------------------------------------
/test/the witcher3/wp1854629-the-witcher-3-wallpapers.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854629-the-witcher-3-wallpapers.png
--------------------------------------------------------------------------------
/test/the witcher3/wp1854635-the-witcher-3-wallpapers.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854635-the-witcher-3-wallpapers.jpg
--------------------------------------------------------------------------------
/test/the witcher3/wp1854636-the-witcher-3-wallpapers.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854636-the-witcher-3-wallpapers.jpg
--------------------------------------------------------------------------------
/test/the witcher3/wp1854637-the-witcher-3-wallpapers.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854637-the-witcher-3-wallpapers.png
--------------------------------------------------------------------------------
/test/the witcher3/wp1854638-the-witcher-3-wallpapers.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854638-the-witcher-3-wallpapers.jpg
--------------------------------------------------------------------------------
/test/the witcher3/wp1854640-the-witcher-3-wallpapers.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854640-the-witcher-3-wallpapers.jpg
--------------------------------------------------------------------------------
/test/the witcher3/wp1854641-the-witcher-3-wallpapers.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854641-the-witcher-3-wallpapers.jpg
--------------------------------------------------------------------------------
/test/the witcher3/wp1854642-the-witcher-3-wallpapers.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854642-the-witcher-3-wallpapers.jpg
--------------------------------------------------------------------------------
/test/the witcher3/wp1854643-the-witcher-3-wallpapers.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854643-the-witcher-3-wallpapers.jpg
--------------------------------------------------------------------------------
/test/the witcher3/wp1854644-the-witcher-3-wallpapers.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854644-the-witcher-3-wallpapers.jpg
--------------------------------------------------------------------------------
/test/the witcher3/wp1854670-the-witcher-3-wallpapers.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854670-the-witcher-3-wallpapers.png
--------------------------------------------------------------------------------
/test/the witcher3/wp1854671-the-witcher-3-wallpapers.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854671-the-witcher-3-wallpapers.jpg
--------------------------------------------------------------------------------
/test/the witcher3/wp1854672-the-witcher-3-wallpapers.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854672-the-witcher-3-wallpapers.jpg
--------------------------------------------------------------------------------
/test/the witcher3/wp1854673-the-witcher-3-wallpapers.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854673-the-witcher-3-wallpapers.jpg
--------------------------------------------------------------------------------
/test/the witcher3/wp1854716-the-witcher-3-wallpapers.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854716-the-witcher-3-wallpapers.jpg
--------------------------------------------------------------------------------
/test/the witcher3/wp1854723-the-witcher-3-wallpapers.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854723-the-witcher-3-wallpapers.jpg
--------------------------------------------------------------------------------
/test/the witcher3/wp1854746-the-witcher-3-wallpapers.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854746-the-witcher-3-wallpapers.jpg
--------------------------------------------------------------------------------
/test/the witcher3/wp1854776-the-witcher-3-wallpapers.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854776-the-witcher-3-wallpapers.png
--------------------------------------------------------------------------------
/test/the witcher3/wp1854777-the-witcher-3-wallpapers.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854777-the-witcher-3-wallpapers.jpg
--------------------------------------------------------------------------------
/test/the witcher3/wp1854784-the-witcher-3-wallpapers.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854784-the-witcher-3-wallpapers.jpg
--------------------------------------------------------------------------------
/test/the witcher3/wp1854785-the-witcher-3-wallpapers.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854785-the-witcher-3-wallpapers.jpg
--------------------------------------------------------------------------------
/test/the witcher3/wp1854787-the-witcher-3-wallpapers.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854787-the-witcher-3-wallpapers.jpg
--------------------------------------------------------------------------------
/test/the witcher3/wp1854844-the-witcher-3-wallpapers.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854844-the-witcher-3-wallpapers.jpg
--------------------------------------------------------------------------------
/test/the witcher3/wp1854853-the-witcher-3-wallpapers.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854853-the-witcher-3-wallpapers.jpg
--------------------------------------------------------------------------------
/test/the witcher3/wp1854864-the-witcher-3-wallpapers.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854864-the-witcher-3-wallpapers.png
--------------------------------------------------------------------------------
/test/the witcher3/wp1854865-the-witcher-3-wallpapers.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854865-the-witcher-3-wallpapers.jpg
--------------------------------------------------------------------------------
/test/the witcher3/wp1854866-the-witcher-3-wallpapers.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854866-the-witcher-3-wallpapers.jpg
--------------------------------------------------------------------------------
/test/the witcher3/wp1854868-the-witcher-3-wallpapers.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854868-the-witcher-3-wallpapers.jpg
--------------------------------------------------------------------------------
/test/the witcher3/wp1854869-the-witcher-3-wallpapers.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854869-the-witcher-3-wallpapers.jpg
--------------------------------------------------------------------------------
/test/the witcher3/wp1854872-the-witcher-3-wallpapers.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854872-the-witcher-3-wallpapers.jpg
--------------------------------------------------------------------------------
/test/the witcher3/wp1854874-the-witcher-3-wallpapers.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854874-the-witcher-3-wallpapers.jpg
--------------------------------------------------------------------------------
/test/the witcher3/wp1854875-the-witcher-3-wallpapers.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854875-the-witcher-3-wallpapers.jpg
--------------------------------------------------------------------------------
/test/the witcher3/wp1854876-the-witcher-3-wallpapers.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854876-the-witcher-3-wallpapers.jpg
--------------------------------------------------------------------------------
/test/the witcher3/wp1854885-the-witcher-3-wallpapers.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854885-the-witcher-3-wallpapers.jpg
--------------------------------------------------------------------------------
/test/the witcher3/wp1854887-the-witcher-3-wallpapers.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854887-the-witcher-3-wallpapers.jpg
--------------------------------------------------------------------------------
/test/the witcher3/wp1854896-the-witcher-3-wallpapers.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854896-the-witcher-3-wallpapers.jpg
--------------------------------------------------------------------------------
/test/the witcher3/wp1854900-the-witcher-3-wallpapers.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854900-the-witcher-3-wallpapers.png
--------------------------------------------------------------------------------
/test/the witcher3/wp1854903-the-witcher-3-wallpapers.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854903-the-witcher-3-wallpapers.jpg
--------------------------------------------------------------------------------
/test/the witcher3/wp1854908-the-witcher-3-wallpapers.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854908-the-witcher-3-wallpapers.jpg
--------------------------------------------------------------------------------
/test/the witcher3/wp1854910-the-witcher-3-wallpapers.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854910-the-witcher-3-wallpapers.jpg
--------------------------------------------------------------------------------
/test/the witcher3/wp1854911-the-witcher-3-wallpapers.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854911-the-witcher-3-wallpapers.jpg
--------------------------------------------------------------------------------
/test/the witcher3/wp1854914-the-witcher-3-wallpapers.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854914-the-witcher-3-wallpapers.jpg
--------------------------------------------------------------------------------
/test/the witcher3/wp1854915-the-witcher-3-wallpapers.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854915-the-witcher-3-wallpapers.png
--------------------------------------------------------------------------------
/test/the witcher3/wp582338-the-witcher-3-wallpapers.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp582338-the-witcher-3-wallpapers.jpg
--------------------------------------------------------------------------------
/test/the witcher3/wp775029-the-witcher-3-wallpapers.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp775029-the-witcher-3-wallpapers.jpg
--------------------------------------------------------------------------------