pythonvisual-studio-codejupyter-notebookhighdpi

How do I render images sharp in Jupyter notebook with VScode when Windows scaling is enabled?


I want to display an image from an array in a Jupyter notebook (inline) in Visual Studio Code. I'm running Windows 11 on a high DPI monitor with scaling set to 150%. Pixels don't render sharp in the notebook. 1 pixel in the source image should be 1 pixel on the screen. If I want to scale the image using integer scaling I can just scale the source image using nearest neighbor. I tried matplotlib and pillow. I can't get it sharp. Here is my python code:

from PIL import Image
from IPython.display import display
import numpy as np

bw_data = np.array([
    [0, 1, 0, 1],
    [1, 0, 1, 0],
    [0, 1, 0, 1],
    [1, 0, 1, 0]
], dtype=np.uint8)

# mode '1' = 1-bit black/white
bw_image = Image.fromarray(bw_data * 255).convert(mode='1')

display(bw_image)

output, Zoomed in with Gimp: Zoomed in via Gimp

This is not sharp.

Saving the image does produce a sharp image:

bw_image.save('image.png')

Result, Zoomed in with Gimp:

Zoomed in with Gimp

I use iconico magnifier V2.4 (with scaling disabled on executable) to inspect the rendered images on pixel level. And using image editors such as Gimp or paint to inspect saved images or saved screenshots (since image viewers use upscaling when zooming in instead of nearest neighbor).

I prefer not to write the image to storage first, but keep it in RAM. But this is not a hard requirement. The only requirement is that it is inline and not a separate window as that is easy to get working.

Edit: This is how this answer (which just scales the source image)(renders on my machine: Zoomed in with Gimp

As you can see scaling the source image does not fix the rendering issue. It just slightly masks it because the edges are relatively smaller. The edges are still blurry. Rending on pixel level is what is faulty. How do I display true pixels?


Solution

  • I have found a workaround to display the image sharp.
    It requires manually changing a value in the code each time you change scaling in Windows.

    Step 1:

    %%javascript
    const ratio = window.devicePixelRatio;
    alert("devicePixelRatio: " + ratio);
    

    notification popup showing a devicePixelRatio  of 1.875

    Step 2:

    devicePixelRatio = 1.875 # manually enter the value that was shown
    

    Step 3:

    from PIL import Image
    from IPython.display import HTML
    import numpy as np
    import io
    import base64
    
    # 32x32 data
    bw_data = np.zeros((32,32),dtype=np.uint8)
    # (odd_rows, even_columns)
    bw_data[1::2,::2] = 1
    # (even_rows, odd_columns)
    bw_data[::2,1::2] = 1
    
    # Build pixel-exact HTML
    def display_pixel_image(np_array):
        # Convert binary image to black & white PIL image
        img = Image.fromarray(np_array * 255).convert('1')
        
        # Convert to base64-encoded PNG
        buf = io.BytesIO()
        img.save(buf, format='PNG')
        b64 = base64.b64encode(buf.getvalue()).decode('utf-8')
        
        # HTML + CSS to counteract scaling
        html = f"""
        <style>
          .pixel-art {{
            width: calc({img.width}px / {devicePixelRatio});
            image-rendering: pixelated;
            display: block;
            margin: 0;
            padding: 0;
          }}
        </style>
        <img class="pixel-art" src="data:image/png;base64,{b64}">
        """
        display(HTML(html))
    
    display_pixel_image(bw_data)
    

    output:

    sharp image

    Visual Studio Code cannot access ipython kernel so I don't know how to retrieve devicePixelRatio from Javascript. I tried to make an ipython widget, but was not able to refresh it automatically. If this can be done automatically then it won't require user input.