Enumerate all fixed-width fonts

本文介绍如何使用EnumFontFamiliesEx函数枚举系统中所有固定宽度字体及其可用大小,包括设置LOGFONT结构体及编写回调函数的具体步骤。

Enumerating fonts can be a little confusing, and unless you want to enumerate all fonts on your system, can be a little more difficult than MSDN suggests. This article will explain exactly the steps you need to use to find every fixed-width font on your system, and also enumerate every possible size for each individual font.

Enumerate fonts

The best function to use for font enumeration is EnumFontFamiliesEx. You must set up a LOGFONT structure which tells EnumFontFamilesEx what fonts you wish to enumerate. You only need to specify three members of this structure.

The lfCharSet member indicates which character set you want to select. The DEFAULT_CHARSET value indicates that all character sets (i.e. OEM, ANSI, Chinese etc) will be enumerated. You could always specify ANSI_CHARSET, but in this case we want OEM character sets as well.

The lfPitchAndFamily member describes the look of a font in a general way. In this case, we don't mind what the font will look like (FF_DONTCARE), because we want to enumerate all fonts anyway, right? The FIXED_PITCH value just tells Windows that we are only interested in fixed-width fonts.

Finally, the lfFaceName member is used to tell windows what font we want to enumerate. If you specify the name of a string here, then Windows will enumerate the available font sizes for that font. First of all though, we want to enumerate the font names themselves, so we will just set this to an empty string. Here's the code.

int EnumFixedFonts(void)
{
    LOGFONT logfont;

    ZeroMemory(&logfont, sizeof logfont);

    logfont.lfCharSet = DEFAULT_CHARSET;
    logfont.lfPitchAndFamily = FIXED_PITCH | FF_DONTCARE;
    lstrcpy(logfont.lfFaceName, "/0");

    hdc = GetDC(0);

    EnumFontFamiliesEx(hdc, &logfont, (FONTENUMPROC)FontNameProc, 0, 0);

    ReleaseDC(0, hdc);

    return 0;
}

Before you can use this call though, you must write a callback function which Windows will call during the font enumeration. This callback procedure is called once for every font windows finds. Bear in mind that this procedure could be called several times for the same font, once for every character set. This is what the callback function should look like.

int CALLBACK FontNameProc(
    ENUMLOGFONTEX    *lpelfe,   /* pointer to logical-font data */
    NEWTEXTMETRICEX  *lpntme,   /* pointer to physical-font data */
    int              FontType,  /* type of font */
    LPARAM           lParam     /* a combo box HWND */
    )
{
    int i;

    if(lpelfe->elfLogFont.lfPitchAndFamily & FIXED_PITCH)
    {
        /* Make sure the fonts are only added once */
        for(i = 0; i < curfont; i++)
        {
            if(lstrcmp(currentfonts[i], (char *)lpelfe->elfFullName) == 0)
                return 1;
        }

        printf("%-16s: ", lpelfe->elfFullName);        
        cursize = 0;
        EnumFontSizes((char *)lpelfe->elfFullName);
        printf("/n");

        lstrcpy(currentfonts[curfont], (char *)lpelfe->elfFullName);
        
        if(++curfont == 200)
            return 0;
    
    }

    return 1;
}

An important part of the callback function is to check if the font has already been enumerated. In this case we keep a list of all fonts that have been enumerated so far. If the callback function executes and the font is already in the list of "processed fonts", then the function just returns 1 (or any non-zero value) to continue to the next font.

Now that we have a method to find every fixed-width font, we can enumerate all of the font sizes for each font.

Enumerate font sizes

Font size enumeration is almost the same as enumerating the fonts themselves. The only difference is that we specify the name of each font to get the sizes for. The following function enumerates all available font sizes for the specified font name

int EnumFontSizes(char *fontname)
{
    LOGFONT logfont;

    ZeroMemory(&logfont, sizeof logfont);

    logfont.lfHeight = 0;
    logfont.lfCharSet = DEFAULT_CHARSET;
    logfont.lfPitchAndFamily = FIXED_PITCH | FF_DONTCARE;
    lstrcpy(logfont.lfFaceName, fontname);

    EnumFontFamiliesEx(hdc, &logfont, (FONTENUMPROC)FontSizesProc, 0, 0);

    return 0;
}

By looking at the font name callback listed previously, you will see that this font size enumeration is executed for every unique font that we find. This is purely for the purpose of this example, as I wanted to display the available font sizes along side each font. In a normal Windows application, you would probably want to just store the font names in a drop-down list box, and whenever a font was selected from this list, fill up another list box with its available sizes.

int CALLBACK FontSizesProc(
    LOGFONT *plf,      /* pointer to logical-font data */
    TEXTMETRIC *ptm,   /* pointer to physical-font data */
    DWORD FontType,    /* font type */
    LPARAM lParam      /* pointer to application-defined data */
    )
{
    static int truetypesize[] = { 8, 9, 10, 11, 12, 14, 16, 18, 20, 
            22, 24, 26, 28, 36, 48, 72 };


    int i;

    if(FontType != TRUETYPE_FONTTYPE)
    {
        int  logsize    = ptm->tmHeight - ptm->tmInternalLeading;
        long pointsize  = MulDiv(logsize, 72, GetDeviceCaps(hdc, LOGPIXELSY));

        for(i = 0; i < cursize; i++)
            if(currentsizes[i] == pointsize) return 1;

        printf("%d ", pointsize);
        
        currentsizes[cursize] = pointsize;
        if(++cursize == 200) return 0;
        
        return 1;   
    }
    else
    {

        for(i = 0; i < (sizeof(truetypesize) / sizeof(truetypesize[0])); i++)
        {
            printf("%d ", truetypesize[i]);
        }

        return 0;
    }

}

An important thing to note for the font-size callback function (above) is the test to see if the font is true-type or not. If it is, then all that is necessary is to print a list of pre-determined point sizes which are deamed suitable. In this case, the callback will only be executed once for a true-type font. However, if the font is a raster font, then the callback will be executed once for every size that the font supports. It is our job to convert the size of the font from logical units into point sizes, which are generally more useful if they are to be presented to a user.

Thats it. All that is needed now is to start off the enumeration!

int main(void)
{
    EnumFixedFonts();
    return 0;
}

Below is the output of the program on my system, which currently has six fixed-width fonts installed. Notice that three of the fonts are obviously raster fonts, and have a limited number of sizes, whereas the Courier New, Lucida and Andale fonts are true-type.

Terminal        : 9 5 6 14 12
Fixedsys        : 9
Courier         : 10 12 15
Courier New     : 8 9 10 11 12 14 16 18 20 22 24 26 28 36 48 72
Lucida Console  : 8 9 10 11 12 14 16 18 20 22 24 26 28 36 48 72
Andale Mono     : 8 9 10 11 12 14 16 18 20 22 24 26 28 36 48 72
 
# 导入必要库 import pandas as pd import matplotlib import matplotlib.pyplot as plt import matplotlib.font_manager as fm import logging import sys import os from matplotlib.patches import Patch # 配置日志 logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') logger = logging.getLogger('PowerBI_YoY_Chart') # 配置中文字体支持 def configure_chinese_font(): """配置中文字体支持""" try: # 尝试加载常用中文字体 font_names = ['SimHei', 'Microsoft YaHei', 'Arial Unicode MS', 'WenQuanYi Micro Hei', 'SimSun'] available_fonts = [f.name for f in fm.fontManager.ttflist] # 记录可用字体 logger.info(f"可用字体: {', '.join(available_fonts[:10])}...") for font_name in font_names: if font_name in available_fonts: plt.rcParams['font.family'] = font_name logger.info(f"使用中文字体: {font_name}") return True # 尝试加载Windows系统字体 if sys.platform == 'win32': font_paths = [ r'C:\Windows\Fonts\simhei.ttf', # 黑体 r'C:\Windows\Fonts\msyh.ttc', # 微软雅黑 r'C:\Windows\Fonts\simsun.ttc', # 宋体 r'C:\Windows\Fonts\STSONG.TTF' # 华文宋体 ] for font_path in font_paths: if os.path.exists(font_path): font_prop = fm.FontProperties(fname=font_path) plt.rcParams['font.family'] = font_prop.get_name() logger.info(f"使用系统字体: {font_path}") return True # 使用默认字体 plt.rcParams['font.family'] = ['sans-serif'] logger.warning("未找到中文字体,可能无法正确显示中文") return False except Exception as e: logger.error(f"字体配置错误: {str(e)}") return False # 配置中文字体 configure_chinese_font() plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题 def sort_months(months): """对月份进行排序:英文月份→数字月份→Average""" month_order = { 'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6, 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12, 'January': 1, 'February': 2, 'March': 3, 'April': 4, 'May': 5, 'June': 6, 'July': 7, 'August': 8, 'September': 9, 'October': 10, 'November': 11, 'December': 12 } numeric_months = [] en_month_list = [] average_month = None for month in months: if pd.isna(month): continue str_month = str(month).strip() if str_month.lower() == 'average': average_month = 'Average' elif str_month in month_order: en_month_list.append(str_month) elif str_month.isdigit(): numeric_months.append(int(str_month)) else: # 尝试匹配其他格式的月份 str_month_lower = str_month.lower() if str_month_lower.startswith('jan'): en_month_list.append('Jan') elif str_month_lower.startswith('feb'): en_month_list.append('Feb') elif str_month_lower.startswith('mar'): en_month_list.append('Mar') elif str_month_lower.startswith('apr'): en_month_list.append('Apr') elif str_month_lower.startswith('may'): en_month_list.append('May') elif str_month_lower.startswith('jun'): en_month_list.append('Jun') elif str_month_lower.startswith('jul'): en_month_list.append('Jul') elif str_month_lower.startswith('aug'): en_month_list.append('Aug') elif str_month_lower.startswith('sep'): en_month_list.append('Sep') elif str_month_lower.startswith('oct'): en_month_list.append('Oct') elif str_month_lower.startswith('nov'): en_month_list.append('Nov') elif str_month_lower.startswith('dec'): en_month_list.append('Dec') else: # 无法识别的月份原样保留 en_month_list.append(str_month) # 排序并拼接结果 en_month_list.sort(key=lambda x: month_order.get(x, month_order.get(x.capitalize(), 13))) numeric_months.sort() sorted_months = en_month_list + [str(m) for m in numeric_months] if average_month: sorted_months.append(average_month) return sorted_months # 主绘图函数 def plot_yoy_chart(dataset): """生成年度同比变化图表""" try: # 记录开始时间 logger.info("开始生成图表...") # 创建图表(增大图表尺寸以适应更大字体) fig, ax = plt.subplots(figsize=(14, 8)) # 复制数据集以防修改原始数据 df = dataset.copy() # 记录初始数据行数 logger.info(f"原始数据行数: {len(df)}") # 检查必需列是否存在 required_columns = ['Month', 'QTY', 'Year', 'Percentage'] missing_columns = [col for col in required_columns if col not in df.columns] if missing_columns: error_msg = f"缺少必需列: {', '.join(missing_columns)}" logger.error(error_msg) ax.text(0.5, 0.5, error_msg, ha='center', va='center', fontsize=16) plt.tight_layout() return plt # 清洗数据 df = df.dropna(subset=required_columns) df = df[df['QTY'] > 0] # 移除无效QTY # 转换数据类型 df['Year'] = pd.to_numeric(df['Year'], errors='coerce').astype('Int64') df['Percentage'] = pd.to_numeric(df['Percentage'], errors='coerce') df['QTY'] = pd.to_numeric(df['QTY'], errors='coerce') df = df.dropna(subset=['Year', 'Percentage', 'QTY']) logger.info(f"清洗后数据行数: {len(df)}") # 检查是否有有效数据 if df.empty: ax.text(0.5, 0.5, '没有有效数据', ha='center', va='center', fontsize=16) plt.tight_layout() return plt # 获取年份数据并检查 years = sorted(df['Year'].unique()) if len(years) < 2: ax.text(0.5, 0.5, '需要至少两年数据才能生成图表', ha='center', va='center', fontsize=16) plt.tight_layout() return plt # 选取最近的两年数据 prev_year, curr_year = years[-2], years[-1] all_months = sort_months(df['Month'].unique()) logger.info(f"排序后的月份: {all_months}") # 准备绘图数据 data = { 'months': [], 'prev_qty': [], 'curr_qty': [], 'diff_percent': [], 'is_average': [] # 标记是否为Average } for month in all_months: prev_data = df[(df['Year'] == prev_year) & (df['Month'] == month)] curr_data = df[(df['Year'] == curr_year) & (df['Month'] == month)] if not prev_data.empty and not curr_data.empty: data['months'].append(month) data['prev_qty'].append(prev_data['QTY'].values[0]) data['curr_qty'].append(curr_data['QTY'].values[0]) # 计算百分比差异 prev_p = prev_data['Percentage'].values[0] curr_p = curr_data['Percentage'].values[0] diff_p = (curr_p - prev_p) * 100 data['diff_percent'].append(diff_p) # 标记是否为Average data['is_average'].append(str(month).strip().lower() == 'average') # 检查是否有共同月份数据 if not data['months']: ax.text(0.5, 0.5, f'{prev_year} 和 {curr_year} 无共同月份数据', ha='center', va='center', fontsize=16) plt.tight_layout() return plt # 创建子图并绘制柱状图 width = 0.35 x_pos = range(len(data['months'])) # 绘制两年数据的柱状图(区分Average组) for i, (month, prev_qty, curr_qty) in enumerate(zip(data['months'], data['prev_qty'], data['curr_qty'])): is_avg = data['is_average'][i] # 设置颜色:Average组使用紫色,其他组使用默认颜色 prev_color = '#C04F15' if is_avg else '#00B0F0' # 紫色 vs 蓝色 118DFF curr_color = '#F6C6AD' if is_avg else '#A0D1FF' # 浅紫色 vs 橙色C04F15 # 绘制柱状图 prev_bar = ax.bar( x_pos[i] - width/2, prev_qty, width, label=f'{prev_year}' if i == 0 else None, color=prev_color ) curr_bar = ax.bar( x_pos[i] + width/2, curr_qty, width, label=f'{curr_year}' if i == 0 else None, color=curr_color ) # 为柱状图添加数值标签(增大字体) for bar in [prev_bar, curr_bar]: height = bar[0].get_height() ax.text( bar[0].get_x() + bar[0].get_width()/2, height * 1.02, f'{int(height):,}', ha='center', va='bottom', fontweight='bold', fontsize=10 ) # 计算所有柱子的最大高度,用于比例计算 max_qty = max(max(data['prev_qty']), max(data['curr_qty'])) # 调整 Y 轴范围,预留足够空间(增加顶部空间以容纳上移的元素) ax.set_ylim(0, max_qty * 1.6) # 增加更多顶部空间 # 固定文本偏移量(增大值使文本上移更多) fixed_text_offset = max_qty * 0.06 # 增加偏移量 # U形框与柱子顶部标签的间距,解决重叠问题 frame_offset = max_qty * 0.05 # 遍历每个月份,绘制U形虚线框、箭头、百分比文本 for i in range(len(data['months'])): # 获取当前组的两根柱子高度 prev_height = data['prev_qty'][i] # 左侧柱子高度 curr_height = data['curr_qty'][i] # 右侧柱子高度 # 计算U形框位置参数 frame_left = x_pos[i] - width/2 frame_right = x_pos[i] + width/2 # 调整U形框与柱子的间距,避免与顶部数字重叠 base_top = max(prev_height, curr_height) + frame_offset # U 形框竖线高度(略微减小,为上移的箭头留出空间) vert_height = max_qty * 0.08 # 稍微减小竖线高度 left_top = base_top + vert_height right_top = base_top + vert_height # U 形框顶部横线的 y 坐标(保持水平) top_line_y = max(left_top, right_top) # 绘制U形虚线框 # 左侧竖线:从柱子顶部上方开始 ax.plot([frame_left, frame_left], [prev_height + frame_offset, left_top], '--', color='gray', linewidth=1.0) # 右侧竖线:从柱子顶部上方开始 ax.plot([frame_right, frame_right], [curr_height + frame_offset, right_top], '--', color='gray', linewidth=1.0) # 顶部横线(连接左右竖线顶端) ax.plot([frame_left, frame_right], [top_line_y, top_line_y], '--', color='gray', linewidth=1.0) # 计算箭头和文本的位置(显著增加垂直间距,使元素上移) arrow_y = top_line_y + max_qty * 0.05 # 箭头位置大幅上移 text_y = arrow_y + fixed_text_offset # 文本位置基于调整后的箭头位置 center_x = (frame_left + frame_right) / 2 # 水平居中 # 根据百分比设置颜色和标记 diff_p = data['diff_percent'][i] if diff_p > 0: arrow_color = '#d62728' # 红色 marker = '^' # 向上三角形 marker_size = 80 elif diff_p < 0: arrow_color = '#2ca02c' # 绿色 marker = 'v' # 向下三角形 marker_size = 80 else: arrow_color = '#7f7f7f' # 灰色 marker = '_' # 水平线标记 marker_size = 80 # 绘制箭头 ax.scatter( center_x, arrow_y, marker=marker, color=arrow_color, s=marker_size, zorder=5 ) # 绘制百分比文本(增大字体并上移) text_color = arrow_color ax.text( center_x, text_y, f'{diff_p:+.1f}%', ha='center', va='bottom', fontsize=11, fontweight='bold', color=text_color ) # 设置图表属性(特别加大X轴字体) ax.set_xlabel('Month', fontsize=20) ax.set_ylabel('QTY', fontsize=20) # 增大Y轴标签字体到20 ax.set_title('YoY OTS QTY Change', fontsize=28, pad=20) ax.set_xticks(x_pos) ax.set_xticklabels(data['months'], fontsize=15, rotation=45, ha='right') # 设置网格线 - 虚线,只显示Y轴网格线,并与刻度对齐 ax.grid(True, axis='y', linestyle='--', linewidth=0.8, alpha=0.7) ax.grid(False, axis='x') # 不显示X轴网格线 # 设置Y轴刻度标签字体大小 ax.tick_params(axis='y', labelsize=14) # 添加自定义图例并放置在右上角 legend_elements = [ Patch(facecolor='#00B0F0', label=f'{prev_year}'), Patch(facecolor='#A0D1FF', label=f'{curr_year}') ] ax.legend( handles=legend_elements, fontsize=14, loc='upper right', # 将图例放置在右上角 frameon=False # 去除图例外框 ) # 去除所有图表边框 for spine in ['top', 'right', 'left', 'bottom']: ax.spines[spine].set_visible(False) # 重新显示底部边框(X轴) ax.spines['bottom'].set_visible(True) ax.spines['bottom'].set_color('#cccccc') # 设置为浅灰色 # 调整布局(增加顶部和底部空间) plt.subplots_adjust(top=0.88, bottom=0.15) plt.tight_layout() logger.info("图表生成成功") return plt except Exception as e: # 记录详细错误信息 logger.exception("图表生成过程中发生错误") # 创建错误信息图表 plt.figure(figsize=(8, 4)) error_msg = f"图表生成错误: {str(e)}" plt.text(0.5, 0.5, error_msg, ha='center', va='center', fontsize=14, color='red') plt.tight_layout() return plt # 在Power BI中执行的主函数 def main(dataset): """Power BI 入口函数""" try: # 检查数据集 if dataset is None or dataset.empty: plt.figure(figsize=(8, 4)) plt.text(0.5, 0.5, "没有提供数据", ha='center', va='center', fontsize=16) plt.tight_layout() plt.show() return # 生成图表 plot = plot_yoy_chart(dataset) # 显示图表 plot.show() except Exception as e: logger.exception("主函数执行错误") plt.figure(figsize=(8, 4)) plt.text(0.5, 0.5, f"执行错误: {str(e)}", ha='center', va='center', fontsize=14, color='red') plt.tight_layout() plt.show() # 执行主函数(Power BI会自动提供dataset变量) main(dataset) 以上代码,我需要将百分比数相减结果是0时,在箭头上方的百分比数前不要显示“+”号,请按我的需求改好,并给出完整代码。
08-09
import cv2 import numpy as np import math from collections import deque from ultralytics import YOLO import time import os try: from PIL import ImageFont, ImageDraw, Image PIL_AVAILABLE = True except ImportError: PIL_AVAILABLE = False # 关键点索引定义 KEYPOINT_INDICES = { "left_shoulder": 5, "right_shoulder": 6, "left_elbow": 7, "right_elbow": 8, "left_wrist": 9, "right_wrist": 10, "left_hip": 11, "right_hip": 12, "left_ear": 3, "right_ear": 4 } def draw_stability_bar(panel, x, stability, color): """绘制稳定性进度条""" bar_width = 60 fill_width = int(bar_width * stability / 100) cv2.rectangle(panel, (x, 20 - 10), (x + bar_width, 20 + 5), (100, 100, 100), -1) cv2.rectangle(panel, (x, 20 - 10), (x + fill_width, 20 + 5), color, -1) stability_text = f"{stability:.0f}%" cv2.putText(panel, stability_text, (x + bar_width + 5, 20), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 1) class DumbbellCurlAnalyzer: def __init__(self, model_path='yolov8s-pose.pt', display_width=1280, display_height=720): """初始化哑铃弯举分析器""" self.display_width = display_width self.display_height = display_height # 尝试加载模型 try: print(f"正在加载模型: {model_path}") self.model = YOLO(model_path) test_img = np.zeros((640, 640, 3), dtype=np.uint8) self.model.predict(test_img, verbose=False) print("模型加载成功") except Exception as e: raise RuntimeError(f"模型加载失败: {str(e)}") # 性能优化参数 self.skip_counter = 0 self.skip_interval = 2 # 每3帧处理1帧 self.last_results = None # 结果缓存 # 多人状态跟踪 self.max_persons = 3 self.person_states = [] # 动态创建状态 # 颜色映射 self.person_colors = [ (0, 255, 0), # 绿色 (0, 165, 255), # 橙色 (0, 0, 255) # 红色 ] # 角度阈值 self.min_angle = 0 self.max_angle = 150 # 代偿参数 self.compensation_threshold = 0.05 # 身高的5% self.compensation_sensitivity = 0.6 # 帧率跟踪 self.prev_frame_time = 0 self.current_fps = 30 self.fps_smoother = deque(maxlen=5) # 中文支持 self.PIL_AVAILABLE = PIL_AVAILABLE self.DEFAULT_FONT_PATH = self._find_font_path() # 初始化历史计数 self.counter_history = {} def _find_font_path(self): """查找系统中可用的中文字体""" possible_fonts = [ "C:/Windows/Fonts/simsun.ttc", # Windows 宋体 "C:/Windows/Fonts/simhei.ttf", # Windows 黑体 "/usr/share/fonts/truetype/wqy/wqy-microhei.ttc", # Linux 文泉驿微米黑 "/System/Library/Fonts/PingFang.ttc", # macOS 苹方 "/Library/Fonts/SimHei.ttf" # macOS 黑体 ] for font_path in possible_fonts: if os.path.exists(font_path): print(f"找到中文字体: {font_path}") return font_path print("警告: 未找到中文字体,中文可能无法正常显示") return None def calculate_angle(self, a, b, c): """计算三点夹角(B为顶点)""" ba = [a[0] - b[0], a[1] - b[1]] bc = [c[0] - b[0], c[1] - b[1]] dot_product = ba[0] * bc[0] + ba[1] * bc[1] magnitude_ba = math.sqrt(ba[0] ** 2 + ba[1] ** 2) magnitude_bc = math.sqrt(bc[0] ** 2 + bc[1] ** 2) if magnitude_ba * magnitude_bc == 0: return 0 cosine = dot_product / (magnitude_ba * magnitude_bc) cosine = max(-1.0, min(1.0, cosine)) return math.degrees(math.acos(cosine)) def detect_compensation(self, keypoints_dict, side, person_state): """检测肩部代偿动作""" shoulder = f"{side}_shoulder" ear = f"{side}_ear" compensation_types = [] confidence = 0 # 身高估计 ref_distance = self.get_reference_distance(keypoints_dict) if ref_distance < 10: return compensation_types, confidence # 肩部位移检测 if shoulder in keypoints_dict: tracker = person_state["shoulder_trackers"][side] current_pos = keypoints_dict[shoulder] # 添加速度计算 avg_speed = 0 if len(tracker["path"]) > 0: # 计算最近5帧的平均移动速度 speeds = [] for i in range(1, min(5, len(tracker["path"]))): dx = tracker["path"][-i][0] - tracker["path"][-i - 1][0] dy = tracker["path"][-i][1] - tracker["path"][-i - 1][1] speed = math.sqrt(dx ** 2 + dy ** 2) / ref_distance speeds.append(speed) if speeds: avg_speed = sum(speeds) / len(speeds) # 速度阈值-小于此值视为静止 SPEED_THRESHOLD = 0.005 # 身高的0.5% if tracker["previous"]: dx = current_pos[0] - tracker["previous"][0] dy = current_pos[1] - tracker["previous"][1] # 相对位移计算 relative_dx = dx / ref_distance relative_dy = dy / ref_distance # 只有当速度超过阈值时才进行代偿检测 if avg_speed > SPEED_THRESHOLD: if abs(relative_dx) > self.compensation_threshold or abs(relative_dy) > self.compensation_threshold: compensation_types.append(f"shoulder_displacement_{side}") confidence += 0.4 # 耸肩检测(相对位移dy为负表示向上) if relative_dy < -self.compensation_threshold: compensation_types.append(f"shoulder_elevation_{side}") confidence += 0.3 # 更新代偿计数 if relative_dx > self.compensation_threshold or relative_dy < -self.compensation_threshold: tracker["compensation_count"] = min(10, tracker["compensation_count"] + 1) else: tracker["compensation_count"] = max(0, tracker["compensation_count"] - 2) else: # 静止状态下减少代偿计数 tracker["compensation_count"] = max(0, tracker["compensation_count"] - 3) # 更新历史位置 tracker["previous"] = current_pos tracker["path"].append(current_pos) else: # 第一次检测到,初始化previous tracker["previous"] = current_pos tracker["path"].append(current_pos) # 连续代偿增强置信度 if "shoulder_trackers" in person_state and side in person_state["shoulder_trackers"]: tracker = person_state["shoulder_trackers"][side] if tracker["compensation_count"] > 3: confidence += min(0.3, tracker["compensation_count"] * 0.1) # 肩耳相对位置检测-仅当有移动时才检测 if avg_speed > SPEED_THRESHOLD and shoulder in keypoints_dict and ear in keypoints_dict: shoulder_y = keypoints_dict[shoulder][1] ear_y = keypoints_dict[ear][1] elevation_ratio = (ear_y - shoulder_y) / ref_distance if elevation_ratio < 0.25: compensation_types.append(f"shoulder_elevation_{side}") confidence += max(0.3, (0.25 - elevation_ratio) * 2) return compensation_types, min(1.0, confidence) def get_reference_distance(self, keypoints_dict): """估计身高作为参考""" if "left_shoulder" in keypoints_dict and "right_shoulder" in keypoints_dict: left = keypoints_dict["left_shoulder"] right = keypoints_dict["right_shoulder"] shoulder_width = math.sqrt((left[0] - right[0]) ** 2 + (left[1] - right[1]) ** 2) return shoulder_width * 4 # 肩宽×4估计身高 elif "left_hip" in keypoints_dict and "right_hip" in keypoints_dict: left = keypoints_dict["left_hip"] right = keypoints_dict["right_hip"] hip_width = math.sqrt((left[0] - right[0]) ** 2 + (left[1] - right[1]) ** 2) return hip_width * 3 # 髋宽×3估计身高 return 0 def analyze_motion_phase(self, side, person_state): """判断动作阶段(上举/下落/保持)""" angles = list(person_state["history_angles"][side]) if len(angles) < 5: return "UNKNOWN" # 计算速度 velocity = np.mean(np.diff(angles[-5:])) if len(angles) >= 5 else 0 if velocity > 7: return "LIFTING" elif velocity < -7: return "LOWERING" else: return "HOLDING" def interpolate_point(self, previous_point, current_pos, max_distance=100): """关键点缺失时插值""" if previous_point is None: return current_pos dx = current_pos[0] - previous_point[0] dy = current_pos[1] - previous_point[1] distance = math.sqrt(dx ** 2 + dy ** 2) if distance > max_distance: return current_pos return previous_point def get_or_create_person_state(self, center): """获取或创建人员状态""" # 如果状态列表为空,直接创建第一个状态 if not self.person_states: return self.create_new_person_state(center) # 寻找最近的现有状态 min_dist = float('inf') closest_idx = None for i, state in enumerate(self.person_states): if state["last_position"]: dist = math.sqrt( (center[0] - state["last_position"][0]) ** 2 + (center[1] - state["last_position"][1]) ** 2 ) if dist < min_dist: min_dist = dist closest_idx = i # 如果没有足够近的现有状态,创建新状态 if min_dist > 100 or closest_idx is None: if len(self.person_states) < self.max_persons: return self.create_new_person_state(center) else: # 已满,返回最旧的状态 return self.person_states[0], 0 # 更新最近状态的位置 self.person_states[closest_idx]["last_position"] = center return self.person_states[closest_idx], closest_idx def create_new_person_state(self, center): """创建新的人员状态""" new_state = { "history_angles": { "left": deque(maxlen=15), "right": deque(maxlen=15) }, "shoulder_trackers": { "left": {"path": deque(maxlen=30), "previous": None, "compensation_count": 0}, "right": {"path": deque(maxlen=30), "previous": None, "compensation_count": 0} }, "prev_keypoints": { "left": {"shoulder": None, "elbow": None, "wrist": None}, "right": {"shoulder": None, "elbow": None, "wrist": None} }, "missing_frames": { "left": {"shoulder": 0, "elbow": 0, "wrist": 0}, "right": {"shoulder": 0, "elbow": 0, "wrist": 0} }, "counter": {"left": 0, "right": 0}, "counter_state": {"left": "down", "right": "down"}, "last_position": center } self.person_states.append(new_state) return new_state, len(self.person_states) - 1 def analyze_frame(self, frame): """分析单帧图像""" # 帧率计算 current_time = time.time() if self.prev_frame_time > 0: self.current_fps = 1 / (current_time - self.prev_frame_time) self.prev_frame_time = current_time # 平滑帧率 self.fps_smoother.append(self.current_fps) if len(self.fps_smoother) > 0: smoothed_fps = sum(self.fps_smoother) / len(self.fps_smoother) else: smoothed_fps = self.current_fps # 跳帧处理 self.skip_counter = (self.skip_counter + 1) % (self.skip_interval + 1) if self.skip_counter != 0 and self.last_results is not None: return self.last_results # 调整帧大小以匹配显示尺寸 frame = cv2.resize(frame, (self.display_width, self.display_height)) # 姿态估计 results = self.model(frame, verbose=False) # 结果初始化 analysis_results = { "fps": smoothed_fps, "persons": [] # 存储每个人的结果 } try: # 动态置信度阈值 conf_threshold = max(0.2, min(0.7, 0.5 * (smoothed_fps / 30))) if len(results) == 0 or results[0].keypoints is None: return analysis_results, frame boxes = results[0].boxes.xyxy.cpu().numpy() kpts = results[0].keypoints.data.cpu().numpy() if len(boxes) == 0: return analysis_results, frame # 根据面积排序 areas = (boxes[:, 2] - boxes[:, 0]) * (boxes[:, 3] - boxes[:, 1]) sorted_idxs = np.argsort(areas)[::-1][:self.max_persons] # 取面积最大的三个 # 处理每个人 for idx in sorted_idxs: kpts_data = kpts[idx] box = boxes[idx] center = ((box[0] + box[2]) / 2, (box[1] + box[3]) / 2) # 获取或创建对应的状态 person_state, person_id = self.get_or_create_person_state(center) # 提取关键点 keypoints_dict = {} for part, idx_kpt in KEYPOINT_INDICES.items(): if idx_kpt < len(kpts_data): x, y, conf = kpts_data[idx_kpt] if conf > conf_threshold: keypoints_dict[part] = (int(x), int(y)) side, point = part.split('_', 1) if point in person_state["missing_frames"][side]: if conf > conf_threshold: person_state["missing_frames"][side][point] = 0 else: person_state["missing_frames"][side][point] += 1 # 关键点插值 for side in ["left", "right"]: for point in ["shoulder", "elbow", "wrist"]: key_name = f"{side}_{point}" if key_name not in keypoints_dict and person_state["prev_keypoints"][side][point] is not None: if person_state["missing_frames"][side][point] < 15: # 最大插值帧数 keypoints_dict[key_name] = self.interpolate_point( person_state["prev_keypoints"][side][point], person_state["prev_keypoints"][side][point] ) # 更新历史关键点 for side in ["left", "right"]: for point in ["shoulder", "elbow", "wrist"]: key_name = f"{side}_{point}" prev_key = person_state["prev_keypoints"][side][point] if key_name in keypoints_dict: person_state["prev_keypoints"][side][point] = keypoints_dict[key_name] else: person_state["prev_keypoints"][side][point] = prev_key # 初始化个人结果 person_result = { "id": person_id, "color": self.person_colors[person_id % len(self.person_colors)], "left_angle": None, "right_angle": None, "left_feedback": "", "right_feedback": "", "left_compensation": [], "right_compensation": [], "left_compensation_confidence": 0, "right_compensation_confidence": 0, "left_phase": "UNKNOWN", "right_phase": "UNKNOWN", "left_count": person_state["counter"]["left"], "right_count": person_state["counter"]["right"], "box": box, "keypoints": keypoints_dict } # 更新历史计数 person_key = f"person_{person_id}" if person_key not in self.counter_history: self.counter_history[person_key] = [] self.counter_history[person_key].append( person_state["counter"]["left"] + person_state["counter"]["right"]) # 分析左右手臂 for side in ["left", "right"]: shoulder = f"{side}_shoulder" elbow = f"{side}_elbow" wrist = f"{side}_wrist" if shoulder in keypoints_dict and elbow in keypoints_dict and wrist in keypoints_dict: # 计算角度 angle = self.calculate_angle( keypoints_dict[shoulder], keypoints_dict[elbow], keypoints_dict[wrist] ) # 添加到历史 person_state["history_angles"][side].append(angle) person_result[f"{side}_angle"] = angle # 动作阶段 phase = self.analyze_motion_phase(side, person_state) person_result[f"{side}_phase"] = phase # 反馈信息 feedback = "" if angle < self.min_angle: feedback = "手臂过度伸展!" elif angle > self.max_angle: feedback = "弯曲角度过大!" else: feedback = "动作规范" feedback += f"|{phase}" # 代偿检测 compensations, confidence = self.detect_compensation(keypoints_dict, side, person_state) person_result[f"{side}_compensation"] = compensations person_result[f"{side}_compensation_confidence"] = confidence # 代偿反馈 if confidence > self.compensation_sensitivity: if f"shoulder_displacement_{side}" in compensations: feedback += "|肩部不稳定!" if f"shoulder_elevation_{side}" in compensations: feedback += "|避免耸肩!" person_result[f"{side}_feedback"] = feedback # 动作计数 if angle < 40 and person_state["counter_state"][side] == "down": person_state["counter_state"][side] = "up" elif angle > 120 and person_state["counter_state"][side] == "up": person_state["counter"][side] += 1 person_state["counter_state"][side] = "down" person_result[f"{side}_count"] = person_state["counter"][side] else: person_result[f"{side}_feedback"] = f"{side}关键点未检测到" analysis_results["persons"].append(person_result) # 可视化 viz_frame = self.visualize_feedback(frame, analysis_results) except Exception as e: print(f"分析错误: {str(e)}") viz_frame = frame analysis_results["persons"] = [] self.last_results = (analysis_results, viz_frame) return analysis_results, viz_frame def visualize_feedback(self, frame, analysis_results): """可视化分析结果""" viz_frame = frame.copy() height, width = frame.shape[:2] # 绘制人物信息(骨架、关键点等) for person in analysis_results["persons"]: color = person["color"] # 绘制边界框 box = person["box"] cv2.rectangle(viz_frame, (int(box[0]), int(box[1])), (int(box[2]), int(box[3])), color, 2) # 绘制ID cv2.putText(viz_frame, f"ID:{person['id']}", (int(box[0]), int(box[1]) - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2) # 显示计数信息 count_text = f"L:{person['left_count']} R:{person['right_count']}" cv2.putText(viz_frame, count_text, (int(box[0]), int(box[1]) + 25), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) # 关键点可视化 for person in analysis_results["persons"]: color = person["color"] keypoints_dict = person.get("keypoints", {}) # 获取对应的person_state if person["id"] < len(self.person_states): person_state = self.person_states[person["id"]] else: continue # 跳过无法找到状态的人 # 绘制身体骨架 (简化版) skeleton_pairs = [ ("left_shoulder", "left_elbow"), ("left_elbow", "left_wrist"), ("right_shoulder", "right_elbow"), ("right_elbow", "right_wrist"), ("left_shoulder", "right_shoulder"), ("left_shoulder", "left_hip"), ("right_shoulder", "right_hip"), ("left_hip", "right_hip") ] for start, end in skeleton_pairs: if start in keypoints_dict and end in keypoints_dict: cv2.line(viz_frame, keypoints_dict[start], keypoints_dict[end], color, 2) # 绘制关节点 for point in keypoints_dict.values(): cv2.circle(viz_frame, point, 5, color, -1) # 绘制手臂角度 for side in ["left", "right"]: elbow_key = f"{side}_elbow" if person[f"{side}_angle"] and elbow_key in keypoints_dict: angle = person[f"{side}_angle"] position = (keypoints_dict[elbow_key][0] + 10, keypoints_dict[elbow_key][1] - 10) cv2.putText(viz_frame, f"{angle:.0f}°", position, cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) # 实时反馈信息板 feedback_height = 120 feedback_panel = np.zeros((feedback_height, width, 3), dtype=np.uint8) feedback_panel[:] = (40, 40, 60) y_offset = 20 for person in analysis_results["persons"]: color = person["color"] id_text = f"ID {person['id']}:" if self.PIL_AVAILABLE and self.DEFAULT_FONT_PATH: feedback_panel = self.put_chinese_text(feedback_panel, id_text, (20, y_offset), color, 22) else: cv2.putText(feedback_panel, id_text, (20, y_offset), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 1) # 左右手臂反馈信息 for side in ["left", "right"]: feedback_text = f"{side}臂: {person[f'{side}_feedback']}" if self.PIL_AVAILABLE and self.DEFAULT_FONT_PATH: feedback_panel = self.put_chinese_text(feedback_panel, feedback_text, (100, y_offset), color, 18) else: cv2.putText(feedback_panel, feedback_text, (100, y_offset), cv2.FONT_HERSHEY_SIMPLEX, 0.4, color, 1) y_offset += 25 # 代偿信息 compensations = person["left_compensation"] + person["right_compensation"] if compensations: compensation_text = "代偿: " + ", ".join(compensations) if self.PIL_AVAILABLE and self.DEFAULT_FONT_PATH: feedback_panel = self.put_chinese_text(feedback_panel, compensation_text, (100, y_offset), (0, 0, 255), 18) else: cv2.putText(feedback_panel, compensation_text, (100, y_offset), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0, 0, 255), 1) y_offset += 25 y_offset += 10 # 人员间间隔 # 叠加反馈面板到右侧 feedback_width = min(400, width // 3) viz_frame[0:feedback_height, -feedback_width:] = cv2.addWeighted( viz_frame[0:feedback_height, -feedback_width:], 0.2, feedback_panel[:, -feedback_width:], 0.8, 0 ) # 绘制固定的数据框(顶部面板) self.draw_fixed_data_panel(viz_frame, analysis_results) return viz_frame def draw_fixed_data_panel(self, frame, analysis_results): """绘制固定在顶部的数据面板""" panel_height = 120 # 增加面板高度以容纳更多内容 panel_width = frame.shape[1] panel = np.zeros((panel_height, panel_width, 3), dtype=np.uint8) panel[:] = (40, 40, 60) # 深蓝灰色背景 # 标题 title = "多人哑铃弯举分析" if self.PIL_AVAILABLE and self.DEFAULT_FONT_PATH: panel = self.put_chinese_text(panel, title, (20, 30), (0, 200, 255), 28) else: cv2.putText(panel, title, (20, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 200, 255), 2) # 列标题和数据 headers = ["ID", "左计数", "右计数", "左肩稳定", "右肩稳定"] col_positions = [100, 200, 300, 420, 540] header_y = 60 # 标题行位置 data_y = 90 # 数据行位置 # 绘制列标题 for i, header in enumerate(headers): if self.PIL_AVAILABLE and self.DEFAULT_FONT_PATH: panel = self.put_chinese_text(panel, header, (col_positions[i], header_y), (0, 255, 255), 20) else: cv2.putText(panel, header, (col_positions[i], header_y), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 255), 1) # 绘制每个人的数据 for i, person in enumerate(analysis_results["persons"]): if i >= 3: # 只显示前3个人 break color = person["color"] x_offset = i * 200 # 为多个人物设置水平偏移 # ID if self.PIL_AVAILABLE and self.DEFAULT_FONT_PATH: panel = self.put_chinese_text(panel, str(person["id"]), (col_positions[0] + x_offset, data_y), color, 20) else: cv2.putText(panel, str(person["id"]), (col_positions[0] + x_offset, data_y), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 1) # 左计数 if self.PIL_AVAILABLE and self.DEFAULT_FONT_PATH: panel = self.put_chinese_text(panel, str(person["left_count"]), (col_positions[1] + x_offset, data_y), color, 20) else: cv2.putText(panel, str(person["left_count"]), (col_positions[1] + x_offset, data_y), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 1) # 右计数 if self.PIL_AVAILABLE and self.DEFAULT_FONT_PATH: panel = self.put_chinese_text(panel, str(person["right_count"]), (col_positions[2] + x_offset, data_y), color, 20) else: cv2.putText(panel, str(person["right_count"]), (col_positions[2] + x_offset, data_y), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 1) # 左肩稳定 left_stability = max(0, min(100, 100 - person["left_compensation_confidence"] * 100)) draw_stability_bar(panel, col_positions[3] + x_offset, left_stability, color) # 右肩稳定 right_stability = max(0, min(100, 100 - person["right_compensation_confidence"] * 100)) draw_stability_bar(panel, col_positions[4] + x_offset, right_stability, color) # 添加帧率信息 fps_text = f"FPS: {analysis_results['fps']:.1f}" cv2.putText(panel, fps_text, (panel_width - 150, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 1) # 添加操作提示 hint_text = "ESC退出 | F全屏 | S截图 | Q切换质量" if self.PIL_AVAILABLE and self.DEFAULT_FONT_PATH: panel = self.put_chinese_text(panel, hint_text, (panel_width - 350, panel_height - 15), (200, 200, 200), 18) else: cv2.putText(panel, hint_text, (panel_width - 350, panel_height - 15), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (200, 200, 200), 1) # 叠加面板到顶部 frame[0:panel_height, 0:panel_width] = cv2.addWeighted( frame[0:panel_height, 0:panel_width], 0.3, panel, 0.7, 0 ) def put_chinese_text(self, img, text, pos, color, font_size): """在图像上绘制中文文本""" if not self.PIL_AVAILABLE or self.DEFAULT_FONT_PATH is None: # 如果无法使用PIL或未找到字体,尝试使用默认字体 cv2.putText(img, text, pos, cv2.FONT_HERSHEY_SIMPLEX, font_size / 30, color, 2) return img try: img_pil = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)) draw = ImageDraw.Draw(img_pil) font = ImageFont.truetype(self.DEFAULT_FONT_PATH, font_size) draw.text(pos, text, font=font, fill=tuple(reversed(color))) return cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR) except Exception as e: print(f"绘制中文失败: {e}") # 回退到默认字体 cv2.putText(img, text, pos, cv2.FONT_HERSHEY_SIMPLEX, font_size / 30, color, 2) return img def main(): """主函数""" try: # 模型选择 model_options = { 's': 'yolov8s-pose.pt', # 小模型,性能优先 'm': 'yolov8m-pose.pt', # 中等模型,平衡速度和精度 'l': 'yolov8l-pose.pt' # 大模型,精度优先 } print("请选择模型:") print("1. 小模型 (yolov8s-pose, 快速但精度较低)") print("2. 中模型 (yolov8m-pose, 平衡速度和精度)") print("3. 大模型 (yolov8l-pose, 高精度但较慢)") model_choice = input("请输入数字 (1-3, 默认2): ") model_key = { '1': 's', '2': 'm', '3': 'l' }.get(model_choice, 'm') model_path = model_options[model_key] print(f"使用模型: {model_path}") # 创建分析器实例,指定显示尺寸 display_width = 1280 display_height = 720 analyzer = DumbbellCurlAnalyzer(model_path, display_width, display_height) # 打开摄像头 print("正在打开摄像头...") cap = cv2.VideoCapture(0) # 设置摄像头分辨率 cap.set(cv2.CAP_PROP_FRAME_WIDTH, display_width) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, display_height) # 检查摄像头是否成功打开 if not cap.isOpened(): print("无法打开摄像头") return # 设置窗口属性 cv2.namedWindow("哑铃弯举分析", cv2.WINDOW_NORMAL) cv2.resizeWindow("哑铃弯举分析", display_width, display_height) # 全屏标志 is_fullscreen = False # 模型质量级别 (影响处理速度) quality_level = 1 # 1=高(完整处理), 2=中(跳帧处理), 3=低(低分辨率) # 主循环 print("开始分析,请进行哑铃弯举动作...") print("操作提示:") print(" ESC: 退出程序") print(" F: 切换全屏显示") print(" S: 保存当前画面截图") print(" Q: 切换处理质量级别") while True: # 读取一帧 ret, frame = cap.read() # 检查是否成功读取帧 if not ret: print("无法获取帧") break # 根据质量级别调整处理方式 if quality_level == 3: # 低质量: 降低分辨率 frame = cv2.resize(frame, (640, 360)) elif quality_level == 2: # 中等质量: 增加跳帧间隔 analyzer.skip_interval = 3 else: # 高质量: 正常处理 analyzer.skip_interval = 2 # 分析帧 analysis_results, viz_frame = analyzer.analyze_frame(frame) # 显示结果 cv2.imshow("哑铃弯举分析", viz_frame) # 处理按键事件 key = cv2.waitKey(1) # ESC键退出 if key == 27: break # F键切换全屏 if key == ord('f') or key == ord('F'): is_fullscreen = not is_fullscreen if is_fullscreen: cv2.setWindowProperty("哑铃弯举分析", cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_FULLSCREEN) else: cv2.setWindowProperty("哑铃弯举分析", cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_NORMAL) # S键保存当前帧 if key == ord('s') or key == ord('S'): timestamp = time.strftime("%Y%m%d-%H%M%S") filename = f"dumbbell_curl_{timestamp}.png" cv2.imwrite(filename, viz_frame) print(f"已保存截图: {filename}") # Q键切换质量级别 if key == ord('q') or key == ord('Q'): quality_level = (quality_level % 3) + 1 quality_names = ["高", "中", "低"] print(f"已切换处理质量: {quality_names[quality_level - 1]}") # 释放资源 cap.release() cv2.destroyAllWindows() print("程序已退出") except Exception as e: print(f"程序运行出错: {str(e)}") if __name__ == "__main__": main()
07-28
option = { grid: { left: '10', right: '10', bottom: '10', top:'10', containLabel: true }, dataZoom: [ { // 设置滚动条的隐藏与显示 show: dataY.length >= 7 ? true: false, // 设置滚动条类型 realtimeSort: true,//排序 type: "slider", // 设置背景颜色 yAxisIndex: [0, 1], backgroundColor: "rgb(19, 63, 100)", // 设置选中范围的填充颜色 fillerColor: "rgb(16, 171, 198)", // 设置边框颜色 borderColor: "rgb(19, 63, 100)", // 是否显示detail,即拖拽时候显示详细数值信息 showDetail: false,//比如滚动条顶部和底部的文字信息 // 数据窗口范围的起始数值 startValue: 0, // 数据窗口范围的结束数值(一页显示多少条数据) endValue: 7, // empty:当前数据窗口外的数据,被设置为空。 // 即不会影响其他轴的数据范围 filterMode: "empty", // 设置滚动条宽度,相对于盒子宽度 height: '80%',//组件高度 left:'99%', //左边的距离 right: 8,//右边的距离 top: 50,//上边边的距离 // 是否锁定选择区域(或叫做数据窗口)的大小 zoomLoxk: true, // 控制手柄的尺寸 handleSize: 0, // dataZoom-slider组件离容器下侧的距离 bottom: 3, }, { // 没有下面这块的话,只能拖动滚动条, // 鼠标滚轮在区域内不能控制外部滚动条 type: "inside", show: true, yAxisIndex: [0, 1], start: 1,//默认为1 end: 9,//默认为100 // 滚轮是否触发缩放 zoomOnMouseWheel: false, // 鼠标滚轮触发滚动 moveOnMouseMove: true, moveOnMouseWheel: true, }, ], tooltip: { trigger: "axis", axisPointer: { type: "cross", }, }, yAxis: { data: dataY, inverse: true,//降序排序 axisLabel: { interval:0 ,//强制都显示 //width: 40, // 文本显示宽度 // overflow: 'truncate', // 超出的部分截断 // ellipsis: '...', // 截断的部分用...代替 }, axisTick:{ show:false }, }, xAxis: { axisTick:{ show:false }, axisLine: { show: false, }, splitLine: { show: true, lineStyle:{ type:'dashed', color: '#fff', width: 0.6 // 网格线宽度 } } }, series: [ { type: "bar", data: data, realtimeSort: true,//排序 showBackground: true, barWidth: 20, itemStyle: { color: '#268dff', borderRadius: [2, 2, 0, 0] }, label: { show: true, position: 'insideRight', color: '#fff', fontSize: 12 } }, ], }; 上述代码横向柱状图,柱子内部没有显示值是什么原因
09-10
import xarray as xr import numpy as np import pandas as pd # 1. 读取NetCDF文件 ds = xr.open_dataset(r'D:\Python\pythonProject1\pre_1960_2014_fixed.nc', chunks={'time': 100}) # 分块读取处理大数据 # 2. 时间维度处理(假设时间单位是天数) # 转换为datetime格式(需根据实际时间单位调整) time_units = ds.time.attrs.get('units', 'days since 1900-01-01') times = xr.decode_cf(ds).time # 提取年份和月份 year = times.dt.year month = times.dt.month # 选择5-8月数据 summer_months = ds.sel(time=ds.time.dt.month.isin([5, 6, 7, 8])) # 3. 标准化降水(按月计算Z-score) def standardize_by_month(data): """按月计算标准化降水""" # 按月分组计算均值和标准差 monthly_mean = data.groupby('time.month').mean('time', skipna=True) monthly_std = data.groupby('time.month').std('time', skipna=True) # 标准化处理 standardized = (data.groupby('time.month') - monthly_mean) standardized = standardized.groupby('time.month') / np.maximum(monthly_std, 1e-6) # 避免除零 return standardized # 应用标准化 pre_std = standardize_by_month(summer_months['pre']) # 4. SDFAI计算函数 def calculate_sdfai(pi, pj): """计算单个月对的SDFAI指数""" diff = pj - pi abs_sum = np.abs(pi) + np.abs(pj) exponent = -np.abs(pi + pj) return diff * abs_sum * np.power(3.2, exponent) # 5. 计算每年的指数对(5-6,6-7,7-8) def compute_yearly_sdfai(dataset): """计算每年的SDFAI指数""" years = np.unique(dataset.time.dt.year) results = [] for year in years: year_data = dataset.sel(time=dataset.time.dt.year == year) # 提取各月数据 may = year_data.sel(time=year_data.time.dt.month == 5).pre june = year_data.sel(time=year_data.time.dt.month == 6).pre july = year_data.sel(time=year_data.time.dt.month == 7).pre august = year_data.sel(time=year_data.time.dt.month == 8).pre # 计算三个指数对 may_june = calculate_sdfai(may, june) june_july = calculate_sdfai(june, july) july_august = calculate_sdfai(july, august) # 组合为年度数据 year_sdfai = xr.concat([may_june, june_july, july_august], dim=xr.DataArray(['May-Jun', 'Jun-Jul', 'Jul-Aug'], name='period')) results.append(year_sdfai.expand_dims(dim={'year': [year]})) return xr.concat(results, dim='year') # 6. 执行计算并保存结果 sdfai_result = compute_yearly_sdfai(pre_std.to_dataset(name='pre')) # 添加描述性属性 sdfai_result.attrs = { 'long_name': 'Short-term Drought-Flood Abrupt Alternation Index', 'units': 'dimensionless', 'formula': '(Pj-Pi)*(|Pi|+|Pj|)*3.2^(-|Pi+Pj|)', 'calculation_period': 'May to August' } # 保存结果 sdfai_result.to_netcdf('sdfai_results.nc') print("SDFAI计算完成,结果已保存至sdfai_results.nc") 这是最开始进行SDFAI计算的程序,能不能直接在这个程序的基础上直接生成分析图,不用生成sdfai_results.nc
最新发布
09-11
2025-06-22 18:56:53.192710: I tensorflow/core/util/port.cc:153] oneDNN custom operations are on. You may see slightly different numerical results due to floating-point round-off errors from different computation orders. To turn them off, set the environment variable `TF_ENABLE_ONEDNN_OPTS=0`. 2025-06-22 18:56:54.136866: I tensorflow/core/util/port.cc:153] oneDNN custom operations are on. You may see slightly different numerical results due to floating-point round-off errors from different computation orders. To turn them off, set the environment variable `TF_ENABLE_ONEDNN_OPTS=0`. 2025-06-22 18:56:56,467 - INFO - 加载并增强数据集: augmented_data 2025-06-22 18:56:56,560 - INFO - 原始数据集: 150 张图片, 5 个类别 2025-06-22 18:56:56,565 - INFO - 类别 book: 30 张原始图像 2025-06-22 18:56:56,989 - INFO - 类别 cup: 30 张原始图像 2025-06-22 18:56:57,403 - INFO - 类别 glasses: 30 张原始图像 2025-06-22 18:56:57,820 - INFO - 类别 phone: 30 张原始图像 2025-06-22 18:56:58,248 - INFO - 类别 shoe: 30 张原始图像 2025-06-22 18:56:58,859 - INFO - 增强后数据集: 450 张图片 2025-06-22 18:56:58,954 - INFO - 构建优化的迁移学习模型... 2025-06-22 18:56:58.959007: I tensorflow/core/platform/cpu_feature_guard.cc:210] This TensorFlow binary is optimized to use available CPU instructions in performance-critical operations. To enable the following instructions: SSE3 SSE4.1 SSE4.2 AVX AVX2 FMA, in other operations, rebuild TensorFlow with the appropriate compiler flags. Model: "functional" ┌─────────────────────┬───────────────────┬────────────┬───────────────────┐ │ Layer (type) │ Output Shape │ Param # │ Connected to │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ input_layer_1 │ (None, 224, 224, │ 0 │ - │ │ (InputLayer) │ 3) │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ efficientnetb0 │ (None, 7, 7, │ 4,049,571 │ input_layer_1[0]… │ │ (Functional) │ 1280) │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ global_average_poo… │ (None, 1280) │ 0 │ efficientnetb0[0… │ │ (GlobalAveragePool… │ │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ dense (Dense) │ (None, 512) │ 655,872 │ global_average_p… │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ dense_1 (Dense) │ (None, 1280) │ 656,640 │ dense[0][0] │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ multiply (Multiply) │ (None, 1280) │ 0 │ global_average_p… │ │ │ │ │ dense_1[0][0] │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ dense_2 (Dense) │ (None, 512) │ 655,872 │ multiply[0][0] │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ batch_normalization │ (None, 512) │ 2,048 │ dense_2[0][0] │ │ (BatchNormalizatio… │ │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ dropout (Dropout) │ (None, 512) │ 0 │ batch_normalizat… │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ dense_3 (Dense) │ (None, 5) │ 2,565 │ dropout[0][0] │ └─────────────────────┴───────────────────┴────────────┴───────────────────┘ Total params: 6,022,568 (22.97 MB) Trainable params: 1,971,973 (7.52 MB) Non-trainable params: 4,050,595 (15.45 MB) 2025-06-22 18:56:59,882 - INFO - 开始高级训练策略... 2025-06-22 18:56:59,882 - INFO - 阶段1: 冻结基础模型训练 Epoch 1/50 23/23 ━━━━━━━━━━━━━━━━━━━━ 16s 442ms/step - accuracy: 0.2001 - loss: 1.7270 - val_accuracy: 0.2000 - val_loss: 1.6104 - learning_rate: 1.0000e-04 Epoch 2/50 23/23 ━━━━━━━━━━━━━━━━━━━━ 8s 335ms/step - accuracy: 0.1486 - loss: 1.7114 - val_accuracy: 0.2000 - val_loss: 1.6100 - learning_rate: 1.0000e-04 Epoch 3/50 23/23 ━━━━━━━━━━━━━━━━━━━━ 8s 332ms/step - accuracy: 0.2337 - loss: 1.7239 - val_accuracy: 0.2000 - val_loss: 1.6117 - learning_rate: 1.0000e-04 Epoch 4/50 23/23 ━━━━━━━━━━━━━━━━━━━━ 8s 335ms/step - accuracy: 0.2558 - loss: 1.6466 - val_accuracy: 0.2000 - val_loss: 1.6104 - learning_rate: 1.0000e-04 Epoch 5/50 23/23 ━━━━━━━━━━━━━━━━━━━━ 0s 270ms/step - accuracy: 0.2281 - loss: 1.6503 Epoch 5: ReduceLROnPlateau reducing learning rate to 4.999999873689376e-05. 23/23 ━━━━━━━━━━━━━━━━━━━━ 8s 367ms/step - accuracy: 0.2271 - loss: 1.6513 - val_accuracy: 0.2111 - val_loss: 1.6118 - learning_rate: 1.0000e-04 Epoch 6/50 23/23 ━━━━━━━━━━━━━━━━━━━━ 8s 333ms/step - accuracy: 0.1899 - loss: 1.6756 - val_accuracy: 0.2000 - val_loss: 1.6112 - learning_rate: 5.0000e-05 Epoch 7/50 23/23 ━━━━━━━━━━━━━━━━━━━━ 8s 333ms/step - accuracy: 0.2394 - loss: 1.6269 - val_accuracy: 0.2000 - val_loss: 1.6128 - learning_rate: 5.0000e-05 Epoch 8/50 23/23 ━━━━━━━━━━━━━━━━━━━━ 0s 266ms/step - accuracy: 0.2041 - loss: 1.7332 Epoch 8: ReduceLROnPlateau reducing learning rate to 2.499999936844688e-05. 23/23 ━━━━━━━━━━━━━━━━━━━━ 8s 333ms/step - accuracy: 0.2042 - loss: 1.7319 - val_accuracy: 0.2000 - val_loss: 1.6103 - learning_rate: 5.0000e-05 Epoch 9/50 23/23 ━━━━━━━━━━━━━━━━━━━━ 8s 355ms/step - accuracy: 0.1765 - loss: 1.6814 - val_accuracy: 0.3333 - val_loss: 1.6107 - learning_rate: 2.5000e-05 2025-06-22 18:58:18,867 - INFO - 阶段2: 微调部分层 Epoch 1/25 23/23 ━━━━━━━━━━━━━━━━━━━━ 18s 460ms/step - accuracy: 0.2374 - loss: 2.4082 - val_accuracy: 0.2000 - val_loss: 1.6107 - learning_rate: 1.0000e-05 Epoch 2/25 23/23 ━━━━━━━━━━━━━━━━━━━━ 9s 390ms/step - accuracy: 0.2021 - loss: 2.2585 - val_accuracy: 0.2000 - val_loss: 1.6112 - learning_rate: 1.0000e-05 Epoch 3/25 23/23 ━━━━━━━━━━━━━━━━━━━━ 9s 375ms/step - accuracy: 0.2259 - loss: 2.3548 - val_accuracy: 0.2111 - val_loss: 1.6121 - learning_rate: 1.0000e-05 Epoch 4/25 23/23 ━━━━━━━━━━━━━━━━━━━━ 0s 307ms/step - accuracy: 0.2416 - loss: 2.0942 Epoch 4: ReduceLROnPlateau reducing learning rate to 4.999999873689376e-06. 23/23 ━━━━━━━━━━━━━━━━━━━━ 9s 374ms/step - accuracy: 0.2405 - loss: 2.1006 - val_accuracy: 0.2000 - val_loss: 1.6127 - learning_rate: 1.0000e-05 Epoch 5/25 23/23 ━━━━━━━━━━━━━━━━━━━━ 9s 377ms/step - accuracy: 0.2053 - loss: 2.1248 - val_accuracy: 0.2000 - val_loss: 1.6136 - learning_rate: 5.0000e-06 Epoch 6/25 23/23 ━━━━━━━━━━━━━━━━━━━━ 9s 382ms/step - accuracy: 0.1995 - loss: 2.2549 - val_accuracy: 0.2000 - val_loss: 1.6150 - learning_rate: 5.0000e-06 Epoch 7/25 23/23 ━━━━━━━━━━━━━━━━━━━━ 0s 305ms/step - accuracy: 0.1949 - loss: 2.1615 Epoch 7: ReduceLROnPlateau reducing learning rate to 2.499999936844688e-06. 23/23 ━━━━━━━━━━━━━━━━━━━━ 9s 373ms/step - accuracy: 0.1962 - loss: 2.1615 - val_accuracy: 0.2000 - val_loss: 1.6165 - learning_rate: 5.0000e-06 Epoch 8/25 23/23 ━━━━━━━━━━━━━━━━━━━━ 9s 375ms/step - accuracy: 0.2320 - loss: 2.1199 - val_accuracy: 0.2000 - val_loss: 1.6186 - learning_rate: 2.5000e-06 Epoch 9/25 23/23 ━━━━━━━━━━━━━━━━━━━━ 9s 378ms/step - accuracy: 0.2379 - loss: 2.1694 - val_accuracy: 0.2000 - val_loss: 1.6204 - learning_rate: 2.5000e-06 2025-06-22 18:59:46,920 - INFO - 训练完成 2025-06-22 18:59:46,920 - INFO - 评估模型... 2025-06-22 18:59:48,600 - INFO - 测试准确率: 20.00% E:\pycharm\study\计算机视觉\物品识别系统.py:313: UserWarning: Glyph 20934 (\N{CJK UNIFIED IDEOGRAPH-51C6}) missing from font(s) DejaVu Sans. plt.tight_layout() E:\pycharm\study\计算机视觉\物品识别系统.py:313: UserWarning: Glyph 30830 (\N{CJK UNIFIED IDEOGRAPH-786E}) missing from font(s) DejaVu Sans. plt.tight_layout() E:\pycharm\study\计算机视觉\物品识别系统.py:313: UserWarning: Glyph 29575 (\N{CJK UNIFIED IDEOGRAPH-7387}) missing from font(s) DejaVu Sans. plt.tight_layout() E:\pycharm\study\计算机视觉\物品识别系统.py:313: UserWarning: Glyph 35757 (\N{CJK UNIFIED IDEOGRAPH-8BAD}) missing from font(s) DejaVu Sans. plt.tight_layout() E:\pycharm\study\计算机视觉\物品识别系统.py:313: UserWarning: Glyph 32451 (\N{CJK UNIFIED IDEOGRAPH-7EC3}) missing from font(s) DejaVu Sans. plt.tight_layout() E:\pycharm\study\计算机视觉\物品识别系统.py:313: UserWarning: Glyph 21644 (\N{CJK UNIFIED IDEOGRAPH-548C}) missing from font(s) DejaVu Sans. plt.tight_layout() E:\pycharm\study\计算机视觉\物品识别系统.py:313: UserWarning: Glyph 39564 (\N{CJK UNIFIED IDEOGRAPH-9A8C}) missing from font(s) DejaVu Sans. plt.tight_layout() E:\pycharm\study\计算机视觉\物品识别系统.py:313: UserWarning: Glyph 35777 (\N{CJK UNIFIED IDEOGRAPH-8BC1}) missing from font(s) DejaVu Sans. plt.tight_layout() E:\pycharm\study\计算机视觉\物品识别系统.py:313: UserWarning: Glyph 25439 (\N{CJK UNIFIED IDEOGRAPH-635F}) missing from font(s) DejaVu Sans. plt.tight_layout() E:\pycharm\study\计算机视觉\物品识别系统.py:313: UserWarning: Glyph 22833 (\N{CJK UNIFIED IDEOGRAPH-5931}) missing from font(s) DejaVu Sans. plt.tight_layout() E:\pycharm\study\计算机视觉\物品识别系统.py:314: UserWarning: Glyph 20934 (\N{CJK UNIFIED IDEOGRAPH-51C6}) missing from font(s) DejaVu Sans. plt.savefig('training_history.png') E:\pycharm\study\计算机视觉\物品识别系统.py:314: UserWarning: Glyph 30830 (\N{CJK UNIFIED IDEOGRAPH-786E}) missing from font(s) DejaVu Sans. plt.savefig('training_history.png') E:\pycharm\study\计算机视觉\物品识别系统.py:314: UserWarning: Glyph 29575 (\N{CJK UNIFIED IDEOGRAPH-7387}) missing from font(s) DejaVu Sans. plt.savefig('training_history.png') E:\pycharm\study\计算机视觉\物品识别系统.py:314: UserWarning: Glyph 35757 (\N{CJK UNIFIED IDEOGRAPH-8BAD}) missing from font(s) DejaVu Sans. plt.savefig('training_history.png') E:\pycharm\study\计算机视觉\物品识别系统.py:314: UserWarning: Glyph 32451 (\N{CJK UNIFIED IDEOGRAPH-7EC3}) missing from font(s) DejaVu Sans. plt.savefig('training_history.png') E:\pycharm\study\计算机视觉\物品识别系统.py:314: UserWarning: Glyph 21644 (\N{CJK UNIFIED IDEOGRAPH-548C}) missing from font(s) DejaVu Sans. plt.savefig('training_history.png') E:\pycharm\study\计算机视觉\物品识别系统.py:314: UserWarning: Glyph 39564 (\N{CJK UNIFIED IDEOGRAPH-9A8C}) missing from font(s) DejaVu Sans. plt.savefig('training_history.png') E:\pycharm\study\计算机视觉\物品识别系统.py:314: UserWarning: Glyph 35777 (\N{CJK UNIFIED IDEOGRAPH-8BC1}) missing from font(s) DejaVu Sans. plt.savefig('training_history.png') E:\pycharm\study\计算机视觉\物品识别系统.py:314: UserWarning: Glyph 25439 (\N{CJK UNIFIED IDEOGRAPH-635F}) missing from font(s) DejaVu Sans. plt.savefig('training_history.png') E:\pycharm\study\计算机视觉\物品识别系统.py:314: UserWarning: Glyph 22833 (\N{CJK UNIFIED IDEOGRAPH-5931}) missing from font(s) DejaVu Sans. plt.savefig('training_history.png') 2025-06-22 18:59:48,905 - INFO - 训练历史图表已保存到 training_history.png 2025-06-22 18:59:49,390 - INFO - 模型已保存到: optimized_model.keras 2025-06-22 18:59:49,390 - INFO - 执行内存清理... WARNING:tensorflow:From E:\python3.9.13\lib\site-packages\keras\src\backend\common\global_state.py:82: The name tf.reset_default_graph is deprecated. Please use tf.compat.v1.reset_default_graph instead. 2025-06-22 18:59:50,195 - WARNING - From E:\python3.9.13\lib\site-packages\keras\src\backend\common\global_state.py:82: The name tf.reset_default_graph is deprecated. Please use tf.compat.v1.reset_default_graph instead. 2025-06-22 18:59:50,743 - INFO - 内存清理完成 E:\pycharm\study\计算机视觉\物品识别系统.py:355: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator. ax2.set_xticklabels(self.class_labels, rotation=45) E:\pycharm\study\计算机视觉\物品识别系统.py:364: UserWarning: Glyph 39044 (\N{CJK UNIFIED IDEOGRAPH-9884}) missing from font(s) DejaVu Sans. plt.tight_layout() E:\pycharm\study\计算机视觉\物品识别系统.py:364: UserWarning: Glyph 27979 (\N{CJK UNIFIED IDEOGRAPH-6D4B}) missing from font(s) DejaVu Sans. plt.tight_layout() E:\pycharm\study\计算机视觉\物品识别系统.py:364: UserWarning: Glyph 27010 (\N{CJK UNIFIED IDEOGRAPH-6982}) missing from font(s) DejaVu Sans. plt.tight_layout() E:\pycharm\study\计算机视觉\物品识别系统.py:364: UserWarning: Glyph 29575 (\N{CJK UNIFIED IDEOGRAPH-7387}) missing from font(s) DejaVu Sans. plt.tight_layout() E:\pycharm\study\计算机视觉\物品识别系统.py:364: UserWarning: Glyph 31867 (\N{CJK UNIFIED IDEOGRAPH-7C7B}) missing from font(s) DejaVu Sans. plt.tight_layout() E:\pycharm\study\计算机视觉\物品识别系统.py:364: UserWarning: Glyph 21035 (\N{CJK UNIFIED IDEOGRAPH-522B}) missing from font(s) DejaVu Sans. plt.tight_layout() E:\pycharm\study\计算机视觉\物品识别系统.py:364: UserWarning: Glyph 20998 (\N{CJK UNIFIED IDEOGRAPH-5206}) missing from font(s) DejaVu Sans. plt.tight_layout() E:\pycharm\study\计算机视觉\物品识别系统.py:364: UserWarning: Glyph 24067 (\N{CJK UNIFIED IDEOGRAPH-5E03}) missing from font(s) DejaVu Sans. plt.tight_layout() E:\pycharm\study\计算机视觉\物品识别系统.py:365: UserWarning: Glyph 39044 (\N{CJK UNIFIED IDEOGRAPH-9884}) missing from font(s) DejaVu Sans. plt.savefig(output_path) E:\pycharm\study\计算机视觉\物品识别系统.py:365: UserWarning: Glyph 27979 (\N{CJK UNIFIED IDEOGRAPH-6D4B}) missing from font(s) DejaVu Sans. plt.savefig(output_path) E:\pycharm\study\计算机视觉\物品识别系统.py:365: UserWarning: Glyph 27010 (\N{CJK UNIFIED IDEOGRAPH-6982}) missing from font(s) DejaVu Sans. plt.savefig(output_path) E:\pycharm\study\计算机视觉\物品识别系统.py:365: UserWarning: Glyph 29575 (\N{CJK UNIFIED IDEOGRAPH-7387}) missing from font(s) DejaVu Sans. plt.savefig(output_path) E:\pycharm\study\计算机视觉\物品识别系统.py:365: UserWarning: Glyph 31867 (\N{CJK UNIFIED IDEOGRAPH-7C7B}) missing from font(s) DejaVu Sans. plt.savefig(output_path) E:\pycharm\study\计算机视觉\物品识别系统.py:365: UserWarning: Glyph 21035 (\N{CJK UNIFIED IDEOGRAPH-522B}) missing from font(s) DejaVu Sans. plt.savefig(output_path) E:\pycharm\study\计算机视觉\物品识别系统.py:365: UserWarning: Glyph 20998 (\N{CJK UNIFIED IDEOGRAPH-5206}) missing from font(s) DejaVu Sans. plt.savefig(output_path) E:\pycharm\study\计算机视觉\物品识别系统.py:365: UserWarning: Glyph 24067 (\N{CJK UNIFIED IDEOGRAPH-5E03}) missing from font(s) DejaVu Sans. plt.savefig(output_path) 2025-06-22 18:59:52,251 - INFO - 预测结果已保存到 prediction_result.png 2025-06-22 18:59:52,252 - INFO - 真实类别: cup
06-23
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值