static bool circular_shift_ (ImageBuf &dst, const ImageBuf &src, int xshift, int yshift, int zshift, ROI dstroi, ROI roi, int nthreads) { if (nthreads != 1 && roi.npixels() >= 1000) { // Possible multiple thread case -- recurse via parallel_image ImageBufAlgo::parallel_image ( boost::bind(circular_shift_<DSTTYPE,SRCTYPE>, boost::ref(dst), boost::cref(src), xshift, yshift, zshift, dstroi, _1 /*roi*/, 1 /*nthreads*/), roi, nthreads); return true; } // Serial case int width = dstroi.width(), height = dstroi.height(), depth = dstroi.depth(); ImageBuf::ConstIterator<SRCTYPE,DSTTYPE> s (src, roi); ImageBuf::Iterator<DSTTYPE,DSTTYPE> d (dst); for ( ; ! s.done(); ++s) { int dx = s.x() + xshift; OIIO::wrap_periodic (dx, dstroi.xbegin, width); int dy = s.y() + yshift; OIIO::wrap_periodic (dy, dstroi.ybegin, height); int dz = s.z() + zshift; OIIO::wrap_periodic (dz, dstroi.zbegin, depth); d.pos (dx, dy, dz); if (! d.exists()) continue; for (int c = roi.chbegin; c < roi.chend; ++c) d[c] = s[c]; } return true; }
static bool transpose_ (ImageBuf &dst, const ImageBuf &src, ROI roi, int nthreads) { if (nthreads != 1 && roi.npixels() >= 1000) { // Possible multiple thread case -- recurse via parallel_image ImageBufAlgo::parallel_image ( boost::bind(transpose_<DSTTYPE,SRCTYPE>, boost::ref(dst), boost::cref(src), _1 /*roi*/, 1 /*nthreads*/), roi, nthreads); return true; } // Serial case ImageBuf::ConstIterator<SRCTYPE,DSTTYPE> s (src, roi); ImageBuf::Iterator<DSTTYPE,DSTTYPE> d (dst); for ( ; ! s.done(); ++s) { d.pos (s.y(), s.x(), s.z()); if (! d.exists()) continue; for (int c = roi.chbegin; c < roi.chend; ++c) d[c] = s[c]; } return true; }
static bool transpose_ (ImageBuf &dst, const ImageBuf &src, ROI roi, int nthreads) { ImageBufAlgo::parallel_image (roi, nthreads, [&](ROI roi){ ImageBuf::ConstIterator<SRCTYPE,DSTTYPE> s (src, roi); ImageBuf::Iterator<DSTTYPE,DSTTYPE> d (dst); for ( ; ! s.done(); ++s) { d.pos (s.y(), s.x(), s.z()); if (! d.exists()) continue; for (int c = roi.chbegin; c < roi.chend; ++c) d[c] = s[c]; } }); return true; }
bool ImageBufAlgo::histogram_draw (ImageBuf &R, const std::vector<imagesize_t> &histogram) { // Fail if there are no bins to draw. int bins = histogram.size(); if (bins == 0) { R.error ("There are no bins to draw, the histogram is empty"); return false; } // Check R and modify it if needed. int height = R.spec().height; if (R.spec().format != TypeDesc::TypeFloat || R.nchannels() != 1 || R.spec().width != bins) { ImageSpec newspec = ImageSpec (bins, height, 1, TypeDesc::FLOAT); R.reset ("dummy", newspec); } // Fill output image R with white color. ImageBuf::Iterator<float, float> r (R); for ( ; ! r.done(); ++r) r[0] = 1; // Draw histogram left->right, bottom->up. imagesize_t max = *std::max_element (histogram.begin(), histogram.end()); for (int b = 0; b < bins; b++) { int bin_height = (int) ((float)histogram[b]/(float)max*height + 0.5f); if (bin_height != 0) { // Draw one bin at column b. for (int j = 1; j <= bin_height; j++) { int row = height - j; r.pos (b, row); r[0] = 0; } } } return true; }
// DEPRECATED version bool ImageBufAlgo::add (ImageBuf &dst, const ImageBuf &A, const ImageBuf &B, int options) { // Sanity checks // dst must be distinct from A and B if ((const void *)&A == (const void *)&dst || (const void *)&B == (const void *)&dst) { dst.error ("destination image must be distinct from source"); return false; } // all three images must have the same number of channels if (A.spec().nchannels != B.spec().nchannels) { dst.error ("channel number mismatch: %d vs. %d", A.spec().nchannels, B.spec().nchannels); return false; } // If dst has not already been allocated, set it to the right size, // make it unconditinally float if (! dst.pixels_valid()) { ImageSpec dstspec = A.spec(); dstspec.set_format (TypeDesc::TypeFloat); dst.alloc (dstspec); } // Clear dst pixels if instructed to do so if (options & ADD_CLEAR_DST) { zero (dst); } ASSERT (A.spec().format == TypeDesc::FLOAT && B.spec().format == TypeDesc::FLOAT && dst.spec().format == TypeDesc::FLOAT); ImageBuf::ConstIterator<float,float> a (A); ImageBuf::ConstIterator<float,float> b (B); ImageBuf::Iterator<float> d (dst); int nchannels = A.nchannels(); // Loop over all pixels in A for ( ; a.valid(); ++a) { // Point the iterators for B and dst to the corresponding pixel if (options & ADD_RETAIN_WINDOWS) { b.pos (a.x(), a.y()); } else { // ADD_ALIGN_WINDOWS: make B line up with A b.pos (a.x()-A.xbegin()+B.xbegin(), a.y()-A.ybegin()+B.ybegin()); } d.pos (a.x(), b.y()); if (! b.valid() || ! d.valid()) continue; // Skip pixels that don't align // Add the pixel for (int c = 0; c < nchannels; ++c) d[c] = a[c] + b[c]; } return true; }
int main (int argc, char *argv[]) { getargs (argc, argv); std::cout << "Comparing \"" << filenames[0] << "\" and \"" << filenames[1] << "\"\n"; // Create a private ImageCache so we can customize its cache size // and instruct it store everything internally as floats. ImageCache *imagecache = ImageCache::create (true); imagecache->attribute ("forcefloat", 1); if (sizeof(void *) == 4) // 32 bit or 64? imagecache->attribute ("max_memory_MB", 512.0); else imagecache->attribute ("max_memory_MB", 2048.0); imagecache->attribute ("autotile", 256); #ifdef DEBUG imagecache->attribute ("statistics:level", 2); #endif ImageBuf img0, img1; if (! read_input (filenames[0], img0, imagecache) || ! read_input (filenames[1], img1, imagecache)) return ErrFile; // ImageSpec spec0 = img0.spec(); // stash it int ret = ErrOK; for (int subimage = 0; subimage < img0.nsubimages(); ++subimage) { if (subimage > 0 && !compareall) break; if (subimage >= img1.nsubimages()) break; if (compareall) { std::cout << "Subimage " << subimage << ": "; std::cout << img0.spec().width << " x " << img0.spec().height; if (img0.spec().depth > 1) std::cout << " x " << img0.spec().depth; std::cout << ", " << img0.spec().nchannels << " channel\n"; } if (! read_input (filenames[0], img0, imagecache, subimage) || ! read_input (filenames[1], img1, imagecache, subimage)) return ErrFile; if (img0.nmiplevels() != img1.nmiplevels()) { std::cout << "Files do not match in their number of MIPmap levels\n"; } for (int m = 0; m < img0.nmiplevels(); ++m) { if (m > 0 && !compareall) break; if (m > 0 && img0.nmiplevels() != img1.nmiplevels()) { std::cout << "Files do not match in their number of MIPmap levels\n"; ret = ErrDifferentSize; break; } if (! read_input (filenames[0], img0, imagecache, subimage, m) || ! read_input (filenames[1], img1, imagecache, subimage, m)) return ErrFile; if (compareall && img0.nmiplevels() > 1) { std::cout << " MIP level " << m << ": "; std::cout << img0.spec().width << " x " << img0.spec().height; if (img0.spec().depth > 1) std::cout << " x " << img0.spec().depth; std::cout << ", " << img0.spec().nchannels << " channel\n"; } // Compare the dimensions of the images. Fail if they // aren't the same resolution and number of channels. No // problem, though, if they aren't the same data type. if (! same_size (img0, img1)) { std::cout << "Images do not match in size: "; std::cout << "(" << img0.spec().width << "x" << img0.spec().height; if (img0.spec().depth > 1) std::cout << "x" << img0.spec().depth; std::cout << "x" << img0.spec().nchannels << ")"; std::cout << " versus "; std::cout << "(" << img1.spec().width << "x" << img1.spec().height; if (img1.spec().depth > 1) std::cout << "x" << img1.spec().depth; std::cout << "x" << img1.spec().nchannels << ")\n"; ret = ErrDifferentSize; break; } int npels = img0.spec().width * img0.spec().height * img0.spec().depth; ASSERT (img0.spec().format == TypeDesc::FLOAT); // Compare the two images. // ImageBufAlgo::CompareResults cr; ImageBufAlgo::compare (img0, img1, failthresh, warnthresh, cr); int yee_failures = 0; if (perceptual) yee_failures = ImageBufAlgo::compare_Yee (img0, img1); // Print the report // std::cout << " Mean error = "; safe_double_print (cr.meanerror); std::cout << " RMS error = "; safe_double_print (cr.rms_error); std::cout << " Peak SNR = "; safe_double_print (cr.PSNR); std::cout << " Max error = " << cr.maxerror; if (cr.maxerror != 0) { std::cout << " @ (" << cr.maxx << ", " << cr.maxy; if (img0.spec().depth > 1) std::cout << ", " << cr.maxz; std::cout << ", " << img0.spec().channelnames[cr.maxc] << ')'; } std::cout << "\n"; // when Visual Studio is used float values in scientific foramt are // printed with three digit exponent. We change this behaviour to fit // Linux way #ifdef _MSC_VER _set_output_format(_TWO_DIGIT_EXPONENT); #endif int precis = std::cout.precision(); std::cout << " " << cr.nwarn << " pixels (" << std::setprecision(3) << (100.0*cr.nwarn / npels) << std::setprecision(precis) << "%) over " << warnthresh << "\n"; std::cout << " " << cr.nfail << " pixels (" << std::setprecision(3) << (100.0*cr.nfail / npels) << std::setprecision(precis) << "%) over " << failthresh << "\n"; if (perceptual) std::cout << " " << yee_failures << " pixels (" << std::setprecision(3) << (100.0*yee_failures / npels) << std::setprecision(precis) << "%) failed the perceptual test\n"; if (cr.nfail > (failpercent/100.0 * npels) || cr.maxerror > hardfail || yee_failures > (failpercent/100.0 * npels)) { ret = ErrFail; } else if (cr.nwarn > (warnpercent/100.0 * npels) || cr.maxerror > hardwarn) { if (ret != ErrFail) ret = ErrWarn; } // If the user requested that a difference image be output, // do that. N.B. we only do this for the first subimage // right now, because ImageBuf doesn't really know how to // write subimages. if (diffimage.size() && (cr.maxerror != 0 || !outdiffonly)) { ImageBuf diff (diffimage, img0.spec()); ImageBuf::ConstIterator<float,float> pix0 (img0); ImageBuf::ConstIterator<float,float> pix1 (img1); ImageBuf::Iterator<float,float> pixdiff (diff); // Subtract the second image from the first. At which // time we no longer need the second image, so free it. if (diffabs) { for ( ; pix0.valid(); ++pix0) { pix1.pos (pix0.x(), pix0.y()); // ensure alignment pixdiff.pos (pix0.x(), pix0.y()); for (int c = 0; c < img0.nchannels(); ++c) pixdiff[c] = diffscale * fabsf (pix0[c] - pix1[c]); } } else { for ( ; pix0.valid(); ++pix0) { pix1.pos (pix0.x(), pix0.y()); // ensure alignment pixdiff.pos (pix0.x(), pix0.y()); for (int c = 0; c < img0.spec().nchannels; ++c) pixdiff[c] = diffscale * (pix0[c] - pix1[c]); } } diff.save (diffimage); // Clear diff image name so we only save the first // non-matching subimage. diffimage = ""; } } } if (compareall && img0.nsubimages() != img1.nsubimages()) { std::cout << "Images had differing numbers of subimages (" << img0.nsubimages() << " vs " << img1.nsubimages() << ")\n"; ret = ErrFail; } if (!compareall && (img0.nsubimages() > 1 || img1.nsubimages() > 1)) { std::cout << "Only compared the first subimage (of " << img0.nsubimages() << " and " << img1.nsubimages() << ", respectively)\n"; } if (ret == ErrOK) std::cout << "PASS\n"; else if (ret == ErrWarn) std::cout << "WARNING\n"; else std::cout << "FAILURE\n"; ImageCache::destroy (imagecache); return ret; }
void IvImage::pixel_transform(bool srgb_to_linear, int color_mode, int select_channel) { /// This table obeys the following function: /// /// unsigned char srgb2linear(unsigned char x) /// { /// float x_f = x/255.0; /// float x_l = 0.0; /// if (x_f <= 0.04045) /// x_l = x_f/12.92; /// else /// x_l = powf((x_f+0.055)/1.055,2.4); /// return (unsigned char)(x_l * 255 + 0.5) /// } /// /// It's used to transform from sRGB color space to linear color space. static const unsigned char srgb_to_linear_lut[256] = { 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 8, 8, 8, 8, 9, 9, 9, 10, 10, 10, 11, 11, 12, 12, 12, 13, 13, 13, 14, 14, 15, 15, 16, 16, 17, 17, 17, 18, 18, 19, 19, 20, 20, 21, 22, 22, 23, 23, 24, 24, 25, 25, 26, 27, 27, 28, 29, 29, 30, 30, 31, 32, 32, 33, 34, 35, 35, 36, 37, 37, 38, 39, 40, 41, 41, 42, 43, 44, 45, 45, 46, 47, 48, 49, 50, 51, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 76, 77, 78, 79, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 95, 96, 97, 99, 100, 101, 103, 104, 105, 107, 108, 109, 111, 112, 114, 115, 116, 118, 119, 121, 122, 124, 125, 127, 128, 130, 131, 133, 134, 136, 138, 139, 141, 142, 144, 146, 147, 149, 151, 152, 154, 156, 157, 159, 161, 163, 164, 166, 168, 170, 171, 173, 175, 177, 179, 181, 183, 184, 186, 188, 190, 192, 194, 196, 198, 200, 202, 204, 206, 208, 210, 212, 214, 216, 218, 220, 222, 224, 226, 229, 231, 233, 235, 237, 239, 242, 244, 246, 248, 250, 253, 255 }; unsigned char correction_table[256]; int total_channels = spec().nchannels; int color_channels = spec().nchannels; int max_channels = m_corrected_image.nchannels(); // FIXME: Now with the iterator and data proxy in place, it should be // trivial to apply the transformations to any kind of data, not just // UINT8. if (spec().format != TypeDesc::UINT8 || ! m_corrected_image.localpixels()) { return; } if (color_channels > 3) { color_channels = 3; } else if (color_channels == 2) { color_channels = 1; } // This image is Luminance or Luminance + Alpha, and we are asked to show // luminance. if (color_channels == 1 && color_mode == 3) { color_mode = 0; // Just copy as usual. } // Happy path: if (! srgb_to_linear && color_mode <= 1 && m_gamma == 1.0 && m_exposure == 0.0) { ImageBuf::ConstIterator<unsigned char, unsigned char> src (*this); ImageBuf::Iterator<unsigned char, unsigned char> dst (m_corrected_image); for ( ; src.valid (); ++src) { dst.pos (src.x(), src.y()); for (int i = 0; i < max_channels; i++) dst[i] = src[i]; } return; } // fill the correction_table if (gamma() == 1.0 && exposure() == 0.0) { for (int pixelvalue = 0; pixelvalue < 256; ++pixelvalue) { correction_table[pixelvalue] = pixelvalue; } } else { float inv_gamma = 1.0/gamma(); float gain = powf (2.0f, exposure()); for (int pixelvalue = 0; pixelvalue < 256; ++pixelvalue) { float pv_f = converter (pixelvalue); pv_f = clamp (calc_exposure (pv_f, gain, inv_gamma), 0.0f, 1.0f); correction_table[pixelvalue] = (unsigned char) (pv_f*255 + 0.5); } } ImageBuf::ConstIterator<unsigned char, unsigned char> src (*this); ImageBuf::Iterator<unsigned char, unsigned char> dst (m_corrected_image); for ( ; src.valid(); ++src) { dst.pos (src.x(), src.y()); if (color_mode == 0 || color_mode == 1) { // RGBA, RGB modes. int ch = 0; for (ch = 0; ch < color_channels; ch++) { if (srgb_to_linear) dst[ch] = correction_table[srgb_to_linear_lut[src[ch]]]; else dst[ch] = correction_table[src[ch]]; } for (; ch < max_channels; ch++) { dst[ch] = src[ch]; } } else if (color_mode == 3) { // Convert RGB to luminance, (Rec. 709 luma coefficients). float luminance; if (srgb_to_linear) { luminance = converter (srgb_to_linear_lut[src[0]])*0.2126f + converter (srgb_to_linear_lut[src[1]])*0.7152f + converter (srgb_to_linear_lut[src[2]])*0.0722f; } else { luminance = converter (src[0])*0.2126f + converter (src[1])*0.7152f + converter (src[2])*0.0722f; } unsigned char val = (unsigned char) (clamp (luminance, 0.0f, 1.0f) * 255.0 + 0.5); val = correction_table[val]; dst[0] = val; dst[1] = val; dst[2] = val; // Handle the rest of the channels for (int ch = 3; ch < total_channels; ++ch) { dst[ch] = src[ch]; } } else { // Single channel, heatmap. unsigned char v = 0; if (select_channel < color_channels) { if (srgb_to_linear) v = correction_table[srgb_to_linear_lut[src[select_channel]]]; else v = correction_table[src[select_channel]]; } else if (select_channel < total_channels) { v = src[select_channel]; } int ch = 0; for (; ch < color_channels; ++ch) { dst[ch] = v; } for (; ch < max_channels; ++ch) { dst[ch] = src[ch]; } } } }
int OiioTool::do_action_diff (ImageRec &ir0, ImageRec &ir1, Oiiotool &ot) { std::cout << "Computing diff of \"" << ir0.name() << "\" vs \"" << ir1.name() << "\"\n"; ir0.read (); ir1.read (); int ret = DiffErrOK; for (int subimage = 0; subimage < ir0.subimages(); ++subimage) { if (subimage > 0 && !ot.allsubimages) break; if (subimage >= ir1.subimages()) break; if (ot.allsubimages) { std::cout << "Subimage " << subimage << ": "; const ImageSpec &spec (*ir0.spec(subimage)); std::cout << spec.width << " x " << spec.height; if (spec.depth > 1) std::cout << " x " << spec.depth; std::cout << ", " << spec.nchannels << " channel\n"; } if (ir0.miplevels(subimage) != ir1.miplevels(subimage)) { std::cout << "Files do not match in their number of MIPmap levels\n"; } for (int m = 0; m < ir0.miplevels(subimage); ++m) { if (m > 0 && !ot.allsubimages) break; if (m > 0 && ir0.miplevels(subimage) != ir1.miplevels(subimage)) { std::cout << "Files do not match in their number of MIPmap levels\n"; ret = DiffErrDifferentSize; break; } ImageBuf &img0 (ir0(subimage,m)); ImageBuf &img1 (ir1(subimage,m)); if (ot.allsubimages && ir0.miplevels(subimage) > 1) { std::cout << " MIP level " << m << ": "; std::cout << img0.spec().width << " x " << img0.spec().height; if (img0.spec().depth > 1) std::cout << " x " << img0.spec().depth; std::cout << ", " << img0.spec().nchannels << " channel\n"; } // Compare the dimensions of the images. Fail if they // aren't the same resolution and number of channels. No // problem, though, if they aren't the same data type. if (! same_size (img0, img1)) { std::cout << "Images do not match in size: "; std::cout << "(" << img0.spec().width << "x" << img0.spec().height; if (img0.spec().depth > 1) std::cout << "x" << img0.spec().depth; std::cout << "x" << img0.spec().nchannels << ")"; std::cout << " versus "; std::cout << "(" << img1.spec().width << "x" << img1.spec().height; if (img1.spec().depth > 1) std::cout << "x" << img1.spec().depth; std::cout << "x" << img1.spec().nchannels << ")\n"; ret = DiffErrDifferentSize; break; } int npels = img0.spec().width * img0.spec().height * img0.spec().depth; ASSERT (img0.spec().format == TypeDesc::FLOAT); // Compare the two images. // ImageBufAlgo::CompareResults cr; ImageBufAlgo::compare (img0, img1, ot.diff_failthresh, ot.diff_warnthresh, cr); int yee_failures = 0; #if 0 if (perceptual) yee_failures = ImageBufAlgo::compare_Yee (img0, img1); #endif // Print the report // std::cout << " Mean error = "; safe_double_print (cr.meanerror); std::cout << " RMS error = "; safe_double_print (cr.rms_error); std::cout << " Peak SNR = "; safe_double_print (cr.PSNR); std::cout << " Max error = " << cr.maxerror; if (cr.maxerror != 0) { std::cout << " @ (" << cr.maxx << ", " << cr.maxy; if (img0.spec().depth > 1) std::cout << ", " << cr.maxz; std::cout << ", " << img0.spec().channelnames[cr.maxc] << ')'; } std::cout << "\n"; // when Visual Studio is used float values in scientific foramt are // printed with three digit exponent. We change this behaviour to fit // Linux way #ifdef _MSC_VER _set_output_format(_TWO_DIGIT_EXPONENT); #endif int precis = std::cout.precision(); std::cout << " " << cr.nwarn << " pixels (" << std::setprecision(3) << (100.0*cr.nwarn / npels) << std::setprecision(precis) << "%) over " << ot.diff_warnthresh << "\n"; std::cout << " " << cr.nfail << " pixels (" << std::setprecision(3) << (100.0*cr.nfail / npels) << std::setprecision(precis) << "%) over " << ot.diff_failthresh << "\n"; #if 0 if (perceptual) std::cout << " " << yee_failures << " pixels (" << std::setprecision(3) << (100.0*yee_failures / npels) << std::setprecision(precis) << "%) failed the perceptual test\n"; #endif if (cr.nfail > (ot.diff_failpercent/100.0 * npels) || cr.maxerror > ot.diff_hardfail || yee_failures > (ot.diff_failpercent/100.0 * npels)) { ret = DiffErrFail; } else if (cr.nwarn > (ot.diff_warnpercent/100.0 * npels) || cr.maxerror > ot.diff_hardwarn) { if (ret != DiffErrFail) ret = DiffErrWarn; } #if 0 // If the user requested that a difference image be output, // do that. N.B. we only do this for the first subimage // right now, because ImageBuf doesn't really know how to // write subimages. if (diffimage.size() && (cr.maxerror != 0 || !outdiffonly)) { ImageBuf diff (diffimage, img0.spec()); ImageBuf::ConstIterator<float,float> pix0 (img0); ImageBuf::ConstIterator<float,float> pix1 (img1); ImageBuf::Iterator<float,float> pixdiff (diff); // Subtract the second image from the first. At which // time we no longer need the second image, so free it. if (diffabs) { for ( ; pix0.valid(); ++pix0) { pix1.pos (pix0.x(), pix0.y()); // ensure alignment pixdiff.pos (pix0.x(), pix0.y()); for (int c = 0; c < img0.nchannels(); ++c) pixdiff[c] = diffscale * fabsf (pix0[c] - pix1[c]); } } else { for ( ; pix0.valid(); ++pix0) { pix1.pos (pix0.x(), pix0.y()); // ensure alignment pixdiff.pos (pix0.x(), pix0.y()); for (int c = 0; c < img0.spec().nchannels; ++c) pixdiff[c] = diffscale * (pix0[c] - pix1[c]); } } diff.save (diffimage); // Clear diff image name so we only save the first // non-matching subimage. diffimage = ""; } #endif } } if (ot.allsubimages && ir0.subimages() != ir1.subimages()) { std::cout << "Images had differing numbers of subimages (" << ir0.subimages() << " vs " << ir1.subimages() << ")\n"; ret = DiffErrFail; } if (!ot.allsubimages && (ir0.subimages() > 1 || ir1.subimages() > 1)) { std::cout << "Only compared the first subimage (of " << ir0.subimages() << " and " << ir1.subimages() << ", respectively)\n"; } if (ret == DiffErrOK) std::cout << "PASS\n"; else if (ret == DiffErrWarn) std::cout << "WARNING\n"; else { std::cout << "FAILURE\n"; } return ret; }