心脏左心房切片分割数据集:从数据准备到2D U-Net训练全指南
2026/9/23 20:16:44 网站建设 项目流程

简介:面向医学图像分割研究者的心脏左心房切片数据集,源于左心房三维数据,按轴位面、冠状面、矢状面划分为三组二维切面,每组均含图片目录与掩码目录,掩码中1代表心脏、0代表背景,可直接用于训练U-Net等分割模型。其中轴位面包含1351张320×320图像,冠状面包含1151张320×130图像,矢状面包含828张320×130图像,覆盖不同解剖视角,便于开展多维对比与消融实验。资源共2000个文件,包括1998张PNG格式图像、1个Python可视化脚本和1个JSON数据集说明文件,压缩包大小93.88MB。可视化脚本可随机抽取图像,将原始图像、真实标注及叠加蒙版效果保存到本地,方便快速核验标注质量与模型输出。已有365人学习下载,适用于需要标准化心脏分割数据的研究人员、算法工程师及医学影像相关专业学生。

1. 心脏左心房切片分割数据集:为什么三个切面和两个类别就够你练手了

做房颤射频消融术前规划时,医生需要把左心房从三维核磁或CT里完整勾出来,传统流程是逐层手动画轮廓,一套序列下来常常要四十分钟到一个小时,而且不同医生勾出来的边界差异不小。这几年自动分割方案越来越多,但所有方案的第一步都落在同一件事上:先把训练数据整理明白。这个心脏左心房切片分割数据集提供的就是这么一套基础材料,三个切面方向(横断面、矢状面、冠状面)的二维切片各自划分好,每张切片只有两个类别——背景和左心房,还配了标签文件和可视化代码。它适合两类人:想在医学图像分割方向入门但苦于找不到能直接跑起来的数据的研究者,以及已经跑过几个模型但对数据组织和标签对齐还没建立手感的一线工程师。这个数据集最大的价值不在模型涨点,而是把从原始图像到能进模型的那一步中间环节完整铺开,让你提前踩一遍数据管线里最容易翻车的坑。

2. 切面划分与数据集结构:拿到手第一件事不是训练,是理清目录

2.1 先看懂三个切面是怎么划分的

"三个切面"这个概念看起来简单,但医学图像里特别容易绕进去。三维医学图像本质是一堆体素,横断面(axial)、矢状面(sagittal)、冠状面(coronal)分别对应沿着人体从头到脚、从左到右、从前到后三个正交方向做切片。同一个左心房,在横断面上看是个近似圆形的腔,在矢状面上看是拉长的漏斗形,在冠状面上看又是不规则椭圆。这个数据集做的事,就是把这套切片按方向分好,每个方向单独建目录,切片编号保持连续,让使用者不用自己写dicom解析和重切逻辑。

我拿到手第一件事是看目录树,用一条命令把结构完全打出来,确认没有隐藏的空目录和断文件。

find data -type d | sort echo "--- 每个切面的图像数量 ---" for d in data/*/; do echo "$d" find "$d" -name "*.png" | wc -l done

逻辑说明:先列出所有目录,再按资源子目录统计PNG数量。很多医学数据集在拷贝、解压环节会丢切片,尤其是打包工具对中文文件名处理不当的情况,先看数量能避免后面训练到一半才发现缺图。参数说明:-name "*.png"按你自己的实际格式改成*.npy*.jpg都可以;for d in data/*/只遍历一级目录,如果你的切面目录是嵌套两级,记得加深度参数。

看完数量后,我一般会随手抽查切片大小是否一致。医学图像在这上面有个很阴的坑:同一套数据的3D体素可能是各向同性的,比如0.625mm×0.625mm×0.625mm,直接沿着某个轴切,出来的每张二维图尺寸必然一致;但如果是各向异性体素,比如层厚1.5mm、平面内分辨率0.5mm,不同切面方向的切片尺寸就完全不同了,横断面可能是512×512,矢状面却成了512×150。处理时要么统一resize,要么按各自尺寸分别喂模型。

2.2 命名规则与数据集划分:文件匹配里的细节

命名规则决定了你后面要写多少行数据清洗代码。好的数据集会统一命名,比如img_axial_0001.png对应mask_axial_0001.png,编号完全对齐;也有的数据集用001.dcm001_label.nii.gz这种混合格式,需要手动解析。这个左心房数据集标注了"划分好的数据集",说明训练集验证集的划分已经做了,但我仍然建议自己重新核对一遍划分逻辑,避免出现训练集和验证集来自同一个病例的切片。

from pathlib import Path img_dir = Path("data/train/axial/img") mask_dir = Path("data/train/axial/mask") img_names = {p.stem for p in img_dir.glob("*.png")} mask_names = {p.stem for p in mask_dir.glob("*.png")} print(f"图像数量: {len(img_names)}") print(f"标签数量: {len(mask_names)}") print(f"缺少标签的图像: {sorted(img_names - mask_names)[:10]}") print(f"缺少图像的标签: {sorted(mask_names - img_names)[:10]}")

逻辑说明:把图像和标签文件名各转成一个集合做差集,直接列出不匹配的文件名。数据集在划分或搬运过程中丢文件、改后缀名的情况很常见,这一步能迅速暴露问题。参数说明:p.stem是Path对象的方法,返回不带后缀的文件名,如果你的命名是img_axial_0001.png,得到的stem是img_axial_0001,两边保持一致就能直接匹配。

另外要留意数据集是否按病人维度做了划分。同一个心脏的横断面切片有几十上百张,如果其中一部分进了训练集、另一部分进了验证集,模型会通过记忆切片间的相关性把验证集的Dice刷得很高,但换到新病人上立刻崩。拿到数据集时先看看文件名前缀,如果前缀包含病人编号,就按病人编号而不是文件数做划分。

2.3 切片读取与形状检查:训练前必须做的三步验证

训练前我习惯写一个最小的读取脚本,把三个切面的数据全部读一遍,打印shape和dtype,再统计每个类别的像素占比。这一步看起来笨拙,但能过滤掉一大堆后面折磨人的问题。

import numpy as np from PIL import Image def inspect_slice(img_path, mask_path): img = np.array(Image.open(img_path)) mask = np.array(Image.open(mask_path)) print(f"图像: {img.shape} {img.dtype} 范围: [{img.min()}, {img.max()}]") print(f"标签: {mask.shape} {mask.dtype} 类别值: {np.unique(mask)}") if img.shape != mask.shape: print("警告: 图像与标签尺寸不一致") return la_pixels = (mask == 1).sum() total_pixels = mask.size print(f"左心房像素占比: {la_pixels / total_pixels:.4f}") inspect_slice( "data/train/axial/img/img_axial_0010.png", "data/train/axial/mask/mask_axial_0010.png", )

逻辑说明:图像和标签尺寸一致性直接决定后续torchvision.transforms能不能正常用,类别值决定了你的损失函数怎么设置。np.unique(mask)输出的是标签里实际存在的值,如果是[0, 1]说明二分类干净,如果出现0.5[0, 1, 2],那说明有人用插值或绘画工具改过标签,必须处理。左心房像素占比这个数字很关键,经常是一整张512×512的图里只有几百个像素点属于目标区域,这时候如果你用普通的cross entropy,模型学到的几乎全是背景。

医学图像的像素范围在不同模态之间差异极大,核磁的原始值可能是几百到几千的范围,CT的HU值范围更是从-1000到3000。如果这个数据集给的是PNG格式,大概率是做过归一化的,但我仍然习惯自己再确认一遍,避免有个别切片因为窗宽窗位设置不同而显得特别亮或特别暗。

3. 标签文件解析与类别映射:把标注变成能训练的二值掩码

3.1 标签文件常见格式:从多边形坐标到像素掩码

医学数据集的标签文件格式分成三个流派:平面图像用JSON或XML存多边形顶点坐标,三维体数据用NIfTI或MHD格式存体素掩码,极少数的老数据集直接给一张画好轮廓的PNG。这个左心房数据集标题里写明包含"标签文件",具体格式不确定的情况下,我倾向于先按最常见的两种可能性做准备,也就是JSON坐标和直接可读的掩码图像。

JSON格式的标签长这样:每个切片对应一个JSON,内部存储左心房轮廓的多边形顶点。

{ "filename": "img_axial_0010.png", "size": [512, 512], "annotations": [ { "class": "left_atrium", "class_id": 1, "polygon": [ [243.5, 187.2], [245.1, 190.4], [251.8, 191.0] ] } ] }

这套结构的逻辑很简单:size记录原始图像尺寸,annotations里每个对象是一条标注,polygon是多边形的顶点坐标列表,单位是像素。需要把多边形栅格化,得到一个和原图尺寸相同的二值掩码,背景为0,左心房区域为1。

也有一种可能,标签文件直接给的是掩码图像数组,存成NIfTI格式,这种格式在三维标注工具ITK-SNAP和3D Slicer里最常见。NIfTI文件包含头信息和体素数据两部分,头信息里有一个关键的仿射矩阵(affine matrix),它记录了体素坐标和真实世界坐标之间的映射关系。如果你用nibabel库读取,这就是nii.affine

3.2 从多边形标签到二值掩码:转换脚本与两个易错参数

把JSON格式的多边形转成掩码,首选工具是opencvfillPoly,它速度快且支持凹多边形,PILImageDraw.polygon对凹多边形的支持在某些版本里并不理想。

import json import numpy as np import cv2 def json_to_mask(json_path, img_shape): with open(json_path, "r", encoding="utf-8") as f: data = json.load(f) mask = np.zeros(img_shape[:2], dtype=np.uint8) for ann in data["annotations"]: polygon = np.array(ann["polygon"], dtype=np.int32) if len(polygon.shape) != 2 or polygon.shape[1] != 2: print(f"跳过异常多边形: {ann['filename']}") continue cv2.fillPoly(mask, [polygon], 1) return mask mask = json_to_mask("data/train/axial/label/img_axial_0010.json", (512, 512)) print(f"类别: {np.unique(mask)} 左心房像素: {(mask == 1).sum()}")

逻辑说明:cv2.fillPoly接受一个多边形顶点数组,顶点类型必须是int32,用1作为填充值得到二值掩码。dtype=np.uint8是个容易忽略的细节,太多格式默认输出64位浮点,存成PNG时会多一层转换风险。参数说明:img_shape[:2]是因为有的JSON里记录的是三维尺寸,但单个切面只需要前两维;fillPoly[polygon]语法是OpenCV的老API要求,必须额外套一层列表。

如果标签文件已经是npy或nii格式,直接np.load或者用nibabel读就行,绕过了多边形栅格化这步,但需要额外注意nii文件的切片顺序。NIfTI的体素数组是[x, y, z]还是[z, y, x],取决于存储时的方向约定,读出来后要和图像数据对齐,最好手动对比一张切片确认左心房位置的左右方向是否正确。

3.3 标签与图像的空间对齐:为什么形状一样不代表对齐了

在三维医学图像里,图像和掩码形状完全一致仍然可能出现错位,这是最搞心态的隐蔽问题。原因在于三维数据的方向矩阵——NIfTI和MHD文件头里记录了体素相对于真实解剖位置的排列方向,如果图像和掩码来自不同的重采样流程,它们的坐标原点或者轴方向可能相差一个翻转或旋转。

常见的翻车方式有:图像是RAS方向(Right-Anterior-Superior),掩码却是LPI方向(Left-Posterior-Inferior),直接叠加的话,左心房会出现在图像的镜像位置。解决办法只有一个:读取三维体数据时同时读取affine矩阵,在重采样到二维切片之前,先把图像和掩码对齐到同一空间。

import nibabel as nib import numpy as np img_nii = nib.load("data/train/axial/img/patient001_axial.nii.gz") mask_nii = nib.load("data/train/axial/mask/patient001_label.nii.gz") img_data = img_nii.get_fdata() mask_data = mask_nii.get_fdata() print(f"图像体素尺寸: {img_nii.header.get_zooms()}") print(f"掩码体素尺寸: {mask_nii.header.get_zooms()}") print(f"图像方向: {img_nii.affine[:3, :3]}") print(f"掩码方向: {mask_nii.affine[:3, :3]}") if not np.allclose(img_nii.affine, mask_nii.affine): print("警告: 图像与掩码的空间定义不一致,需要对齐")

逻辑说明:header.get_zooms()返回的是每个轴上的体素间隔比如(0.625, 0.625, 0.625),如果图像和掩码的体素间隔不一致,直接沿着哪个轴切片都会出现几何失真。affine[:3, :3]是旋转缩放矩阵,对角线为正代表对应轴方向一致,出现负值说明某个轴被翻转。一旦检查出不一致,最省事的对齐方案是把掩码用nibabel.probe_orientationnibabel.orientations模块重排到与图像一致的方向。视觉上核对时,重点看左心房的左右位置和上下位置,这是方向翻转最容易暴露的地方。

4. 用可视化代码核对标签质量:切片叠加显示与逐层翻阅

4.1 一张切片加掩码叠加显示:最小可视化代码

数据集自带的可视化代码通常能跑通,但我还是习惯自己写一套更简单的,因为自带代码往往为了兼顾三种切面格式写得比较复杂,改成自己熟悉的形式,后面的调试效率更高。最小可视化代码只需要matplotlib和numpy。

import matplotlib.pyplot as plt import numpy as np from PIL import Image def show_slice(img_path, mask_path, save_path=None): img = np.array(Image.open(img_path)) mask = np.array(Image.open(mask_path)) fig, axes = plt.subplots(1, 3, figsize=(15, 5)) axes[0].imshow(img, cmap="gray") axes[0].set_title("Original Image") axes[1].imshow(mask, cmap="gray") axes[1].set_title("Mask") axes[2].imshow(img, cmap="gray") axes[2].imshow(mask, cmap="jet", alpha=0.5) axes[2].set_title("Overlay") for ax in axes: ax.axis("off") if save_path: plt.savefig(save_path, dpi=150, bbox_inches="tight") plt.show() show_slice( "data/train/axial/img/img_axial_0010.png", "data/train/axial/mask/mask_axial_0010.png", )

逻辑说明:这是我在拿到任何医学分割数据集时都会先跑的一段脚本。核心逻辑分三步:原图灰度显示、掩码单独显示、原图上叠加半透明掩码。cmap="jet"用的是matplotlib内置的彩色映射,让左心房区域变成红色系,在灰度背景下非常显眼。alpha=0.5是透明度参数,调大一些会完全遮盖原图,调小一些边界看不清,0.5通常是比较舒服的居中值。save_path参数在数据集清洗时特别有用,把所有切面的叠加图批量输出成一张长图,用眼睛扫一遍就能发现大部分标签问题。

参数说明:dpi=150可以理解为每英寸像素点数,数值越大图片越清晰,但文件也越大,批量生成时我一般调到100就够用。bbox_inches="tight"的作用是裁掉图片周围的空白边缘,否则保存出来的图会有一圈白边。

4.2 逐层翻阅工具:把几百张切片当短视频刷

医学图像分割数据集动不动就几百上千张切片,一张一张调用show_slice效率太低。我一般会做个批量输出长图的函数,把每个切面的前20层切片按网格铺到一张大图上,快速浏览。

import matplotlib.pyplot as plt import numpy as np from PIL import Image from pathlib import Path def preview_slices(img_dir, mask_dir, num_slices=20, cols=4): img_paths = sorted(Path(img_dir).glob("*.png"))[:num_slices] rows = int(np.ceil(len(img_paths) / cols)) fig, axes = plt.subplots(rows, cols, figsize=(cols * 4, rows * 4)) axes = np.array(axes).reshape(-1) for ax, img_path in zip(axes, img_paths): mask_path = Path(mask_dir) / f"mask_{img_path.stem}.png" if not mask_path.exists(): ax.set_title(f"Missing: {img_path.name}") ax.axis("off") continue img = np.array(Image.open(img_path)) mask = np.array(Image.open(mask_path)) ax.imshow(img, cmap="gray") ax.imshow(mask, cmap="jet", alpha=0.5) ax.set_title(img_path.name, fontsize=8) for ax in axes.flatten(): ax.axis("off") plt.tight_layout() plt.show() preview_slices( "data/train/axial/img", "data/train/axial/mask", num_slices=20, cols=4, )

逻辑说明:核心逻辑是循环读图叠加再排网格。num_slicescols控制网格大小,20张4列刚好排成5行,一眼就能覆盖一个切面方向的开头部分。mask_{img_path.stem}.png这个命名拼接方式依赖命名规则的一致性,如果你的数据集是纯数字编号命名,这一行需要对应改掉。出现"Missing"字样时,说明文件名匹配有问题,需要回头看2.2节的集合差集检查。

其实更高效的翻阅方式是把切片做成轮播动画或视频,用matplotlib.animation或者opencv写个视频导出脚本,把20帧合并成一个小视频,用播放器拖动时间轴快速找异常。尤其是矢状面和冠状面这两个方向,左心房的边界变化幅度大,视频刷一圈比看静态图更直观。

4.3 可视化细节:窗宽窗位、内外膜与标签噪声

可视化代码里最容易踩的坑是窗宽窗位处理不当。很多医学图像原始数据是16位整数,直接用PIL.Image.open读到的是一个范围极大的数组,直接imshow会出现整张图黑乎乎的、左心房完全看不清的情况。

def normalize_img(img): img = img.astype(np.float32) lower, upper = np.percentile(img, 2), np.percentile(img, 98) img = np.clip(img, lower, upper) img = (img - lower) / (upper - lower + 1e-6) return img

逻辑说明:用2%和98%分位数代替固定阈值的原因是,医学图像的灰度值偶尔会有极端的噪声点,比如金属伪影或扫描时留下的高亮标记,用百分位数可以避免这些极端点破坏整体的可视化效果。np.clip把所有低于下限的像素拉低下限,高于上限的拉高上限,(img - lower) / (upper - lower)做的是归一化,把线性区间映射到0到1之间。

标签文件的噪声又是另一回事。左心房手工标注时,医生通常沿着心内膜边界画一圈,但为了保险会略微向内收缩一点,这就导致标签里出现极细的、只有一两个像素宽的不规则凸起。某些数据集明明标注为"2类别",实际跑出来却有三四个类别值,打开标签看一眼就会发现,有些孤立像素值被误填成了别的数。这类噪声通常不影响大局,但会让训练时的Dice曲线变得非常毛糙,如果发现验证集Dice在0.7附近死活上不去,先用可视化脚本把所有切面的标签扫一遍。

5. 医学图像分割数据集避坑:标注、方向与训练前检查的5个血泪教训

5.1 图像和标签数量对不上,训练时索引越界

现象:训练脚本跑了一半报IndexError: index 997 is out of bounds for axis 0 with size 997,或者损失突然变成nan

原因:数据目录里图像有1000张,标签只有997张,大部分数据加载器按最短数据集的长度构建索引,多出来的3张图要么被忽略要么越界。产生这个问题的根源是在数据集划分或压缩打包时漏文件,常见于通过网盘断点续传下载的情况。

解决:训练前先用2.2节的集合差集脚本做一次全量检查,打印出所有缺失文件名单。把缺失文件补齐或从数据列表里剔除后,重新统计数据量,确认img和mask数量完全一致后再开始训练。另外,数据加载器里加上assert len(img_paths) == len(mask_paths),这在调试时能立刻暴露问题。

5.2 mask用resize插值,标签出现灰色或小数

现象:训练时发现某些batch的标签里有0.5或者0.4这种非整数像素值,损失函数一路乱跳,Dice永远上不去。

原因:数据增强阶段调用transform.Resize时,默认的插值方式是双线性插值,对mask做双线性插值会把边缘像素变成0和1之间的过渡值。这些过渡值既不算是背景也算不上前景,模型收到的是个不伦不类的训练目标。

解决:所有对mask的resize操作必须单独指定nearest插值,在PyTorch的torchvision.transforms里有对应写法。图像用默认的双线性,mask和图像不能走同一个transform流程,做一个分开的transform链。写完数据加载器后,打印一个batch的target.unique(),确认只有0和1,这一步能帮你拦住大量类似的低级错误。

5.3 三维NIfTI方向翻转,左心房出现在镜像位置

现象:可视化时发现叠加图上左心房的位置偏到了右侧,看起来像左右镜像。如果不检查直接训练,模型的Dice会低到难以置信,因为输入和标签在空间上互相矛盾。

原因:原始核磁扫描时,不同设备或不同后处理流程产生的NIfTI文件,方向约定可能不同。有的数据用RAS坐标系,有的用LPS坐标系,坐标轴指向反了,数组内容的左右顺序就是反的。

解决:按3.3节的方法读取affine矩阵,用nibabel.orientations把标签映射到图像坐标系。更简单的做法是直接用nibabel.as_closest_canonical函数把两者都转成标准方向,但这会改变体素数组的顺序,需要同步调整图像数据。最常见且稳妥的方案是:用nibabel.probe_orientation检查方向,如果不同,用nibabel.orientations.axcodes2orntapply_orientation重排数组顺序。

5.4 标签边界存在断裂,掩码内部出现空洞

现象:三维可视化时左心房表面坑坑洼洼,有个别层切片上左心房区域被分成了两半,中间出现一条窄缝,或者掩码内部有个一个像素大小的黑点。

原因:标注是逐层手绘的,医生在连续层之间可能做出了不一致的决策,或者某些层因为图像对比度不佳,把左心房和肺静脉连接处画丢了。更常见的是某些标注工具在导出多边形栅格化时,对顶点坐标做了四舍五入,导致细小的空洞。

解决:这类小空洞和二值噪声一般不需要逐层手工修复,直接用一个形态学闭运算处理掩码即可,具体用scipy.ndimage.binary_closing,结构元素大小推荐3×3像素。如果空洞大到影响训练,说明数据质量本身有问题,建议检查整层标签是否需要重新标注。另外,2D训练本身对标签内的小洞不太敏感,模型天然倾向于补全空洞区域,关键是边缘不能有系统性错位。

5.5 类别不平衡严重,模型收敛到全背景

现象:训练持续几十个epoch后,模型的预测图变成全黑,Dice在0到0.2之间反复横跳,损失函数看起来降了但验证指标原地不动。

原因:左心房在横断面上的像素占比通常只有2%到5%,在矢状面或冠状面某些极端切片上甚至不到1%,如果损失函数用普通交叉熵,背景类别的梯度完全淹没了前景类别。

解决:把损失函数换成Dice Loss或Focal Loss,Dice Loss直接优化目标指标本身,比较适合这种前景占比极小的场景;Focal Loss通过调制系数让模型把注意力放在难分类的前景像素上。如果只能用交叉熵,那就必须给前景类别设置权重,权重值与前景像素占比成反比。这个问题在数据集层面还能用预处理缓解:训练时只选取前景像素占比大于某个阈值的切片参与训练,或者对前景占比极低的切片做重采样。

6. 把数据集跑成能用的模型:2D U-Net最简训练脚本与Dice验证

6.1 最简训练脚本:从数据读取到Dice输出

可视化没问题、标签对齐没问题之后,接下来用这个数据集验证分割管线能不能通。2D U-Net是医学图像分割的默认骨架,配合Dice Loss训练,用最简脚本就能跑通。

import torch import torch.nn as nn class DiceLoss(nn.Module): def __init__(self, smooth=1.0): super().__init__() self.smooth = smooth def forward(self, logits, targets): probs = torch.sigmoid(logits) intersection = (probs * targets).sum() union = probs.sum() + targets.sum() return 1.0 - (2.0 * intersection + self.smooth) / (union + self.smooth)

逻辑说明:Dice Loss是1减去Dice系数,Dice系数衡量的是预测和标签之间的重叠程度。smooth这个平滑项防止分母为0,同时也起到软化损失的作用;smooth设置为1.0是医学分割里比较常见的经验值,如果你的数据里左心房非常小,建议调大到2.0或更高,避免损失值抖动太剧烈。训练时配合Adam优化器,学习率从1e-4起步,配合ReduceLROnPlateau在Dice不再提升时自动把学习率降到原来的十分之一。

6.2 验证与下一步的验证参考

验证阶段用两个指标:Dice系数和95%豪斯多夫距离。Dice衡量的是区域重叠率,适合判断整体分割准不准;豪斯多夫距离衡量的是边界最大偏差,临床场景里更关心这个,因为边界偏移会直接影响消融范围规划。这两个指标配合使用,能同时看到"区域对不对"和"边界贴不贴"两个维度的表现。

训练完成后,我会把预测结果重新叠加到原始切片上,生成一张带边界的对比图,人工检查边界是否落在了解剖上合理的位置。这个步骤没有定量指标能替代,模型Dice再高,只要边界位置偏离心内膜超过3毫米,落地阶段就难以为继。

我现在拿到一个新的医学图像分割数据集,第一件事永远是先抽查20层切片的叠加图,再谈训练;这个习惯救过我太多次了,很多看似神秘的问题其实都出在最基本的数据组织上。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询