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)
This is not sharp.
Saving the image does produce a sharp image:
bw_image.save('image.png')
Result, 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:
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?
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);
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:
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.