scipy.io.loadmat 嵌套结构(即字典)

41

使用给定的例程(如何使用scipy加载Matlab .mat文件),我无法访问更深层嵌套结构并将其恢复为字典。

为了更详细地说明我遇到的问题,我提供以下玩具示例:

load scipy.io as spio
a = {'b':{'c':{'d': 3}}}
# my dictionary: a['b']['c']['d'] = 3
spio.savemat('xy.mat',a)

现在我想把mat-File读取回Python中。 我尝试了以下代码:

vig=spio.loadmat('xy.mat',squeeze_me=True)
如果现在我想访问这些字段,我会得到:
>> vig['b']
array(((array(3),),), dtype=[('c', '|O8')])
>> vig['b']['c']
array(array((3,), dtype=[('d', '|O8')]), dtype=object)
>> vig['b']['c']['d']
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)

/<ipython console> in <module>()

ValueError: field named d not found.

然而,通过使用选项 struct_as_record=False 可以访问该字段:

v=spio.loadmat('xy.mat',squeeze_me=True,struct_as_record=False)

现在可以通过以下方式访问它

>> v['b'].c.d
array(3)

使用默认设置,可以通过以下表达式深入嵌套:vig['b']['c'].item()['d'].item(),解析结构化数组和对象数组的混合。虽然 ['b'] 是字典索引,但其他的都是字段名称索引。 - hpaulj
6个回答

65

以下是重构字典的函数,只需使用该loadmat而不是scipy.io的loadmat即可:

import scipy.io as spio

def loadmat(filename):
    '''
    this function should be called instead of direct spio.loadmat
    as it cures the problem of not properly recovering python dictionaries
    from mat files. It calls the function check keys to cure all entries
    which are still mat-objects
    '''
    data = spio.loadmat(filename, struct_as_record=False, squeeze_me=True)
    return _check_keys(data)

def _check_keys(dict):
    '''
    checks if entries in dictionary are mat-objects. If yes
    todict is called to change them to nested dictionaries
    '''
    for key in dict:
        if isinstance(dict[key], spio.matlab.mio5_params.mat_struct):
            dict[key] = _todict(dict[key])
    return dict        

def _todict(matobj):
    '''
    A recursive function which constructs from matobjects nested dictionaries
    '''
    dict = {}
    for strg in matobj._fieldnames:
        elem = matobj.__dict__[strg]
        if isinstance(elem, spio.matlab.mio5_params.mat_struct):
            dict[strg] = _todict(elem)
        else:
            dict[strg] = elem
    return dict

3
这需要更好地宣传。目前scipy的loadmat实现真的很难处理。干得好! - TomNorway
1
实际上,@jpapon在下面提供的方法甚至更好,在处理像图像这样的数组时是必需的。 - TomNorway
你应该一直向上前进!请将此消息发送给Mathworks并告诉他们要振作起来。 - Rakshit Kothari
这绝对是最好的答案,但仍然不完美,因为它压缩了1元素维度。我可能有这个修复程序的不寻常需求+需要保持1元素维度。 - bpops
我甚至还没有完全理解我遇到的问题是什么,但当我偶然发现这个答案时,我立刻理解了它。现在这才是一个好的stackoverflow答案。 - J B
显示剩余3条评论

31

这是对mergen答案的补充,不幸的是,如果它到达一个对象的单元数组,它将停止递归。下面的版本将改为创建对象列表,并在可能的情况下继续递归到单元数组元素中。

import scipy.io as spio
import numpy as np


def loadmat(filename):
    '''
    this function should be called instead of direct spio.loadmat
    as it cures the problem of not properly recovering python dictionaries
    from mat files. It calls the function check keys to cure all entries
    which are still mat-objects
    '''
    def _check_keys(d):
        '''
        checks if entries in dictionary are mat-objects. If yes
        todict is called to change them to nested dictionaries
        '''
        for key in d:
            if isinstance(d[key], spio.matlab.mio5_params.mat_struct):
                d[key] = _todict(d[key])
        return d

    def _todict(matobj):
        '''
        A recursive function which constructs from matobjects nested dictionaries
        '''
        d = {}
        for strg in matobj._fieldnames:
            elem = matobj.__dict__[strg]
            if isinstance(elem, spio.matlab.mio5_params.mat_struct):
                d[strg] = _todict(elem)
            elif isinstance(elem, np.ndarray):
                d[strg] = _tolist(elem)
            else:
                d[strg] = elem
        return d

    def _tolist(ndarray):
        '''
        A recursive function which constructs lists from cellarrays
        (which are loaded as numpy ndarrays), recursing into the elements
        if they contain matobjects.
        '''
        elem_list = []
        for sub_elem in ndarray:
            if isinstance(sub_elem, spio.matlab.mio5_params.mat_struct):
                elem_list.append(_todict(sub_elem))
            elif isinstance(sub_elem, np.ndarray):
                elem_list.append(_tolist(sub_elem))
            else:
                elem_list.append(sub_elem)
        return elem_list
    data = spio.loadmat(filename, struct_as_record=False, squeeze_me=True)
    return _check_keys(data)

7
做得很好。如果能将此内容并入scipy中将是很棒的。 - TomNorway
1
这段代码将包含双精度数组的字段的Matlab结构体转换为包含双精度列表的Python字典,这可能是作者的意图,但不一定是大多数人想要的。更好的返回值是一个值为ndarray的字典。 - jcbsv
2
我建议使用改进版,在将数组转换为列表之前测试结构体的数组内容。 - jcbsv

7
截至scipy >= 1.5.0,使用simplify_cells参数现在内置了此功能。
from scipy.io import loadmat

mat_dict = loadmat(file_name, simplify_cells=True)

4
我在scipy邮件列表上得到建议(https://mail.python.org/pipermail/scipy-user/),可以通过另外两种方式访问这些数据。
这个方法可行:
import scipy.io as spio
vig=spio.loadmat('xy.mat')
print vig['b'][0, 0]['c'][0, 0]['d'][0, 0]

我的电脑上的输出为: 3

这种访问方式的原因是:“由于历史原因,在Matlab中,即使是标量,所有内容也至少是2D数组。”因此,默认情况下,scipy.io.loadmat模仿了Matlab的行为。


1
我曾经盲目地遇到了[0,0]这个东西,自己不知道它为什么存在,但是我在逻辑上无法将其与级联的[0,0]扩展,因此感到非常困惑。很高兴我找到了这个页面。 - J B

2
发现了一个解决方案,可以通过以下方式访问“scipy.io.matlab.mio5_params.mat_struct对象”的内容:
v['b'].__dict__['c'].__dict__['d']

你在loadmat中使用了哪些选项? - alf3000

1
另一种可行的方法:

import scipy.io as spio
vig=spio.loadmat('xy.mat',squeeze_me=True)
print vig['b']['c'].item()['d']

输出:

3

我也是在scipy邮件列表上学到了这种方法。我当然还不理解为什么需要添加'.item()',以及:

print vig['b']['c']['d']

将会抛出错误:

IndexError: 只有整数、切片(:)、省略号(...)、numpy.newaxis(None)和整数或布尔类型的数组是有效索引。

但当我了解后,我会补充解释。numpy.ndarray.item 的解释(来自 numpy 参考文献):将数组的一个元素复制到标准 Python 标量并返回它。

(请注意,这个答案基本上与 hpaulj 对最初问题的评论相同,但我觉得评论不够“可见”或易于理解。当我几周前第一次搜索解决方案时,我肯定没有注意到它)。


为什么print vig['b']['c']['d']无法正常工作:vig['b']['c']返回一个numpy.void对象,因此如果您尝试直接访问其中的项,则Python会抛出错误。方法item()返回缓冲区对象(https://numpy.org/doc/stable/reference/generated/numpy.ndarray.item.html),然后您可以访问其内容。 - Adrian

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接