遥感小白必看:RPC 和 RPB 文件到底是什么?怎么用 Python 轻松读取它们?

大家好,我是你们的老朋友——遥感码农小白。

之前有人问我:“RPC 文件是什么鬼?RPB 又是啥?为什么有的卫星给 .rpc,有的是 .rpb?读取的时候总是报错怎么办?”

今天这篇就来手把手帮你搞懂这件事,保证零基础也能看懂、能上手!

一、先搞清楚:RPC 是什么?为什么遥感离不开它?

简单说,RPC = Rational Polynomial Coefficients(有理多项式系数)。

卫星拍下来的影像,原始的样子是“歪的、斜的、扭曲的”,因为卫星在飞、地球是圆的、镜头有畸变……直接拿来用会叠不对地图。

RPC 就是卫星厂家提前帮你算好的一组“魔法公式”,告诉你:

“图像上的这个像素(行、列),对应地球上的哪个经纬度(+高度)?”

用这组系数,你就能做:

  • 几何校正(让影像“躺平”)
  • 正射纠正(去掉地形起伏影响)
  • 影像配准、镶嵌、入库……

一句话:没有 RPC,你就没法把卫星照片精准叠到 Google 地图上

我遇到不提供RPC文件的卫星影像有:

风云4号静止卫星

日本葵花8静止卫星

sentinel3海洋卫星

它们的特点是nc数据,它们把定位信息直接写在nc文件内部,要通过插值实现几何定位。

landsat系列也不提供rpc文件,因为usgs直接做好了系统级的几何粗校正,也就是说,我们下载的landsat影像已经做好了粗几何校正无需rpc文件。

二、RPC 文件 vs RPB 文件:长得一样,其实差不多

项目 RPC 文件 RPB 文件
全称 Rational Polynomial Coefficients Rapid Positioning Binary(其实也是文本)
常见卫星 北京三号、吉林一号 高分1号、高分2号、高分3号、高分6号、高分7号、DigitalGlobe(WorldView、QuickBird、GeoEye)
文件扩展名 .rpc / .txt .rpb
格式 键值对 + 系数列表 直接四个括号系数列表 + 少量元数据
内容示例 LINE_OFF = 2457.5 SAMP_NUM_COEFF = (1.23, -0.45, …) satId = “WV02”; LINE_NUMCOEF = ( … );

核心结论:两者本质都是存同一套 RPC 系数,只是写法不同。

  • RPC 更规范,像配置文件
  • RPB 更“简陋”,像数组

.rpc文件如下:

image-20260306160528571

.rpb文件如下:

image-20260306155736115

截图没截全,不过不影响理解。你们对比这两张图就能看出区别了。

所以写代码的时候,得同时兼容两种格式。

三、新手最头疼的:怎么用 Python 把它们读出来?

好消息:用几行代码就能搞定。

坏消息:不同厂家写法五花八门,一不小心就解析失败。

下面给你们一个我自己写的“万能解析器”,直接复制就能用。

def extract_coefficients(text: str):
    """
    从 RPB 或类似格式的文本中提取四个系数数组(每个应有 20 个浮点数)。

    返回:
        四个浮点数列表,按顺序对应:
        [LINE_NUM_COEFF, LINE_DEN_COEFF, SAMP_NUM_COEFF, SAMP_DEN_COEFF]
    """
    # 匹配括号内的内容(非贪婪,允许多行)
    pattern = r'\(\s*([^)]+?)\s*\)'
    matches = re.findall(pattern, text, re.DOTALL)

    if len(matches) != 4:
        raise ValueError(f"预期找到 4 个系数数组,实际找到 {len(matches)} 个")

    result = []
    for block in matches:
        # 清理多余空白、换行、逗号分隔
        cleaned = re.sub(r'\s+', ' ', block.strip())
        numbers_str = [x.strip() for x in cleaned.split(',') if x.strip()]

        if len(numbers_str) != 20:
            raise ValueError(f"系数块长度不为 20,得到 {len(numbers_str)} 个元素:\n{cleaned[:100]}...")

        try:
            floats = [float(x) for x in numbers_str]
            result.append(floats)
        except ValueError as e:
            raise ValueError(f"系数转换失败:{e}\n原始块:{cleaned[:200]}...")

    return result


def parse_rpc_file(rpc_path: str):
    """
    通用解析 RPC 或 RPB 文件,返回标准化的字典。
    支持标准 RPC 键值格式、带索引格式、RPB 纯括号格式。

    返回的 data 字典中至少包含:
    - LINE_OFF, SAMP_OFF, LAT_OFF, LONG_OFF, HEIGHT_OFF
    - LINE_SCALE, SAMP_SCALE, LAT_SCALE, LONG_SCALE, HEIGHT_SCALE
    - LINE_NUM_COEFF, LINE_DEN_COEFF, SAMP_NUM_COEFF, SAMP_DEN_COEFF (均为 list[float])
    """
    with open(rpc_path, 'r', encoding='utf-8', errors='ignore') as f:
        content = f.read()

    data = {}

    # ------------------- 步骤1:清理文本 -------------------
    lines = content.splitlines()
    cleaned_lines = []
    for line in lines:
        # 去注释、单位、结尾分号
        line = line.split('//')[0].split(';')[0]
        line = re.sub(r'(pixels|degrees|meters)', '', line, flags=re.I)
        cleaned_lines.append(line.strip())

    # ------------------- 步骤2:标准键值对解析 -------------------
    key_map = {
        'LINEOFFSET': 'LINE_OFF', 'LINE_OFF': 'LINE_OFF',
        'SAMPOFFSET': 'SAMP_OFF', 'SAMP_OFF': 'SAMP_OFF', 'SAMPLEOFFSET': 'SAMP_OFF',
        'LATOFFSET': 'LAT_OFF', 'LAT_OFF': 'LAT_OFF',
        'LONGOFFSET': 'LONG_OFF', 'LONG_OFF': 'LONG_OFF',
        'HEIGHTOFFSET': 'HEIGHT_OFF', 'HEIGHT_OFF': 'HEIGHT_OFF',
        'LINESCALE': 'LINE_SCALE', 'LINE_SCALE': 'LINE_SCALE',
        'SAMPSCALE': 'SAMP_SCALE', 'SAMP_SCALE': 'SAMP_SCALE',
        'LATSCALE': 'LAT_SCALE', 'LAT_SCALE': 'LAT_SCALE',
        'LONGSCALE': 'LONG_SCALE', 'LONG_SCALE': 'LONG_SCALE',
        'HEIGHTSCALE': 'HEIGHT_SCALE', 'HEIGHT_SCALE': 'HEIGHT_SCALE',
        # 系数键(部分厂商用不同写法)
        'LINENUMCOEF': 'LINE_NUM_COEFF', 'LINE_NUM_COEFF': 'LINE_NUM_COEFF',
        'LINEDENCOEF': 'LINE_DEN_COEFF', 'LINE_DEN_COEFF': 'LINE_DEN_COEFF',
        'SAMPNUMCOEF': 'SAMP_NUM_COEFF', 'SAMP_NUM_COEFF': 'SAMP_NUM_COEFF',
        'SAMPDENCOEF': 'SAMP_DEN_COEFF', 'SAMP_DEN_COEFF': 'SAMP_DEN_COEFF',
    }

    for line in cleaned_lines:
        if not line:
            continue
        parts = re.split(r'[:=]', line, maxsplit=1)
        if len(parts) == 2:
            k = parts[0].strip().upper()
            v = parts[1].strip()
            if k in key_map:
                std_key = key_map[k]
                # 如果是系数,保留原始字符串,后续统一处理
                data[std_key] = v

    # ------------------- 步骤3:系数兜底解析(正则匹配括号) -------------------
    coeff_keys = ['LINE_NUM_COEFF', 'LINE_DEN_COEFF', 'SAMP_NUM_COEFF', 'SAMP_DEN_COEFF']

    for std_key in coeff_keys:
        if std_key in data and isinstance(data[std_key], str) and len(data[std_key]) > 50:
            # 已经有内容,且看起来像系数字符串,跳过
            continue

        # 尝试匹配 XXX = ( ... )
        for raw_key in [k for k, v in key_map.items() if v == std_key]:
            pattern = re.compile(
                re.escape(raw_key) + r'\s*[:=]\s*[(\[]\s*(.*?)\s*[)\]]',
                re.DOTALL | re.IGNORECASE
            )
            match = pattern.search(content)
            if match:
                val = match.group(1).replace('\n', ' ').replace('\r', ' ')
                data[std_key] = val
                break

    # ------------------- 步骤4:带索引的逐行解析(极少数情况) -------------------
    for std_key in coeff_keys:
        if std_key in data:
            continue
        collected = {}
        for line in cleaned_lines:
            parts = re.split(r'[:=]', line, maxsplit=1)
            if len(parts) != 2:
                continue
            k = parts[0].strip().upper()
            v = parts[1].strip()
            m = re.match(r'^([A-Z]+(?:NUM|DEN)COEF)_?(\d+)$', k)
            if m and key_map.get(m.group(1)) == std_key:
                idx = int(m.group(2))
                if 1 <= idx <= 20:
                    collected[idx] = v
        if len(collected) == 20:
            data[std_key] = " ".join(str(collected[i]) for i in range(1, 21))

    # ------------------- 步骤5:RPB 格式兜底 -------------------
    # 如果四个系数都没解析出来,或者 LINE_NUM_COEFF 明显太短
    missing_coeffs = any(k not in data for k in coeff_keys)
    short_coeff = 'LINE_NUM_COEFF' in data and isinstance(data['LINE_NUM_COEFF'], str) and len(
        data['LINE_NUM_COEFF'].split()) < 15

    if missing_coeffs or short_coeff:
        try:
            coef_lists = extract_coefficients(content)
            if len(coef_lists) == 4:
                data['LINE_NUM_COEFF'] = coef_lists[0]
                data['LINE_DEN_COEFF'] = coef_lists[1]
                data['SAMP_NUM_COEFF'] = coef_lists[2]
                data['SAMP_DEN_COEFF'] = coef_lists[3]
                print("检测到 RPB 格式,已从括号中提取系数")
        except Exception as e:
            print(f"尝试解析 RPB 格式失败:{e}")

    # ------------------- 最终转换:字符串 → float 列表 -------------------
    for key in coeff_keys:
        if key in data and isinstance(data[key], str):
            try:
                nums = [float(x) for x in re.findall(r'[-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?', data[key])]
                if len(nums) == 20:
                    data[key] = nums
                else:
                    print(f"警告:{key} 解析得到 {len(nums)} 个系数(应为20)")
            except:
                print(f"警告:无法将 {key} 转换为浮点数列表")

    # 偏移和尺度保持字符串或转 float
    for k in ['LINE_OFF', 'SAMP_OFF', 'LAT_OFF', 'LONG_OFF', 'HEIGHT_OFF',
              'LINE_SCALE', 'SAMP_SCALE', 'LAT_SCALE', 'LONG_SCALE', 'HEIGHT_SCALE']:
        if k in data and isinstance(data[k], str):
            try:
                data[k] = float(data[k])
            except:
                pass

    return data

最后送新人一句话

遥感入门最坑的不是算法,而是数据格式千奇百怪

喜欢就点个在看+分享给同路的遥感小伙伴吧!