2025, Dec 15 06:03
Чёткие пиксели в Jupyter внутри VS Code на Windows: решение для высокого DPI
Избавьтесь от размытых inline‑изображений в Jupyter в VS Code на Windows с высоким DPI: devicePixelRatio, CSS image-rendering: pixelated и anywidget.
Пиксельно-точное отображение изображений в ноутбуке Jupyter внутри Visual Studio Code может «рассыпаться» на конфигурациях Windows с высоким DPI. Когда масштабирование ОС, скажем, 150%, шахматная сетка с шагом в 1 пиксель, показанная inline, обычно размывается — хотя сохранение того же изображения в PNG даёт идеальную резкость. Если вы рассчитываете, что 1 исходный пиксель совпадает с 1 физическим пикселем экрана и хотите масштабирование только целыми коэффициентами с ближайшим соседом, стандартный путь встроенного рендеринга этого не гарантирует.
Как воспроизвести проблему
Пример ниже создаёт небольшой чёрно-белый узор из массива NumPy и выводит его inline. В ноутбуке на мониторе с высоким DPI результат выглядит «мягким», хотя сохранение на диск даёт идеально резкие пиксели.
from PIL import Image
from IPython.display import display
import numpy as np
grid_bits = np.array([
[0, 1, 0, 1],
[1, 0, 1, 0],
[0, 1, 0, 1],
[1, 0, 1, 0]
], dtype=np.uint8)
mono_img = Image.fromarray(grid_bits * 255).convert(mode='1')
display(mono_img)
Сохранение этого же изображения в файл и просмотр в редакторе показывают, что само содержимое корректно; размытость возникает из‑за того, как фронтенд ноутбука отрисовывает пиксели inline.
Почему пиксели размываются при inline‑отображении
Фронтенд ноутбука работает в контексте браузера. В Windows с масштабированием дисплея браузер сопоставляет CSS пиксели с пикселями устройства с помощью window.devicePixelRatio. Если отрисовать изображение при его номинальной CSS ширине и позволить браузеру масштабировать, гарантированного соответствия 1:1 не будет. При изменении размера браузер также сглаживает, поэтому резкие чёрно-белые границы смягчаются. Простое предварительное увеличение исходного изображения методом ближайшего соседа проблему полностью не решает, потому что фронтенд всё равно применяет собственное масштабирование. Чтобы получить «настоящие» пиксели inline, изображение нужно рисовать с CSS, которое компенсирует devicePixelRatio и принудительно включает дискретное масштабирование (nearest neighbor).
Обходной путь: вручную учитывать devicePixelRatio и задать пиксельно-точное HTML
Подход ниже считывает devicePixelRatio в ячейке JavaScript, затем использует это значение в Python для задания размеров через CSS и принудительного «пиксельного» рендеринга. В итоге картинка отображается чётко прямо в ячейке. Значение придётся обновлять вручную при изменении масштабирования Windows.
%%javascript
const dprVal = window.devicePixelRatio;
alert("devicePixelRatio: " + dprVal);
dpr_value = 1.875 # установите значение, показанное во всплывающем окне
from PIL import Image
from IPython.display import HTML, display
import numpy as np
import io
import base64
# шахматная сетка 32×32
canvas_bits = np.zeros((32, 32), dtype=np.uint8)
canvas_bits[1::2, ::2] = 1
canvas_bits[::2, 1::2] = 1
def show_pixel_grid(bit_array):
img_obj = Image.fromarray(bit_array * 255).convert('1')
buf = io.BytesIO()
img_obj.save(buf, format='PNG')
b64_img = base64.b64encode(buf.getvalue()).decode('utf-8')
html_snippet = f"""
<style>
.pix-art {{
width: calc({img_obj.width}px / {dpr_value});
image-rendering: pixelated;
display: block;
margin: 0;
padding: 0;
}}
</style>
<img class="pix-art" src="data:image/png;base64,{b64_img}">
"""
display(HTML(html_snippet))
show_pixel_grid(canvas_bits)
Этот HTML заставляет использовать ближайшего соседа через image-rendering: pixelated и делит CSS ширину на devicePixelRatio, благодаря чему один пиксель изображения соответствует одному физическому пикселю.
Улучшенный вариант: anywidget и автоматическая коррекция масштабирования
Если не хочется вводить devicePixelRatio вручную, можно поручить вычисление фронтенду и применять поправку во время отрисовки. В примере ниже используется anywidget для вывода PNG в base64 и корректировки ширины и высоты на стороне клиента через window.devicePixelRatio. Заодно убирается белый фон выходной ячейки.
import anywidget
from traitlets import Unicode
from IPython.display import display
from IPython.display import HTML
import io
import base64
class PixelInlineWidget(anywidget.AnyWidget):
_esm = """
export function render({ model, el }) {
const imgEl = document.createElement("img");
el.appendChild(imgEl);
function refreshImg() {
const encoded = model.get("png_b64");
if (encoded) {
const w = model.get("w_css");
const h = model.get("h_css");
imgEl.src = `data:image/png;base64,${encoded}`;
imgEl.style.imageRendering = "pixelated";
imgEl.style.width = (w / window.devicePixelRatio) + "px"; // поправка на масштабирование браузера/ОС
imgEl.style.height = (h / window.devicePixelRatio) + "px"; // поправка на масштабирование браузера/ОС
imgEl.style.display = "block";
imgEl.style.margin = "0";
imgEl.style.padding = "0";
}
}
model.on("change:png_b64", refreshImg);
refreshImg();
}
"""
png_b64 = Unicode("").tag(sync=True)
w_css = Unicode("").tag(sync=True)
h_css = Unicode("").tag(sync=True)
def show_crisp(pil_img, scale: int = 1):
mem = io.BytesIO()
pil_img.save(mem, format='PNG')
encoded_png = base64.b64encode(mem.getvalue()).decode('utf-8')
widget = PixelInlineWidget(png_b64=encoded_png,
w_css=str(pil_img.width * scale),
h_css=str(pil_img.width * scale))
display(widget)
display(HTML("""
<style>
.cell-output-ipywidget-background, .jp-OutputArea, .jp-Cell-outputWrapper {
background-color: transparent !important;
padding: 0 !important;
}
</style>
"""))
Использование остаётся простым: сформируйте массив, преобразуйте его в 1‑битное изображение PIL и выведите inline с пиксельной точностью.
import numpy as np
from PIL import Image
s = 4
pattern = np.tile(np.array([[0, 1], [1, 0]], dtype=np.uint8), (s // 2, s // 2))
img_out = Image.fromarray(pattern * 255).convert('1')
show_crisp(img_out)
Почему это важно
При анализе данных на уровне пикселей «мягкие» границы скрывают реальную картину. Будь то отладка битмапов, визуализация масок или проверка бинарных шаблонов, необходима строгая привязка один пиксель к одному пикселю. Масштабирование браузера нарушает эту гарантию, если не компенсировать devicePixelRatio и не отключить сглаживание.
Выводы
Если изображения в ноутбуке на Windows с высоким DPI выглядят расплывчато, дело не в содержимом — дело в рендеринге. Кодируйте изображение в PNG в памяти, выводите его с CSS image-rendering: pixelated и задавайте размер, разделив CSS габариты на текущий devicePixelRatio. Это можно сделать вручную: считать коэффициент в JavaScript и передать в Python; либо поручить anywidget автоматически внести поправку при отрисовке. В обоих вариантах изображение остаётся в памяти и показывается inline, так что вы видите ровно те пиксели, которые посчитали.