samizdatco / skia-canvas

A GPU-accelerated 2D graphics environment for Node.js
MIT License
1.7k stars 66 forks source link

Perspective transforms image smoothing not working properly #85

Closed ewanhowell5195 closed 2 years ago

ewanhowell5195 commented 2 years ago

Left image produced using skia-canvas, right image using ImageMagick comparison The outer edge of the image seems to work correctly however

Changing the imageSmoothingQuality doesn't seem to improve anything

This doesn't seem to affect drawing things like rectangles and text, only images and canvases out

The higher resolution the input image, the worse it gets

256x265 input image squares out

1024x1024 input image squares out

Code i tested with:

import {Canvas, loadImage} from "skia-canvas"

const img = await loadImage("squares.png")

const canvas = new Canvas(256, 256)
const ctx = canvas.getContext("2d")

ctx.setTransform(ctx.createProjection([65, 10, 212, 112, 250, 200, 0, 256]))

ctx.imageSmoothingQuality = "high"

ctx.drawImage(img, 0, 0, canvas.width, canvas.height)

canvas.saveAs("out.png")
samizdatco commented 2 years ago

This seems to be a known issue with Skia itself—specifically that it doesn't use a high-quality image filter when downscaling images, despite doing so when upscaling. I've tried fiddling with the SamplingOptions values that are used for the different imageSmoothingQuality settings, but (as described in this discussion and this one) the cubic resampler is only applied when upsampling. Unfortunately, enabling bi-linear filtering didn't have a noticeable effect against your sample code.

SkiaSharp has also been wrestling with the for some time.

So if there is a solution to this problem, it's going to be more complicated. Some possibilities that might be worth investigating:

  1. Using GPU-backed rendering (which, I believe, is why these artifacts aren't seen in Chrome)
  2. Applying transforms using an ImageFilter rather than leaving that to the canvas
  3. Manually applying a convolution when downscaling images (as suggested here)

In the meantime, the best workaround might be to generate an interim bitmap of your canvas at a higher resolution (e.g., using 3 or 4 for the density export argument), then scale that image down to your desired size:

const img = await loadImage("squares.png")
const canvas = new Canvas(256, 256)
const ctx = canvas.getContext("2d")

ctx.setTransform(ctx.createProjection([65, 10, 212, 112, 250, 200, 0, 256]))
ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
let snapshot = await canvas.toBuffer('png', {density:4})

ctx.clearRect(0, 0, canvas.width, canvas.height)
ctx.resetTransform()
ctx.drawImage(await loadImage(snapshot), 0, 0, canvas.width, canvas.height)
canvas.saveAs("out.png")
out@2x