resize.go raw
1 package imaging
2
3 import (
4 "image"
5 "math"
6 )
7
8 type indexWeight struct {
9 index int
10 weight float64
11 }
12
13 func precomputeWeights(dstSize, srcSize int, filter ResampleFilter) [][]indexWeight {
14 du := float64(srcSize) / float64(dstSize)
15 scale := du
16 if scale < 1.0 {
17 scale = 1.0
18 }
19 ru := math.Ceil(scale * filter.Support)
20
21 out := make([][]indexWeight, dstSize)
22 tmp := make([]indexWeight, 0, dstSize*int(ru+2)*2)
23
24 for v := 0; v < dstSize; v++ {
25 fu := (float64(v)+0.5)*du - 0.5
26
27 begin := int(math.Ceil(fu - ru))
28 if begin < 0 {
29 begin = 0
30 }
31 end := int(math.Floor(fu + ru))
32 if end > srcSize-1 {
33 end = srcSize - 1
34 }
35
36 var sum float64
37 for u := begin; u <= end; u++ {
38 w := filter.Kernel((float64(u) - fu) / scale)
39 if w != 0 {
40 sum += w
41 tmp = append(tmp, indexWeight{index: u, weight: w})
42 }
43 }
44 if sum != 0 {
45 for i := range tmp {
46 tmp[i].weight /= sum
47 }
48 }
49
50 out[v] = tmp
51 tmp = tmp[len(tmp):]
52 }
53
54 return out
55 }
56
57 // Resize resizes the image to the specified width and height using the specified resampling
58 // filter and returns the transformed image. If one of width or height is 0, the image aspect
59 // ratio is preserved.
60 //
61 // Example:
62 //
63 // dstImage := imaging.Resize(srcImage, 800, 600, imaging.Lanczos)
64 //
65 func Resize(img image.Image, width, height int, filter ResampleFilter) *image.NRGBA {
66 dstW, dstH := width, height
67 if dstW < 0 || dstH < 0 {
68 return &image.NRGBA{}
69 }
70 if dstW == 0 && dstH == 0 {
71 return &image.NRGBA{}
72 }
73
74 srcW := img.Bounds().Dx()
75 srcH := img.Bounds().Dy()
76 if srcW <= 0 || srcH <= 0 {
77 return &image.NRGBA{}
78 }
79
80 // If new width or height is 0 then preserve aspect ratio, minimum 1px.
81 if dstW == 0 {
82 tmpW := float64(dstH) * float64(srcW) / float64(srcH)
83 dstW = int(math.Max(1.0, math.Floor(tmpW+0.5)))
84 }
85 if dstH == 0 {
86 tmpH := float64(dstW) * float64(srcH) / float64(srcW)
87 dstH = int(math.Max(1.0, math.Floor(tmpH+0.5)))
88 }
89
90 if filter.Support <= 0 {
91 // Nearest-neighbor special case.
92 return resizeNearest(img, dstW, dstH)
93 }
94
95 if srcW != dstW && srcH != dstH {
96 return resizeVertical(resizeHorizontal(img, dstW, filter), dstH, filter)
97 }
98 if srcW != dstW {
99 return resizeHorizontal(img, dstW, filter)
100 }
101 if srcH != dstH {
102 return resizeVertical(img, dstH, filter)
103 }
104 return Clone(img)
105 }
106
107 func resizeHorizontal(img image.Image, width int, filter ResampleFilter) *image.NRGBA {
108 src := newScanner(img)
109 dst := image.NewNRGBA(image.Rect(0, 0, width, src.h))
110 weights := precomputeWeights(width, src.w, filter)
111 parallel(0, src.h, func(ys <-chan int) {
112 scanLine := make([]uint8, src.w*4)
113 for y := range ys {
114 src.scan(0, y, src.w, y+1, scanLine)
115 j0 := y * dst.Stride
116 for x := range weights {
117 var r, g, b, a float64
118 for _, w := range weights[x] {
119 i := w.index * 4
120 s := scanLine[i : i+4 : i+4]
121 aw := float64(s[3]) * w.weight
122 r += float64(s[0]) * aw
123 g += float64(s[1]) * aw
124 b += float64(s[2]) * aw
125 a += aw
126 }
127 if a != 0 {
128 aInv := 1 / a
129 j := j0 + x*4
130 d := dst.Pix[j : j+4 : j+4]
131 d[0] = clamp(r * aInv)
132 d[1] = clamp(g * aInv)
133 d[2] = clamp(b * aInv)
134 d[3] = clamp(a)
135 }
136 }
137 }
138 })
139 return dst
140 }
141
142 func resizeVertical(img image.Image, height int, filter ResampleFilter) *image.NRGBA {
143 src := newScanner(img)
144 dst := image.NewNRGBA(image.Rect(0, 0, src.w, height))
145 weights := precomputeWeights(height, src.h, filter)
146 parallel(0, src.w, func(xs <-chan int) {
147 scanLine := make([]uint8, src.h*4)
148 for x := range xs {
149 src.scan(x, 0, x+1, src.h, scanLine)
150 for y := range weights {
151 var r, g, b, a float64
152 for _, w := range weights[y] {
153 i := w.index * 4
154 s := scanLine[i : i+4 : i+4]
155 aw := float64(s[3]) * w.weight
156 r += float64(s[0]) * aw
157 g += float64(s[1]) * aw
158 b += float64(s[2]) * aw
159 a += aw
160 }
161 if a != 0 {
162 aInv := 1 / a
163 j := y*dst.Stride + x*4
164 d := dst.Pix[j : j+4 : j+4]
165 d[0] = clamp(r * aInv)
166 d[1] = clamp(g * aInv)
167 d[2] = clamp(b * aInv)
168 d[3] = clamp(a)
169 }
170 }
171 }
172 })
173 return dst
174 }
175
176 // resizeNearest is a fast nearest-neighbor resize, no filtering.
177 func resizeNearest(img image.Image, width, height int) *image.NRGBA {
178 dst := image.NewNRGBA(image.Rect(0, 0, width, height))
179 dx := float64(img.Bounds().Dx()) / float64(width)
180 dy := float64(img.Bounds().Dy()) / float64(height)
181
182 if dx > 1 && dy > 1 {
183 src := newScanner(img)
184 parallel(0, height, func(ys <-chan int) {
185 for y := range ys {
186 srcY := int((float64(y) + 0.5) * dy)
187 dstOff := y * dst.Stride
188 for x := 0; x < width; x++ {
189 srcX := int((float64(x) + 0.5) * dx)
190 src.scan(srcX, srcY, srcX+1, srcY+1, dst.Pix[dstOff:dstOff+4])
191 dstOff += 4
192 }
193 }
194 })
195 } else {
196 src := toNRGBA(img)
197 parallel(0, height, func(ys <-chan int) {
198 for y := range ys {
199 srcY := int((float64(y) + 0.5) * dy)
200 srcOff0 := srcY * src.Stride
201 dstOff := y * dst.Stride
202 for x := 0; x < width; x++ {
203 srcX := int((float64(x) + 0.5) * dx)
204 srcOff := srcOff0 + srcX*4
205 copy(dst.Pix[dstOff:dstOff+4], src.Pix[srcOff:srcOff+4])
206 dstOff += 4
207 }
208 }
209 })
210 }
211
212 return dst
213 }
214
215 // Fit scales down the image using the specified resample filter to fit the specified
216 // maximum width and height and returns the transformed image.
217 //
218 // Example:
219 //
220 // dstImage := imaging.Fit(srcImage, 800, 600, imaging.Lanczos)
221 //
222 func Fit(img image.Image, width, height int, filter ResampleFilter) *image.NRGBA {
223 maxW, maxH := width, height
224
225 if maxW <= 0 || maxH <= 0 {
226 return &image.NRGBA{}
227 }
228
229 srcBounds := img.Bounds()
230 srcW := srcBounds.Dx()
231 srcH := srcBounds.Dy()
232
233 if srcW <= 0 || srcH <= 0 {
234 return &image.NRGBA{}
235 }
236
237 if srcW <= maxW && srcH <= maxH {
238 return Clone(img)
239 }
240
241 srcAspectRatio := float64(srcW) / float64(srcH)
242 maxAspectRatio := float64(maxW) / float64(maxH)
243
244 var newW, newH int
245 if srcAspectRatio > maxAspectRatio {
246 newW = maxW
247 newH = int(float64(newW) / srcAspectRatio)
248 } else {
249 newH = maxH
250 newW = int(float64(newH) * srcAspectRatio)
251 }
252
253 return Resize(img, newW, newH, filter)
254 }
255
256 // Fill creates an image with the specified dimensions and fills it with the scaled source image.
257 // To achieve the correct aspect ratio without stretching, the source image will be cropped.
258 //
259 // Example:
260 //
261 // dstImage := imaging.Fill(srcImage, 800, 600, imaging.Center, imaging.Lanczos)
262 //
263 func Fill(img image.Image, width, height int, anchor Anchor, filter ResampleFilter) *image.NRGBA {
264 dstW, dstH := width, height
265
266 if dstW <= 0 || dstH <= 0 {
267 return &image.NRGBA{}
268 }
269
270 srcBounds := img.Bounds()
271 srcW := srcBounds.Dx()
272 srcH := srcBounds.Dy()
273
274 if srcW <= 0 || srcH <= 0 {
275 return &image.NRGBA{}
276 }
277
278 if srcW == dstW && srcH == dstH {
279 return Clone(img)
280 }
281
282 if srcW >= 100 && srcH >= 100 {
283 return cropAndResize(img, dstW, dstH, anchor, filter)
284 }
285 return resizeAndCrop(img, dstW, dstH, anchor, filter)
286 }
287
288 // cropAndResize crops the image to the smallest possible size that has the required aspect ratio using
289 // the given anchor point, then scales it to the specified dimensions and returns the transformed image.
290 //
291 // This is generally faster than resizing first, but may result in inaccuracies when used on small source images.
292 func cropAndResize(img image.Image, width, height int, anchor Anchor, filter ResampleFilter) *image.NRGBA {
293 dstW, dstH := width, height
294
295 srcBounds := img.Bounds()
296 srcW := srcBounds.Dx()
297 srcH := srcBounds.Dy()
298 srcAspectRatio := float64(srcW) / float64(srcH)
299 dstAspectRatio := float64(dstW) / float64(dstH)
300
301 var tmp *image.NRGBA
302 if srcAspectRatio < dstAspectRatio {
303 cropH := float64(srcW) * float64(dstH) / float64(dstW)
304 tmp = CropAnchor(img, srcW, int(math.Max(1, cropH)+0.5), anchor)
305 } else {
306 cropW := float64(srcH) * float64(dstW) / float64(dstH)
307 tmp = CropAnchor(img, int(math.Max(1, cropW)+0.5), srcH, anchor)
308 }
309
310 return Resize(tmp, dstW, dstH, filter)
311 }
312
313 // resizeAndCrop resizes the image to the smallest possible size that will cover the specified dimensions,
314 // crops the resized image to the specified dimensions using the given anchor point and returns
315 // the transformed image.
316 func resizeAndCrop(img image.Image, width, height int, anchor Anchor, filter ResampleFilter) *image.NRGBA {
317 dstW, dstH := width, height
318
319 srcBounds := img.Bounds()
320 srcW := srcBounds.Dx()
321 srcH := srcBounds.Dy()
322 srcAspectRatio := float64(srcW) / float64(srcH)
323 dstAspectRatio := float64(dstW) / float64(dstH)
324
325 var tmp *image.NRGBA
326 if srcAspectRatio < dstAspectRatio {
327 tmp = Resize(img, dstW, 0, filter)
328 } else {
329 tmp = Resize(img, 0, dstH, filter)
330 }
331
332 return CropAnchor(tmp, dstW, dstH, anchor)
333 }
334
335 // Thumbnail scales the image up or down using the specified resample filter, crops it
336 // to the specified width and hight and returns the transformed image.
337 //
338 // Example:
339 //
340 // dstImage := imaging.Thumbnail(srcImage, 100, 100, imaging.Lanczos)
341 //
342 func Thumbnail(img image.Image, width, height int, filter ResampleFilter) *image.NRGBA {
343 return Fill(img, width, height, Center, filter)
344 }
345
346 // ResampleFilter specifies a resampling filter to be used for image resizing.
347 //
348 // General filter recommendations:
349 //
350 // - Lanczos
351 // A high-quality resampling filter for photographic images yielding sharp results.
352 //
353 // - CatmullRom
354 // A sharp cubic filter that is faster than Lanczos filter while providing similar results.
355 //
356 // - MitchellNetravali
357 // A cubic filter that produces smoother results with less ringing artifacts than CatmullRom.
358 //
359 // - Linear
360 // Bilinear resampling filter, produces a smooth output. Faster than cubic filters.
361 //
362 // - Box
363 // Simple and fast averaging filter appropriate for downscaling.
364 // When upscaling it's similar to NearestNeighbor.
365 //
366 // - NearestNeighbor
367 // Fastest resampling filter, no antialiasing.
368 //
369 type ResampleFilter struct {
370 Support float64
371 Kernel func(float64) float64
372 }
373
374 // NearestNeighbor is a nearest-neighbor filter (no anti-aliasing).
375 var NearestNeighbor ResampleFilter
376
377 // Box filter (averaging pixels).
378 var Box ResampleFilter
379
380 // Linear filter.
381 var Linear ResampleFilter
382
383 // Hermite cubic spline filter (BC-spline; B=0; C=0).
384 var Hermite ResampleFilter
385
386 // MitchellNetravali is Mitchell-Netravali cubic filter (BC-spline; B=1/3; C=1/3).
387 var MitchellNetravali ResampleFilter
388
389 // CatmullRom is a Catmull-Rom - sharp cubic filter (BC-spline; B=0; C=0.5).
390 var CatmullRom ResampleFilter
391
392 // BSpline is a smooth cubic filter (BC-spline; B=1; C=0).
393 var BSpline ResampleFilter
394
395 // Gaussian is a Gaussian blurring filter.
396 var Gaussian ResampleFilter
397
398 // Bartlett is a Bartlett-windowed sinc filter (3 lobes).
399 var Bartlett ResampleFilter
400
401 // Lanczos filter (3 lobes).
402 var Lanczos ResampleFilter
403
404 // Hann is a Hann-windowed sinc filter (3 lobes).
405 var Hann ResampleFilter
406
407 // Hamming is a Hamming-windowed sinc filter (3 lobes).
408 var Hamming ResampleFilter
409
410 // Blackman is a Blackman-windowed sinc filter (3 lobes).
411 var Blackman ResampleFilter
412
413 // Welch is a Welch-windowed sinc filter (parabolic window, 3 lobes).
414 var Welch ResampleFilter
415
416 // Cosine is a Cosine-windowed sinc filter (3 lobes).
417 var Cosine ResampleFilter
418
419 func bcspline(x, b, c float64) float64 {
420 var y float64
421 x = math.Abs(x)
422 if x < 1.0 {
423 y = ((12-9*b-6*c)*x*x*x + (-18+12*b+6*c)*x*x + (6 - 2*b)) / 6
424 } else if x < 2.0 {
425 y = ((-b-6*c)*x*x*x + (6*b+30*c)*x*x + (-12*b-48*c)*x + (8*b + 24*c)) / 6
426 }
427 return y
428 }
429
430 func sinc(x float64) float64 {
431 if x == 0 {
432 return 1
433 }
434 return math.Sin(math.Pi*x) / (math.Pi * x)
435 }
436
437 func init() {
438 NearestNeighbor = ResampleFilter{
439 Support: 0.0, // special case - not applying the filter
440 }
441
442 Box = ResampleFilter{
443 Support: 0.5,
444 Kernel: func(x float64) float64 {
445 x = math.Abs(x)
446 if x <= 0.5 {
447 return 1.0
448 }
449 return 0
450 },
451 }
452
453 Linear = ResampleFilter{
454 Support: 1.0,
455 Kernel: func(x float64) float64 {
456 x = math.Abs(x)
457 if x < 1.0 {
458 return 1.0 - x
459 }
460 return 0
461 },
462 }
463
464 Hermite = ResampleFilter{
465 Support: 1.0,
466 Kernel: func(x float64) float64 {
467 x = math.Abs(x)
468 if x < 1.0 {
469 return bcspline(x, 0.0, 0.0)
470 }
471 return 0
472 },
473 }
474
475 MitchellNetravali = ResampleFilter{
476 Support: 2.0,
477 Kernel: func(x float64) float64 {
478 x = math.Abs(x)
479 if x < 2.0 {
480 return bcspline(x, 1.0/3.0, 1.0/3.0)
481 }
482 return 0
483 },
484 }
485
486 CatmullRom = ResampleFilter{
487 Support: 2.0,
488 Kernel: func(x float64) float64 {
489 x = math.Abs(x)
490 if x < 2.0 {
491 return bcspline(x, 0.0, 0.5)
492 }
493 return 0
494 },
495 }
496
497 BSpline = ResampleFilter{
498 Support: 2.0,
499 Kernel: func(x float64) float64 {
500 x = math.Abs(x)
501 if x < 2.0 {
502 return bcspline(x, 1.0, 0.0)
503 }
504 return 0
505 },
506 }
507
508 Gaussian = ResampleFilter{
509 Support: 2.0,
510 Kernel: func(x float64) float64 {
511 x = math.Abs(x)
512 if x < 2.0 {
513 return math.Exp(-2 * x * x)
514 }
515 return 0
516 },
517 }
518
519 Bartlett = ResampleFilter{
520 Support: 3.0,
521 Kernel: func(x float64) float64 {
522 x = math.Abs(x)
523 if x < 3.0 {
524 return sinc(x) * (3.0 - x) / 3.0
525 }
526 return 0
527 },
528 }
529
530 Lanczos = ResampleFilter{
531 Support: 3.0,
532 Kernel: func(x float64) float64 {
533 x = math.Abs(x)
534 if x < 3.0 {
535 return sinc(x) * sinc(x/3.0)
536 }
537 return 0
538 },
539 }
540
541 Hann = ResampleFilter{
542 Support: 3.0,
543 Kernel: func(x float64) float64 {
544 x = math.Abs(x)
545 if x < 3.0 {
546 return sinc(x) * (0.5 + 0.5*math.Cos(math.Pi*x/3.0))
547 }
548 return 0
549 },
550 }
551
552 Hamming = ResampleFilter{
553 Support: 3.0,
554 Kernel: func(x float64) float64 {
555 x = math.Abs(x)
556 if x < 3.0 {
557 return sinc(x) * (0.54 + 0.46*math.Cos(math.Pi*x/3.0))
558 }
559 return 0
560 },
561 }
562
563 Blackman = ResampleFilter{
564 Support: 3.0,
565 Kernel: func(x float64) float64 {
566 x = math.Abs(x)
567 if x < 3.0 {
568 return sinc(x) * (0.42 - 0.5*math.Cos(math.Pi*x/3.0+math.Pi) + 0.08*math.Cos(2.0*math.Pi*x/3.0))
569 }
570 return 0
571 },
572 }
573
574 Welch = ResampleFilter{
575 Support: 3.0,
576 Kernel: func(x float64) float64 {
577 x = math.Abs(x)
578 if x < 3.0 {
579 return sinc(x) * (1.0 - (x * x / 9.0))
580 }
581 return 0
582 },
583 }
584
585 Cosine = ResampleFilter{
586 Support: 3.0,
587 Kernel: func(x float64) float64 {
588 x = math.Abs(x)
589 if x < 3.0 {
590 return sinc(x) * math.Cos((math.Pi/2.0)*(x/3.0))
591 }
592 return 0
593 },
594 }
595 }
596