4. H.265帧类型识别与NAL单元分类
任务目标
编写一个Python程序,读取H.265/HEVC裸码流(Annex B格式),逐字节解析NAL单元头,识别NAL类型,并根据VPS/SPS/PPS及关键帧规则区分I帧、P帧、B帧。
核心实现
import os
import sys
# HEVC NAL类型定义
NAL_TYPES = {
0: 'TRAIL_N', 1: 'TRAIL_R', 2: 'TSA_N', 3: 'TSA_R',
4: 'STSA_N', 5: 'STSA_R', 6: 'RADL_N', 7: 'RADL_R',
8: 'RASL_N', 9: 'RASL_R', 10: 'BLA_W_LP', 11: 'BLA_W_RADL',
12: 'BLA_N_LP', 13: 'IDR_W_RADL', 14: 'IDR_N_LP', 15: 'CRA_NUT',
16: 'IRAP_VCL22', 17: 'IRAP_VCL23', 18: 'IRAP_VCL24', 19: 'IRAP_VCL25',
20: 'IRAP_VCL26', 21: 'IRAP_VCL27', 22: 'IRAP_VCL28', 23: 'IRAP_VCL29',
24: 'IRAP_VCL30', 25: 'IRAP_VCL31', 32: 'VPS', 33: 'SPS',
34: 'PPS', 35: 'AUD', 36: 'EOS', 37: 'EOB', 38: 'FD',
39: 'PREFIX_SEI', 40: 'SUFFIX_SEI', 41: 'AP', 42: 'UNSPEC42',
43: 'UNSPEC43', 44: 'UNSPEC44', 45: 'UNSPEC45', 46: 'UNSPEC46',
47: 'UNSPEC47', 48: 'UNSPEC48'
}
def find_start_codes(data):
"""查找所有起始码位置(00 00 01 或 00 00 00 01)"""
positions = []
i = 0
while i < len(data) - 3:
if data[i] == 0 and data[i+1] == 0:
if data[i+2] == 1:
positions.append((i, 3))
i += 3
elif data[i+2] == 0 and data[i+3] == 1:
positions.append((i, 4))
i += 4
else:
i += 1
else:
i += 1
return positions
def parse_nal_header(byte1, byte2):
"""解析2字节NAL头,返回(nal_type, temporal_id, layer_id)"""
forbidden = (byte1 & 0x80) >> 7
nal_type = (byte1 & 0x7E) >> 1
layer_id = ((byte1 & 0x01) << 5) | ((byte2 & 0xF8) >> 3)
temporal_id = byte2 & 0x07
return nal_type, temporal_id, layer_id
def classify_frame(nal_type):
"""根据NAL类型判断帧类型"""
if nal_type in (13, 14): # IDR_W_RADL, IDR_N_LP
return 'I帧'
elif nal_type == 15: # CRA_NUT
return 'I帧(CR)'
elif nal_type in (0, 1): # TRAIL_N/R
return 'P/B帧(需参考前帧判断)'
elif nal_type in (2, 3): # TSA_N/R
return 'P/B帧(TSA)'
elif nal_type in (4, 5): # STSA_N/R
return 'P/B帧(STSA)'
elif nal_type in (6, 7): # RADL_N/R
return 'B帧(RADL)'
elif nal_type in (8, 9): # RASL_N/R
return 'B帧(RASL)'
elif nal_type in (16, 17, 18, 19, 20, 21, 22, 23, 24, 25):
return 'IRAP帧'
else:
return None # 非VCL
def analyze_hevc(filepath):
with open(filepath, 'rb') as f:
data = f.read()
starts = find_start_codes(data)
if not starts:
print('未找到起始码,文件可能不是Annex B格式')
return
nal_list = []
for idx, (pos, sc_len) in enumerate(starts):
nal_start = pos + sc_len
if idx + 1 < len(starts):
nal_end = starts[idx + 1][0]
else:
nal_end = len(data)
if nal_end - nal_start < 2:
continue
byte1 = data[nal_start]
byte2 = data[nal_start + 1]
nal_type, temporal_id, layer_id = parse_nal_header(byte1, byte2)
# 跳过填充字节(0x00)后的NAL
if nal_type == 0 and byte1 == 0 and byte2 == 0:
continue
nal_list.append({
'offset': pos,
'size': nal_end - pos,
'type_id': nal_type,
'type_name': NAL_TYPES.get(nal_type, f'UNKNOWN_{nal_type}'),
'temporal_id': temporal_id,
'layer_id': layer_id
})
# 输出NAL统计
print(f'文件: {filepath}')
print(f'总大小: {len(data)} 字节, NAL单元数: {len(nal_list)}\n')
print(f'{"序号":<6}{"偏移":<10}{"大小":<8}{"类型ID":<8}{"NAL类型":<18}{"时域ID":<8}{"帧分类"}')
print('-' * 80)
vcl_count = 0
for i, nal in enumerate(nal_list):
frame_class = classify_frame(nal['type_id'])
if frame_class:
vcl_count += 1
print(f'{i:<6}{nal["offset"]:<10}{nal["size"]:<8}'
f'{nal["type_id"]:<8}{nal["type_name"]:<18}'
f'{nal["temporal_id"]:<8}{frame_class or "-"}')
print(f'\nVCL NAL单元数: {vcl_count}')
if __name__ == '__main__':
if len(sys.argv) < 2:
print('用法: python hevc_nal_parser.py <输入.h265>')
sys.exit(1)
analyze_hevc(sys.argv[1])
运行与验证
- 准备输入:使用任意H.265裸码流文件(如用ffmpeg生成测试文件):
ffmpeg -f lavfi -i testsrc=duration=2:size=320x240:rate=30 -c:v libx265 -x265-params log-level=error test.h265 - 运行程序:
python hevc_nal_parser.py test.h265 - 通过条件:
- 输出中应包含VPS(32)、SPS(33)、PPS(34)各至少一个;
- 至少有一个IDR_W_RADL(13)或IDR_N_LP(14)类型的I帧;
- 存在TRAIL_R(1)类型的P/B帧;
- 所有NAL的偏移量递增且不重叠。
判定说明
HEVC不像H.264那样通过slice_type直接区分P/B帧。本程序通过NAL类型识别帧类型:IDR和CRA为I帧;RADL/RASL为B帧;TRAIL/TSA/STSA可能是P帧或B帧,需进一步解析slice_segment_header中的slice_type字段(值为0=B,1=P,2=I)才能精确区分。如需精确区分P/B,可在识别到TRAIL/TSA/STSA后,用解析器(如h265nal库)读取slice_type。