| 1 | // tResample.cpp  |
| 2 | //  |
| 3 | // Resample an image using various filers like nearest-neighbour, box, bilinear, and various bicubics.  |
| 4 | //  |
| 5 | // Copyright (c) 2020 Tristan Grimmer.  |
| 6 | // Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby  |
| 7 | // granted, provided that the above copyright notice and this permission notice appear in all copies.  |
| 8 | //  |
| 9 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL  |
| 10 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,  |
| 11 | // INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN  |
| 12 | // AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR  |
| 13 | // PERFORMANCE OF THIS SOFTWARE.  |
| 14 |   |
| 15 | #include "Image/tResample.h"  |
| 16 | #include "System/tPrint.h"  |
| 17 | using namespace tMath;  |
| 18 |   |
| 19 |   |
| 20 | namespace tImage  |
| 21 | {  |
| 22 | enum class FilterDirection  |
| 23 | {  |
| 24 | Horizontal,  |
| 25 | Vertical  |
| 26 | };  |
| 27 |   |
| 28 | struct FilterParams  |
| 29 | {  |
| 30 | FilterParams() : RatioH(0.0f), RatioV(0.0f) { }  |
| 31 | union { float RatioH; float CubicCoeffB; float LanczosA; };  |
| 32 | union { float RatioV; float CubicCoeffC; };  |
| 33 | };  |
| 34 |   |
| 35 | tPixel KernelFilterNearest (const tPixel* src, int srcW, int srcH, float x, float y, FilterDirection, tResampleEdgeMode, const FilterParams&);  |
| 36 | tPixel KernelFilterBox (const tPixel* src, int srcW, int srcH, float x, float y, FilterDirection, tResampleEdgeMode, const FilterParams&);  |
| 37 | tPixel KernelFilterBilinear (const tPixel* src, int srcW, int srcH, float x, float y, FilterDirection, tResampleEdgeMode, const FilterParams&);  |
| 38 | tPixel KernelFilterBicubic (const tPixel* src, int srcW, int srcH, float x, float y, FilterDirection, tResampleEdgeMode, const FilterParams&);  |
| 39 | tPixel KernelFilterLanczos (const tPixel* src, int srcW, int srcH, float x, float y, FilterDirection, tResampleEdgeMode, const FilterParams&);  |
| 40 |   |
| 41 | int GetSrcIndex(int idx, int count, tResampleEdgeMode);  |
| 42 | float ComputeCubicWeight(float x, float b, float c);  |
| 43 | float ComputeLanczosWeight(float x, float a);  |
| 44 | }  |
| 45 |   |
| 46 |   |
| 47 | const char* tImage::tResampleFilterNames[int(tResampleFilter::NumFilters)] =  |
| 48 | {  |
| 49 | "Nearest Neighbour" ,  |
| 50 | "Box" ,  |
| 51 | "Bilinear" ,  |
| 52 | "Bicubic Standard" ,  |
| 53 | "Bicubic CatmullRom" ,  |
| 54 | "Bicubic Mitchell" ,  |
| 55 | "Bicubic Cardinal" ,  |
| 56 | "Bicubic BSpline" ,  |
| 57 | "Lanczos Narrow" ,  |
| 58 | "Lanczos Normal" ,  |
| 59 | "Lanczos Wide"   |
| 60 | };  |
| 61 |   |
| 62 |   |
| 63 | const char* tImage::tResampleEdgeModeNames[int(tResampleEdgeMode::NumEdgeModes)] =  |
| 64 | {  |
| 65 | "Clamp" ,  |
| 66 | "Wrap"   |
| 67 | };  |
| 68 |   |
| 69 |   |
| 70 | inline int tImage::GetSrcIndex(int idx, int count, tResampleEdgeMode edgeMode)  |
| 71 | {  |
| 72 | tAssert(count > 0);  |
| 73 | switch (edgeMode)  |
| 74 | {  |
| 75 | case tResampleEdgeMode::Clamp:  |
| 76 | return tClamp(idx, 0, count-1);  |
| 77 |   |
| 78 | case tResampleEdgeMode::Wrap:  |
| 79 | return tMod(idx, count);  |
| 80 | }  |
| 81 |   |
| 82 | return -1;  |
| 83 | }  |
| 84 |   |
| 85 |   |
| 86 | bool tImage::Resample  |
| 87 | (  |
| 88 | tPixel* src, int srcW, int srcH,  |
| 89 | tPixel* dst, int dstW, int dstH,  |
| 90 | tResampleFilter resampleFilter,  |
| 91 | tResampleEdgeMode edgeMode  |
| 92 | )  |
| 93 | {  |
| 94 | if (!src || !dst || srcW<=0 || srcH<=0 || dstW<=0 || dstH<=0)  |
| 95 | return false;  |
| 96 |   |
| 97 | if ((srcW == dstW) && (srcH == dstH))  |
| 98 | {  |
| 99 | for (int p = 0; p < srcW*srcH; p++)  |
| 100 | dst[p] = src[p];  |
| 101 |   |
| 102 | return true;  |
| 103 | }  |
| 104 |   |
| 105 | float ratioH = (dstW > 1) ? (float(srcW) - 1.0f) / float(dstW - 1) : 1.0f;  |
| 106 | float ratioV = (dstH > 1) ? (float(srcH) - 1.0f) / float(dstH - 1) : 1.0f;  |
| 107 |   |
| 108 | // Decide what filer kernel to use. Different kernels may set different values in FilterParams.  |
| 109 | FilterParams params;  |
| 110 | typedef tPixel (*KernelFilterFn)(const tPixel* src, int srcW, int srcH, float x, float y, FilterDirection, tResampleEdgeMode, const FilterParams&);  |
| 111 | KernelFilterFn kernel;  |
| 112 | switch (resampleFilter)  |
| 113 | {  |
| 114 | case tResampleFilter::Nearest:  |
| 115 | kernel = KernelFilterNearest;  |
| 116 | break;  |
| 117 |   |
| 118 | case tResampleFilter::Box:  |
| 119 | params.RatioH = ratioH;  |
| 120 | params.RatioV = ratioV;  |
| 121 | kernel = KernelFilterBox;  |
| 122 | break;  |
| 123 |   |
| 124 | case tResampleFilter::Bilinear:  |
| 125 | kernel = KernelFilterBilinear;  |
| 126 | break;  |
| 127 |   |
| 128 | case tResampleFilter::Bicubic_Standard: // Cardinal. B=0 C=3/4  |
| 129 | params.CubicCoeffB = 0.0f;  |
| 130 | params.CubicCoeffC = 3.0f/4.0f;  |
| 131 | kernel = KernelFilterBicubic;  |
| 132 | break;  |
| 133 |   |
| 134 | case tResampleFilter::Bicubic_CatmullRom: // Cardinal. B=0 C=1/2  |
| 135 | params.CubicCoeffB = 0.0f;  |
| 136 | params.CubicCoeffC = 1.0f/2.0f;  |
| 137 | kernel = KernelFilterBicubic;  |
| 138 | break;  |
| 139 |   |
| 140 | case tResampleFilter::Bicubic_Mitchell: // Balanced. B=1/3 C=1/3  |
| 141 | params.CubicCoeffB = 1.0f/3.0f;  |
| 142 | params.CubicCoeffC = 1.0f/3.0f;  |
| 143 | kernel = KernelFilterBicubic;  |
| 144 | break;  |
| 145 |   |
| 146 | case tResampleFilter::Bicubic_Cardinal: // Pure Cardinal. B=0 C=1  |
| 147 | params.CubicCoeffB = 0.0f;  |
| 148 | params.CubicCoeffC = 1.0f;  |
| 149 | kernel = KernelFilterBicubic;  |
| 150 | break;  |
| 151 |   |
| 152 | case tResampleFilter::Bicubic_BSpline: // Pure BSpline. Blurry. B=1 C=0  |
| 153 | params.CubicCoeffB = 1.0f;  |
| 154 | params.CubicCoeffC = 0.0f;  |
| 155 | kernel = KernelFilterBicubic;  |
| 156 | break;  |
| 157 |   |
| 158 | case tResampleFilter::Lanczos_Narrow: // Lanczos. Ringy/Sharp. A=2  |
| 159 | params.LanczosA = 2.0f;  |
| 160 | kernel = KernelFilterLanczos;  |
| 161 | break;  |
| 162 |   |
| 163 | case tResampleFilter::Lanczos_Normal: // Lanczos. Ringy/Sharp. A=3  |
| 164 | params.LanczosA = 3.0f;  |
| 165 | kernel = KernelFilterLanczos;  |
| 166 | break;  |
| 167 |   |
| 168 | case tResampleFilter::Lanczos_Wide: // Lanczos. Ringy/Sharp. A=4  |
| 169 | params.LanczosA = 4.0f;  |
| 170 | kernel = KernelFilterLanczos;  |
| 171 | break;  |
| 172 |   |
| 173 | case tResampleFilter::Invalid:  |
| 174 | default:  |
| 175 | return false;  |
| 176 | }   |
| 177 |   |
| 178 | // By convention do horizontal first. Outer loop is for each src row.  |
| 179 | // hri stands for hozontal-resized-image.  |
| 180 | tPixel* hri = new tPixel[dstW*srcH];  |
| 181 | for (int r = 0; r < srcH; r++)  |
| 182 | {  |
| 183 | // Fill in each dst pixel for the src row,  |
| 184 | float y = float(r);  |
| 185 | for (int c = 0; c < dstW; c++)  |
| 186 | {  |
| 187 | tPixel& dstPixel = hri[dstW*r + c];  |
| 188 | float x = float(c) * ratioH;  |
| 189 | dstPixel = kernel(src, srcW, srcH, x, y, FilterDirection::Horizontal, edgeMode, params);  |
| 190 | }  |
| 191 | }  |
| 192 |   |
| 193 | // Vertical resampling. Source is the horizontally resized image.  |
| 194 | for (int c = 0; c < dstW; c++)  |
| 195 | {  |
| 196 | float x = float(c);  |
| 197 | for (int r = 0; r < dstH; r++)  |
| 198 | {  |
| 199 | tPixel& dstPixel = dst[dstW*r + c];  |
| 200 | float y = float(r) * ratioV;  |
| 201 | dstPixel = kernel(hri, dstW, srcH, x, y, FilterDirection::Vertical, edgeMode, params);  |
| 202 | }  |
| 203 | }  |
| 204 |   |
| 205 | delete[] hri;  |
| 206 | return true;  |
| 207 | }  |
| 208 |   |
| 209 |   |
| 210 | tPixel tImage::KernelFilterNearest  |
| 211 | (  |
| 212 | const tPixel* src, int srcW, int srcH, float x, float y,  |
| 213 | FilterDirection dir, tResampleEdgeMode edgeMode, const FilterParams& params  |
| 214 | )  |
| 215 | {  |
| 216 | int ix = tClamp(int(x + 0.5f), 0, srcW-1);  |
| 217 | int iy = tClamp(int(y + 0.5f), 0, srcH-1);  |
| 218 | return src[srcW*iy + ix];  |
| 219 | }  |
| 220 |   |
| 221 |   |
| 222 | tPixel tImage::KernelFilterBox  |
| 223 | (  |
| 224 | const tPixel* src, int srcW, int srcH, float x, float y,  |
| 225 | FilterDirection dir, tResampleEdgeMode edgeMode, const FilterParams& params  |
| 226 | )  |
| 227 | {  |
| 228 | float ratio = (dir == FilterDirection::Horizontal) ? params.RatioH : params.RatioV;  |
| 229 | int pixelDist = int(ratio + 1.0f);  |
| 230 | float maxDist = ratio;  |
| 231 | float weightTotal = 0.0f;  |
| 232 | tVector4 sampleTotal = tVector4::zero;  |
| 233 |   |
| 234 | for (int ks = 1 - pixelDist; ks <= pixelDist; ks++)  |
| 235 | {  |
| 236 | int ix = (dir == FilterDirection::Horizontal) ? int(x) + ks : int(x);  |
| 237 | int iy = (dir == FilterDirection::Horizontal) ? int(y) : int(y) + ks;  |
| 238 |   |
| 239 | float dist = tAbs( (dir == FilterDirection::Horizontal) ? x - float(ix) : y - float(iy) );  |
| 240 | float weight = 0.0f;  |
| 241 |   |
| 242 | int srcX = GetSrcIndex(ix ,srcW, edgeMode);  |
| 243 | int srcY = GetSrcIndex(iy ,srcH, edgeMode);  |
| 244 | tPixel srcPixel = src[srcW*srcY + srcX];  |
| 245 |   |
| 246 | if (ratio >= 1.0f)  |
| 247 | {  |
| 248 | weight = 1.0f - tMin(maxDist, dist)/maxDist;  |
| 249 | }  |
| 250 | else  |
| 251 | {  |
| 252 | if (dist >= (0.5f - ratio))  |
| 253 | weight = 1.0f - dist;  |
| 254 | else  |
| 255 | return srcPixel; // Box is inside src pixel. Done.  |
| 256 | }  |
| 257 |   |
| 258 | sampleTotal.x += srcPixel.R * weight;  |
| 259 | sampleTotal.y += srcPixel.G * weight;  |
| 260 | sampleTotal.z += srcPixel.B * weight;  |
| 261 | sampleTotal.w += srcPixel.A * weight;  |
| 262 | weightTotal += weight;  |
| 263 | }  |
| 264 |   |
| 265 | // Renormalize sampleTotal back to [0, 256).  |
| 266 | sampleTotal /= weightTotal;  |
| 267 | return tPixel  |
| 268 | (  |
| 269 | tClamp(int(tRound(sampleTotal.x)), 0, 255),  |
| 270 | tClamp(int(tRound(sampleTotal.y)), 0, 255),  |
| 271 | tClamp(int(tRound(sampleTotal.z)), 0, 255),  |
| 272 | tClamp(int(tRound(sampleTotal.w)), 0, 255)  |
| 273 | );  |
| 274 | }  |
| 275 |   |
| 276 |   |
| 277 | tPixel tImage::KernelFilterBilinear  |
| 278 | (  |
| 279 | const tPixel* src, int srcW, int srcH, float x, float y,  |
| 280 | FilterDirection dir, tResampleEdgeMode edgeMode, const FilterParams& params  |
| 281 | )  |
| 282 | {  |
| 283 | int ix = int(x);  |
| 284 | int iy = int(y);  |
| 285 |   |
| 286 | int srcXa = GetSrcIndex(ix , srcW, edgeMode);  |
| 287 | int srcYa = GetSrcIndex(iy , srcH, edgeMode);  |
| 288 | int srcXb = GetSrcIndex(ix+1, srcW, edgeMode);  |
| 289 | int srcYb = GetSrcIndex(iy+1, srcH, edgeMode);  |
| 290 |   |
| 291 | tPixel a = src[srcW*srcYa + srcXa];  |
| 292 | tPixel b = (dir == FilterDirection::Horizontal) ?  |
| 293 | src[srcW*srcYa + srcXb] :  |
| 294 | src[srcW*srcYb + srcXa];  |
| 295 |   |
| 296 | float weight = (dir == FilterDirection::Horizontal) ? float(x)-ix : float(y)-iy;  |
| 297 |   |
| 298 | tVector4 av, bv, rv;  |
| 299 | a.GetDenorm(av);  |
| 300 | b.GetDenorm(bv);  |
| 301 | rv = av*(1.0f-weight) + bv*weight;  |
| 302 | tiClamp(tiRound(rv.x), 0.0f, 255.0f);  |
| 303 | tiClamp(tiRound(rv.y), 0.0f, 255.0f);  |
| 304 | tiClamp(tiRound(rv.z), 0.0f, 255.0f);  |
| 305 | tiClamp(tiRound(rv.w), 0.0f, 255.0f);  |
| 306 |   |
| 307 | return tPixel(int(rv.x), int(rv.y), int(rv.z), int(rv.w));  |
| 308 | }  |
| 309 |   |
| 310 |   |
| 311 | // This function is the cubic filter workhorse. It implements the weight function k(x) found  |
| 312 | // at https://entropymine.com/imageworsener/bicubic/  |
| 313 | // If that site ever goes down, the original paper is from Mitchell and Netravali (1988).  |
| 314 | float tImage::ComputeCubicWeight(float x, float b, float c)  |
| 315 | {  |
| 316 | float xa = tAbs(x);  |
| 317 |   |
| 318 | // Case 3. Early exit the 'otherwise' case.  |
| 319 | if (xa >= 2.0f)  |
| 320 | return 0.0f;  |
| 321 |   |
| 322 | // Common terms in the other two cases.  |
| 323 | float c6 = 6.0f*c;  |
| 324 | float xc = tCube(xa);  |
| 325 | float b12 = 12.0f*b;  |
| 326 | float xs = tSquare(xa);  |
| 327 | float r = 0.0f;  |
| 328 |   |
| 329 | if (xa < 1.0f)  |
| 330 | {  |
| 331 | // Case 1.  |
| 332 | float b9 = 9.0f*b;  |
| 333 | float b2 = 2.0f*b;  |
| 334 | r = (12.0f-b9-c6)*xc + (-18.0f+b12+c6)*xs + (6.0f-b2);  |
| 335 | }  |
| 336 | else  |
| 337 | {  |
| 338 | // Case 2.  |
| 339 | float b6 = 6.0f*b;  |
| 340 | float c30 = 30.0f*c;  |
| 341 | float c48 = 48.0f*c;  |
| 342 | float b8 = 8.0f*b;  |
| 343 | float c24 = 24.0f*c;  |
| 344 | r = (-b-c6)*xc + (b6+c30)*xs + (-b12-c48)*xa + (b8+c24);  |
| 345 | }  |
| 346 |   |
| 347 | return tClampMin(r/6.0f, 0.0f);  |
| 348 | }  |
| 349 |   |
| 350 |   |
| 351 | tPixel tImage::KernelFilterBicubic  |
| 352 | (  |
| 353 | const tPixel* src, int srcW, int srcH, float x, float y,  |
| 354 | FilterDirection dir, tResampleEdgeMode edgeMode, const FilterParams& params  |
| 355 | )  |
| 356 | {  |
| 357 | float weightTotal = 0.0f;  |
| 358 | tVector4 sampleTotal = tVector4::zero;  |
| 359 |   |
| 360 | for (int ks = -2; ks < 2; ks++)  |
| 361 | {  |
| 362 | int ix = (dir == FilterDirection::Horizontal) ? int(x) + ks : int(x);  |
| 363 | int iy = (dir == FilterDirection::Horizontal) ? int(y) : int(y) + ks;  |
| 364 |   |
| 365 | float diff = (dir == FilterDirection::Horizontal) ? x - float(ix) : y - float(iy);  |
| 366 | float weight = ComputeCubicWeight(diff, params.CubicCoeffB, params.CubicCoeffC);  |
| 367 |   |
| 368 | int srcX = GetSrcIndex(ix, srcW, edgeMode);  |
| 369 | int srcY = GetSrcIndex(iy, srcH, edgeMode);  |
| 370 | tPixel srcPixel = src[srcW*srcY + srcX];  |
| 371 |   |
| 372 | sampleTotal.x += srcPixel.R * weight;  |
| 373 | sampleTotal.y += srcPixel.G * weight;  |
| 374 | sampleTotal.z += srcPixel.B * weight;  |
| 375 | sampleTotal.w += srcPixel.A * weight;  |
| 376 | weightTotal += weight;  |
| 377 | }  |
| 378 |   |
| 379 | // Renormalize sampleTotal back to [0, 256).  |
| 380 | sampleTotal /= weightTotal;  |
| 381 | return tPixel  |
| 382 | (  |
| 383 | tClamp(int(tRound(sampleTotal.x)), 0, 255),  |
| 384 | tClamp(int(tRound(sampleTotal.y)), 0, 255),  |
| 385 | tClamp(int(tRound(sampleTotal.z)), 0, 255),  |
| 386 | tClamp(int(tRound(sampleTotal.w)), 0, 255)  |
| 387 | );  |
| 388 | }  |
| 389 |   |
| 390 |   |
| 391 | inline float tImage::ComputeLanczosWeight(float x, float a)  |
| 392 | {  |
| 393 | // See https://en.wikipedia.org/wiki/Lanczos_resampling  |
| 394 | return ((x >= -a) && (x <= a)) ? tSinc(x) * tSinc(x/a) : 0.0f;  |
| 395 | }  |
| 396 |   |
| 397 |   |
| 398 | tPixel tImage::KernelFilterLanczos  |
| 399 | (  |
| 400 | const tPixel* src, int srcW, int srcH, float x, float y,  |
| 401 | FilterDirection dir, tResampleEdgeMode edgeMode, const FilterParams& params  |
| 402 | )  |
| 403 | {  |
| 404 | int pixelDist = int(params.LanczosA);  |
| 405 | float weightTotal = 0.0f;  |
| 406 | tVector4 sampleTotal = tVector4::zero;  |
| 407 |   |
| 408 | for (int ks = -pixelDist; ks < pixelDist; ks++)  |
| 409 | {  |
| 410 | int ix = (dir == FilterDirection::Horizontal) ? int(x) + ks : int(x);  |
| 411 | int iy = (dir == FilterDirection::Horizontal) ? int(y) : int(y) + ks;  |
| 412 |   |
| 413 | float diff = (dir == FilterDirection::Horizontal) ? x - float(ix) : y - float(iy);  |
| 414 | float weight = ComputeLanczosWeight(tAbs(diff), params.LanczosA);  |
| 415 |   |
| 416 | int srcX = GetSrcIndex(ix, srcW, edgeMode);  |
| 417 | int srcY = GetSrcIndex(iy, srcH, edgeMode);  |
| 418 | tPixel srcPixel = src[srcW*srcY + srcX];  |
| 419 |   |
| 420 | sampleTotal.x += srcPixel.R * weight;  |
| 421 | sampleTotal.y += srcPixel.G * weight;  |
| 422 | sampleTotal.z += srcPixel.B * weight;  |
| 423 | sampleTotal.w += srcPixel.A * weight;  |
| 424 | weightTotal += weight;  |
| 425 | }  |
| 426 |   |
| 427 | // Renormalize totalSamples back to [0, 256).  |
| 428 | sampleTotal /= weightTotal;  |
| 429 | return tPixel  |
| 430 | (  |
| 431 | tClamp(int(tRound(sampleTotal.x)), 0, 255),  |
| 432 | tClamp(int(tRound(sampleTotal.y)), 0, 255),  |
| 433 | tClamp(int(tRound(sampleTotal.z)), 0, 255),  |
| 434 | tClamp(int(tRound(sampleTotal.w)), 0, 255)  |
| 435 | );  |
| 436 | }  |
| 437 | |