关于isinstance使用(节选)

博客提到在函数提供对象类型时,可使用函数测试对象,以此确定对象是否为特定类型或定制类的实例,聚焦于对象类型测试相关信息技术内容。

type() 函数提供对象的类型时,还可以使用 isinstance() 函数测试对象,以确定它是否是某个特定类型或定制类的实例:


>>> print isinstance.__doc__
isinstance(object, class-or-type-or-tuple) -> Boolean

Return whether an object is an instance of a class or of a subclass thereof.
With a type as second argument, return whether that is the object's type.
The form using a tuple, isinstance(x, (A, B, ...)), is a shortcut for
isinstance(x, A) or isinstance(x, B) or ... (etc.).
>>> isinstance(42, str)
0
>>> isinstance('a string', int)
0
>>> isinstance(42, int)
1
>>> isinstance('a string', str)
1
import cv2 import numpy as np import mss from paddleocr import PaddleOCR import time import os from PIL import Image # ================== 🎯 前台配置区 ================== OCR_LANG = 'ch' # PaddleOCR 支持 'ch', 'en', 'fr', etc. MATCH_THRESHOLD = 0.8 # ✅ 新增:最小磨损度结果存储文件 LOG_FILE = "min_wear_log.txt" # 场景定义(已添加磨损度区域) SCENES = [ { "name": "场景1", "template_path": r"D:\yunfe\手机悠悠\零.png", "check_region": {"x": 0, "y": 0, "w": 2560, "h": 1600}, "ocr_fields": { "卖价": {'x': 200, 'y': 651, 'w': 104, 'h': 36} }, "wear_fields": [ # 多个磨损度位置(最多两个) {'x': 300, 'y': 700, 'w': 60, 'h': 25}, # 磨损度1 {'x': 400, 'y': 700, 'w': 60, 'h': 25} # 磨损度2 ] }, { "name": "场景2", "template_path": r"D:\yunfe\手机悠悠\崭新出厂.png", "check_region": {"x": 0, "y": 0, "w": 2560, "h": 1600}, "ocr_fields": { "定价": {'x': 58, 'y': 378, 'w': 68, 'h': 32} } } ] IMAGE_EXTENSIONS = {'.jpg', '.jpeg', '.png', '.bmp', '.gif', '.tiff', '.webp'} # 初始化 PaddleOCR(只初始化一次,全局使用) ocr_engine = PaddleOCR( use_textline_orientation=False, # 替代 use_angle_cls lang=OCR_LANG, use_gpu=False ) def validate_images_in_scenes(scenes): valid_templates = [] for scene in scenes: path = scene["template_path"] ext = os.path.splitext(path.lower())[1] if ext not in IMAGE_EXTENSIONS: print(f"[⚠️ ] 不支持的格式: {path}") continue if not os.path.exists(path): print(f"[❌] 文件不存在: {path}") continue try: with Image.open(path) as img: img_rgb = np.array(img.convert("RGB")) if img.mode == "RGBA": img_rgb = img_rgb[:, :, :3] img_bgr = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2BGR) print(f"[✅] 加载成功: {scene['name']} -> {path} | 尺寸={img.size}, 格式={img.format}") valid_templates.append((scene, img_bgr)) except Exception as e: print(f"[❌] 无法打开图像: {path} | 错误: {e}") print(f"\n=== 图像验证完成 ===\n✅ 成功加载 {len(valid_templates)} 个模板,准备启动...\n") return valid_templates def capture_screen(x, y, w, h): with mss.mss() as sct: img = np.array(sct.grab({"left": x, "top": y, "width": w, "height": h})) return cv2.cvtColor(img, cv2.COLOR_BGRA2RGB) def match_template_on_screen(template_img, search_region, threshold=0.8): screen_rgb = capture_screen( search_region['x'], search_region['y'], search_region['w'], search_region['h'] ) res = cv2.matchTemplate(screen_rgb, template_img, cv2.TM_CCOEFF_NORMED) _, max_val, _, max_loc = cv2.minMaxLoc(res) return (True, max_loc, max_val) if max_val >= threshold else (False, None, max_val) def preprocess_for_paddleocr(image_gray): """ 针对 PaddleOCR 的图像预处理:增强对比度 + 降噪 """ clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(4, 4)) enhanced = clahe.apply(image_gray) # 可选:轻微高斯模糊去噪 denoised = cv2.GaussianBlur(enhanced, (3, 3), 0) return denoised def read_single_number_with_paddle(x, y, w, h, desc="字段"): """ 使用 PaddleOCR 读取指定区域中的数字内容(仅提取数字和小数点) """ with mss.mss() as sct: img = np.array(sct.grab({"left": x, "top": y, "width": w, "height": h})) gray = cv2.cvtColor(img, cv2.COLOR_BGRA2GRAY) processed = preprocess_for_paddleocr(gray) # 转成三通道图像(PaddleOCR 输入要求是 HWC 彩色图) rgb_image = cv2.cvtColor(processed, cv2.COLOR_GRAY2RGB) # 使用 PaddleOCR 进行识别(关闭检测+方向分类,只做识别) result = ocr_engine.ocr(rgb_image, det=False, rec=True, cls=False) if result and isinstance(result, list) and len(result) > 0: raw_text = result[0][0] if isinstance(result[0], list) else result[0] # 清洗:只保留数字和小数点 cleaned = ''.join(c for c in raw_text if c.isdigit() or c == '.').strip('.') print(f"🔍 [{desc}] OCR原始输出: '{raw_text}' → 提取后: '{cleaned}'") try: return float(cleaned) if cleaned else None except ValueError: print(f"🟡 [{desc}] 解析失败,清洗后无效: '{cleaned}'") return None else: print(f"🟡 [{desc}] OCR未识别出任何内容") return None def read_ocr_fields(field_configs): results = {} for name, cfg in field_configs.items(): value = read_single_number_with_paddle(cfg['x'], cfg['y'], cfg['w'], cfg['h'], name) print(f"📊 [{name}]: {value}") results[name] = value return results def extract_min_wear_value(wear_fields): """ 从多个磨损度区域提取数值,返回最小的一个 """ wears = [] for i, wf in enumerate(wear_fields): val = read_single_number_with_paddle(wf['x'], wf['y'], wf['w'], wf['h'], f"磨损度{i+1}") if val is not None: wears.append(val) if wears: min_wear = min(wears) print(f"🟢 检测到磨损度: {wears} → 最小磨损度 = {min_wear}") return min_wear else: print("🟡 未检测到任何有效磨损度") return None def log_min_wear(scene_name, price_diff, min_wear): """记录日志到文件""" timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) with open(LOG_FILE, "a", encoding="utf-8") as f: f.write(f"[{timestamp}] 场景='{scene_name}', 差价={price_diff:.2f}, 最小磨损度={min_wear}\n") print(f"📝 已记录日志: {scene_name}, 差价={price_diff:.2f}, 最小磨损度={min_wear}") # ================== 🔁 主程序入口 ================== if __name__ == "__main__": print("🔍 正在扫描并验证模板图像...") templates = validate_images_in_scenes(SCENES) if not templates: print("🛑 没有可用的模板图像,程序退出。") exit(1) pricing_scene = next((s for s, _ in templates if s["name"] == "场景2"), None) selling_scene = next((s, t) for s, t in templates if s["name"] == "场景1") print("✅ 所有模板加载成功,启动监控...\n") try: while True: detected_pricing = False detected_selling = False pricing_value = selling_value = None # 检查【定价】是否存在 if pricing_scene: found, loc, conf = match_template_on_screen( [t for s, t in templates if s["name"] == "场景2"][0], pricing_scene["check_region"], MATCH_THRESHOLD ) if found: print(f"\n💰 检测到【{pricing_scene['name']}】相似度: {conf:.3f}") result = read_ocr_fields(pricing_scene["ocr_fields"]) pricing_value = result.get("定价") detected_pricing = pricing_value is not None # 检查【卖价】是否存在 found, loc, conf = match_template_on_screen( selling_scene[1], selling_scene[0]["check_region"], MATCH_THRESHOLD ) if found: print(f"\n💰 检测到【{selling_scene[0]['name']}】相似度: {conf:.3f}") result = read_ocr_fields(selling_scene[0]["ocr_fields"]) selling_value = result.get("卖价") detected_selling = selling_value is not None # ✅ 条件判断:卖价 - 定价 > 5 if detected_selling and detected_pricing: diff = selling_value - pricing_value print(f"📈 卖价={selling_value}, 定价={pricing_value}, 差值={diff:.2f}") if diff > 5: print("🔥 触发条件:差价 > 5,开始识别磨损度...") min_wear = extract_min_wear_value(selling_scene[0]["wear_fields"]) if min_wear is not None: log_min_wear(selling_scene[0]["name"], diff, min_wear) time.sleep(2) # 防止重复触发 else: print("💡 差价不足,跳过磨损度识别") else: print("⏳ 等待匹配模板图像...") time.sleep(1) except KeyboardInterrupt: print("\n👋 程序已退出") 哪里出错了
最新发布
11-28
import os import tkinter as tk import subprocess import multiprocessing from multiprocessing import Queue from tkinter import filedialog, messagebox, ttk import time # 确保Windows系统支持多进程 if os.name == 'nt': multiprocessing.set_start_method('spawn', force=True) class FileTypeFinder: def __init__(self, root): self.root = root self.root.title("非指定格式文件查找器") self.root.geometry("900x600") self.root.resizable(True, True) # 设置中文字体支持 self.font = ('SimHei', 10) # 定义要排除的文件格式(不区分大小写) self.excluded_extensions = {'.jpg', '.jpeg', '.png', '.mp4', '.mov', '.avi', '.mts', '.mkv', '.avif', '.jpe', '.bmp', '.jpeg', '.jpg', '.gif', '.m4v', '.webf', '.tiff', '.heic', '.wmv', '.mov', '.cr2', '.cr3', '.ts', '.jfif', '.arw', '.pdf', '.livp', '.flv'} # 存储找到的文件列表和进程相关变量 self.found_files = [] self.search_process = None self.queue = None self.is_searching = False # 创建UI组件 self.create_widgets() def create_widgets(self): # 路径输入框架 path_frame = tk.Frame(self.root) path_frame.pack(padx=10, pady=10, fill=tk.X) # 路径输入框 self.path_var = tk.StringVar() path_entry = tk.Entry(path_frame, textvariable=self.path_var, width=60, font=self.font) path_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 5)) # 浏览按钮 browse_btn = tk.Button(path_frame, text="浏览...", command=self.browse_directory, font=self.font) browse_btn.pack(side=tk.LEFT, padx=(0, 5)) # 运行/停止按钮 self.run_btn = tk.Button(path_frame, text="运行查找", command=self.toggle_search, font=self.font) self.run_btn.pack(side=tk.LEFT) # 进度显示框架 progress_frame = tk.Frame(self.root) progress_frame.pack(padx=10, fill=tk.X) # 进度标签 self.progress_var = tk.StringVar() self.progress_var.set("等待开始...") progress_label = tk.Label(progress_frame, textvariable=self.progress_var, font=self.font, fg="blue") progress_label.pack(anchor=tk.W) # 结果显示区域 result_frame = tk.Frame(self.root) result_frame.pack(padx=10, pady=(5, 10), fill=tk.BOTH, expand=True) # 结果标签 result_label = tk.Label(result_frame, text="查找结果:", font=self.font) result_label.pack(anchor=tk.W, pady=(0, 5)) # 创建带滚动条的树状视图来显示文件列表 columns = ("文件路径", "操作") self.file_tree = ttk.Treeview(result_frame, columns=columns, show="headings") # 设置列宽和标题 self.file_tree.heading("文件路径", text="文件路径") self.file_tree.heading("操作", text="操作") self.file_tree.column("文件路径", width=600, anchor=tk.W) self.file_tree.column("操作", width=100, anchor=tk.CENTER) # 添加滚动条 scrollbar = ttk.Scrollbar(result_frame, orient="vertical", command=self.file_tree.yview) self.file_tree.configure(yscrollcommand=scrollbar.set) # 布局树状视图和滚动条 self.file_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) scrollbar.pack(side=tk.RIGHT, fill=tk.Y) # 绑定双击事件来打开文件位置 self.file_tree.bind("<Double-1>", self.open_file_location) # 状态栏 self.status_var = tk.StringVar() self.status_var.set("就绪") status_bar = tk.Label(self.root, textvariable=self.status_var, bd=1, relief=tk.SUNKEN, anchor=tk.W, font=self.font) status_bar.pack(side=tk.BOTTOM, fill=tk.X) def browse_directory(self): """打开文件夹选择对话框""" directory = filedialog.askdirectory() if directory: self.path_var.set(directory) def toggle_search(self): """切换查找状态(开始/停止)""" if self.is_searching: self.stop_search() else: self.start_search() def start_search(self): """开始查找文件(使用子进程)""" directory = self.path_var.get() # 检查路径是否有效 if not directory: messagebox.showerror("错误", "请选择一个文件夹") return if not os.path.isdir(directory): messagebox.showerror("错误", f"路径不存在或不是一个文件夹:\n{directory}") return # 清空之前的结果 for item in self.file_tree.get_children(): self.file_tree.delete(item) self.found_files = [] # 更新UI状态 self.is_searching = True self.run_btn.config(text="停止查找") self.status_var.set("正在查找...") self.progress_var.set("已处理: 0 个文件 | 找到: 0 个目标文件") # 创建队列用于进程间通信 self.queue = Queue() # 启动子进程 self.search_process = multiprocessing.Process( target=search_files, args=(directory, self.excluded_extensions, self.queue) ) self.search_process.daemon = True self.search_process.start() # 开始监听队列中的消息 self.listen_to_queue() def stop_search(self): """停止查找文件""" if self.search_process and self.search_process.is_alive(): self.search_process.terminate() self.search_process.join() # 更新UI状态 self.is_searching = False self.run_btn.config(text="运行查找") self.status_var.set("查找已停止") def listen_to_queue(self): """监听子进程发送的消息并更新UI""" if not self.is_searching: return # 检查队列中是否有消息 try: while not self.queue.empty(): message = self.queue.get_nowait() # 处理进度消息 if isinstance(message, tuple) and message[0] == 'progress': total_processed, total_found = message[1], message[2] self.progress_var.set(f"已处理: {total_processed} 个文件 | 找到: {total_found} 个目标文件") # 处理找到的文件 elif isinstance(message, tuple) and message[0] == 'file': file_path = message[1] self.found_files.append(file_path) self.file_tree.insert("", tk.END, values=(file_path, "打开位置")) # 处理完成消息 elif message == 'done': self.is_searching = False self.run_btn.config(text="运行查找") self.status_var.set(f"查找完成 - 共处理 {len(self.found_files)} 个文件") return except Exception as e: print(f"处理队列消息时出错: {e}") # 继续监听(每100毫秒检查一次) self.root.after(100, self.listen_to_queue) def open_file_location(self, event): """打开文件所在的文件夹并选中该文件""" # 获取被点击的项目 try: item = self.file_tree.selection()[0] # 获取文件路径 file_path = self.file_tree.item(item, "values")[0] if os.path.exists(file_path): try: if os.name == 'nt': # Windows系统 # 使用资源管理器打开并选中文件 subprocess.run(f'explorer /select,"{file_path}"', shell=True) else: # macOS或Linux系统 # 打开文件所在目录 dir_path = os.path.dirname(file_path) if os.name == 'posix': # macOS subprocess.run(['open', dir_path]) else: # Linux subprocess.run(['xdg-open', dir_path]) except Exception as e: messagebox.showerror("错误", f"无法打开文件位置:\n{str(e)}") else: messagebox.showerror("错误", f"文件不存在:\n{file_path}") except IndexError: # 未选中任何项目时不做处理 pass def search_files(directory, excluded_extensions, queue): """在子进程中查找文件""" try: total_processed = 0 total_found = 0 # 遍历目录 for root_dir, _, files in os.walk(directory): for file in files: total_processed += 1 file_path = os.path.join(root_dir, file) # 获取文件扩展名(小写) _, ext = os.path.splitext(file) ext = ext.lower() # 检查是否为非目标文件 if ext not in excluded_extensions: total_found += 1 # 发送找到的文件路径 queue.put(('file', file_path)) # 每处理10个文件发送一次进度更新,避免消息过多 if total_processed % 10 == 0: queue.put(('progress', total_processed, total_found)) # 发送最终进度 queue.put(('progress', total_processed, total_found)) # 发送完成消息 queue.put('done') except Exception as e: print(f"查找过程中发生错误:{e}") queue.put('done') if __name__ == "__main__": root = tk.Tk() app = FileTypeFinder(root) root.mainloop() 修改让里面的每个类型‘.jpg’变成UI下面添加可以勾选的,并且每次重新打开软件会恢复成之前勾选的
08-05
""" 传感器“卡值”(stuck value) 轻量级检测脚本(稳定相位+模板Z过滤版,Python 3.8+) ------------------------------------------------ 特点: - 仅依赖 numpy / pandas / matplotlib - 同时支持: 1) 绝对卡值/重复值段(平坦段) 2) 低变化段(导数接近0) 3) 周期性数据的“相位差残差”卡值(去除周期基线后再检测) - 自动估计或手动指定主周期 - 新增:跨周期“天然稳定相位”与模板Z分数过滤,显著降低对天然平台的误报 - 返回区间级别的告警(起止索引/时间、类型、置信度) 用法示例见底部 __main__ 区域。 """ from dataclasses import dataclass from typing import List, Optional, Tuple, Dict import numpy as np import pandas as pd # ----------------------------- # 工具函数 # ----------------------------- def _rolling_std(x: np.ndarray, window: int) -> np.ndarray: if window <= 1: return np.zeros_like(x, dtype=float) s = pd.Series(x) return s.rolling(window, min_periods=window).std(ddof=0).to_numpy() def _autocorr_via_fft(x: np.ndarray) -> np.ndarray: """快速自相关(归一化),返回与 x 等长的自相关数组。""" x = np.asarray(x, dtype=float) x = x - np.nanmean(x) x[np.isnan(x)] = 0.0 n = int(1 << (len(x) * 2 - 1).bit_length()) fx = np.fft.rfft(x, n=n) acf = np.fft.irfft(fx * np.conjugate(fx), n=n)[: len(x)] acf /= np.maximum(acf[0], 1e-12) return acf def estimate_period( x: np.ndarray, min_period: int = 5, max_period: Optional[int] = None, ) -> Optional[int]: """粗略估计主周期(样本点单位)。返回最可能的周期长度,找不到返回 None。""" n = len(x) if n < 3 * min_period: return None if max_period is None: max_period = max(min(n // 3, 2000), min_period + 1) acf = _autocorr_via_fft(x) seg = acf[min_period : max_period] if len(seg) == 0: return None k = int(np.nanargmax(seg)) + min_period if acf[k] < 0.15: return None return k def _group_runs(mask: np.ndarray) -> List[Tuple[int, int]]: """将布尔序列中为 True 的连续段转为 [start, end](含 end)。""" runs: List[Tuple[int, int]] = [] i = 0 n = len(mask) while i < n: if mask[i]: j = i while j + 1 < n and mask[j + 1]: j += 1 runs.append((i, j)) i = j + 1 else: i += 1 return runs def _phase_template_sigma(x: np.ndarray, period: int): """ 计算“同相位跨周期”的模板(相位中位数)与 σ(由MAD近似)。 返回 (template, sigma),若周期样本不足(<2个周期)则返回 (None, None)。 """ m = len(x) // period if m < 2: return None, None X = x[: m * period].reshape(m, period) template = np.nanmedian(X, axis=0) mad = np.nanmedian(np.abs(X - template[None, :]), axis=0) sigma = 1.4826 * mad + 1e-12 return template, sigma # ----------------------------- # 结果数据结构 # ----------------------------- @dataclass class StuckInterval: start_idx: int end_idx: int kind: str # "flat", "low_var", "seasonal_flat" score: float # 0~1 大致置信度 value_summary: str # ----------------------------- # 主检测器 # ----------------------------- class StuckDetector: def __init__( self, min_flat_run: int = 5, value_tol: float = 0.0, deriv_window: int = 3, deriv_tol: float = 1e-6, lowvar_window: int = 15, lowvar_std_tol: float = 1e-4, seasonal_period: Optional[int] = None, seasonal_robust: bool = True, seasonal_min_run: int = 5, seasonal_value_tol: float = 0.0, # —— 稳定相位+Z过滤参数 —— stable_phase_q: float = 0.20, # σ 的分位阈值,以下视为“天然稳定相位” stable_overlap_thr: float = 0.60, # 区间与稳定相位重叠比例阈值 require_z_k: float = 3.0 # 在稳定相位里仍要报卡值的最小Z阈值 ) -> None: self.min_flat_run = min_flat_run self.value_tol = value_tol self.deriv_window = deriv_window self.deriv_tol = deriv_tol self.lowvar_window = lowvar_window self.lowvar_std_tol = lowvar_std_tol self.seasonal_period = seasonal_period self.seasonal_robust = seasonal_robust self.seasonal_min_run = seasonal_min_run self.seasonal_value_tol = seasonal_value_tol self.stable_phase_q = stable_phase_q self.stable_overlap_thr = stable_overlap_thr self.require_z_k = require_z_k # ---- 基础检测 ---- def _flat_runs(self, x: np.ndarray) -> List[StuckInterval]: eq = np.abs(np.diff(x, prepend=x[0])) <= self.value_tol runs = _group_runs(eq) out: List[StuckInterval] = [] for s, e in runs: if e - s + 1 >= self.min_flat_run: v = np.median(x[s : e + 1]) out.append(StuckInterval(s, e, "flat", score=0.7, value_summary="~{:.6g}".format(v))) return out def _low_variability(self, x: np.ndarray) -> List[StuckInterval]: sd = _rolling_std(x, self.lowvar_window) mask = sd <= self.lowvar_std_tol runs = _group_runs(mask) out: List[StuckInterval] = [] for s, e in runs: if e - s + 1 >= max(self.lowvar_window, self.min_flat_run): v = np.median(x[s : e + 1]) local_sd = float(np.nanmax(sd[s : e + 1])) if np.isfinite(sd[s : e + 1]).any() else 0.0 score = float(np.clip(1.0 - local_sd / (self.lowvar_std_tol + 1e-12), 0, 1)) out.append(StuckInterval(s, e, "low_var", score=score, value_summary="~{:.6g}".format(v))) return out def _derivative_flat(self, x: np.ndarray) -> List[StuckInterval]: dx = np.diff(x, prepend=x[0]) if self.deriv_window > 1: dx = pd.Series(dx).rolling(self.deriv_window, min_periods=1, center=True).mean().to_numpy() mask = np.abs(dx) <= self.deriv_tol runs = _group_runs(mask) out: List[StuckInterval] = [] for s, e in runs: if e - s + 1 >= self.min_flat_run: v = np.median(x[s : e + 1]) local_absmax = float(np.nanmax(np.abs(dx[s : e + 1]))) if np.isfinite(dx[s : e + 1]).any() else 0.0 score = float(np.clip(1.0 - local_absmax / (self.deriv_tol + 1e-12), 0, 1)) out.append(StuckInterval(s, e, "flat", score=score, value_summary="~{:.6g}".format(v))) return out # ---- 周期性处理 ---- def _seasonal_baseline(self, x: np.ndarray, period: int) -> np.ndarray: phase_vals = [x[i::period] for i in range(period)] if self.seasonal_robust: phase_stats = [np.nanmedian(v) for v in phase_vals] else: phase_stats = [np.nanmean(v) for v in phase_vals] baseline = np.empty_like(x, dtype=float) for i in range(period): baseline[i::period] = phase_stats[i] return baseline def _seasonal_flat_runs(self, x: np.ndarray, period: int) -> List[StuckInterval]: baseline = self._seasonal_baseline(x, period) resid = x - baseline template, sigma = _phase_template_sigma(x, period) use_z = template is not None and sigma is not None eps = 1e-12 eq = np.abs(np.diff(resid, prepend=resid[0])) <= self.seasonal_value_tol runs = _group_runs(eq) out: List[StuckInterval] = [] for s, e in runs: if e - s + 1 >= self.seasonal_min_run: v = np.median(x[s : e + 1]) rmad_series = pd.Series(resid).rolling(period, min_periods=period)\ .apply(lambda w: np.nanmedian(np.abs(w - np.nanmedian(w))), raw=False) # 取窗口末端的RMAD,防止 NaN rmad_val = rmad_series.iloc[e] if pd.isna(rmad_val): rmad_val = 0.0 rmad = float(rmad_val) flat_strength = 1.0 if self.seasonal_value_tol <= 0 else float( np.clip(1.0 - np.nanmax(np.abs(np.diff(resid[s : e + 1], prepend=resid[s]))) / (self.seasonal_value_tol + 1e-12), 0, 1) ) season_stability = float(np.clip(1.0 - rmad / (np.nanstd(resid) + 1e-12), 0, 1)) if np.isfinite(rmad) else 0.5 score = float(np.clip(0.5 * flat_strength + 0.5 * season_stability, 0, 1)) if use_z: idx = np.arange(s, e + 1) phase = idx % period z = np.abs(x[idx] - template[phase]) / (sigma[phase] + eps) z_med = float(np.nanmedian(z)) if np.isfinite(z).any() else 0.0 if z_med < max(2.5, self.require_z_k - 0.5): continue out.append(StuckInterval(s, e, "seasonal_flat", score=score, value_summary="~{:.6g}".format(v))) return out # ---- 稳定相位+Z过滤 ---- def _filter_by_stability_and_z( self, x: np.ndarray, intervals: List[StuckInterval], period: int, template: np.ndarray, sigma: np.ndarray, stable_mask: np.ndarray, ) -> List[StuckInterval]: keep: List[StuckInterval] = [] eps = 1e-12 n = len(x) for it in intervals: s, e = it.start_idx, it.end_idx s = max(0, int(s)); e = min(n - 1, int(e)) if s > e: continue overlap = float(np.mean(stable_mask[s:e+1])) if e >= s else 0.0 idx = np.arange(s, e + 1) phase = idx % period z = np.abs(x[idx] - template[phase]) / (sigma[phase] + eps) z_med = float(np.nanmedian(z)) if np.isfinite(z).any() else 0.0 if overlap >= self.stable_overlap_thr and z_med < self.require_z_k: continue keep.append(it) return keep # ---- 主入口 ---- def detect(self, series: pd.Series) -> Tuple[List[StuckInterval], Dict[str, Optional[int]]]: """ 输入:时间序列(pd.Series,索引可为时间戳或整数) 输出: - intervals: StuckInterval 列表(不重叠;若重叠会做简单合并) - meta: {"period": 周期估计} """ x = series.to_numpy(dtype=float) n = len(x) intervals: List[StuckInterval] = [] period = self.seasonal_period or estimate_period(x) template = sigma = None stable_phase = None stable_mask = np.zeros(n, dtype=bool) if period is not None and period >= 3 and period * 2 <= n: template, sigma = _phase_template_sigma(x, period) if template is not None: thr = np.nanquantile(sigma, self.stable_phase_q) stable_phase = sigma <= thr m = n // period for i in range(m): stable_mask[i * period : i * period + period] = stable_phase rem = n - m * period if rem > 0: stable_mask[m * period : m * period + rem] = stable_phase[:rem] intervals.extend(self._flat_runs(x)) intervals.extend(self._low_variability(x)) intervals.extend(self._derivative_flat(x)) if period is not None and period >= 3 and period * 2 <= n: intervals.extend(self._seasonal_flat_runs(x, period)) if (period is not None) and (template is not None) and (sigma is not None): intervals = self._filter_by_stability_and_z(x, intervals, period, template, sigma, stable_mask) intervals = self._merge_intervals(intervals) return intervals, {"period": period} @staticmethod def _merge_intervals(intervals: List[StuckInterval]) -> List[StuckInterval]: if not intervals: return [] intervals = sorted(intervals, key=lambda z: (z.start_idx, z.end_idx)) merged: List[StuckInterval] = [] cur = intervals[0] for nx in intervals[1:]: if nx.start_idx <= cur.end_idx + 1 and nx.kind == cur.kind: new_s = cur.start_idx new_e = max(cur.end_idx, nx.end_idx) score = max(cur.score, nx.score) cur = StuckInterval(new_s, new_e, cur.kind, score=score, value_summary=cur.value_summary) elif nx.start_idx <= cur.end_idx + 1 and nx.kind != cur.kind: if nx.score >= cur.score: cur = StuckInterval(cur.start_idx, max(cur.end_idx, nx.end_idx), nx.kind, nx.score, nx.value_summary) else: cur = StuckInterval(cur.start_idx, max(cur.end_idx, nx.end_idx), cur.kind, cur.score, cur.value_summary) else: merged.append(cur) cur = nx merged.append(cur) return merged # ----------------------------- # 便捷接口 # ----------------------------- def detect_stuck_segments( series: pd.Series, sampling_period: Optional[pd.Timedelta] = None, **kwargs, ) -> pd.DataFrame: """一次性运行并返回 DataFrame 结果。""" det = StuckDetector(**kwargs) intervals, meta = det.detect(series) rows: List[Dict[str, object]] = [] for it in intervals: start_time = end_time = None if isinstance(series.index, pd.DatetimeIndex): if sampling_period is None: start_time = series.index[it.start_idx] end_time = series.index[it.end_idx] else: start_time = series.index[0] + it.start_idx * sampling_period end_time = series.index[0] + it.end_idx * sampling_period rows.append( { "start_idx": it.start_idx, "end_idx": it.end_idx, "start_time": start_time, "end_time": end_time, "kind": it.kind, "score": it.score, "value_summary": it.value_summary, "length": it.end_idx - it.start_idx + 1, } ) df = pd.DataFrame(rows) if "period" in meta: df.attrs["estimated_period"] = meta["period"] return df # ----------------------------- # 进阶:基于“同周期模板”的卡值定位(周期内卡段) # ----------------------------- def detect_within_cycle_stuck( series: pd.Series, period: Optional[int] = None, # 支持自动估计 min_run: int = 5, value_tol: float = 0.0, run_std_tol: float = 1e-4, peer_z_k: float = 4.0, ) -> pd.DataFrame: x = series.to_numpy(dtype=float) n = len(x) # 自动估计周期 if period is None: period = estimate_period(x) if period is None or period < 3: return pd.DataFrame() m = n // period if m < 2: return pd.DataFrame() X = x[: m * period].reshape(m, period) template = np.nanmedian(X, axis=0) mad = np.nanmedian(np.abs(X - template[None, :]), axis=0) sigma = 1.4826 * mad + 1e-12 rows: List[Dict[str, object]] = [] for ci in range(m): cyc = X[ci] diff = np.abs(cyc - template) eq = np.abs(np.diff(cyc, prepend=cyc[0])) <= value_tol run_std = pd.Series(cyc).rolling(min_run, min_periods=min_run).std(ddof=0).to_numpy() std_mask = run_std <= run_std_tol mask = np.zeros(period, dtype=bool) for i in range(period): L = max(0, i - min_run + 1) R = i if R - L + 1 >= min_run: cond_std = (not np.isnan(std_mask[R])) and bool(std_mask[R]) if eq[L:R+1].all() and cond_std: mask[L:R+1] = True runs = _group_runs(mask) for s, e in runs: if float(np.nanmedian(sigma[s:e+1])) < run_std_tol: continue z = diff[s:e+1] / sigma[s:e+1] z_med = float(np.nanmedian(z)) if np.isfinite(z_med) and z_med >= peer_z_k: abs_s = ci * period + s abs_e = ci * period + e mean_diff = float(np.nanmean(np.sign(cyc[s:e+1] - template[s:e+1]) * diff[s:e+1])) score = float(np.tanh((z_med - peer_z_k) / 2 + 1)) rows.append({ "cycle_idx": ci, "start_phase": s, "end_phase": e, "abs_start_idx": abs_s, "abs_end_idx": abs_e, "mean_diff_to_template": mean_diff, "kind": "cycle_flat", "score": score, }) return pd.DataFrame(rows) # ----------------------------- # 合成温度型周期数据(非正弦,含天然稳定段 + 可选卡值段) # ----------------------------- def generate_temperature_like_series( start_time: str = "2025-01-01", period: int = 60, cycles: int = 20, noise_std: float = 0.03, stable_plateau_len: int = 8, stable_values: Tuple[float, float] = (31.0, 34.0), temp_range: Tuple[float, float] = (30.0, 35.0), stuck_cycle_idx: Optional[int] = 8, stuck_phase_range: Tuple[int, int] = (20, 35), stuck_value: Optional[float] = None, seed: Optional[int] = 42, ) -> pd.Series: """生成更接近温度传感器的周期信号(非正弦)。""" rng = np.random.RandomState(seed) # 为兼容旧版 NumPy a = max(6, (period // 3) - stable_plateau_len) b = max(6, (period - a - 2 * stable_plateau_len)) v_low, v_high = stable_values v_low = float(np.clip(v_low, *temp_range)) v_high = float(np.clip(v_high, *temp_range)) one_cycle_parts = [] if a > 0: ramp_up = np.linspace(v_low, v_high, a, endpoint=False) ramp_up = ramp_up + rng.normal(0, noise_std, size=a) one_cycle_parts.append(ramp_up) plateau1 = np.full(stable_plateau_len, v_high) one_cycle_parts.append(plateau1) if b > 0: mid = v_low + 0.5 * (v_high - v_low) ramp_var = np.linspace(v_high, mid, b, endpoint=False) ramp_var = ramp_var + rng.normal(0, noise_std, size=b) one_cycle_parts.append(ramp_var) plateau2 = np.full(stable_plateau_len, v_low) one_cycle_parts.append(plateau2) one_cycle = np.concatenate(one_cycle_parts) if len(one_cycle) < period: pad = np.full(period - len(one_cycle), v_low) one_cycle = np.concatenate([one_cycle, pad]) else: one_cycle = one_cycle[:period] sig = np.tile(one_cycle, cycles) plateau_mask = np.zeros(period, dtype=bool) plateau_mask[a:a + stable_plateau_len] = True plateau_mask[a + stable_plateau_len + b : a + stable_plateau_len + b + stable_plateau_len] = True plateau_mask_full = np.tile(plateau_mask, cycles) non_plateau_idx = np.where(~plateau_mask_full)[0] sig[non_plateau_idx] += rng.normal(0, noise_std, size=len(non_plateau_idx)) if stuck_cycle_idx is not None: s = stuck_cycle_idx * period + stuck_phase_range[0] e = stuck_cycle_idx * period + stuck_phase_range[1] s = int(np.clip(s, 0, len(sig) - 1)) e = int(np.clip(e, 0, len(sig) - 1)) if s <= e: if stuck_value is None: seg = sig[s:e + 1] sv = float(np.round(np.median(seg), 3)) else: sv = float(np.clip(stuck_value, *temp_range)) sig[s:e + 1] = sv sig = np.clip(sig, *temp_range) idx = pd.date_range(start_time, periods=len(sig), freq="S") return pd.Series(sig, index=idx) # ----------------------------- # 可视化:原始数据 + 卡值区间 + 稳定点 标注 # ----------------------------- import matplotlib.pyplot as plt def _mask_from_intervals(n: int, intervals_df: pd.DataFrame, start_col: str, end_col: str) -> np.ndarray: m = np.zeros(n, dtype=bool) if intervals_df is None or len(intervals_df) == 0: return m for s, e in intervals_df[[start_col, end_col]].to_numpy(): s = int(max(0, s)) e = int(min(n - 1, e)) if s <= e: m[s : e + 1] = True return m def detect_stable_points( series: pd.Series, period: Optional[int] = None, stuck_mask: Optional[np.ndarray] = None, method: str = "cycle_sigma", stable_sigma_q: float = 0.2, rolling_window: int = 20, rolling_std_tol: float = 1e-3, ) -> np.ndarray: """返回布尔数组,表示“本身为稳定的点”。 method="cycle_sigma" 时若未提供 period,会尝试自动估计;失败则退回滚动标准差法。 """ x = series.to_numpy(dtype=float) n = len(x) stable = np.zeros(n, dtype=bool) if stuck_mask is None: stuck_mask = np.zeros(n, dtype=bool) if method == "cycle_sigma": p = period if p is None: p = estimate_period(x) if p is not None and n // p >= 2: m = n // p X = x[: m * p].reshape(m, p) template = np.nanmedian(X, axis=0) mad = np.nanmedian(np.abs(X - template[None, :]), axis=0) sigma = 1.4826 * mad + 1e-12 thr = np.nanquantile(sigma, stable_sigma_q) stable_phase = sigma <= thr for i in range(m): idx0 = i * p stable[idx0 : idx0 + p] = stable_phase rem = n - m * p if rem > 0: stable[m * p : m * p + rem] = stable_phase[:rem] else: sd = _rolling_std(x, rolling_window) stable = sd <= rolling_std_tol else: sd = _rolling_std(x, rolling_window) stable = sd <= rolling_std_tol stable = np.logical_and(stable, ~stuck_mask) return stable def plot_stuck_overview( series: pd.Series, res_basic: Optional[pd.DataFrame] = None, res_within: Optional[pd.DataFrame] = None, period: Optional[int] = None, stable_method: str = "cycle_sigma", stable_sigma_q: float = 0.2, rolling_window: int = 20, rolling_std_tol: float = 1e-3, figsize: Tuple[int, int] = (12, 5), save_path: Optional[str] = None, ): """绘制原始数据,并叠加:卡值区间(红)+ 稳定点(绿)。""" x = series.to_numpy(dtype=float) n = len(x) mask_basic = _mask_from_intervals(n, res_basic, "start_idx", "end_idx") if res_basic is not None else np.zeros(n, dtype=bool) mask_within = _mask_from_intervals(n, res_within, "abs_start_idx", "abs_end_idx") if res_within is not None else np.zeros(n, dtype=bool) stuck_mask = np.logical_or(mask_basic, mask_within) stable_mask = detect_stable_points( series, period=period, stuck_mask=stuck_mask, method=stable_method, stable_sigma_q=stable_sigma_q, rolling_window=rolling_window, rolling_std_tol=rolling_std_tol, ) fig, ax = plt.subplots(1, 1, figsize=figsize) if isinstance(series.index, pd.DatetimeIndex): t = series.index else: t = np.arange(n) ax.plot(t, x, linewidth=1.2, label="signal") def _add_spans(mask, color="#ff4d4f", alpha=0.25, label="stuck"): runs = _group_runs(mask) for i, (s, e) in enumerate(runs): ax.axvspan(t[s], t[e], color=color, alpha=alpha, lw=0, label=(label if i == 0 else None)) _add_spans(stuck_mask, label="stuck") idx_stable = np.where(stable_mask)[0] if len(idx_stable) > 0: ax.scatter(t[idx_stable], x[idx_stable], s=12, color="#52c41a", label="stable points", zorder=3) ax.set_title("Signal with Stuck Segments and Stable Points") ax.set_xlabel("Time" if isinstance(series.index, pd.DatetimeIndex) else "Index") ax.set_ylabel("Value") ax.legend() ax.grid(True, linestyle=":", alpha=0.5) if save_path: fig.savefig(save_path, dpi=160, bbox_inches="tight") return fig, ax # ----------------------------- # 示例(可注释/删除) # ----------------------------- if __name__ == "__main__": s = generate_temperature_like_series( start_time="2025-01-01", period=60, cycles=20, noise_std=0.03, stable_plateau_len=8, stable_values=(31.0, 33.0), temp_range=(30.0, 35.0), stuck_cycle_idx=8, stuck_phase_range=(20, 35), stuck_value=None, seed=42 ) # 基础检测:seasonal_period 未给则自动估计;此处演示手动指定为 60 res1 = detect_stuck_segments( s, sampling_period=pd.Timedelta(seconds=1), min_flat_run=5, value_tol=5e-4, deriv_window=3, deriv_tol=5e-4, lowvar_window=10, lowvar_std_tol=1e-3, seasonal_period=60, # 可改为 None 让其自动估计 seasonal_min_run=5, seasonal_value_tol=5e-4, stable_phase_q=0.20, stable_overlap_thr=0.60, require_z_k=3.0 ) # 周期内卡段检测:period=None 将自动估计 res2 = detect_within_cycle_stuck( s, period=None, # ← 自动估计 min_run=6, value_tol=1e-4, run_std_tol=8e-4, peer_z_k=4.0, ) plot_stuck_overview( s, res_basic=res1, res_within=res2, period=None, # 绘图也会尝试自动估计周期,不行则退回滚动法 stable_method="cycle_sigma", stable_sigma_q=0.2, figsize=(12, 4.5), save_path=None, ) import matplotlib.pyplot as plt plt.show()
11-05
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值