Imagick / imagick

🌈 The Imagick PHP extension 🌈
http://pecl.php.net/imagick
Other
540 stars 136 forks source link

Random pixels change color after applying filters that should not be affecting them #504

Open Wes0617 opened 2 years ago

Wes0617 commented 2 years ago

Original:

Original Image

<?php

$imagick = new Imagick(__DIR__ . "/og.png");
// https://i.stack.imgur.com/t0eJO.png 

// first filter
$imagick->modulateImage(188., 100., 100.);

//  The pixels are now fully white, so I don't see why increasing the saturation
// should change them (and not even all of them... just some... ?)

// second filter    
$imagick->modulateImage(100., 145., 100.);

// save
$imagick->writeImage(__DIR__ . "/result1.png");

Resulting Pixel artifacts:

Pixel artifacts

Magnified artifacts:

magnified

But if I save the image, and then load it back into Imagick, I get the correct result:

<?php

$imagick = new Imagick(__DIR__ . "/og.png");
// https://i.stack.imgur.com/t0eJO.png

// first filter
$imagick->modulateImage(188., 100., 100.);
$imagick->setImageFormat("png");
$data = $imagick->getImageBlob();

// second filter
$imagick = new Imagick();
$imagick->readImageBlob($data);
$imagick->modulateImage(100., 145., 100.);

// save
$imagick->writeImage(__DIR__ . "/result2.png");

Expected Result:

Expected Result

Is this caused by bit depth? Color profile? Or is it just a bug? I am using ImageMagick 7.0.7-11

Thank you in advance.

Danack commented 2 years ago

Can you try doing the same on the command line with ImageMagick convert program?

If you can get the same very different result, then it can be reported upstream straight away, and they will probably look at it more quickly.

Danack commented 2 years ago

The pixels are now fully white,

btw, that's wrong.....with hdri they are close to white, but are unlikely to all be 255.

Saving to PNG will convert them to full 255 white, as that is not a HDR image format.....I'm about to try with OpenEXR as my guess is that will not 'flatten' the images.

Danack commented 2 years ago

I don't think it's a bug. Have some code:

$scale = 8;

// Resize just the source image.
$imagick = new Imagick(__DIR__ . "/og_crop.png");
$imagick->interpolativeResizeImage(
    $scale * $imagick->getImageWidth(),
    $scale * $imagick->getImageHeight(),
    Imagick::INTERPOLATE_NEAREST_PIXEL
);
$imagick->writeImage(__DIR__ . "/og_crop_before.png");

// Do in one pass
$imagick = new Imagick(__DIR__ . "/og_crop.png");
$imagick->modulateImage(188., 100., 100.);
$imagick->modulateImage(100.0, 145.0, 100.);
$imagick->interpolativeResizeImage(
    8 * $imagick->getImageWidth(),
    8 * $imagick->getImageHeight(),
    Imagick::INTERPOLATE_NEAREST_PIXEL
);
$imagick->writeImage(__DIR__ . "/og_crop_in_one_pass.png");

// Do in two pass
$imagick = new Imagick(__DIR__ . "/og_crop.png");
$imagick->modulateImage(188., 100., 100.);

$data = $imagick->getImageBlob();
$imagick2 = new Imagick();
$imagick2->readImageBlob($data);
$imagick2->modulateImage(100.0, 145.0, 100.);

$imagick2->interpolativeResizeImage(
    8 * $imagick2->getImageWidth(),
    8 * $imagick2->getImageHeight(),
    Imagick::INTERPOLATE_NEAREST_PIXEL
);
$imagick2->writeImage(__DIR__ . "/og_crop_in_two_passes_via_png.png");

// Do in two passes via EXR
$imagick = new Imagick(__DIR__ . "/og_crop.png");
$imagick->modulateImage(188., 100., 100.);

$imagick->setOption('png:bit-depth', '16');
$imagick->setOption('png:color-type', "6");

$imagick->writeImage(__DIR__ . "/og_crop_after_first.png");
$imagick->writeImage(__DIR__ . "/og_crop_after_first.exr");
$imagick2 = new Imagick(__DIR__ . "/og_crop_after_first.exr");
$imagick2->modulateImage(100.0, 145.0, 100.);
$imagick2->interpolativeResizeImage(
    8 * $imagick2->getImageWidth(),
    8 * $imagick2->getImageHeight(),
    Imagick::INTERPOLATE_NEAREST_PIXEL
);
$imagick2->writeImage(__DIR__ . "/og_crop_in_two_pass_via_exr.png");

// Do in one pass and threshold
$imagick = new Imagick(__DIR__ . "/og_crop.png");
$imagick->modulateImage(188., 100., 100.);
$imagick->whiteThresholdImage("rgb(254, 254, 254)");
$imagick->modulateImage(100.0, 145.0, 100.);
$imagick->interpolativeResizeImage(
    8 * $imagick->getImageWidth(),
    8 * $imagick->getImageHeight(),
    Imagick::INTERPOLATE_NEAREST_PIXEL
);
$imagick->writeImage(__DIR__ . "/og_crop_in_one_pass_and_threshold.png");

og_crop_in_two_pass_via_exr.png og_crop_in_two_pass_via_exr

This shows that the issue is that the pixels are not pure white, and so when you increase the saturation, it's doing the correct thing according to the maths.

og_crop_in_one_pass_and_threshold.png og_crop_in_one_pass_and_threshold

Adding a threshold command, that turns all the nearly white colours in 255 white prevents this phenomenon from affecting the output image..

Danack commented 2 years ago

Or another option:

// Do in one pass with better colour space
$imagick = new Imagick(__DIR__ . "/og_crop.png");
$imagick->transformImageColorspace(Imagick::COLORSPACE_LOG);

$imagick->modulateImage(188., 100., 100.);
$imagick->modulateImage(100.0, 145.0, 100.);
$imagick->interpolativeResizeImage(
    8 * $imagick->getImageWidth(),
    8 * $imagick->getImageHeight(),
    Imagick::INTERPOLATE_NEAREST_PIXEL
);

$imagick->transformImageColorspace(Imagick::COLORSPACE_SRGB);
$imagick->writeImage(__DIR__ . "/og_crop_in_one_pass_colorspace_log.png");

og_crop_in_one_pass_colorspace_log

Danack commented 2 years ago

Or with clamp...

// Do in one pass and clamp
$imagick = new Imagick(__DIR__ . "/og_crop.png");
$imagick->modulateImage(188., 100., 100.);
$imagick->clampImage();
$imagick->modulateImage(100.0, 145.0, 100.);

$imagick->interpolativeResizeImage(
    8 * $imagick->getImageWidth(),
    8 * $imagick->getImageHeight(),
    Imagick::INTERPOLATE_NEAREST_PIXEL
);
$imagick->writeImage(__DIR__ . "/og_crop_in_one_pass_and_clamp.png");
Danack commented 2 years ago

So,

<?php

declare(strict_types=1);

$imagick = new Imagick(__DIR__ . "/og_crop.png");

$imagick->quantizeImage(1024, \Imagick::COLORSPACE_YIQ, 0, false, false);

$imagick->uniqueImageColors();

$imagick->cropImage(32, 1, $imagick->getImageWidth() - 32, 0);

$imagick->scaleimage(
    $imagick->getImageWidth(),
    $imagick->getImageHeight() * 20
);

echo "wut\n";

$imagick->writeImage(__DIR__ . "/unique_colours.png");

$export_type = Imagick::PIXEL_QUANTUM;
$digits = 14;

$export_type = Imagick::PIXEL_FLOAT;
$digits = 4;

$imagick2 = new Imagick(__DIR__ . "/unique_colours.png");

$step_0_values = $imagick2->exportImagePixels(
    0,
    1,
    32,
    1,
    "RGB",
    $export_type
);

$imagick2->modulateImage(188., 100., 100.);

$step_1_values = $imagick2->exportImagePixels(
    0,
    1,
    32,
    1,
    "RGB",
    $export_type
);

$imagick2->writeImage(__DIR__ . "/unique_colours_pass_1.png");
$imagick2->modulateImage(100.0, 145.0, 100.);
$imagick2->writeImage(__DIR__ . "/unique_colours_pass_2.png");

$step_2_values = $imagick2->exportImagePixels(
    0,
    1,
    32,
    1,
    "RGB",
    $export_type
);

$imagick3 = new Imagick(__DIR__ . "/unique_colours_pass_2.png");
$step_3_values = $imagick3->exportImagePixels(
    0,
    1,
    32,
    1,
    "RGB",
    $export_type
);

for ($i = 0; $i<32; $i += 1) {

    $r_0 = $step_0_values[(3 * $i)    ];
    $g_0 = $step_0_values[(3 * $i) + 1];
    $b_0 = $step_0_values[(3 * $i) + 2];

    $r_1 = $step_1_values[(3 * $i)    ];
    $g_1 = $step_1_values[(3 * $i) + 1];
    $b_1 = $step_1_values[(3 * $i) + 2];

    $r_2 = $step_2_values[(3 * $i)    ];
    $g_2 = $step_2_values[(3 * $i) + 1];
    $b_2 = $step_2_values[(3 * $i) + 2];

    $r_3 = $step_3_values[(3 * $i)    ];
    $g_3 = $step_3_values[(3 * $i) + 1];
    $b_3 = $step_3_values[(3 * $i) + 2];

    $format = "rgb(%$digits.2f, %$digits.2f, %$digits.2f)";
    $format_string = "%2d  $format, $format, $format, $format\n";

    printf(
        $format_string,
        $i,
        $r_0, $g_0, $b_0,
        $r_1, $g_1, $b_1,
        $r_2, $g_2, $b_2,
        $r_3, $g_3, $b_3,
    );
}

HDRI images are not bound 0-1.

 0  rgb(0.19, 0.04, 0.01), rgb(0.35, 0.08, 0.01), rgb(0.43, 0.03, -0.06), rgb(0.43, 0.04, 0.00)
 1  rgb(0.20, 0.05, 0.02), rgb(0.37, 0.10, 0.04), rgb(0.44, 0.06, -0.04), rgb(0.44, 0.06, 0.00)
 2  rgb(0.22, 0.07, 0.04), rgb(0.42, 0.13, 0.07), rgb(0.50, 0.08, -0.00), rgb(0.50, 0.08, 0.00)
 3  rgb(0.23, 0.08, 0.05), rgb(0.43, 0.15, 0.09), rgb(0.51, 0.10, 0.01), rgb(0.51, 0.09, 0.01)
 4  rgb(0.22, 0.03, 0.01), rgb(0.41, 0.06, 0.01), rgb(0.50, -0.01, -0.07), rgb(0.50, 0.00, 0.00)
 5  rgb(0.24, 0.05, 0.03), rgb(0.45, 0.10, 0.05), rgb(0.54, 0.03, -0.04), rgb(0.54, 0.03, 0.00)
 6  rgb(0.18, 0.11, 0.08), rgb(0.34, 0.21, 0.15), rgb(0.38, 0.20, 0.10), rgb(0.38, 0.20, 0.11)
 7  rgb(0.19, 0.11, 0.07), rgb(0.35, 0.21, 0.13), rgb(0.41, 0.19, 0.07), rgb(0.40, 0.19, 0.07)
 8  rgb(0.21, 0.13, 0.09), rgb(0.40, 0.24, 0.18), rgb(0.45, 0.21, 0.13), rgb(0.45, 0.21, 0.13)
 9  rgb(0.21, 0.15, 0.11), rgb(0.40, 0.28, 0.20), rgb(0.44, 0.27, 0.15), rgb(0.44, 0.27, 0.15)
10  rgb(0.22, 0.16, 0.13), rgb(0.42, 0.31, 0.24), rgb(0.46, 0.30, 0.19), rgb(0.46, 0.30, 0.20)
11  rgb(0.26, 0.19, 0.16), rgb(0.49, 0.35, 0.30), rgb(0.53, 0.34, 0.26), rgb(0.53, 0.34, 0.26)
12  rgb(0.19, 0.11, 0.06), rgb(0.36, 0.20, 0.11), rgb(0.42, 0.18, 0.05), rgb(0.42, 0.18, 0.05)
13  rgb(0.21, 0.12, 0.08), rgb(0.39, 0.23, 0.15), rgb(0.45, 0.21, 0.09), rgb(0.45, 0.21, 0.09)
14  rgb(0.24, 0.13, 0.06), rgb(0.44, 0.24, 0.12), rgb(0.52, 0.22, 0.04), rgb(0.51, 0.22, 0.04)
15  rgb(0.22, 0.09, 0.07), rgb(0.42, 0.18, 0.13), rgb(0.48, 0.13, 0.07), rgb(0.49, 0.13, 0.07)
16  rgb(0.24, 0.13, 0.07), rgb(0.45, 0.25, 0.13), rgb(0.52, 0.23, 0.06), rgb(0.52, 0.23, 0.06)
17  rgb(0.26, 0.16, 0.09), rgb(0.49, 0.29, 0.17), rgb(0.57, 0.28, 0.10), rgb(0.57, 0.28, 0.10)
18  rgb(0.22, 0.10, 0.04), rgb(0.42, 0.18, 0.07), rgb(0.50, 0.16, -0.01), rgb(0.50, 0.16, 0.00)
19  rgb(0.24, 0.12, 0.05), rgb(0.46, 0.22, 0.10), rgb(0.54, 0.19, 0.02), rgb(0.54, 0.20, 0.02)
20  rgb(0.29, 0.18, 0.11), rgb(0.54, 0.33, 0.21), rgb(0.61, 0.31, 0.13), rgb(0.61, 0.31, 0.13)
21  rgb(0.38, 0.06, 0.04), rgb(0.72, 0.11, 0.08), rgb(0.86, -0.02, -0.06), rgb(0.86, 0.00, 0.00)
22  rgb(0.45, 0.13, 0.12), rgb(0.81, 0.29, 0.27), rgb(0.93, 0.18, 0.14), rgb(0.93, 0.18, 0.15)
23  rgb(0.32, 0.28, 0.27), rgb(0.60, 0.54, 0.53), rgb(0.61, 0.53, 0.51), rgb(0.61, 0.53, 0.51)
24  rgb(0.49, 0.45, 0.44), rgb(0.88, 0.87, 0.87), rgb(0.89, 0.87, 0.87), rgb(0.89, 0.87, 0.87)
25  rgb(0.52, 0.20, 0.18), rgb(0.82, 0.51, 0.49), rgb(0.90, 0.44, 0.42), rgb(0.90, 0.44, 0.42)
26  rgb(0.66, 0.62, 0.62), rgb(1.19, 1.21, 1.21), rgb(1.19, 1.22, 1.22), rgb(1.00, 1.00, 1.00)
27  rgb(0.84, 0.52, 0.51), rgb(1.13, 1.39, 1.41), rgb(1.07, 1.45, 1.47), rgb(1.00, 1.00, 1.00)
28  rgb(0.84, 0.80, 0.79), rgb(1.46, 1.57, 1.59), rgb(1.43, 1.59, 1.62), rgb(1.00, 1.00, 1.00)
29  rgb(0.98, 0.97, 0.98), rgb(1.70, 1.98, 1.84), rgb(1.63, 2.04, 1.84), rgb(1.00, 1.00, 1.00)
30  rgb(1.00, 0.99, 0.99), rgb(1.58, 2.16, 2.16), rgb(1.45, 2.29, 2.29), rgb(1.00, 1.00, 1.00)
31  rgb(1.00, 0.97, 0.97), rgb(1.00, 2.70, 2.70), rgb(0.62, 3.08, 3.08), rgb(0.62, 1.00, 1.00)