1. 先搞清楚一件事:Jupyter Notebook 里的图片到底从哪里来
做数据分析或者图像处理的人,几乎每天都要和 Jupyter Notebook 打交道。写代码、跑结果、看图表,这一套流程下来,图片展示是绕不开的环节。但我在实际带新人的过程中发现,很多人对“在 Jupyter Notebook 里展示图片”这件事的理解是零散的——今天用matplotlib画图能显示,明天想读取一张本地照片却怎么都显示不出来;换个环境、换个路径,又踩一遍坑。
这篇文章就把这个问题一次性讲透。
先说清楚一个前提:在 Jupyter Notebook 里展示图片,和传统的桌面软件完全不是一回事。你用 Python 的PIL或者OpenCV读取图片之后,默认只是把图片文件当作二进制数据读进了内存,它不会像双击图片查看器那样自动弹出一个窗口。在 Notebook 这种网页式的交互环境里,所有输出最终都会被渲染成 HTML,因此展示图片的核心逻辑是:把内存中的图像数据或者文件路径,交给一个能输出 HTML<img>标签的渲染机制。
理解了这一点,你就能看懂接下来的所有方案,其实都是在解决同一个问题:怎么把图片数据“喂”给网页渲染层。
不管你是刚入门 Python 的数据分析新手,还是已经在用 Notebook 做深度学习实验、图像处理项目、数据可视化报表的开发者,下面这些方法都会覆盖到你的日常场景。
2. 把图片“请”进 Notebook:三种核心展示方式选型
2.1 用 PIL 读取本地图片并展示
Pillow 是 Python 图像处理的事实标准库,它把图片读进来之后是一个PIL.Image.Image对象。如果你直接写img.show(),系统会调用你的桌面默认图片查看器弹窗。这在实际工作中并不友好:你是在网页端写代码,结果图片跑到桌面窗口里,尤其在远程服务器或者 Docker 容器里跑 Notebook 时,show()方法几乎都是无效的。
在 Notebook 里展示 PIL 读出的图片,正确的姿势是搭配IPython.display:
from PIL import Image from IPython.display import display img = Image.open("image_001.jpg") display(img)display()是 Jupyter 内核提供的通用输出函数,任何实现了_repr_html_或者_repr_png_等协议的对象,都能在 Notebook 的单元格输出区渲染出来。PIL 的Image对象恰好实现了这些方法,所以一行display(img)就能在网页端看到图片。
这里有一个容易被忽略的细节:Image.open()是懒加载的,它只是读取了文件头信息,真正的像素数据在你需要的时候才加载。这意味着如果文件路径写错了,Image.open()不会立刻报错,而是在你执行display()或者调用img.load()的时候才抛出异常。排查问题时要注意这个时序。
2.2 用 Matplotlib 显示图片(含图像数组)
Matplotlib 是数据可视化最常用的库,它不仅能画折线图、散点图,也能直接显示图片。这个场景在图像处理相关的项目里非常常见:
import matplotlib.pyplot as plt import matplotlib.image as mpimg img_arr = mpimg.imread("image_001.jpg") plt.imshow(img_arr) plt.axis("off") # 去掉坐标轴,显示更干净 plt.show()imread()读回来的是一个 NumPy 数组,通道顺序是 RGB,所以在imshow()里显示的颜色和原图是一致的。这比 OpenCV 更直觉一些,OpenCV 读出来的是 BGR 顺序,直接显示会偏蓝。
用 Matplotlib 展示图片的最大优势是可以组合子图。你可以把原图和处理图并排放在一起对比:
fig, axes = plt.subplots(1, 2, figsize=(12, 6)) axes[0].imshow(original_img) axes[0].set_title("Original") axes[0].axis("off") axes[1].imshow(processed_img) axes[1].set_title("Processed") axes[1].axis("off") plt.tight_layout() plt.show()实际做实验的时候,这种对比展示比单张图有价值得多,写报告、做分享的时候也方便把图直接存下来。
2.3 用 IPython.display 的 Image 类展示图片文件
前面说过display()能展示 PIL 对象,其实它也能直接展示图片文件本身。IPython.display模块里有一个Image类,专门负责把磁盘上的图片文件塞进 Notebook 输出区:
from IPython.display import Image, display display(Image(filename="image_001.jpg"))这个方法相比 PIL 的好处是少了一步内存中的对象转换,对超大图片文件更友好。Image(filename=...)本质上就是告诉 Notebook 渲染一个<img src="image_001.jpg">,因此你还能控制显示的宽度和高度:
from IPython.display import Image display(Image(filename="image_001.jpg", width=600, height=400))需要说明的是,width和height只是 HTML 层面的缩放,不改变图片本身的像素尺寸。如果你的图片是 4000×3000 的原始分辨率,用width=600展示时看起来变小了,但文件体积没变,数据量和内存占用也没变。这一点在 Notebook 里同时展示几十张超大图片时影响很明显,页面会卡。
2.4 OpenCV 读进来的图片怎么展示(BGR 通道问题)
图像处理的项目基本离不开 OpenCV。但直接cv2.imread()之后用 Matplotlib 展示,你会得到一个颜色诡异、整体偏蓝的画面。这是因为 OpenCV 默认按 BGR 顺序读入图片,而 Matplotlib 按 RGB 顺序来解析。
解决办法有两个:
import cv2 import matplotlib.pyplot as plt img_bgr = cv2.imread("image_001.jpg") # 方案一:通道转换 img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB) plt.imshow(img_rgb) plt.axis("off") plt.show() # 方案二:通道反转 img_rgb = img_bgr[:, :, ::-1] plt.imshow(img_rgb) plt.axis("off") plt.show()这两种方式等价。方案一的cvtColor是逐像素的颜色空间转换,方案二的切片反转是数组操作,效率上方案二通常更快,但你如果对cvtColor有更复杂的处理需求(比如同时做色彩空间转换和通道提取),方案一更灵活。我个人在展示环节更常用切片反转,因为代码更短,速度也更快一点。
3. 在 Markdown 单元格里直接插入图片
3.1 相对路径与绝对路径的选择
上面讲的都是通过 Python 代码输出图片。但有时候你并不想把图片“跑出来”给别人看,而是希望在 Notebook 里写一段说明文字,中间顺便插一张架构图、流程图或者截图。这时候你要用的是 Markdown 单元格,而不是代码单元格。
Markdown 里插入图片的标准语法是:
图片路径可以用相对路径,也可以用绝对路径。实操中有一个非常大的坑:Notebook 的当前工作目录并不一定是 Notebook 文件所在的目录。如果你是在某个目录下启动 jupyter notebook 服务,那么工作目录通常是启动时所在的目录;如果你用 JupyterLab 打开一个 ipynb 文件,工作目录可能是该文件所在目录。在代码单元格里跑一下import os; os.getcwd()就能确认。
提示:建议在 Markdown 里统一用相对路径,并且把图片文件和 ipynb 放在同一个目录下,再建立子目录(比如
images/)统一管理图片资源。这样 Notebook 分发出去之后,图片引用不会因为换了一台机器就失效。
3.2 用 HTML 标签控制图片尺寸
Markdown 语法本身不支持自定义图片显示尺寸,如果你插入的图片是 4000 宽的大图,它会直接溢出单元格,影响阅读。解决办法是用 HTML 标签:
<img src="images/architecture.png" width="600" alt="系统架构图">Jupyter 的 Markdown 单元格是对 HTML 标签开放的,你可以直接在src后面加width、height属性。用 HTML 插入图片还有一个好处:支持style="width: 80%"这样的比例缩放,在做文档排版的时候非常实用。
3.3 把图片内嵌进 Notebook 文件本身
有时候你需要把 Notebook 发给别人,又不方便附带整个图片文件夹。这时可以用 Notebook 支持的“内嵌输出”机制:在代码单元格里用display()展示图片后,Notebook 的 ipynb 文件会把图片的 Base64 编码直接存进文件里。接收方打开 Notebook 时,即使没有原始图片文件,也能看到完整的图片输出。
但 Markdown 引用外部图片不会内嵌,发送出去对方看到的会是裂掉的图片图标。解决办法是写一小段代码:
import base64 from IPython.display import Image, display with open("architecture.png", "rb") as f: data = base64.b64encode(f.read()).decode("utf-8") display(Image(data=data))这样图片数据会以 Base64 的形态嵌入到指定单元格的输出区。保存 Notebook 之后,这个输出会被一起存进 ipynb,别人打开就能看到图。
4. 展示网络图片:从 URL 到字节码
4.1 直接在 Notebook 里渲染 URL 图片
Notebook 最大的优势之一就是可以借助 Python 生态直接访问网络资源。想在 Notebook 里展示一个网络图片 URL,最简单的方案是:
from IPython.display import Image, display display(Image(url="https://example.com/images/sample.jpg"))这个操作等同于渲染<img src="https://xxx/sample.jpg">。由于图片是由浏览器加载的,网络不稳定或图片服务器跨域限制时,会显示不出来。但好处是不需要把图片下载到本地,节省磁盘空间,适合快速验证数据链路。
4.2 把网络图片下载到本地再展示
如果是做爬虫、下载数据集、批量拉取图片的场景,我更倾向先下载再展示。原因很简单:网络图片是外部依赖,本地图片是可控的。下载之后不仅能在 Notebook 里展示,还能直接用于后续的图像处理、模型训练或者存档备份。
import requests from PIL import Image from io import BytesIO from IPython.display import display response = requests.get("https://example.com/images/sample.jpg") response.raise_for_status() img = Image.open(BytesIO(response.content)) display(img)这里用到BytesIO把网络上的字节流包装成文件对象,Image.open()就能直接读取。注意response.raise_for_status()不能省——如果你不加这行,请求失败时(比如 404)代码不会报错,而是继续执行到Image.open()时抛出一个看不出真实原因的异常,排查浪费时间。
4.3 批量下载并展示多张网络图片
真实项目中很少只处理一张网络图片。以批量爬取图片为例,你可以用循环配合os.getcwd()路径拼接来批量存储和展示。下面是一个省事、可控的模板:
import os import requests from PIL import Image from IPython.display import display save_dir = "downloaded" os.makedirs(save_dir, exist_ok=True) urls = [ "https://example.com/images/1.jpg", "https://example.com/images/2.jpg", "https://example.com/images/3.jpg", ] for idx, url in enumerate(urls): resp = requests.get(url, timeout=10) if resp.status_code != 200: print(f"第 {idx+1} 张图下载失败:{url}") continue filepath = os.path.join(save_dir, f"image_{idx+1}.jpg") with open(filepath, "wb") as f: f.write(resp.content) img = Image.open(filepath) display(img)这里面的逻辑非常简单,但有几个设计值得解释:
timeout=10确保请求不会无限卡住,尤其是碰到失效链接时。os.makedirs(..., exist_ok=True)避免每次重复建目录时报错。- 每张图下载成功后立即展示,能及时发现哪个链接有问题。
批量场景下还可以配合glob模块,把目录里的图片全部按文件名顺序展示,特别适合快速检查一个数据集里有没有混入损坏文件。
5. 实用进阶:多图布局、批量展示与文件目录路径
5.1 一行代码展示多张图片子图
在用 Matplotlib 展示多图的时候,subplots的处理方式大家都很熟。但如果你只是想在 Notebook 里快速浏览一个目录下所有图片,完全没有必要动用 Matplotlib。用IPython.display加一个循环就够了:
from IPython.display import display, Image import glob for path in sorted(glob.glob("images/*.jpg")): display(Image(filename=path, width=400))这种方式的缺点是每张图片各占一行,比较占页面空间。如果你希望图片并排显示、紧凑一点,可以用 HTML 的flex布局:
from IPython.display import HTML, Image, display paths = sorted(glob.glob("images/*.jpg")) img_tags = "".join([ f"<img src='{path}' style='width:200px; margin-right:10px;'>" for path in paths ]) display(HTML(f"<div style='display:flex; flex-wrap:wrap;'>{img_tags}</div>"))这算是一个比较取巧的做法,但效果直观,特别适合给非技术人员做快速演示。
5.2 路径处理:绝对路径、相对路径和中文路径
图片显示不出来,十有八九是路径问题。关于路径,我有几个切实的排错经验:
getcwd()是确定性最高的锚点,所有相对路径都基于它来判断是否正确。- Windows 下文件名带反斜杠
\,在 Python 字符串里要写成"images\\test.jpg"或者加r前缀变成r"images\test.jpg",否则反斜杠会被当成转义符,最常见的就是\t被解析成制表符。 - 路径中的中文和空格:Notebook 对中文路径的支持比传统终端好很多,但万一你用了
matplotlib的imread()读取中文路径时还是会遇到问题,PIL和IPython.display会稳一点。所以项目目录命名尽量用英文和下划线,能少掉很多麻烦。
import os # 推荐:显式拼接路径 base_dir = r"D:\projects\image_demo" img_path = os.path.join(base_dir, "images", "sample.jpg")5.3 用 Notebook 导出带图片的 HTML 或 PDF
很多朋友会在 Notebook 里做完分析后,直接导出 HTML 或者 PDF 给同事看。这里也有一个坑:大部分 HTML 模板要求图片必须是相对路径或者内嵌 Base64,如果你在 Markdown 里写了绝对路径C:/Users/xxx/...,导出之后别人打开 HTML 就会看到图片全部丢失。
最省心的方法是在代码里展示图片时多写一个display(Image(filename=..., width=500)),然后从菜单 File → Export Notebook As → HTML导出。Jupyter 默认会把输出区里的图片转成 Base64 嵌入到 HTML 中,别人拿到单文件就能看。
如果你用nbconvert命令导出 PDF,建议先去熟悉 LaTeX 对图片路径的解析规则——图片路径中的空格和下划线需要特殊处理,第一次导出大概率会报错。中文文件名更是重灾区。做技术分享和存档之前,先用英文名称整理好所有图片资源,是我吃过亏之后固定的习惯。
5.4 Markdown 与代码单元格协作的展示技巧
在认真写一个实验报告或者项目总结的时候,Markdown 和代码单元格是配合使用的。Markdown 负责解释,代码负责验证。图片也遵循这个规律:在 Markdown 里放一张整体结构的示意图,然后代码区具体展示每一步的输出结果。
实际操作时,我会把静态的架构图、示意图放在 Markdown 引用里,用相对路径引用;把动态的实验输出、结果对比图通过代码生成并展示出来。这样整个 Notebook 的阅读体验非常接近一篇图文并茂的技术博客,而不是干巴巴的代码流。
6. 常见问题与排查技巧速查
下面这张表整理了我自己在日常使用和数据抓取中踩过的高频问题,可以直接对照排查:
| 现象 | 大概率原因 | 解决办法 |
|---|---|---|
Image.open不报错,但display时报错 | 文件路径错误,或文件不是有效图片 | 先print(os.path.exists(path))确认文件存在 |
imread()读图后显示偏蓝 | OpenCV BGR 顺序问题 | 用cv2.cvtColor(img, cv2.COLOR_BGR2RGB)转通道 |
| 图片显示出来是一大块空白 | 网络图片未加载完成或 URL 过期 | 先下载到本地再展示,用requests加timeout |
| Matplotlib 显示的图片被坐标轴覆盖 | 忘了关闭坐标轴 | 加plt.axis("off") |
| 导出 HTML 后图片丢失 | 使用了绝对路径或外部 URL 图片 | 改用相对路径,并且在代码输出里展示图片 |
| 图片太大导致页面卡死 | 原图分辨率太高 | 用Image(filename=..., width=500)控制 HTML 展示尺寸 |
| 文件路径带反斜杠报错 | Windows 下被当成转义符 | 用r"..."原始字符串或os.path.join拼接 |
远程服务器上img.show()无效 | 没有图形界面或桌面窗口 | 改用IPython.display.display(img) |
这个表格是我把带过的几个项目里最常见的问题汇总出来的,基本覆盖了九成以上的“图片显示不出来”的场景。排查时从文件路径开始查,再查通道顺序,最后查网络和导出配置,一般都能快速定位。
7. 展示图片这件事,可以顺手存档
最后再分享一个小小的经验心得:在 Notebook 里展示图片不只是为了“看一眼”。我在做实操项目时,经常用这段代码把当前文件夹下的所有图片整理成一张带索引的校视图并重新保存为纯文本清单,方便随时归档:
from PIL import Image import glob file_list = sorted(glob.glob("*.jpg")) for idx, f in enumerate(file_list): img = Image.open(f) print(idx + 1, f, img.size)输出结果可以作为一个“图片资源清单”,写进项目的README或实验笔记里。配合IPython.display,这个玩法可以扩展成自动生成相册式的 Notebook 索引页。
结合我自己的使用体验,还有两点值得强调:
第一,不要把展示图片的手段和图片数据的生命周期混在一起。展示只是渲染层的事,真正重要的是数据本身的读取、处理和保存路径。如果你还没搞清楚PIL、Matplotlib、OpenCV这三种库在读取图片时的通道顺序和数据格式差异,建议先动手做一个简单的对比实验:用同一张彩色图片分别用三个库读取,再用display展示出来,你会非常直观地看到 BGR/RGB 的区别。
第二,学会用IPython.display是投入产出比最高的选择。它原生、稳定、可控,几乎所有常见图片展示需求都能解决,不需要额外安装插件或配置。Matplotlib 是图表展示的延伸,PIL 只是中间环节。回到最初的那个问题:图片在 Notebook 里显示不出来,八成不是环境坏了,而是这些通道和路径的细节没有捋顺。照着上面的方案走一遍,这个问题基本就彻底解决了。