Same rationale as with the offset; we need resampling for proper zoom.
The look at heavy zoom isn't _quite_ what I had hoped for (although it's OK),
and there's a hint of shimmering in the zoom center if there's high-contrast
material there. For now, I'll write off the latter as Lanczos ringing;
I'll need to see what it does to video eventually (only tested with stills).
ResampleEffect::ResampleEffect()
: input_width(1280),
- input_height(720)
+ input_height(720),
+ offset_x(0.0f), offset_y(0.0f),
+ zoom_x(1.0f), zoom_y(1.0f),
+ zoom_center_x(0.5f), zoom_center_y(0.5f)
{
register_int("width", &output_width);
register_int("height", &output_height);
ok |= vpass->set_int("output_height", output_height);
assert(ok);
+
+ // The offset added due to zoom may have changed with the size.
+ update_offset_and_zoom();
+}
+
+void ResampleEffect::update_offset_and_zoom()
+{
+ bool ok = true;
+
+ // Zoom from the right origin. (zoom_center is given in normalized coordinates,
+ // i.e. 0..1.)
+ float extra_offset_x = zoom_center_x * (1.0f - 1.0f / zoom_x) * input_width;
+ float extra_offset_y = (1.0f - zoom_center_y) * (1.0f - 1.0f / zoom_y) * input_height;
+
+ ok |= hpass->set_float("offset", extra_offset_x + offset_x);
+ ok |= vpass->set_float("offset", extra_offset_y - offset_y); // Compensate for the bottom-left origin.
+ ok |= hpass->set_float("zoom", zoom_x);
+ ok |= vpass->set_float("zoom", zoom_y);
+
+ assert(ok);
}
bool ResampleEffect::set_float(const string &key, float value) {
return true;
}
if (key == "top") {
- // Compensate for the bottom-left origin.
- return vpass->set_float("offset", -value);
+ offset_y = value;
+ update_offset_and_zoom();
+ return true;
}
if (key == "left") {
- return hpass->set_float("offset", value);
+ offset_x = value;
+ update_offset_and_zoom();
+ return true;
+ }
+ if (key == "zoom_x") {
+ if (value <= 0.0f) {
+ return false;
+ }
+ zoom_x = value;
+ update_offset_and_zoom();
+ return true;
+ }
+ if (key == "zoom_y") {
+ if (value <= 0.0f) {
+ return false;
+ }
+ zoom_y = value;
+ update_offset_and_zoom();
+ return true;
+ }
+ if (key == "zoom_center_x") {
+ zoom_center_x = value;
+ update_offset_and_zoom();
+ return true;
+ }
+ if (key == "zoom_center_y") {
+ zoom_center_y = value;
+ update_offset_and_zoom();
+ return true;
}
return false;
}
input_width(1280),
input_height(720),
offset(0.0),
+ zoom(1.0),
last_input_width(-1),
last_input_height(-1),
last_output_width(-1),
last_output_height(-1),
- last_offset(0.0 / 0.0) // NaN.
+ last_offset(0.0 / 0.0), // NaN.
+ last_zoom(0.0 / 0.0) // NaN.
{
register_int("direction", (int *)&direction);
register_int("input_width", &input_width);
register_int("output_width", &output_width);
register_int("output_height", &output_height);
register_float("offset", &offset);
+ register_float("zoom", &zoom);
glGenTextures(1, &texnum);
}
// the first such loop, and then ask the card to repeat the texture for us.
// This is both easier on the texture cache and lowers our CPU cost for
// generating the kernel somewhat.
- num_loops = gcd(src_size, dst_size);
+ float scaling_factor;
+ if (fabs(zoom - 1.0f) < 1e-6) {
+ num_loops = gcd(src_size, dst_size);
+ scaling_factor = float(dst_size) / float(src_size);
+ } else {
+ // If zooming is enabled (ie., zoom != 1), we turn off the looping.
+ // We _could_ perhaps do it for rational zoom levels (especially
+ // things like 2:1), but it doesn't seem to be worth it, given that
+ // the most common use case would seem to be varying the zoom
+ // from frame to frame.
+ num_loops = 1;
+ scaling_factor = zoom * float(dst_size) / float(src_size);
+ }
slice_height = 1.0f / num_loops;
unsigned dst_samples = dst_size / num_loops;
// Anyhow, in this case we clearly need to look at more source pixels
// to compute the destination pixel, and how many depend on the scaling factor.
// Thus, the kernel width will vary with how much we scale.
- float radius_scaling_factor = min(float(dst_size) / float(src_size), 1.0f);
+ float radius_scaling_factor = min(scaling_factor, 1.0f);
int int_radius = lrintf(LANCZOS_RADIUS / radius_scaling_factor);
int src_samples = int_radius * 2 + 1;
float *weights = new float[dst_samples * src_samples * 2];
for (unsigned y = 0; y < dst_samples; ++y) {
// Find the point around which we want to sample the source image,
// compensating for differing pixel centers as the scale changes.
- float center_src_y = (y + 0.5f) * float(src_size) / float(dst_size) - 0.5f;
+ float center_src_y = (y + 0.5f) / scaling_factor - 0.5f;
int base_src_y = lrintf(center_src_y);
// Now sample <int_radius> pixels on each side around that point.
// Normalize so that the sum becomes one. Note that we do it twice;
// this sometimes helps a tiny little bit when we have many samples.
+#if 0
for (int normalize_pass = 0; normalize_pass < 2; ++normalize_pass) {
double sum = 0.0;
for (int i = 0; i < src_bilinear_samples; ++i) {
fp16_to_fp64(bilinear_weights_fp16_ptr[i * 2 + 0]) / sum);
}
}
+#endif
}
// Encode as a two-component texture. Note the GL_REPEAT.
input_height != last_input_height ||
output_width != last_output_width ||
output_height != last_output_height ||
- offset != last_offset) {
+ offset != last_offset ||
+ zoom != last_zoom) {
update_texture(glsl_program_num, prefix, sampler_num);
last_input_width = input_width;
last_input_height = input_height;
last_output_width = output_width;
last_output_height = output_height;
last_offset = offset;
+ last_zoom = zoom;
}
glActiveTexture(GL_TEXTURE0 + *sampler_num);
// We put the fractional part of the offset (-0.5 to 0.5 pixels) in the weights
// because we have to (otherwise they'd do nothing). However, the support texture
-// has limited numerical precision and we'd need as much of it as we can for
+// has limited numerical precision; we'd need as much of it as we can for
// getting the subpixel sampling right, and adding a large constant to each value
// will reduce the precision further. Thus, the non-fractional part of the offset
-// is sent in through a uniform that we simply add in the beginning of the shader.
+// is sent in through a uniform that we simply add in. (It should be said that
+// for high values of (dst_size/num_loop), we're pretty much hosed anyway wrt.
+// this accuracy.)
+//
+// Unfortunately, we cannot just do it at the beginning of the shader,
+// since the texcoord value is used to index into the support texture,
+// and if zoom != 1, the support texture will not wrap properly, causing
+// us to read the wrong texels. (Also remember that whole_pixel_offset is
+// measured in _input_ pixels and tc is in _output_ pixels, although we could
+// compensate for that.) However, the shader should be mostly bandwidth bound
+// and not ALU bound, so an extra add per sample shouldn't be too hopeless.
uniform float PREFIX(whole_pixel_offset);
// Sample a single weight. First fetch information about where to sample
vec2 sample = tex2D(PREFIX(sample_tex), sample_tc).rg;
#if DIRECTION_VERTICAL
- tc.y = sample.g + floor(sample_tc.y) * PREFIX(slice_height);
+ tc.y = sample.g + floor(sample_tc.y) * PREFIX(slice_height) + PREFIX(whole_pixel_offset);
#else
- tc.x = sample.g + floor(sample_tc.y) * PREFIX(slice_height);
+ tc.x = sample.g + floor(sample_tc.y) * PREFIX(slice_height) + PREFIX(whole_pixel_offset);
#endif
return vec4(sample.r) * INPUT(tc);
}
vec4 FUNCNAME(vec2 tc) {
-#if DIRECTION_VERTICAL
- tc.y += PREFIX(whole_pixel_offset);
-#else
- tc.x += PREFIX(whole_pixel_offset);
-#endif
vec4 sum = PREFIX(do_sample)(tc, 0);
for (int i = 1; i < PREFIX(num_samples); ++i) {
sum += PREFIX(do_sample)(tc, i);
private:
void update_size();
+ void update_offset_and_zoom();
SingleResamplePassEffect *hpass, *vpass;
int input_width, input_height, output_width, output_height;
+
+ float offset_x, offset_y;
+ float zoom_x, zoom_y;
+ float zoom_center_x, zoom_center_y;
};
class SingleResamplePassEffect : public Effect {
Direction direction;
GLuint texnum;
int input_width, input_height, output_width, output_height;
- float offset;
+ float offset, zoom;
int last_input_width, last_input_height, last_output_width, last_output_height;
- float last_offset;
+ float last_offset, last_zoom;
int src_bilinear_samples, num_loops;
float slice_height;
};
expect_equal(expected_data, out_data, dst_width, 1);
}
+TEST(ResampleEffectTest, Zoom) {
+ const int width = 5;
+ const int height = 3;
+
+ float data[width * height] = {
+ 0.0, 0.0, 0.0, 0.0, 0.0,
+ 0.2, 0.4, 0.6, 0.4, 0.2,
+ 0.0, 0.0, 0.0, 0.0, 0.0,
+ };
+ float expected_data[width * height] = {
+ 0.0, 0.0, 0.0, 0.0, 0.0,
+ 0.4, 0.5396, 0.6, 0.5396, 0.4,
+ 0.0, 0.0, 0.0, 0.0, 0.0,
+ };
+ float out_data[width * height];
+
+ EffectChainTester tester(data, width, height, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
+ Effect *resample_effect = tester.get_chain()->add_effect(new ResampleEffect());
+ ASSERT_TRUE(resample_effect->set_int("width", width));
+ ASSERT_TRUE(resample_effect->set_int("height", height));
+ ASSERT_TRUE(resample_effect->set_float("zoom_x", 2.0f));
+ tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
+
+ expect_equal(expected_data, out_data, width, height);
+}
+
+TEST(ResampleEffectTest, VerticalZoomFromTop) {
+ const int width = 5;
+ const int height = 5;
+
+ float data[width * height] = {
+ 0.2, 0.4, 0.6, 0.4, 0.2,
+ 0.0, 0.0, 0.0, 0.0, 0.0,
+ 0.0, 0.0, 0.0, 0.0, 0.0,
+ 0.0, 0.0, 0.0, 0.0, 0.0,
+ 0.0, 0.0, 0.0, 0.0, 0.0,
+ };
+
+ // Largely empirical data; the main point is that the top line
+ // is unchanged, since that's our zooming point.
+ float expected_data[width * height] = {
+ 0.2000, 0.4000, 0.6000, 0.4000, 0.2000,
+ 0.1389, 0.2778, 0.4167, 0.2778, 0.1389,
+ 0.0600, 0.1199, 0.1798, 0.1199, 0.0600,
+ 0.0000, 0.0000, 0.0000, 0.0000, 0.0000,
+ -0.0229, -0.0459, -0.0688, -0.0459, -0.0229,
+ };
+ float out_data[width * height];
+
+ EffectChainTester tester(data, width, height, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
+ Effect *resample_effect = tester.get_chain()->add_effect(new ResampleEffect());
+ ASSERT_TRUE(resample_effect->set_int("width", width));
+ ASSERT_TRUE(resample_effect->set_int("height", height));
+ ASSERT_TRUE(resample_effect->set_float("zoom_y", 3.0f));
+ ASSERT_TRUE(resample_effect->set_float("zoom_center_y", 0.5f / height));
+ tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
+
+ expect_equal(expected_data, out_data, width, height);
+}
+
} // namespace movit