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