generate_anchors.py代码解析

理解Anchor机制
本文解析了目标检测中Anchor机制的本质,即SPP思想的逆向应用,详细介绍了如何通过不同纵横比和尺度生成9种基本Anchor。

anchor
首先我们需要知道anchor的本质是什么,本质是SPP(spatial pyramid pooling)思想的逆向。而SPP本身是做什么的呢,就是将不同尺寸的输入resize成为相同尺寸的输出。所以SPP的逆向就是,将相同尺寸的输出,倒推得到不同尺寸的输入。接下来是anchor的窗口尺寸,这个不难理解,三个面积尺寸(1282,2562,512^2),然后在每个面积尺寸下,取三种不同的长宽比例(1:1,1:2,2:1).这样一来,我们得到了一共9种面积尺寸各异的anchor。

作者:马塔
链接:https://www.zhihu.com/question/42205480/answer/155759667
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
/home/ubuntu/tf-faster-rcnn-master/lib/layer_utils/generate_anchors.py

# --------------------------------------------------------
# Faster R-CNN
# Copyright (c) 2015 Microsoft
# Licensed under The MIT License [see LICENSE for details]
# Written by Ross Girshick and Sean Bell
# --------------------------------------------------------
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import numpy as np


# Verify that we compute the same anchors as Shaoqing's matlab implementation:
#
#    >> load output/rpn_cachedir/faster_rcnn_VOC2007_ZF_stage1_rpn/anchors.mat
#    >> anchors
#
#    anchors =
#
#       -83   -39   100    56  w=184 h=96
#      -175   -87   192   104  w=
#      -359  -183   376   200
#       -55   -55    72    72
#      -119  -119   136   136
#      -247  -247   264   264
#       -35   -79    52    96
#       -79  -167    96   184
#      -167  -343   184   360

# array([[ -83.,  -39.,  100.,   56.],
#       [-175.,  -87.,  192.,  104.],
#       [-359., -183.,  376.,  200.],
#       [ -55.,  -55.,   72.,   72.],
#       [-119., -119.,  136.,  136.],
#       [-247., -247.,  264.,  264.],
#       [ -35.,  -79.,   52.,   96.],
#       [ -79., -167.,   96.,  184.],
#       [-167., -343.,  184.,  360.]])

def generate_anchors(base_size=16, ratios=[0.5, 1, 2],
                     scales=2 ** np.arange(3, 6)):#anchors的大小,尺度分别为2^3,2^4,2^5,(8,16,32)
                     # anchor的面积尺寸就是16*8,16*16,16*32
  """
  Generate形成 anchor (reference) windows by enumerating列举 aspect ratios 纵横比X
  scales wrt a reference (0, 0, 15, 15) window.
  """

  base_anchor = np.array([1, 1, base_size, base_size]) - 1 #(0,0,15,15)参考anchors
  ratio_anchors = _ratio_enum(base_anchor, ratios) #得到ws,hs,x_ctr, y_ctr anchor 矩阵
  anchors = np.vstack([_scale_enum(ratio_anchors[i, :], scales)
                       for i in range(ratio_anchors.shape[0])])#将不同纵横比的anchor,进行不同尺度变换,并将结果沿竖直(按行顺序)方法把数组给堆叠起来
  return anchors


def _whctrs(anchor):
  """
  Return width, height, x center, and y center for an anchor (window)。anchors中心的(x,y)坐标
  """

  w = anchor[2] - anchor[0] + 1
  h = anchor[3] - anchor[1] + 1
  x_ctr = anchor[0] + 0.5 * (w - 1)
  y_ctr = anchor[1] + 0.5 * (h - 1)
  return w, h, x_ctr, y_ctr


def _mkanchors(ws, hs, x_ctr, y_ctr):
  """
  Given a vector of widths (ws) and heights (hs) around a center
  (x_ctr, y_ctr), output a set of anchors (windows).将w,h,x_ctr,yctr放入一个向量中,得到一组anchors 窗口坐标
  """

  ws = ws[:, np.newaxis] #np.newaxis插入新的维度
  hs = hs[:, np.newaxis]
  anchors = np.hstack((x_ctr - 0.5 * (ws - 1),#xmin
                       y_ctr - 0.5 * (hs - 1),#ymin 
                       x_ctr + 0.5 * (ws - 1),#xmax
                       y_ctr + 0.5 * (hs - 1)))#水平(按列顺序)把数组给堆叠起来
  return anchors


def _ratio_enum(anchor, ratios):
  """
  Enumerate列举 a set of anchors for each aspect ratio wrt an anchor.不同纵横比的anchor
  """

  w, h, x_ctr, y_ctr = _whctrs(anchor)# 获取宽、高、中心坐标
  size = w * h#面积
  size_ratios = size / ratios#ratios=[0.5, 1, 2],获得三种尺寸的anchor
  ws = np.round(np.sqrt(size_ratios))#开平方,然后四舍五入到个位数
  hs = np.round(ws * ratios)#返回浮点数x的四舍五入值,默认小数位为0
  anchors = _mkanchors(ws, hs, x_ctr, y_ctr)#添加到矩阵中
  return anchors #返回anchor矩阵


def _scale_enum(anchor, scales):
  """
  Enumerate a set of anchors for each scale wrt an anchor.不同尺度的anchor
  """

  w, h, x_ctr, y_ctr = _whctrs(anchor)
  ws = w * scales
  hs = h * scales
  anchors = _mkanchors(ws, hs, x_ctr, y_ctr)
  return anchors


if __name__ == '__main__':
  import time

  t = time.time()#开始计时
  a = generate_anchors()#得到anchors
  print(time.time() - t)#生成anchors所用的时间
  print(a)
  from IPython import embed;
#断点调试
  embed()

ipython与python的区别:
IPython的开发者吸收了标准解释器的基本概念,在此基础上进行了大量的改进,创造出一个令人惊奇的工具。在它的主页上是这么说的:“这是一个增强的交互式Python
shell。”具有tab补全,对象自省,强大的历史机制,内嵌的源代码编辑,集成Python调试器,%run机制,宏,创建多个环境以及调用系统shell的能力。
1)IPython与标准Python的最大区别在于,Ipython会对命令提示符的每一行进行编号。如下图:这里写图片描述

2)tab补全

作为例子,我们先引入 sys 模块,之后再输入 sys. (注意有个点),此时按下 tab 键,IPython 会列出所有 sys
模块下的方法和属性。
这里写图片描述
接着上面的例子,我们输入 sys?再回车,这样会显示出 sys 模块的 docstring及相关信息。很多时候这个也是很方便的功能。这里写图片描述

3)历史机制

hist可以快速查看那些输入的历史记录。
hist -n可以快速查看并去掉历史记录中的序号,这样你就可以方便的将代码复制到一个文本编辑器中。
一个更简单的方法是edit加Python列表的切片(slice)语法:
edit 4:7 % 将第4,5,6,7句代码导出到编辑器

4)断点调试:如果你的程序是由命令行开始执行的,即在命令行下输入 python foo.py(大部分 Python 程序都是),那么你还可以利用 IPython 在你的程序任意地方进行断点调试。
在你程序中任意地方,加入如下语句:
from IPython.Shell import IPShellEmbed
IPShellEmbed([])()
注意:最近 IPython 发布了 0.11 版本,各方面变化都非常大,API 也经过了重新设计。如果你使用的是 0.11 那么上面两行对应的是这样的:
from IPython import embed
embed()
再和平常一样运行你的程序,你会发现在程序运行到插入语句的地方时,会转到 IPython 环境下。你可以试试运行些指令,就会发现此刻 IPython 的环境就是在程序的那个位置。你可以逐个浏览当前状态下的各个变量,调用各种函数,输出你感兴趣的值来帮助调试。之后你可以照常退出 IPython,然后程序会继续运行下去,自然地你在当时 IPython 下执行的语句也会对程序接下来的运行造成影响。
这个方法是在这里(http://lukeplant.me.uk/blog/posts/exploratory-programming-with-ipython/)看到的。想象一下,这样做就像让高速运转的程序暂停下来,你再对运行中的程序进行检查和修改,之后再让他继续运行下去。这里举一个例子,比如编写网页 bot ,你在每取回一个页面后你都得看看它的内容,再尝试如何处理他获得下一个页面的地址。运用这个技巧,你可以在取回页面后让程序中断,再那里实验各种处理方 法,在找到正确的处理方式后写回到你的代码中,再进行下一步。这种工作流程只有像 Python 这种动态语言才可以做到。

以上摘自http://blog.sina.com.cn/s/blog_6fb8aa0d0101r5o1.html

ui_main_window.py import os from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QTextEdit, QFileDialog, QComboBox, QGroupBox, QApplication, QMessageBox, QDialog ) from PyQt6.QtCore import QObject, pyqtSignal import logging from pathlib import Path print(f"📍 当前工作目录: {os.getcwd()}“) print(f"📄 文件所在目录: {Path(file).resolve()}”) from excel_to_clm import ExcelToCLMConverter from channel.range_sync import CLMRangeSynchronizer # 使用统一模块 ============= 日志重定向器 ============== class LogEmitter(QObject): log_signal = pyqtSignal(str) ============= 主窗口类 ============== class CLMGeneratorUI(QWidget): def init(self, generator=None): super().init() self.generator = generator or ExcelToCLMConverter() self.log_emitter = LogEmitter() self.log_emitter.log_signal.connect(self.update_log_area) # 设置日志系统以捕获同步器输出 self._setup_logging_redirect() self.setWindowTitle("Wi-Fi CLM Generator Tool v1.2") self.resize(700, 600) self.init_ui() def _setup_logging_redirect(self): """仅将 channel.* 日志转发到 UI,不写文件""" class QtStreamHandler(logging.Handler): def __init__(self, emitter): super().__init__() self.emitter = emitter self.setFormatter(logging.Formatter('%(levelname)s: %(message)s')) def emit(self, record): try: msg = self.format(record) if msg.strip(): self.emitter.log_signal.emit(msg.strip()) except Exception: self.handleError(record) logger = logging.getLogger("channel") logger.setLevel(logging.INFO) if logger.hasHandlers(): logger.handlers.clear() handler = QtStreamHandler(self.log_emitter) logger.addHandler(handler) logger.propagate = False class LogStream: """伪装成文件对象的日志流,用于 logging.Handler""" def __init__(self, emitter): self.emitter = emitter def write(self, msg): if msg.strip(): self.emitter.log_signal.emit(msg.strip()) def flush(self): pass def init_ui(self): layout = QVBoxLayout() # === 输入区:Excel & Config === input_group = QGroupBox("输入配置") input_layout = QVBoxLayout() # Excel 文件 excel_row = QHBoxLayout() excel_row.addWidget(QLabel("Excel 文件:")) self.excel_path = QLineEdit("input/Archer BE900US 2.xlsx") self.btn_browse_excel = QPushButton("📁 浏览...") self.btn_browse_excel.clicked.connect(self.select_excel) excel_row.addWidget(self.excel_path) excel_row.addWidget(self.btn_browse_excel) input_layout.addLayout(excel_row) # Config 文件 config_row = QHBoxLayout() config_row.addWidget(QLabel("Config 文件:")) self.config_path = QLineEdit("config/config.json") self.btn_browse_config = QPushButton("📁 浏览...") self.btn_browse_config.clicked.connect(self.select_config_file) config_row.addWidget(self.config_path) config_row.addWidget(self.btn_browse_config) input_layout.addLayout(config_row) # Locale locale_row = QHBoxLayout() locale_row.addWidget(QLabel("Locale ID:")) self.locale_combo = QComboBox() self.locale_combo.addItems(["DEFAULT", "FCC", "ETSI", "JP", "CN"]) locale_row.addWidget(self.locale_combo) input_layout.addLayout(locale_row) input_group.setLayout(input_layout) layout.addWidget(input_group) # === CLM 同步区 === sync_group = QGroupBox("CLM 范围同步") sync_layout = QVBoxLayout() # Manifest 文件(可选覆盖,默认使用 generator 输出路径) manifest_row = QHBoxLayout() manifest_row.addWidget(QLabel("Manifest 文件:")) self.manifest_path = QLineEdit("output/generated_ranges_manifest.json") self.btn_browse_manifest = QPushButton("📁 浏览...") self.btn_browse_manifest.clicked.connect(self.select_manifest) manifest_row.addWidget(self.manifest_path) manifest_row.addWidget(self.btn_browse_manifest) sync_layout.addLayout(manifest_row) # C 文件 cfile_row = QHBoxLayout() cfile_row.addWidget(QLabel("C 源文件:")) self.c_file_path = QLineEdit("input/wlc_clm_data_6726b0.c") self.btn_browse_cfile = QPushButton("📁 浏览...") self.btn_browse_cfile.clicked.connect(self.select_c_file) cfile_row.addWidget(self.c_file_path) cfile_row.addWidget(self.btn_browse_cfile) sync_layout.addLayout(cfile_row) sync_group.setLayout(sync_layout) layout.addWidget(sync_group) # === 操作流程按钮组 === btn_group = QGroupBox("操作流程控制") btn_layout = QHBoxLayout() self.btn_parse = QPushButton("📊 解析 Excel") self.btn_dry_run = QPushButton("🔍 预览同步") self.btn_sync = QPushButton("💾 执行同步") self.btn_full_run = QPushButton("🚀 一键生成并同步") self.btn_parse.clicked.connect(self.run_parse) self.btn_dry_run.clicked.connect(lambda: self.run_sync(dry_run=True)) self.btn_sync.clicked.connect(lambda: self.run_sync(dry_run=False)) self.btn_full_run.clicked.connect(self.run_full_process) btn_layout.addWidget(self.btn_parse) btn_layout.addWidget(self.btn_dry_run) btn_layout.addWidget(self.btn_sync) btn_layout.addWidget(self.btn_full_run) btn_group.setLayout(btn_layout) layout.addWidget(btn_group) # === 日志输出 === self.log_area = QTextEdit() self.log_area.setReadOnly(True) self.log_area.append("✅ CLM Generator 已启动,等待输入...") layout.addWidget(self.log_area) self.setLayout(layout) def run_parse(self): """解析 Excel 并生成 manifest.json,不清除 generator 实例""" excel_path = self.excel_path.text().strip() config_path = self.config_path.text().strip() or "config/config.json" if not excel_path: self.log("⚠️ 请先选择 Excel 文件!") return try: # ✅ 复用已有 generator,仅更新参数 self.generator.config_file_path = config_path self.generator.output_dir = "output" self.generator.locale_display_name = self.locale_combo.currentText() self.generator.input_file = excel_path self.log("🧹 清理旧数据...") self.generator.reset() # 统一调用 reset 方法 self.log("📊 开始解析 Excel 文件...") self.generator.parse_excel() # 注意:确保方法名是 parse_excel,不是 read_excel self.generator.generate_outputs(finalize_manifest=True) num_ranges = len(self.generator.used_ranges) manifest_path = self.generator.last_generated_manifest self.log(f"✅ 成功生成 {num_ranges} 个 RANGE 宏 → {manifest_path}") # 自动填充 UI 中的 manifest 路径(用户仍可手动修改) self.manifest_path.setText(manifest_path) self.log("📌 提示:现在可以点击【预览同步】查看对 .c 文件的影响") except Exception as e: self.log(f"❌ 解析失败: {str(e)}") import traceback self.log(f"📝 错误详情:\n{traceback.format_exc()}") def select_excel(self): file, _ = QFileDialog.getOpenFileName( self, "选择 Excel 文件", "", "Excel Files (*.xlsx *.xls)") if file: self.excel_path.setText(file) def select_config_file(self): file, _ = QFileDialog.getOpenFileName( self, "选择配置文件", "config/", "JSON 配置文件 (*.json);;All Files (*)") if file: self.config_path.setText(file) def select_manifest(self): file, _ = QFileDialog.getOpenFileName( self, "选择 generated_ranges_manifest.json", "output/", "JSON 文件 (*.json)") if file: self.manifest_path.setText(file) def select_c_file(self): file, _ = QFileDialog.getOpenFileName( self, "选择 C 源文件", "input/", "C Source Files (*.c)") if file: self.c_file_path.setText(file) def log(self, msg): """发送日志消息到信号队列""" self.log_emitter.log_signal.emit(msg) def update_log_area(self, msg): """更新日志文本框""" self.log_area.append(f"> {msg}") def clear_log(self): self.log_area.clear() def show_preview(self): try: output_file = "output/tx_limit_table.c" with open(output_file, "r", encoding="utf-8") as f: content = f.read() dialog = QDialog(self) dialog.setWindowTitle(f"预览: {output_file}") dialog.resize(700, 500) layout = QVBoxLayout() text_edit = QTextEdit() text_edit.setReadOnly(True) text_edit.setText(content[:2000] + "\n...(后续内容已截断)") close_btn = QPushButton("关闭") close_btn.clicked.connect(dialog.accept) layout.addWidget(text_edit) layout.addWidget(close_btn) dialog.setLayout(layout) dialog.exec() except FileNotFoundError: self.log(f"❌ 无法预览: 文件未生成 '{output_file}',请先点击【开始生成】") except Exception as e: self.log(f"❌ 预览失败: {str(e)}") def run_sync(self, dry_run=False): """运行同步:使用 generator 生成的 manifest 路径自动同步到 C 文件""" from pathlib import Path import logging from datetime import datetime # 检查是否已生成 manifest if not hasattr(self.generator, 'last_generated_manifest') or not self.generator.last_generated_manifest: self.log("❌ 尚未生成任何 manifest 文件,请先点击【解析 Excel】") return # 默认使用自动生成的路径,允许用户手动覆盖 manifest_path = self.manifest_path.text().strip() or self.generator.last_generated_manifest c_file_path = self.c_file_path.text().strip() if not c_file_path: self.log("❌ 未指定目标 C 文件路径,请在下方输入或选择") return # 验证文件存在性 if not Path(manifest_path).exists(): self.log(f"❌ manifest 文件不存在: {manifest_path}") return if not Path(c_file_path).exists(): self.log(f"❌ 目标 C 文件不存在: {c_file_path}") return # ========== 【新增】准备日志文件(仅在执行模式)========== file_handler = None log_file_path = None if not dry_run: # 只有执行模式才记录到文件 timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") log_dir = Path(__file__).parent / "output" / "log" log_dir.mkdir(parents=True, exist_ok=True) log_file_path = log_dir / f"range_sync_{timestamp}.log" # 创建专用 FileHandler file_handler = logging.FileHandler(log_file_path, encoding='utf-8') file_handler.setFormatter( logging.Formatter('%(asctime)s [%(levelname)s] %(message)s', datefmt='%H:%M:%S') ) # 添加到 channel logger(与 UI 输出共存) target_logger = logging.getLogger("channel") target_logger.addHandler(file_handler) self.log(f"📄 日志文件已创建: {log_file_path}") try: mode = "预览模式" if dry_run else "执行模式" self.log(f"🔄 开始同步... ({mode})") self.log(f"📄 使用清单: {manifest_path}") self.log(f"🔧 修改文件: {c_file_path}") # 创建同步器实例 sync = CLMRangeSynchronizer( manifest_path=manifest_path, c_file_path=c_file_path, dry_run=dry_run ) was_modified = sync.run() if dry_run: if was_modified: self.log("🔍 发现需要更新的内容,确认无误后可点击【执行同步】") else: self.log("✅ 所有 RANGE 已存在,无需修改") else: if was_modified: self.log(f"✅ 同步成功!已更新 {c_file_path}") self.log(f"📁 备份已保存为 {c_file_path}.bak") else: self.log("✅ 文件已是最新,无需修改") except Exception as e: self.log(f"❌ 同步失败: {str(e)}") import traceback tb_msg = traceback.format_exc() self.log(f"📝 错误详情:\n{tb_msg}") # 即使出错也要确保 file_handler 正确移除 raise finally: # ========== 【关键】清理资源:移除并关闭 FileHandler ========== if file_handler: try: # 确保缓冲区写入磁盘 file_handler.flush() file_handler.close() logging.getLogger("channel").removeHandler(file_handler) self.log("🔒 日志文件已关闭") except Exception as e: print(f"Warning: failed to close file handler: {e}") # 可选:在 UI 显示最终提示 if not dry_run and log_file_path: self.log(f"📎 完整日志已保存至:\n{log_file_path}") def run_full_process(self): """一键完成:解析 Excel → 生成 manifest → 同步到 .c 文件""" reply = QMessageBox.question( self, "确认执行", "是否要执行【解析Excel + 更新C文件】全流程?\n\n" "建议先使用【预览同步】检查变更。", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No ) if reply != QMessageBox.StandardButton.Yes: return self.run_parse() if hasattr(self.generator, 'last_generated_manifest') and self.generator.last_generated_manifest: self.run_sync(dry_run=False) else: self.log("❌ 解析失败,无法继续同步") if name == ‘main’: app = QApplication([]) window = CLMGeneratorUI() window.show() app.exec() 我现在需要添加# power/power_sync.py import re import shutil from pathlib import Path import logging from typing import Dict, List from datetime import datetime import json def setup_logger(log_dir: Path) -> logging.Logger: “”“初始化日志系统”“” log_dir.mkdir(parents=True, exist_ok=True) log_file = log_dir / f"sync_{datetime.now().strftime(‘%Y%m%d_%H%M%S’)}.log" logger = logging.getLogger("clm_sync") if logger.handlers: logger.handlers.clear() logger.setLevel(logging.DEBUG) fmt = logging.Formatter( '%(asctime)s | %(levelname)-8s | %(funcName)s:%(lineno)d | %(message)s', datefmt='%Y-%m-%d %H:%M:%S' ) fh = logging.FileHandler(log_file, encoding='utf-8') fh.setLevel(logging.DEBUG) fh.setFormatter(fmt) logger.addHandler(fh) ch = logging.StreamHandler() ch.setLevel(logging.INFO) ch.setFormatter(fmt) logger.addHandler(ch) logger.info(f"📄 日志已启动,写入文件: {log_file}") return logger class PowerTableSynchronizer: # —————————————————————————— # 1. 初始化与配置加载 # —————————————————————————— def __init__(self, c_file_path: str, config_path: str = "config/config.json", dry_run: bool = False): script_dir = Path(__file__).parent project_root = script_dir.parent # 解析路径 if not Path(config_path).is_absolute(): config_path = (project_root / config_path).resolve() self.c_file_path = Path(c_file_path) if not self.c_file_path.is_absolute(): self.c_file_path = (project_root / self.c_file_path).resolve() self.dry_run = dry_run self.output_dir = project_root / "output" self.log_dir = self.output_dir / "log" self.logger = setup_logger(self.log_dir) self.config = self._load_config(config_path) self.platform = self.config.get("platform_rules", {}) self._validate() def _load_config(self, config_path: Path) -> dict: config_path = Path(config_path) if not config_path.exists(): raise FileNotFoundError(f"❌ 配置文件不存在: {config_path}") try: with open(config_path, 'r', encoding='utf-8') as f: return json.load(f) except json.JSONDecodeError as e: raise ValueError(f"配置文件格式错误: {e}") from e def _validate(self): if not self.c_file_path.exists(): msg = f"❌ 文件不存在: {self.c_file_path.resolve()}" self.logger.error(msg) raise FileNotFoundError(msg) self.logger.info(f"✅ 目标 C 文件已找到: {self.c_file_path.name}") # —————————————————————————— # 2. 主接口(核心入口放最前面) # —————————————————————————— def sync_all(self, locale_entries: Dict[str, str], power_tables: Dict[str, str], country_entries: List[str]): """ 一站式同步所有 CLM 内容 """ self.logger.info("🚀 开始执行 CLM 数据同步任务...") self.logger.info(f"目标文件: {self.c_file_path.name}") self.logger.info(f"预览模式: {'是' if self.dry_run else '否'}") try: content = self.load_content() content = self.sync_locales_enum(content, locale_entries) content = self.sync_power_tables(content, power_tables) content = self.sync_country_definitions(content, country_entries) self.save_content(content) self.logger.info("🎉 全部同步任务完成!") except Exception as e: self.logger.error(f"⛔ 同步过程中发生错误: {str(e)}") raise # —————————————————————————— # 3. 核心同步流程(按执行顺序排列) # —————————————————————————— def load_content(self) -> str: """读取目标 C 文件内容""" try: content = self.c_file_path.read_text(encoding='utf-8') self.logger.info(f"📄 已读取文件: {self.c_file_path.name} ({len(content)} 字符)") return content except Exception as e: self.logger.error(f"❌ 读取文件失败: {e}") raise def sync_locales_enum(self, content: str, new_entries: Dict[str, str]) -> str: """同步 enum locale_xxx_idx 区域""" self.logger.info("🔄 开始同步 LOCALE ENUMS...") try: block, start, end = self._extract_section(content, "enums") except ValueError: return content lines = [line.rstrip() for line in block.splitlines()] output_lines = [] current_enum = None modified = False i = 0 while i < len(lines): line = lines[i] stripped = line.strip() enum_match = re.match(r"enum\s+([a-zA-Z0-9_]+)", stripped) if enum_match: enum_name = enum_match.group(1) pattern = self.platform.get("locale_enum", {}).get("pattern", r"locale_(\w+)_idx") if re.fullmatch(pattern.replace("\\", ""), enum_name): current_enum = enum_name output_lines.append(line) i += 1 continue if stripped.endswith("};") and current_enum: key = current_enum.lower() if key in new_entries: entry_code = new_entries[key] last_line = output_lines[-1].rstrip() if not (last_line.endswith(",") or last_line.endswith("{")): output_lines[-1] = last_line + "," output_lines.append(f" {entry_code},") self.logger.info(f"📌 新增枚举: {key} → {entry_code}") modified = True output_lines.append(line) current_enum = None i += 1 continue output_lines.append(line) i += 1 if not modified: self.logger.info("🟨 枚举区无变更") return content new_block = "\n".join(output_lines) updated = self._replace_section(content, "enums", new_block) self.logger.info("✅ 枚举区更新完成") return updated def sync_power_tables(self, content: str, tables: Dict[str, str]) -> str: """同步功率表数组""" self.logger.info("🔄 开始同步 POWER TABLES...") try: block, start, end = self._extract_section(content, "power_tables") except ValueError: return content prefix = self.platform["power_table"]["prefix"] suffix = self.platform["power_table"]["suffix"].replace("[", "\$$").replace("]", "\$$") array_pattern = r'static\s+const\s+unsigned\s+char\s+(' + prefix + r'[a-zA-Z0-9_]+)' + suffix + r'\s*=\s*\{[^}]*\}\s*;' existing_names = set(re.findall(array_pattern, block, re.IGNORECASE)) all_arrays = [] for match in re.finditer(array_pattern, block, re.DOTALL | re.IGNORECASE): all_arrays.append(match.group(0)) added = [] for key, declaration in tables.items(): if key not in existing_names: all_arrays.append(declaration.strip() + ";") added.append(key) self.logger.info(f"📌 新增功率表: {prefix}{key}{suffix}") if not added: self.logger.info("🟨 功率表区无新增") return content new_block = "\n\n".join(all_arrays) updated = self._replace_section(content, "power_tables", new_block) self.logger.info(f"✅ 功率表更新完成,新增: {', '.join(added)}") return updated def sync_country_definitions(self, content: str, country_entries: List[str]) -> str: """同步国家码定义 REGION(...)""" self.logger.info("🔄 开始同步 COUNTRY DEFINITIONS...") try: block, start, end = self._extract_section(content, "country_definitions") except ValueError: return content pattern = self.platform["country_definition"]["pattern"] existing_lines = [line.strip() for line in block.split('\n') if line.strip()] country_map = {} for line in existing_lines: cc_match = re.search(pattern, line) if cc_match: country_map[cc_match.group(1)] = line new_entries = [] for entry in country_entries: cc_match = re.search(pattern, entry) if cc_match: cc = cc_match.group(1) if cc not in country_map: new_entries.append(entry.strip()) self.logger.info(f"📌 新增国家码: {cc}") if not new_entries: self.logger.info("🟨 国家码无新增") return content all_lines = existing_lines + new_entries new_block = "\n".join(all_lines) updated = self._replace_section(content, "country_definitions", new_block) self.logger.info(f"✅ 国家码更新完成,新增 {len(new_entries)} 个") return updated # —————————————————————————— # 4. 内部文本操作辅助方法 # —————————————————————————— def _extract_section(self, content: str, section: str) -> tuple[str, int, int]: """提取指定区域内容""" try: anchors = self.platform["anchors"][section] start_marker = anchors["begin"] end_marker = anchors["end"] except KeyError as e: raise ValueError(f"配置错误:缺少锚点定义 {section}") from e start_pos = content.find(start_marker) end_pos = content.find(end_marker) if start_pos == -1 or end_pos == -1: missing = "起始标记" if start_pos == -1 else "结束标记" msg = f"未找到区域 {section} 的 {missing}: {start_marker[:30]}..." self.logger.warning(msg) raise ValueError(msg) inner_start = start_pos + len(start_marker) inner_end = end_pos extracted = content[inner_start:inner_end].strip() self.logger.debug(f"📦 提取区域 [{section}]: {len(extracted)} 字符") return extracted, inner_start, inner_end def _replace_section(self, content: str, section: str, new_content: str) -> str: """替换指定区域内容""" try: anchors = self.platform["anchors"][section] start_marker = anchors["begin"] end_marker = anchors["end"] except KeyError as e: raise ValueError(f"配置错误:无法替换区域 {section}") from e start_pos = content.find(start_marker) end_pos = content.find(end_marker) if start_pos == -1 or end_pos == -1: raise ValueError(f"无法替换区域 {section},缺失锚点") return content[:start_pos + len(start_marker)] + "\n" + new_content + "\n" + content[end_pos:] def save_content(self, content: str): """保存内容并创建备份""" if self.dry_run: self.logger.info("🔍 【预览模式】跳过实际写入") return try: backup_path = self.c_file_path.with_suffix(self.c_file_path.suffix + ".bak") shutil.copy2(self.c_file_path, backup_path) self.logger.info(f"📁 已创建备份: {backup_path.name}") with open(self.c_file_path, 'w', encoding='utf-8') as f: f.write(content) self.logger.info(f"✅ 成功更新文件: {self.c_file_path.name}") except Exception as e: self.logger.error(f"写入文件失败: {e}") raise # —————————————————————————— # 5. 工具函数(静态方法,独立于实例) # —————————————————————————— @staticmethod def _extract_locale_block(content: str, locale_name: str) -> str | None: """ 从 tx_limit_table.c 中提取 /* Locale NAME */ 对应的数据块 返回去注释后的干净数据字符串 """ pattern = rf'/\*\s*Locale\s+{locale_name}\s*\*/\s*([^/]*)' match = re.search(pattern, content, re.DOTALL) if not match: return None block = match.group(1).strip() block = re.sub(r'/\*.*?\*/', '', block) # 去除注释 block = re.sub(r'\s+', ' ', block).strip() # 归一化空白 return block —————————————————————————— 主函数:注入 DEFAULT 功率表 —————————————————————————— def main(): “”" 主函数:注入 DEFAULT 功率表 使用项目根目录自动推导路径,不依赖当前工作目录 “”" import sys from pathlib import Path project_root = Path(__file__).parent.parent # F:\issue CONFIG_FILE = project_root / "config" / "config.json" GENERATED_FILE = project_root / "output" / "tx_limit_table.c" TARGET_C_FILE = project_root / "input" / "wlc_clm_data_6726b0.c" # 检查必要文件 for path, desc in [(CONFIG_FILE, "配置文件"), (GENERATED_FILE, "生成数据文件"), (TARGET_C_FILE, "目标C文件")]: if not path.exists(): print(f"❌ {desc}不存在: {path}", file=sys.stderr) sys.exit(1) try: gen_content = GENERATED_FILE.read_text(encoding='utf-8') base_block = PowerTableSynchronizer._extract_locale_block(gen_content, "DEFAULT") ht_block = PowerTableSynchronizer._extract_locale_block(gen_content, "DEFAULT_HT") if not base_block and not ht_block: raise ValueError("未提取到任何 DEFAULT 功率表数据") locale_entries = { "locale_2g_idx": "LOCALE_2G_IDX_DEFAULT" } power_tables = {} if base_block: power_tables["2g_base_DEFAULT"] = ( f"static const unsigned char locales_2g_base_DEFAULT[] = {{\n {base_block}\n}}" ) if ht_block: power_tables["2g_ht_DEFAULT"] = ( f"static const unsigned char locales_2g_ht_DEFAULT[] = {{\n {ht_block}\n}}" ) country_entries = [ 'REGION("CN", LOCALE_2G_IDX_DEFAULT)', 'REGION("US", LOCALE_2G_IDX_DEFAULT)', ] synchronizer = PowerTableSynchronizer( c_file_path=str(TARGET_C_FILE), config_path=str(CONFIG_FILE), dry_run=False ) synchronizer.sync_all( locale_entries=locale_entries, power_tables=power_tables, country_entries=country_entries ) print("✅ 成功注入 DEFAULT 功率表!") except Exception as e: print(f"💥 错误: {e}", file=sys.stderr) sys.exit(1) —————————————————————————— 运行入口(可选) —————————————————————————— if name == “main”: main() 里面的接口
最新发布
10-16
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值