You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

272 line
10 KiB

  1. # -*- coding: utf-8 -*-
  2. """
  3. .bin 波形文件 -> 时频曲线查看/出图工具(PLSR 项目用)
  4. 数据格式: 逻辑分析仪导出的单通道数字采样,每字节 1 个采样点;
  5. 电平 >= 阈值(默认128) 判为高(脉冲),否则为低。
  6. 频率定义: 每脉冲频率 = 采样率 / 脉冲周期;
  7. 周期 = 本脉冲高电平起点 -> 下一脉冲高电平起点;
  8. 最后一个脉冲无下一脉冲,按高电平宽度 x2 近似(50% 占空比)。
  9. 时间定义: 脉冲时间 = 高电平中点(与低电平/空闲段无关)。
  10. 用法:
  11. python bin_to_time_freq.py <bin文件> [采样率Hz] [选项]
  12. 示例:
  13. python bin_to_time_freq.py "Document/PLSR_document/波形/10段.bin" 6250000
  14. python bin_to_time_freq.py xxx.bin 6250000 --ymax=12000
  15. python bin_to_time_freq.py xxx.bin 6250000 --save=out.png
  16. python bin_to_time_freq.py xxx.bin 6250000 --selftest
  17. 选项:
  18. --thresh=128 电平阈值(默认128)
  19. --ymax=12000 频率轴上限(默认自适应)
  20. --save=路径 保存 PNG 后退出(不弹窗)
  21. --selftest 仅打印统计,不画图
  22. 交互(弹窗模式):
  23. 滚轮 缩放 X 轴(向上放大/向下缩小,以鼠标位置为中心)
  24. Ctrl+滚轮 缩放 Y 轴(以鼠标 Y 位置为中心)
  25. 左键拖拽 双向平移(上下左右跟随鼠标)
  26. 双击 复位到全图
  27. 鼠标悬停 高亮最近的点并显示其时间与频率
  28. 依赖: pip install numpy matplotlib
  29. """
  30. import sys
  31. import os
  32. import numpy as np
  33. try:
  34. import matplotlib
  35. matplotlib.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei']
  36. matplotlib.rcParams['axes.unicode_minus'] = False
  37. except Exception:
  38. pass
  39. def load_time_freq(path, fs, threshold=128):
  40. """读取 bin,返回 (t_sec, freq_hz, hi_width_sec, meta)。
  41. t_sec 以第一个脉冲为 0 时刻;hi_width 为每脉冲高电平宽度。"""
  42. data = np.fromfile(path, dtype=np.uint8)
  43. if data.size == 0:
  44. raise ValueError("文件为空: %s" % path)
  45. samples = (data >= threshold).astype(np.int8)
  46. # run 级压缩
  47. changes = np.flatnonzero(np.diff(samples) != 0) + 1
  48. starts = np.concatenate(([0], changes))
  49. ends = np.concatenate((changes, [len(samples)]))
  50. runs = np.column_stack((starts, ends, samples[starts]))
  51. # 脉冲 = 高电平 run,且其后紧跟低电平 run
  52. hi = np.flatnonzero(runs[:, 2] == 1)
  53. hi = hi[hi + 1 < len(runs)]
  54. lo_ok = runs[hi + 1, 2] == 0
  55. hi = hi[lo_ok]
  56. t_start = runs[hi, 0].astype(np.int64)
  57. hi_width = (runs[hi, 1] - runs[hi, 0]).astype(np.int64)
  58. # 周期:高到高;末脉冲按 2x 高电平宽度近似
  59. next_start = np.append(t_start[1:], [t_start[-1] + 2 * hi_width[-1]])
  60. period = next_start - t_start
  61. freq = fs / np.maximum(period, 1)
  62. # 时间:高电平中点,去起始偏移
  63. t_sec = (t_start + hi_width / 2.0) / fs
  64. t_sec = t_sec - t_sec[0]
  65. meta = {
  66. 'total_samples': int(len(samples)),
  67. 'duration': len(samples) / fs,
  68. 'pulses': int(len(freq)),
  69. 'offset_ms': t_start[0] / fs * 1000,
  70. }
  71. return t_sec, freq, hi_width / fs, meta
  72. def selftest(path, fs, thresh):
  73. import statistics
  74. t, freq, hi_w, meta = load_time_freq(path, fs, thresh)
  75. print("文件: %s" % path)
  76. print("总采样: %d (%.3f s @ %.2f MS/s)" % (meta['total_samples'], meta['duration'], fs / 1e6))
  77. print("起始采集偏移: %.1f ms" % meta['offset_ms'])
  78. print("脉冲总数: %d" % meta['pulses'])
  79. if meta['pulses']:
  80. print("频率 min=%.0f max=%.0f 中位=%.0f Hz" % (
  81. freq.min(), freq.max(), statistics.median(freq)))
  82. print("末脉冲: t=%.3f ms, 高电平 %.3f ms, %.0f Hz" % (
  83. t[-1] * 1000, hi_w[-1] * 1000, freq[-1]))
  84. print("波形活动时长: %.1f ms" % ((t[-1] + hi_w[-1] / 2) * 1000))
  85. def plot_show(path, fs, thresh, ymax, save_path=None):
  86. t, freq, hi_w, meta = load_time_freq(path, fs, thresh)
  87. import matplotlib.pyplot as plt
  88. from matplotlib.ticker import MaxNLocator
  89. fig, ax = plt.subplots(figsize=(15, 7))
  90. fig.subplots_adjust(bottom=0.10, top=0.92)
  91. # 全脉冲散点
  92. ax.plot(t * 1000, freq, '.', ms=1.5, color='C0', alpha=0.5, zorder=1)
  93. ax.set_xlim(0.0, (t[-1] + hi_w[-1]) * 1000 * 1.02)
  94. ax.set_ylim(0, ymax if ymax > 0 else freq.max() * 1.08)
  95. ax.set_title("%s 时频曲线(%d 脉冲,活动时长 %.0f ms)" % (
  96. os.path.basename(path), meta['pulses'],
  97. (t[-1] + hi_w[-1] / 2) * 1000))
  98. ax.set_xlabel("时间 (ms)")
  99. ax.set_ylabel("频率 (Hz)")
  100. # 更密的刻度
  101. ax.xaxis.set_major_locator(MaxNLocator(nbins=20))
  102. ax.yaxis.set_major_locator(MaxNLocator(nbins=15))
  103. ax.grid(True, which='major', alpha=0.35)
  104. ax.grid(True, which='minor', alpha=0.15)
  105. ax.minorticks_on()
  106. if save_path:
  107. fig.savefig(save_path, dpi=130)
  108. print("已保存: %s" % save_path)
  109. plt.close(fig)
  110. return
  111. # 交互:滚轮缩放 X(Ctrl+滚轮缩放 Y)/ 左键双向拖拽平移 / 双击复位 / 悬停高亮
  112. state = {'press_x': None, 'press_y': None,
  113. 'press_xlim': None, 'press_ylim': None}
  114. # 悬停高亮:一个红点标记 + 一个带框文本
  115. (hl_marker,) = ax.plot([], [], 'o', ms=9, mfc='red', mec='white',
  116. mew=1.0, zorder=5, visible=False)
  117. hl_text = ax.text(0, 0, '', fontsize=10, color='black',
  118. bbox=dict(boxstyle='round,pad=0.3', fc='yellow', ec='red', alpha=0.9),
  119. zorder=6, visible=False)
  120. hover_last = {'idx': -1, 'visible': False}
  121. def on_scroll(event):
  122. if event.inaxes is not ax or event.xdata is None:
  123. return
  124. zoom_in = event.button == 'up'
  125. factor = 1.0 / 1.5 if zoom_in else 1.5
  126. if event.key in ('control', 'ctrl'):
  127. # Ctrl+滚轮:缩放 Y 轴,以鼠标 Y 位置为锚点(锚点数据点不动)
  128. y0, y1 = ax.get_ylim()
  129. cy = event.ydata
  130. n0 = cy - (cy - y0) * factor
  131. n1 = cy + (y1 - cy) * factor
  132. if n1 - n0 < 1.0:
  133. return
  134. ax.set_ylim(n0, n1)
  135. else:
  136. # 普通滚轮:缩放 X 轴,以鼠标 X 位置为锚点(锚点数据点不动)
  137. x0, x1 = ax.get_xlim()
  138. n0 = event.xdata - (event.xdata - x0) * factor
  139. n1 = event.xdata + (x1 - event.xdata) * factor
  140. if n1 - n0 < 1e-6:
  141. return
  142. ax.set_xlim(n0, n1)
  143. fig.canvas.draw_idle()
  144. def on_press(event):
  145. if event.inaxes is ax and event.button == 1:
  146. state['press_x'] = event.xdata
  147. state['press_y'] = event.ydata
  148. state['press_xlim'] = ax.get_xlim()
  149. state['press_ylim'] = ax.get_ylim()
  150. def on_motion(event):
  151. if event.inaxes is not ax or event.xdata is None:
  152. return
  153. if state['press_x'] is not None:
  154. # 拖拽平移:X/Y 双向跟随鼠标
  155. x0, x1 = state['press_xlim']
  156. y0, y1 = state['press_ylim']
  157. dx = event.xdata - state['press_x']
  158. dy = event.ydata - state['press_y']
  159. ax.set_xlim(x0 - dx, x1 - dx)
  160. ax.set_ylim(y0 - dy, y1 - dy)
  161. fig.canvas.draw_idle()
  162. return
  163. # 悬停:找可见范围内距鼠标最近的脉冲点(屏幕像素距离 < 20px 才高亮)
  164. x0, x1 = ax.get_xlim()
  165. tx_ms = t * 1000
  166. mask = (tx_ms >= x0) & (tx_ms <= x1)
  167. if not np.any(mask):
  168. hl_marker.set_visible(False)
  169. hl_text.set_visible(False)
  170. hover_last['visible'] = False
  171. fig.canvas.draw_idle()
  172. return
  173. px, py = ax.transData.transform(np.column_stack([tx_ms[mask], freq[mask]])).T
  174. dist = np.hypot(px - event.x, py - event.y)
  175. k = int(np.argmin(dist))
  176. if dist[k] <= 20.0:
  177. idx = int(np.flatnonzero(mask)[k])
  178. hl_marker.set_data([tx_ms[idx]], [freq[idx]])
  179. hl_text.set_text("t = %.3f ms\nf = %.0f Hz" % (tx_ms[idx], freq[idx]))
  180. hl_text.set_position((tx_ms[idx] + (x1 - x0) * 0.01,
  181. freq[idx] + (ax.get_ylim()[1] - ax.get_ylim()[0]) * 0.02))
  182. hl_marker.set_visible(True)
  183. hl_text.set_visible(True)
  184. if hover_last['idx'] != idx or not hover_last['visible']:
  185. fig.canvas.draw_idle()
  186. hover_last['idx'] = idx
  187. hover_last['visible'] = True
  188. else:
  189. if hover_last['visible']:
  190. hl_marker.set_visible(False)
  191. hl_text.set_visible(False)
  192. fig.canvas.draw_idle()
  193. hover_last['visible'] = False
  194. def on_release(event):
  195. state['press_x'] = None
  196. def on_double(event):
  197. if event.dblclick:
  198. ax.set_xlim(0.0, (t[-1] + hi_w[-1]) * 1000 * 1.02)
  199. ax.set_ylim(0, ymax if ymax > 0 else freq.max() * 1.08)
  200. fig.canvas.draw_idle()
  201. fig.canvas.mpl_connect('scroll_event', on_scroll)
  202. fig.canvas.mpl_connect('button_press_event', on_press)
  203. fig.canvas.mpl_connect('button_release_event', on_release)
  204. fig.canvas.mpl_connect('motion_notify_event', on_motion)
  205. fig.canvas.mpl_connect('button_press_event', on_double)
  206. print("打开窗口:滚轮缩放X / Ctrl+滚轮缩放Y / 左键双向拖拽平移 / 双击复位")
  207. print("脉冲 %d 个,活动时长 %.1f ms" % (
  208. meta['pulses'], (t[-1] + hi_w[-1] / 2) * 1000))
  209. plt.show()
  210. def main():
  211. args = [a for a in sys.argv[1:] if not a.startswith('--')]
  212. opts = {a.split('=', 1)[0]: (a.split('=', 1)[1] if '=' in a else True)
  213. for a in sys.argv[1:] if a.startswith('--')}
  214. if len(args) < 1 or '--help' in opts or '-h' in opts:
  215. print(__doc__)
  216. sys.exit(0)
  217. path = args[0]
  218. fs = float(args[1]) if len(args) > 1 else 6.25e6
  219. thresh = int(opts.get('--thresh', 128))
  220. ymax = float(opts.get('--ymax', 0))
  221. save_path = opts.get('--save', '')
  222. if not os.path.isfile(path):
  223. print("错误:文件不存在 - %s" % path)
  224. sys.exit(1)
  225. if '--selftest' in opts:
  226. selftest(path, fs, thresh)
  227. else:
  228. plot_show(path, fs, thresh, ymax, save_path or None)
  229. if __name__ == '__main__':
  230. main()