# -*- coding: utf-8 -*- """ .bin 波形文件 -> 时频曲线查看/出图工具(PLSR 项目用) 数据格式: 逻辑分析仪导出的单通道数字采样,每字节 1 个采样点; 电平 >= 阈值(默认128) 判为高(脉冲),否则为低。 频率定义: 每脉冲频率 = 采样率 / 脉冲周期; 周期 = 本脉冲高电平起点 -> 下一脉冲高电平起点; 最后一个脉冲无下一脉冲,按高电平宽度 x2 近似(50% 占空比)。 时间定义: 脉冲时间 = 高电平中点(与低电平/空闲段无关)。 用法: python bin_to_time_freq.py [采样率Hz] [选项] 示例: python bin_to_time_freq.py "Document/PLSR_document/波形/10段.bin" 6250000 python bin_to_time_freq.py xxx.bin 6250000 --ymax=12000 python bin_to_time_freq.py xxx.bin 6250000 --save=out.png python bin_to_time_freq.py xxx.bin 6250000 --selftest 选项: --thresh=128 电平阈值(默认128) --ymax=12000 频率轴上限(默认自适应) --save=路径 保存 PNG 后退出(不弹窗) --selftest 仅打印统计,不画图 交互(弹窗模式): 滚轮 缩放 X 轴(向上放大/向下缩小,以鼠标位置为中心) Ctrl+滚轮 缩放 Y 轴(以鼠标 Y 位置为中心) 左键拖拽 双向平移(上下左右跟随鼠标) 双击 复位到全图 鼠标悬停 高亮最近的点并显示其脉冲序号、时间与频率 依赖: pip install numpy matplotlib """ import sys import os import numpy as np try: import matplotlib matplotlib.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei'] matplotlib.rcParams['axes.unicode_minus'] = False except Exception: pass def load_time_freq(path, fs, threshold=128): """读取 bin,返回 (t_sec, freq_hz, hi_width_sec, meta)。 t_sec 以第一个脉冲为 0 时刻;hi_width 为每脉冲高电平宽度。""" data = np.fromfile(path, dtype=np.uint8) if data.size == 0: raise ValueError("文件为空: %s" % path) samples = (data >= threshold).astype(np.int8) # run 级压缩 changes = np.flatnonzero(np.diff(samples) != 0) + 1 starts = np.concatenate(([0], changes)) ends = np.concatenate((changes, [len(samples)])) runs = np.column_stack((starts, ends, samples[starts])) # 脉冲 = 高电平 run,且其后紧跟低电平 run hi = np.flatnonzero(runs[:, 2] == 1) hi = hi[hi + 1 < len(runs)] lo_ok = runs[hi + 1, 2] == 0 hi = hi[lo_ok] t_start = runs[hi, 0].astype(np.int64) hi_width = (runs[hi, 1] - runs[hi, 0]).astype(np.int64) # 周期:高到高;末脉冲按 2x 高电平宽度近似 next_start = np.append(t_start[1:], [t_start[-1] + 2 * hi_width[-1]]) period = next_start - t_start freq = fs / np.maximum(period, 1) # 时间:高电平中点,去起始偏移 t_sec = (t_start + hi_width / 2.0) / fs t_sec = t_sec - t_sec[0] meta = { 'total_samples': int(len(samples)), 'duration': len(samples) / fs, 'pulses': int(len(freq)), 'offset_ms': t_start[0] / fs * 1000, } return t_sec, freq, hi_width / fs, meta def selftest(path, fs, thresh): import statistics t, freq, hi_w, meta = load_time_freq(path, fs, thresh) print("文件: %s" % path) print("总采样: %d (%.3f s @ %.2f MS/s)" % (meta['total_samples'], meta['duration'], fs / 1e6)) print("起始采集偏移: %.1f ms" % meta['offset_ms']) print("脉冲总数: %d" % meta['pulses']) if meta['pulses']: print("频率 min=%.0f max=%.0f 中位=%.0f Hz" % ( freq.min(), freq.max(), statistics.median(freq))) print("末脉冲: t=%.3f ms, 高电平 %.3f ms, %.0f Hz" % ( t[-1] * 1000, hi_w[-1] * 1000, freq[-1])) print("波形活动时长: %.1f ms" % ((t[-1] + hi_w[-1] / 2) * 1000)) def plot_show(path, fs, thresh, ymax, save_path=None): t, freq, hi_w, meta = load_time_freq(path, fs, thresh) import matplotlib.pyplot as plt from matplotlib.ticker import MaxNLocator fig, ax = plt.subplots(figsize=(15, 7)) fig.subplots_adjust(bottom=0.10, top=0.92) # 全脉冲散点:复用 artist,重绘时按可见范围抽稀(大文件流畅) MAX_VISIBLE_POINTS = 5000 # 可见范围内最多绘制的点数,超过则等间隔抽稀 (scatter,) = ax.plot([], [], '.', ms=1.5, color='C0', alpha=0.5, zorder=1) tx_ms = t * 1000 x_full = tx_ms y_full = freq ax.set_xlim(0.0, (t[-1] + hi_w[-1]) * 1000 * 1.02) ax.set_ylim(0, ymax if ymax > 0 else freq.max() * 1.08) ax.set_title("%s 时频曲线(%d 脉冲,活动时长 %.0f ms)" % ( os.path.basename(path), meta['pulses'], (t[-1] + hi_w[-1] / 2) * 1000)) ax.set_xlabel("时间 (ms)") ax.set_ylabel("频率 (Hz)") # 更密的刻度 ax.xaxis.set_major_locator(MaxNLocator(nbins=20)) ax.yaxis.set_major_locator(MaxNLocator(nbins=15)) ax.grid(True, which='major', alpha=0.35) ax.grid(True, which='minor', alpha=0.15) ax.minorticks_on() if save_path: fig.savefig(save_path, dpi=130) print("已保存: %s" % save_path) plt.close(fig) return # 交互:滚轮缩放 X(Ctrl+滚轮缩放 Y)/ 左键双向拖拽平移 / 双击复位 / 悬停高亮 state = {'press_x': None, 'press_y': None, 'press_xlim': None, 'press_ylim': None} # 悬停高亮:一个红点标记 + 一个带框文本 (hl_marker,) = ax.plot([], [], 'o', ms=9, mfc='red', mec='white', mew=1.0, zorder=5, visible=False) hl_text = ax.text(0, 0, '', fontsize=10, color='black', bbox=dict(boxstyle='round,pad=0.3', fc='yellow', ec='red', alpha=0.9), zorder=6, visible=False) hover_last = {'idx': -1, 'visible': False} # 可见范围内抽稀绘制散点(大文件性能优化) def update_scatter(): x0, x1 = ax.get_xlim() mask = (x_full >= x0) & (x_full <= x1) n_vis = int(np.count_nonzero(mask)) if n_vis > MAX_VISIBLE_POINTS: # 等间隔抽稀:取 n_vis 中的 MAX_VISIBLE_POINTS 个 step = (n_vis + MAX_VISIBLE_POINTS - 1) // MAX_VISIBLE_POINTS idx = np.flatnonzero(mask)[::step] else: idx = np.flatnonzero(mask) scatter.set_data(x_full[idx], y_full[idx]) def on_scroll(event): if event.inaxes is not ax or event.xdata is None: return zoom_in = event.button == 'up' factor = 1.0 / 1.5 if zoom_in else 1.5 if event.key in ('control', 'ctrl'): # Ctrl+滚轮:缩放 Y 轴,以鼠标 Y 位置为锚点(锚点数据点不动) y0, y1 = ax.get_ylim() cy = event.ydata n0 = cy - (cy - y0) * factor n1 = cy + (y1 - cy) * factor if n1 - n0 < 1.0: return ax.set_ylim(n0, n1) else: # 普通滚轮:缩放 X 轴,以鼠标 X 位置为锚点(锚点数据点不动) x0, x1 = ax.get_xlim() n0 = event.xdata - (event.xdata - x0) * factor n1 = event.xdata + (x1 - event.xdata) * factor if n1 - n0 < 1e-6: return ax.set_xlim(n0, n1) update_scatter() fig.canvas.draw_idle() def on_press(event): if event.inaxes is ax and event.button == 1: state['press_x'] = event.xdata state['press_y'] = event.ydata state['press_xlim'] = ax.get_xlim() state['press_ylim'] = ax.get_ylim() def on_motion(event): if event.inaxes is not ax or event.xdata is None: return if state['press_x'] is not None: # 拖拽平移:X/Y 双向跟随鼠标 x0, x1 = state['press_xlim'] y0, y1 = state['press_ylim'] dx = event.xdata - state['press_x'] dy = event.ydata - state['press_y'] ax.set_xlim(x0 - dx, x1 - dx) ax.set_ylim(y0 - dy, y1 - dy) update_scatter() fig.canvas.draw_idle() return # 悬停:找可见范围内距鼠标最近的脉冲点(屏幕像素距离 < 20px 才高亮) x0, x1 = ax.get_xlim() mask = (x_full >= x0) & (x_full <= x1) if not np.any(mask): if hover_last['visible']: hl_marker.set_visible(False) hl_text.set_visible(False) fig.canvas.draw_idle() hover_last['visible'] = False return px, py = ax.transData.transform(np.column_stack([x_full[mask], y_full[mask]])).T dist = np.hypot(px - event.x, py - event.y) k = int(np.argmin(dist)) if dist[k] <= 20.0: idx = int(np.flatnonzero(mask)[k]) hl_marker.set_data([x_full[idx]], [y_full[idx]]) hl_text.set_text("脉冲 #%d\nt = %.3f ms\nf = %.0f Hz" % (idx + 1, x_full[idx], y_full[idx])) hl_text.set_position((x_full[idx] + (x1 - x0) * 0.01, y_full[idx] + (ax.get_ylim()[1] - ax.get_ylim()[0]) * 0.02)) hl_marker.set_visible(True) hl_text.set_visible(True) if hover_last['idx'] != idx or not hover_last['visible']: fig.canvas.draw_idle() hover_last['idx'] = idx hover_last['visible'] = True else: if hover_last['visible']: hl_marker.set_visible(False) hl_text.set_visible(False) fig.canvas.draw_idle() hover_last['visible'] = False def on_release(event): state['press_x'] = None def on_double(event): if event.dblclick: ax.set_xlim(0.0, (t[-1] + hi_w[-1]) * 1000 * 1.02) ax.set_ylim(0, ymax if ymax > 0 else freq.max() * 1.08) update_scatter() fig.canvas.draw_idle() update_scatter() fig.canvas.mpl_connect('scroll_event', on_scroll) fig.canvas.mpl_connect('button_press_event', on_press) fig.canvas.mpl_connect('button_release_event', on_release) fig.canvas.mpl_connect('motion_notify_event', on_motion) fig.canvas.mpl_connect('button_press_event', on_double) print("打开窗口:滚轮缩放X / Ctrl+滚轮缩放Y / 左键双向拖拽平移 / 双击复位") print("脉冲 %d 个,活动时长 %.1f ms" % ( meta['pulses'], (t[-1] + hi_w[-1] / 2) * 1000)) plt.show() def main(): args = [a for a in sys.argv[1:] if not a.startswith('--')] opts = {a.split('=', 1)[0]: (a.split('=', 1)[1] if '=' in a else True) for a in sys.argv[1:] if a.startswith('--')} if len(args) < 1 or '--help' in opts or '-h' in opts: print(__doc__) sys.exit(0) path = args[0] fs = float(args[1]) if len(args) > 1 else 6.25e6 thresh = int(opts.get('--thresh', 128)) ymax = float(opts.get('--ymax', 0)) save_path = opts.get('--save', '') if not os.path.isfile(path): print("错误:文件不存在 - %s" % path) sys.exit(1) if '--selftest' in opts: selftest(path, fs, thresh) else: plot_show(path, fs, thresh, ymax, save_path or None) if __name__ == '__main__': main()