libvips / php-vips

php binding for libvips
MIT License
615 stars 25 forks source link

Having trouble implementing brightness #193

Closed axkirillov closed 1 year ago

axkirillov commented 1 year ago

Hi!

I am trying to implement the following javascript algorithm in php-vips. It seems to me that the calculations are identical. But the result differs. Here is the javascript


for (let i = 0; i < length; i += 4) {
  const avg = (r + g + b) / 765;

  const factor = 1 + (brightness * (1 - avg));

  r *= factor;
  g *= factor;
  b *= factor;
}

here is the php-vips version

$sum = $r->sum([$g, $b]);
$avg = $sum->linear(1 / (3 * 255), 0);
$factor = $avg->linear(-1 * $brightness, 1 + $brightness);
$r = $r->multiply($factor);
$g = $g->multiply($factor);
$b = $b->multiply($factor);
$imageResult = $r->bandjoin([$g, $b]);

Result fot -1 brightness: js image php-vips canvas_1679415206326

axkirillov commented 1 year ago

Figured it out, I was using ::sum incorrectly the correct usage is as static function: $sum = Vips\Image::sum([$r, $g, $b]);

jcupitt commented 1 year ago

Hi @axkirillov,

You could also write this as (untested):

$brightness = 0.7;
$image = Vips\Image::newFromFile("xxx");
// scrgb is linear 0-1 pixels, bandmean makes a one-band image by averaging bandwise
$factor = $image->colourspace("scrgb")->bandmean()->linear(-1 * $brightness, 1 + $brightness);
$image = $image * factor;
// back to standard 8 bit rgb
$image = $image->colourspace("srgb");

I'd expect working in linear space to give more pleasing results as well.

I wish php had operator overloads :(

axkirillov commented 1 year ago

Thank you for the advice!

axkirillov commented 1 year ago

The first part of your suggestion works great

$factor = $image->colourspace("scrgb")->bandmean()->linear(-1 * $brightness, 1 + $brightness);

doing this however results in a different image than expected

$image = $image->multiply($factor);
$image = $image->colourspace("srgb");

expected (brightness -1)

image

result image

axkirillov commented 1 year ago

Another thing that probably stems from me not understanding colorspaces and such is that when the original image is png, the following implementation also produces not quite expected result. I'll paste the entire code here since there might be smth relevant I am missing.

        $start = microtime(true);
        $hasAlpha = $image->hasAlpha();

        // remove alpha, if any
        if ($hasAlpha) {
            $alpha = $image->extract_band($image->bands - 1, ['n' => 1]);
            $image = $image->extract_band(0, ['n' => $image->bands - 1]);
        }

        /** 
         * @var Vips\Image $r 
         * @var Vips\Image $g 
         * @var Vips\Image $b 
         * */
        [$r, $g, $b] = $image->bandsplit();
        $brightness = $this->brightness / 100;

        // equivalent javascript

        //for (let i = 0; i < length; i += 4) {
        //  const avg = (data[i] + data[i + 1] + data[i + 2]) / 765;
        //
        //  const factor = 1 + (valueToUse * (1 - avg));
        //
        //  data[i] *= factor;
        //  data[i + 1] *= factor;
        //  data[i + 2] *= factor;
        //
        // scrgb is linear 0-1 pixels, bandmean makes a one-band image by averaging bandwise

        $factor = $image->colourspace("scrgb")->bandmean()->linear(-1 * $brightness, 1 + $brightness);  //}
        $r = $r->multiply($factor);
        $g = $g->multiply($factor);
        $b = $b->multiply($factor);
        $image = $r->bandjoin([$g, $b]);

        // add alpha back
        if ($hasAlpha) {
            $image = $image->bandjoin($alpha);
        }

        $blob = $image->writeToBuffer($hasAlpha ? '.png' : '.jpg');
        $imagick = new \Imagick();
        $imagick->readImageBlob($blob);

Prior to this the image is simply loaded like this:

Vips\Image::newFromBuffer($this->getRawContent($url));

expected (brightness is 1)

image

result

image
jcupitt commented 1 year ago

doing this however results in a different image than expected

Yes, it'll look a bit different.

sRGB images have a gamma, meaning that numeric values in the file have a power law relationship with brightness (usually 2.2). Converting to scrgb (note the c) makes an image with pixels in 0-1 and a linear relationship with light. Lightness adjustments made in the space should look more natural (ie. better).

If you prefer the gamma look, you can just do:

$image = $image->divide(255);

instead.

jcupitt commented 1 year ago

You don't need to bandsplit and bandjoin. You can just do:

$image = $image->linear([1, 2, 3], [4, 5, 6]);

To scale each band by a different amount (if that's what you want to do).

jcupitt commented 1 year ago

So just write:

        $brightness = $this->brightness / 100;

        // equivalent javascript

        //for (let i = 0; i < length; i += 4) {
        //  const avg = (data[i] + data[i + 1] + data[i + 2]) / 765;
        //
        //  const factor = 1 + (valueToUse * (1 - avg));
        //
        //  data[i] *= factor;
        //  data[i + 1] *= factor;
        //  data[i + 2] *= factor;
        //
        // scrgb is linear 0-1 pixels, bandmean makes a one-band image by averaging bandwise

        $factor = $image->colourspace("scrgb")->bandmean()->linear(-1 * $brightness, 1 + $brightness);
        $image = $image->multiply($factor);
axkirillov commented 1 year ago

Thank you for the suggestions! Indeed, divide(255) works as expected for both png and jpg $image = $image->multiply($factor); also works, I might have confused the two problems at first.