变电站表计读数识别系统分析
python知识
Python continue
语句详细讲解
continue
是 Python 中的控制流语句,用于改变循环的正常执行顺序。
基本语法和功能
for item in sequence:if condition:continue# 其他代码
功能:当遇到 continue
语句时,它会立即跳过当前循环迭代的剩余代码,直接开始下一次迭代。
在您代码中的具体应用
在您的代码中:
original_img = cv2.imread(input_path)
if original_img is None:logger.warning(f"无法读取图片 {filename}, 跳过")continue # 跳过当前图像的后续处理
这段代码的意思是:
-
尝试读取图像文件
-
如果读取失败(
original_img is None
) -
记录警告日志
-
使用
continue
跳过当前图像的后续所有处理步骤 -
直接开始处理下一个图像文件
与 break
和 pass
的区别
为了更好地理解 continue
,让我们比较一下类似的循环控制语句:
语句 | 功能 | 示例 |
---|---|---|
continue | 跳过当前迭代的剩余代码,继续下一次迭代 | for i in range(5): if i == 2: continue; print(i) 输出: 0, 1, 3, 4 |
break | 完全终止整个循环 | for i in range(5): if i == 2: break; print(i) 输出: 0, 1 |
pass | 什么都不做,只是一个占位符 | for i in range(5): if i == 2: pass; print(i) 输出: 0, 1, 2, 3, 4 |
实际应用示例
示例 1:跳过特定值的处理
# 只处理偶数
numbers = [1, 2, 3, 4, 5, 6]
for num in numbers:if num % 2 != 0: # 如果是奇数continue # 跳过后续处理print(f"处理偶数: {num}")# 更多处理代码...
输出:
处理偶数: 2
处理偶数: 4
处理偶数: 6
示例 2:跳过无效数据(类似您的代码)
files = ["data1.txt", "corrupted.txt", "data3.txt"]
for file in files:content = read_file(file)if content is None: # 如果文件读取失败print(f"跳过无效文件: {file}")continue# 处理有效文件的内容process_content(content)
示例 3:在嵌套循环中使用
for i in range(3):print(f"外层循环: {i}")for j in range(3):if j == 1:continue # 只跳过内层循环的当前迭代print(f" 内层循环: {j}")
输出:
外层循环: 0内层循环: 0内层循环: 2
外层循环: 1内层循环: 0内层循环: 2
外层循环: 2内层循环: 0内层循环: 2
总结
continue
语句是 Python 循环控制中的重要工具,它:
-
用于跳过当前迭代的剩余代码
-
直接开始下一次循环迭代
-
特别适合处理错误情况和异常数据
-
在批处理程序中非常有用,可以提高程序的健壮性和效率
Python os.path.dirname() 函数详解
1. 什么是 os.path.dirname()
os.path.dirname()
是 Python 中 os.path
模块的一个函数,用于从文件路径中提取目录部分。它的作用是去掉文件名,返回上级目录的路径
2. 基本语法和用法
首先需要导入 os 模块:
import os
然后使用以下语法:
os.path.dirname(path)
参数 path
是一个字符串,表示文件或目录的路径(可以是相对路径或绝对路径)。函数返回去掉文件名后的路径(目录部分);如果路径没有目录部分,则返回空字符串 ""
3. 使用示例
3.1 获取文件所在目录
import osfile_path = "/home/user/documents/report.docx"
directory = os.path.dirname(file_path)
print(directory) # 输出: /home/user/documents
3.2 处理相对路径
import osfile_path = "documents/report.docx"
directory = os.path.dirname(file_path)
print(directory) # 输出: documents
3.3 处理目录路径
当传入一个目录路径时,os.path.dirname()
会返回上一级目录:
import osdir_path = "/home/user/documents/"
directory = os.path.dirname(dir_path)
print(directory) # 输出: /home/user
3.4 特殊路径情况
import osprint(os.path.dirname("/")) # 输出: (空字符串)
print(os.path.dirname(".")) # 输出: (空字符串)
print(os.path.dirname("file.txt")) # 输出: (空字符串)
print(os.path.dirname("data/")) # 输出: data
4. 实用技巧与搭配使用
4.1 结合 os.path.basename() 获取文件名
如果你需要同时获取目录和文件名,可以结合使用 os.path.dirname()
和 os.path.basename()
:
import osfile_path = "/home/user/documents/report.docx"
directory = os.path.dirname(file_path) # 获取目录部分
filename = os.path.basename(file_path) # 获取文件名print(directory) # 输出: /home/user/documents
print(filename) # 输出: report.docx
4.2 使用 os.path.split() 同时获取目录和文件名
os.path.split()
可以一次性返回目录和文件名:
import ospath = "/home/user/documents/report.docx"
dir_name, file_name = os.path.split(path)print(dir_name) # 输出: /home/user/documents
print(file_name) # 输出: report.docx
4.3 获取当前脚本所在目录(常用技巧)
在实际开发中,经常需要获取当前 Python 脚本所在的目录,可以使用以下代码:
import osscript_dir = os.path.dirname(os.path.abspath(__file__))
print(script_dir)
这段代码的工作原理是:
1.__file__
表示当前模块的路径
2.os.path.abspath()
将其转换为绝对路径
3.os.path.dirname()
提取目录部分
4.4 获取多级上级目录
如果需要获取更上层的目录,可以嵌套使用 os.path.dirname()
:
import osfile_path = "/home/user/documents/report.docx"# 获取上一级目录
parent_dir = os.path.dirname(file_path)
print(parent_dir) # 输出: /home/user/documents# 获取上两级目录
grand_parent_dir = os.path.dirname(os.path.dirname(file_path))
print(grand_parent_dir) # 输出: /home/user
5. 注意事项
1.路径存在性检查:os.path.dirname()
只处理路径字符串,不检查路径是否实际存在。
2.斜杠处理:如果路径以斜杠结尾,函数会忽略最后的斜杠后再处理。
3.相对路径:对于相对路径,函数也会返回相对路径的上一级目录。
4.符号链接:函数不解析文件系统中的符号链接。如果需要获取真实路径,应先调用 os.path.realpath()
下表总结了不同路径处理函数的区别:
方法 | 返回值 | 适用场景 |
---|---|---|
os.path.dirname() | 目录部分 | 只需要获取目录 |
os.path.basename() | 文件名 | 只需要获取文件名 |
os.path.split() | (目录, 文件名) 元组 | 需要同时获取目录和文件名 |
pathlib.Path.parent | 目录部分 | Python 3.4+ 的现代路径处理方式 |
8. 常见问题解答
Q: 为什么在相对路径下有时获取不到正确目录?
A: 因为 os.path.dirname()
只解析字符串,若路径是相对路径且当前工作目录不一致,会导致结果错误。解决方法是在处理前使用 os.path.abspath()
将路径转换为绝对路径
Q: 如何确保脚本在不同环境中都能正确获取自身目录?
A: 始终使用 os.path.dirname(os.path.abspath(__file__))
,这样可以确保无论脚本在何处执行,都能正确获取到其所在目录
Q: 什么时候使用 os.path.dirname(),什么时候使用 pathlib?
A: 如果你使用的是 Python 3.4 及以上版本,建议使用 pathlib
,因为它提供了更现代、更直观的API。如果是维护旧代码或需要与早期Python版本兼容,则使用 os.path.dirname()
。
掌握了 os.path.dirname()
函数,你就具备了处理文件路径的基础能力。随着你对 Python 的深入理解,可以进一步学习 pathlib
模块,它提供了更加现代和面向对象的路径操作方式。
主程序入口代码详细分析
这段代码是程序的入口点,负责初始化系统、处理整个文件夹中的图像,并生成最终的报告。下面是对这段代码的详细分析:
if __name__ == "__main__":try:# 确保Excel输出目录存在excel_dir = os.path.dirname(CONFIG['excel_output'])if excel_dir and not os.path.exists(excel_dir):os.makedirs(excel_dir, exist_ok=True)# 初始化主模型main_model = model_init(CONFIG['main_model_file'])# 初始化液位表计处理器liquid_processor = LiquidGaugeProcessor(CONFIG['liquid_seg_model'])# 加载JSON配置json_config, stretch_ids = load_json_config(CONFIG['json_config'])# 批量处理整个文件夹process_folder(main_model=main_model,liquid_processor=liquid_processor,input_folder=CONFIG['input_folder'], output_folder=CONFIG['output_folder'],conf_thresh=CONFIG['conf_thresh'],json_config=json_config,stretch_ids=stretch_ids # 添加这个参数)logger.info("\n处理完成!")logger.info(f"所有结果已保存到: {os.path.abspath(CONFIG['output_folder'])}")logger.info(f"Excel报告已保存到: {os.path.abspath(CONFIG['excel_output'])}")except Exception as e:import tracebacklogger.error(f"程序错误: {e}")logger.error(traceback.format_exc())sys.exit(1)
1. 代码结构概述
if __name__ == "__main__":try:# 初始化设置# 模型初始化# 配置文件加载# 批量处理# 完成提示except Exception as e:# 异常处理
这是一个典型的Python程序入口结构,使用if __name__ == "__main__":
来确保代码只在直接运行脚本时执行,而不是在作为模块导入时执行。
if __name__ == "__main__":
详细讲解
if __name__ == "__main__":# 这里写代码
这是一个条件判断,意思是"如果这个文件是直接运行的,那么就执行下面的代码"。
为什么要这样写?
1. 理解 __name__
变量
在Python中,每个文件(模块)都有一个内置变量 __name__
:
-
当文件直接运行时:
__name__
的值是"__main__"
-
当文件被导入时:
__name__
的值是文件名(不带.py后缀)
2. 实际例子
假设有两个文件:
math_tools.py(工具文件):
def add(a, b):return a + bdef multiply(a, b):return a * b# 测试代码
if __name__ == "__main__":print("测试数学函数:")print(f"2 + 3 = {add(2, 3)}")print(f"2 × 3 = {multiply(2, 3)}")
main_program.py(主程序):
# 导入math_tools模块
import math_toolsprint("使用数学工具:")
result = math_tools.add(5, 10)
print(f"5 + 10 = {result}")
运行结果:
-
如果直接运行
math_tools.py
:
测试数学函数:
2 + 3 = 5
2 × 3 = 6
2.如果运行 main_program.py
:
使用数学工具:
5 + 10 = 15
注意:当 main_program.py
导入 math_tools.py
时,math_tools.py
中的测试代码不会执行!
在你代码中的具体作用
if __name__ == "__main__":try:# 初始化设置# 模型初始化# 配置文件加载# 批量处理# 完成提示except Exception as e:# 异常处理
为什么要这样设计?
优点1:代码复用性
你可以把写好的函数放在一个文件中,既可以直接运行测试,也可以被其他程序导入使用。
优点2:组织清晰
将主要逻辑放在 if __name__ == "__main__":
下面,让代码结构更清晰。
优点3:避免意外执行
防止导入时意外执行不应该运行的代码。
简单比喻
可以把这想象成一个家电:
-
直接运行文件:就像按下电器的"开关"按钮,电器开始工作
-
导入文件:就像把电器拆开,只使用里面的某个零件
-
if __name__ == "__main__":
:就像电器的开关,确保只有按下开关时整个电器才工作
总结
情况 | __name__ 的值 | 是否执行条件内的代码 |
---|---|---|
直接运行文件 | "__main__" | ✅ 执行 |
被其他文件导入 | 文件名 | ❌ 不执行 |
这种写法是Python编程的最佳实践,让你的代码更加灵活、可重用,并且结构清晰。
希望这个解释对你有帮助!如果还有不明白的地方,欢迎继续提问。
2. 初始化设置
这段代码是用于确保Excel输出目录存在的常见Python代码模式。让我为您详细解析每一部分:
主要目的是:检查Excel输出文件所在的目录是否存在,如果不存在则创建该目录。
# 确保Excel输出目录存在
excel_dir = os.path.dirname(CONFIG['excel_output'])
if excel_dir and not os.path.exists(excel_dir):os.makedirs(excel_dir, exist_ok=True)
逐行解析
第1行:获取目录路径
excel_dir = os.path.dirname(CONFIG['excel_output'])
-
CONFIG['excel_output']
:从配置字典中获取Excel文件的完整路径,例如"D:/results/report.xlsx"
-
os.path.dirname()
:Python内置函数,用于提取路径中的目录部分 -
结果:如果是
"D:/results/report.xlsx"
,则excel_dir
变为"D:/results"
第2行:条件检查
if excel_dir and not os.path.exists(excel_dir):
-
excel_dir and ...
:首先检查excel_dir
是否非空-
如果Excel文件在当前目录(如
"report.xlsx"
),os.path.dirname()
返回空字符串""
-
空字符串在布尔上下文中为假,因此条件不满足,跳过创建步骤
-
-
not os.path.exists(excel_dir)
:检查目录是否不存在-
os.path.exists()
检查指定路径是否存在 -
如果目录不存在,条件为真,需要创建目录
-
第3行:创建目录
os.makedirs(excel_dir, exist_ok=True)
-
os.makedirs()
:递归创建目录的函数-
与
os.mkdir()
不同,makedirs()
可以创建多级目录 -
例如,如果路径是
"D:/a/b/c"
而a
和b
都不存在,makedirs()
会一并创建
-
-
exist_ok=True
:重要参数,表示如果目录已存在不会抛出异常-
如果没有这个参数,当目录已存在时,
makedirs()
会抛出FileExistsError
-
实际示例
假设配置中:
CONFIG = {'excel_output': 'D:/project/results/final_report.xlsx'
}
代码执行过程:
-
excel_dir = os.path.dirname('D:/project/results/final_report.xlsx')
→'D:/project/results'
-
if 'D:/project/results' and not os.path.exists('D:/project/results'):
-
如果
D:/project/results
目录不存在,条件为真
-
-
os.makedirs('D:/project/results', exist_ok=True)
→ 创建目录
为什么需要这段代码?
-
预防错误:如果尝试在不存在的目录中创建文件,Python会抛出异常
-
自动化处理:不需要用户手动创建输出目录
-
健壮性:确保程序在各种环境下都能正常工作
# 初始化主模型main_model = model_init(CONFIG['main_model_file'])# 初始化液位表计处理器liquid_processor = LiquidGaugeProcessor(CONFIG['liquid_seg_model'])# 加载JSON配置json_config, stretch_ids = load_json_config(CONFIG['json_config'])
1. 初始化主模型
main_model = model_init(CONFIG['main_model_file'])
功能解释:
这行代码加载并初始化主要的AI模型,用于检测仪表中的指针和刻度。
详细分解:
-
CONFIG['main_model_file']
:从配置字典中获取模型文件的路径-
例如:
'D:/job/bj_inference_0827/weights/biaoji_best_0821.pt'
-
-
model_init(...)
:调用一个自定义函数来初始化模型-
这个函数会加载模型文件并准备好用于图像识别
-
-
main_model = ...
:将初始化后的模型赋值给变量main_model
-
这样后续代码就可以使用这个模型来处理图像了
-
简单比喻:
就像你要做一道菜,这行代码相当于:
-
从食谱(CONFIG)中找到主要厨具的位置
-
把厨具(模型)从柜子里拿出来并准备好
-
给这个厨具起个名字(main_model),方便后面使用
2. 初始化液位表计处理器
liquid_processor = LiquidGaugeProcessor(CONFIG['liquid_seg_model'])
功能解释:
这行代码专门为处理液位表计(如水位计)创建了一个处理器。
详细分解:
-
CONFIG['liquid_seg_model']
:从配置中获取液位表计模型的路径 -
LiquidGaugeProcessor(...)
:创建一个液位表计处理器的实例-
这是一个自定义的类,专门处理液位表计的识别
-
-
liquid_processor = ...
:将这个处理器赋值给变量liquid_processor
简单比喻:
就像你有一道特殊的菜需要专门的厨具:
-
从食谱中找到特殊厨具的位置
-
拿出这个特殊厨具(液位表计处理器)
-
给这个特殊厨具起个名字(liquid_processor)
3. 加载JSON配置
json_config, stretch_ids = load_json_config(CONFIG['json_config'])
功能解释:
这行代码加载一个JSON配置文件,这个文件包含了各种仪表的参数信息。
详细分解:
-
CONFIG['json_config']
:从配置中获取JSON文件的路径 -
load_json_config(...)
:调用一个函数来加载和解析JSON文件 -
json_config, stretch_ids = ...
:这个函数返回两个值-
json_config
:包含所有仪表配置信息的字典或列表 -
stretch_ids
:需要特殊处理(拉伸)的仪表ID列表
-
简单比喻:
就像你有一本详细的食谱:
-
找到食谱书的位置(JSON配置文件路径)
-
打开食谱书并阅读内容(加载和解析JSON)
-
从书中获取两种信息:
-
所有菜的详细做法(json_config)
-
需要特殊处理的菜列表(stretch_ids)
-
整体作用
这三行代码一起完成了程序的"准备工作":
-
准备主要工具:加载主模型,用于检测大多数仪表
-
准备专用工具:加载液位表计处理器,用于处理特殊类型的仪表
-
准备说明书:加载配置文件,告诉程序各种仪表的具体参数
为什么需要这样做?
-
模块化设计:不同的功能由不同的组件处理,使代码更清晰
-
灵活性:可以轻松更换模型或配置文件,而不需要修改主要逻辑
-
效率:提前加载好所有需要的资源,使后续处理更快速
实际应用示例
假设我们要处理一个变电站的各种仪表:
-
主模型可以识别指针式仪表(压力表、电压表等)
-
液位处理器专门处理水位计、油位计等
-
JSON配置文件告诉程序每个仪表的具体参数(量程、单位等)
通过这三行代码,程序就准备好了所有需要的工具和信息,可以开始处理图像了。
这段代码的主要作用是:调用处理函数处理整个文件夹中的图像,并在完成后显示处理结果和保存位置。
# 批量处理整个文件夹
process_folder(main_model=main_model,liquid_processor=liquid_processor,input_folder=CONFIG['input_folder'], output_folder=CONFIG['output_folder'],conf_thresh=CONFIG['conf_thresh'],json_config=json_config,stretch_ids=stretch_ids # 添加这个参数
)logger.info("\n处理完成!")
logger.info(f"所有结果已保存到: {os.path.abspath(CONFIG['output_folder'])}")
logger.info(f"Excel报告已保存到: {os.path.abspath(CONFIG['excel_output'])}")
这是调用一个名为 process_folder
的函数,并传递了7个参数:
-
main_model=main_model
:传递之前初始化好的主模型 -
liquid_processor=liquid_processor
:传递液位表计处理器 -
input_folder=CONFIG['input_folder']
:从配置中获取输入文件夹路径 -
output_folder=CONFIG['output_folder']
:从配置中获取输出文件夹路径 -
conf_thresh=CONFIG['conf_thresh']
:从配置中获取置信度阈值 -
json_config=json_config
:传递加载的JSON配置 -
stretch_ids=stretch_ids
:传递需要拉伸处理的仪表ID列表
关键字参数的使用(如 main_model=main_model
)使得代码更加清晰,即使参数顺序变化也不会影响函数调用。
2. 处理完成提示
logger.info("\n处理完成!")
这行代码使用日志记录器输出"处理完成!"的消息:
-
logger.info()
:输出信息级别的日志 -
"\n"
:换行符,使输出更加清晰 -
这是在所有图像处理完成后执行的消息
3. 结果保存位置提示
logger.info(f"所有结果已保存到: {os.path.abspath(CONFIG['output_folder'])}")
logger.info(f"Excel报告已保存到: {os.path.abspath(CONFIG['excel_output'])}")
这两行代码告诉用户处理结果的保存位置:
-
f"..."
:f-string格式化字符串,可以在字符串中直接嵌入变量 -
os.path.abspath(...)
:获取绝对路径,确保路径格式正确 -
第一行显示处理后的图像保存位置
-
第二行显示Excel报告保存位置
深入理解
1. 函数调用与参数传递
process_folder
函数很可能是一个复杂的函数,它会:
-
遍历输入文件夹中的所有图像文件
-
对每个图像进行预处理(如拉伸)
-
使用模型检测仪表和指针
-
计算仪表读数
-
保存处理后的图像和结果
-
生成Excel报告
2. 路径处理的重要性
使用 os.path.abspath()
非常重要,因为它:
-
将相对路径转换为绝对路径
-
确保路径格式正确(特别是跨平台时)
-
使用户能够轻松找到结果文件
例如,如果配置中是相对路径 "./results"
,os.path.abspath()
会将其转换为完整路径如 "D:/project/results"
。
3. 日志记录的好处
使用 logger.info()
而不是简单的 print()
有以下优势:
-
可以控制日志级别(信息、警告、错误等)
-
可以同时输出到控制台和文件
-
可以添加时间戳等额外信息
-
更适合生产环境使用
实际工作流程
假设我们有以下配置:
CONFIG = {'input_folder': 'D:/images','output_folder': 'D:/results','excel_output': 'D:/results/report.xlsx','conf_thresh': 0.5
}
代码执行过程:
-
process_folder
函数会处理D:/images
文件夹中的所有图像 -
将处理后的图像保存到
D:/results
文件夹 -
生成Excel报告并保存为
D:/results/report.xlsx
-
完成后输出:
处理完成!
所有结果已保存到: D:/results
Excel报告已保存到: D:/results/report.xlsx
异常处理代码详细讲解
这是一个标准的异常处理结构,用于确保程序在遇到错误时能够优雅地处理而不是突然崩溃。
try:# 这里放置可能会出错的代码
except Exception as e:import tracebacklogger.error(f"程序错误: {e}")logger.error(traceback.format_exc())sys.exit(1)
逐行解析
1. try 块
try:# 这里放置可能会出错的代码
-
try:
:开始一个异常处理块 -
在
try
块中放置可能会引发异常的代码 -
如果这些代码执行时出现任何错误,程序不会崩溃,而是跳转到
except
块
2. except 块
except Exception as e:
-
except Exception as e:
:捕获所有类型的异常-
Exception
是所有内置异常的基类 -
as e
将捕获的异常对象赋值给变量e
,以便后续使用
-
-
当
try
块中的代码出现任何错误时,程序会跳转到这里执行
3. 导入 traceback 模块
import traceback
-
traceback
是Python的标准库模块,用于提取、格式化和打印异常的回溯信息 -
这里在
except
块内部导入,而不是在文件顶部,这是一种常见的做法
4. 记录错误信息
logger.error(f"程序错误: {e}")
-
使用日志记录器记录错误的基本信息
-
f"程序错误: {e}"
:格式化字符串,将异常对象e
转换为字符串并包含在消息中 -
这会输出类似 "程序错误: division by zero" 的消息
5. 记录完整的堆栈跟踪
logger.error(traceback.format_exc())
-
traceback.format_exc()
:获取当前异常的完整堆栈跟踪信息 -
这比简单的错误消息包含更多详细信息,包括:
-
错误发生的文件名和行号
-
函数调用链
-
完整的错误类型和消息
-
-
这对于调试和定位问题非常重要
6. 退出程序
sys.exit(1)
-
sys.exit(1)
:以非零状态码退出程序 -
参数
1
表示程序因错误而退出 -
在Unix/Linux系统中,非零退出码通常表示程序执行失败
-
这确保了程序不会在错误状态下继续执行
process_folder
函数详细分析
def process_folder(main_model: YOLO, liquid_processor: LiquidGaugeProcessor, input_folder: str, output_folder: str, conf_thresh: float, json_config: List[Dict],stretch_ids: List[str]): # 新增参数"""批量处理文件夹中的所有图片(基于JSON配置)"""
1. 函数概述
1.1 主要功能
-
批量处理文件夹中的所有仪表图像
-
根据JSON配置识别和处理不同类型的仪表
-
计算仪表读数并可视化结果
-
生成处理结果和Excel报告
1.2 输入参数
- `main_model`: 主YOLO模型,用于检测指针式仪表的关键点(表盘中心、起始刻度、结束刻度、指针等)。
- `liquid_processor`: 液位表计处理器,用于处理液位表计。
- `input_folder`: 输入图片文件夹路径。
- `output_folder`: 输出结果文件夹路径。
- `conf_thresh`: 目标检测的置信度阈值。
- `json_config`: 从JSON配置文件中加载的仪表配置列表。
1. 初始化与准备工作
os.makedirs(output_folder, exist_ok=True)
supported_exts = ['.jpg', '.jpeg', '.png', '.bmp', '.tiff']
image_files = [f for f in os.listdir(input_folder) if os.path.splitext(f)[1].lower() in supported_exts]
-
功能:创建输出目录,收集支持的图片格式文件
-
细节:
-
os.makedirs(..., exist_ok=True)
确保输出目录存在 -
只处理常见图片格式,避免处理其他类型文件
-
使用列表推导式高效筛选文件
-
os.makedirs(..., exist_ok=True)
使用说明
python os如何创建文件夹 | PingCode智库
supported_exts = ['.jpg', '.jpeg', '.png', '.bmp', '.tiff']
- 定义支持的图片扩展名列表。
image_files = [f for f in os.listdir(input_folder) if os.path.splitext(f)[1].lower() in supported_exts]
这段代码的作用是从指定文件夹(input_folder
)中获取所有文件,然后筛选出扩展名在supported_exts
列表中的图片文件。
2. 代码分解
2.1 os.listdir(input_folder)
-
功能:列出指定目录中的所有文件和子目录
-
返回值:包含文件和目录名称的列表
-
示例:如果
input_folder
包含['image1.jpg', 'image2.png', 'document.txt', 'subfolder']
,则返回这个列表
2.2 for f in os.listdir(input_folder)
-
功能:遍历目录中的所有文件和子目录
-
说明:
f
是循环变量,代表每个文件或目录的名称
2.3 os.path.splitext(f)
-
功能:分割文件名和扩展名
-
返回值:一个包含两个元素的元组
(文件名, 扩展名)
-
示例:
-
os.path.splitext('image.jpg')
→('image', '.jpg')
-
os.path.splitext('document.txt')
→('document', '.txt')
-
os.path.splitext('file.with.dots.png')
→('file.with.dots', '.png')
-
2.4 os.path.splitext(f)[1]
-
功能:获取元组的第二个元素,即文件扩展名
-
说明:Python列表/元组索引从0开始,所以
[1]
表示第二个元素 -
示例:
-
os.path.splitext('image.jpg')[1]
→'.jpg'
-
os.path.splitext('document.txt')[1]
→'.txt'
-
2.5 .lower()
-
功能:将字符串转换为小写
-
目的:确保扩展名比较时不区分大小写
-
示例:
-
'.JPG'.lower()
→'.jpg'
-
'.PNG'.lower()
→'.png'
-
2.6 in supported_exts
-
功能:检查扩展名是否在支持的扩展名列表中
-
说明:
supported_exts
应该是一个包含支持的图片扩展名的列表 -
示例:如果
supported_exts = ['.jpg', '.jpeg', '.png']
,则:-
'.jpg' in supported_exts
→True
-
'.txt' in supported_exts
→False
-
2.7 列表推导式
-
结构:
[expression for item in iterable if condition]
-
工作流程:
-
遍历
os.listdir(input_folder)
中的每个项目 -
对每个项目应用条件
if os.path.splitext(f)[1].lower() in supported_exts
-
如果条件为真,将项目
f
添加到结果列表中 -
如果条件为假,跳过该项目
-
3. 完整示例
假设:
-
input_folder = '/home/user/images'
-
文件夹内容:
['cat.jpg', 'dog.png', 'document.txt', 'vacation.JPG', 'notes.docx']
-
supported_exts = ['.jpg', '.jpeg', '.png', '.bmp', '.tiff']
代码执行过程:
-
os.listdir('/home/user/images')
→['cat.jpg', 'dog.png', 'document.txt', 'vacation.JPG', 'notes.docx']
-
对每个文件检查扩展名:
-
'cat.jpg'
→.jpg
→ 小写 →.jpg
→ 在列表中 → 保留 -
'dog.png'
→.png
→ 小写 →.png
→ 在列表中 → 保留 -
'document.txt'
→.txt
→ 小写 →.txt
→ 不在列表中 → 跳过 -
'vacation.JPG'
→.JPG
→ 小写 →.jpg
→ 在列表中 → 保留 -
'notes.docx'
→.docx
→ 小写 →.docx
→ 不在列表中 → 跳过
-
-
最终结果:
image_files = ['cat.jpg', 'dog.png', 'vacation.JPG']
for的循环没看明白
2. 文件检查与日志记录
if not image_files:logger.error(f"输入文件夹中没有找到图片文件 {input_folder}")returnlogger.info(f"找到 {len(image_files)} 张图片需要处理")
-
功能:检查是否有有效图片文件
-
细节:
-
如果没有找到图片,记录错误并提前返回
-
使用日志记录找到的图片数量
-
return
立即终止当前函数执行,避免后续代码因空列表报错(如遍历空列表导致索引错误)。
3. 主循环处理每张图片
for filename in tqdm(image_files, desc="处理图片"):input_path = os.path.join(input_folder, filename)output_path = os.path.join(output_folder, os.path.splitext(filename)[0] + "_result.jpg")
这行代码创建一个循环,遍历image_files
列表中的所有文件,并在控制台显示一个进度条,直观地展示处理进度。
for filename in tqdm(image_files, desc="处理图片"):
组成部分解析
1. 基本for循环结构
for filename in image_files:# 处理每个文件
这是一个标准的Python for循环,它会遍历image_files
列表中的每个元素,并将当前元素赋值给变量filename
。
2. 使用tqdm包装
for filename in tqdm(image_files, desc="处理图片"):
-
tqdm()
: 这是一个Python库函数,用于创建进度条 -
image_files
: 要遍历的文件列表 -
desc="处理图片"
: 设置进度条的描述文本
tqdm库的作用
没有tqdm的情况
如果没有进度条,处理大量文件时,用户不知道:
-
已经处理了多少文件
-
还需要多长时间完成
-
程序是否在正常运行(还是卡住了)
有tqdm的情况
使用tqdm后,控制台会显示类似这样的进度条:
处理图片: 45%|████▌ | 45/100 [00:30<00:35, 1.5文件/秒]
这个进度条包含以下信息:
-
描述文本:"处理图片"
-
进度百分比:45%
-
进度条图形:████▌
-
完成数量:45/100
-
已用时间:00:30
-
预计剩余时间:<00:35
-
处理速度:1.5文件/秒
实际应用示例
假设我们有100张图片需要处理:
不使用tqdm的代码
image_files = ["img1.jpg", "img2.jpg", ..., "img100.jpg"]for filename in image_files:process_image(filename) # 处理图片的函数# 用户看不到任何进度信息
使用tqdm的代码
from tqdm import tqdmimage_files = ["img1.jpg", "img2.jpg", ..., "img100.jpg"]for filename in tqdm(image_files, desc="处理图片"):process_image(filename) # 处理图片的函数# 用户可以看到实时进度条
tqdm的常用参数
除了desc
,tqdm还支持许多其他有用参数:
for filename in tqdm(image_files, desc="处理图片", # 进度条描述total=len(image_files), # 总数量(自动检测,可省略)unit="张", # 单位(默认为"it")ncols=100, # 进度条宽度(字符数)leave=True # 完成后是否保留进度条
):process_image(filename)
在您代码中的具体应用
在您的代码中:
for filename in tqdm(image_files, desc="处理图片"):input_path = os.path.join(input_folder, filename)output_path = os.path.join(output_folder, os.path.splitext(filename)[0] + "_result.jpg")logger.info(f"\n处理图片: {filename}")# ... 其他处理逻辑
这意味着:
-
程序会处理
image_files
中的每个文件 -
在控制台显示一个进度条,描述为"处理图片"
-
即使处理每个文件需要较长时间,用户也能看到整体进度
-
同时,每个文件的详细处理情况还会通过
logger.info
记录
这三行代码是文件处理循环中的关键部分,负责构建文件路径和记录处理信息。
input_path = os.path.join(input_folder, filename)
output_path = os.path.join(output_folder, os.path.splitext(filename)[0] + "_result.jpg")
logger.info(f"\n处理图片: {filename}")
这段代码的主要功能是:
-
构建输入文件的完整路径
-
构建输出文件的完整路径(添加"_result"后缀)
-
记录当前正在处理的图片信息
逐行解析
第1行:构建输入文件路径
input_path = os.path.join(input_folder, filename)
-
os.path.join()
: Python内置函数,用于将多个路径组件智能地连接成一个完整的路径 -
input_folder
: 输入文件夹的路径,例如"D:/images"
-
filename
: 当前处理的文件名,例如"gauge001.jpg"
-
结果:
input_path
变为"D:/images/gauge001.jpg"
为什么使用 os.path.join() 而不是字符串拼接?
-
跨平台兼容性:自动处理不同操作系统的路径分隔符(Windows用
\
,Unix用/
) -
智能处理多余分隔符:避免出现
"D:/images//gauge001.jpg"
这样的情况 -
更清晰和Pythonic
第2行:构建输出文件路径
output_path = os.path.join(output_folder, os.path.splitext(filename)[0] + "_result.jpg")
这行代码更复杂,可以分为几个部分:
-
提取文件名(不含扩展名):
os.path.splitext(filename)[0]
-
-
os.path.splitext()
: 将文件名分割为名称和扩展名两部分 -
例如:
os.path.splitext("gauge001.jpg")
返回("gauge001", ".jpg")
-
[0]
取第一部分(名称部分),所以得到"gauge001"
-
-
添加后缀并构建完整输出路径:
"gauge001" + "_result.jpg" → "gauge001_result.jpg"
os.path.join(output_folder, "gauge001_result.jpg") → "D:/results/gauge001_result.jpg"
设计考虑:
-
保留原文件名的基础上添加"_result"后缀,便于识别处理后的文件
-
保持相同的文件格式(.jpg)
-
确保输出文件不会覆盖原始文件
第3行:记录处理信息
logger.info(f"\n处理图片: {filename}")
-
logger.info()
: 使用配置的日志记录器输出信息级别的日志 -
f"\n处理图片: {filename}"
: f-string格式化字符串-
\n
: 换行符,使每条日志从新行开始,提高可读性 -
{filename}
: 插入当前处理的文件名
-
日志记录的重要性:
-
提供处理进度信息
-
便于调试和问题排查
-
创建处理历史记录
实际示例
假设有以下变量值:
-
input_folder = "D:/input_images"
-
output_folder = "D:/output_results"
-
filename = "meter_005.png"
代码执行结果:
-
input_path = os.path.join("D:/input_images", "meter_005.png") → "D:/input_images/meter_005.png"
-
output_path = os.path.join("D:/output_results", os.path.splitext("meter_005.png")[0] + "_result.jpg")
-
os.path.splitext("meter_005.png")[0] → "meter_005"
-
"meter_005" + "_result.jpg" → "meter_005_result.jpg"
-
os.path.join("D:/output_results", "meter_005_result.jpg") → "D:/output_results/meter_005_result.jpg"
-
-
日志输出:
处理图片: meter_005.png
这段代码负责读取图像文件并进行错误检查,是图像处理流程中的关键步骤。
# 读取原图
original_img = cv2.imread(input_path)
if original_img is None:logger.warning(f"无法读取图片 {filename}, 跳过")continue
这段代码的主要功能是:
-
使用OpenCV读取图像文件
-
检查图像是否成功读取
-
如果读取失败,记录警告信息并跳过当前图像的处理
逐行解析
第1行:读取图像
original_img = cv2.imread(input_path)
-
cv2.imread()
: OpenCV库中的图像读取函数 -
input_path
: 图像的完整路径,例如"D:/images/gauge001.jpg"
-
original_img
: 存储读取的图像数据,如果读取成功,是一个NumPy数组;如果读取失败,返回None
OpenCV的imread函数特点:
-
支持多种图像格式:JPEG、PNG、BMP、TIFF等
-
默认以BGR格式(而不是RGB)读取彩色图像
-
返回一个三维NumPy数组,形状为(高度, 宽度, 通道数)
-
如果读取失败,返回
None
而不是抛出异常
第2-4行:错误检查和跳过
if original_img is None:logger.warning(f"无法读取图片 {filename}, 跳过")continue
-
if original_img is None:
: 检查图像是否读取失败 -
logger.warning(...)
: 记录警告级别的日志信息 -
continue
: 跳过当前循环的剩余部分,直接处理下一个文件
错误处理逻辑:
-
如果
cv2.imread()
返回None
,表示图像读取失败 -
使用
logger.warning()
记录警告信息,包括文件名 -
使用
continue
跳过当前图像的后续处理步骤,使用continue
确保即使某些图像无法读取,程序也能继续处理其他图像。
常见的图像读取失败原因
-
文件不存在: 路径错误或文件已被移动/删除
-
文件格式不支持: OpenCV无法解析的图像格式
-
文件损坏: 图像文件已损坏或不完整
-
权限问题: 没有读取文件的权限
-
路径包含特殊字符: 路径中的特殊字符可能导致读取失败
结果数据存储和表计类型映射代码详细分析
# 用于存储结果数据的列表
results_data = []# 表计类型映射
meter_type_mapping = {1: '指针式仪表',5: '液位刻度表计',6: '数显类表计'
}
这段代码的主要功能是:
-
初始化一个空列表用于存储所有图像的处理结果
-
定义一个字典,将数字类型的表计代码映射为人类可读的中文名称
逐行解析
第1-2行:初始化结果数据列表
# 用于存储结果数据的列表
results_data = []
-
results_data = []
: 创建一个空列表 -
这个列表将在后续的循环中用于存储每个图像的处理结果
设计目的:
-
收集所有图像的处理结果,便于后续生成报告
-
提供统一的数据结构,方便数据访问和处理
-
支持批量操作,如生成Excel报告
第4-8行:定义表计类型映射
# 表计类型映射
meter_type_mapping = {1: '指针式仪表',5: '液位刻度表计',6: '数显类表计'
}
-
meter_type_mapping
: 一个字典,将数字代码映射为中文描述 -
键(Key): 数字代码(如1、5、6),这些代码可能来自配置文件或数据库
-
值(Value): 对应的中文描述,用于显示和报告
映射关系:
-
1
→'指针式仪表'
: 传统的指针式测量仪表 -
5
→'液位刻度表计'
: 用于测量液体高度的刻度表 -
6
→'数显类表计'
: 数字显示式仪表
在完整上下文中的作用
在您的完整代码中,这些定义位于图像处理循环之前:
extract_key_points_from_config
函数详细分析
这个函数负责从JSON配置中提取表计的关键点信息,并进行几何计算以确定刻度点的有效方向点。下面进行详细分析:
函数概述
def extract_key_points_from_config(meter_config: Dict) -> Dict:"""从JSON配置中提取关键点信息(使用窄边中点)"""
这个函数的主要目的是:
-
从JSON配置中提取表盘中心点信息
-
处理所有刻度点,计算它们的有效方向点(窄边中点)
-
计算每个刻度点相对于表盘中心的角度
-
识别起始和结束刻度点
-
计算仪表的最小和最大量程值
逐部分详细分析
1. 初始化与表盘中心提取
这段代码的目的是从仪表配置中提取表盘中心点的坐标信息,并将其存储为一个标准化的数据结构。
key_points = {}# 表盘中心信息
if 'bp_info' in meter_config and meter_config['bp_info']:bp_info = meter_config['bp_info'][0]if 'bp_center' in bp_info:key_points['o'] = {'cx': bp_info['bp_center'][0],'cy': bp_info['bp_center'][1],'w': 0, # 表盘中心没有宽高'h': 0,'angle': 0,'value': None}
1. 初始化关键点字典
key_points = {}
-
创建一个空字典
key_points
,用于存储所有提取的关键点信息 -
这个字典将包含表盘中心、刻度点等各种关键信息
2. 检查配置信息存在性
if 'bp_info' in meter_config and meter_config['bp_info']:
这行代码执行两个连续的检查:
-
检查字典
meter_config
中是否存在键'bp_info'
-
如果存在,检查
meter_config['bp_info']
的值是否为真值(非空、非None、非False等)
1. 第一部分:键存在性检查
'bp_info' in meter_config
-
in
操作符:检查字典中是否存在指定的键 -
返回值:如果键存在返回
True
,否则返回False
-
避免KeyError:这样检查可以避免直接访问不存在的键时引发
KeyError
异常
2. 第二部分:值有效性检查
meter_config['bp_info']
-
字典访问:使用键访问字典中的值
-
真值测试:在布尔上下文中,Python会对值进行真值测试
-
为假的情况:
None
、空列表[]
、空字典{}
、空字符串''
、0
、False
等都会被视为假
3. 逻辑与操作
condition1 and condition2
-
短路求值:Python的
and
操作符使用短路求值 -
执行顺序:先计算第一个条件,只有当第一个条件为真时,才计算第二个条件
-
效率优势:如果第一个条件为假,第二个条件不会被执行,提高了效率并避免了可能的错误
等价的其他写法
1. 使用嵌套的if语句
if 'bp_info' in meter_config:if meter_config['bp_info']:# 执行代码
2. 使用get()方法
if meter_config.get('bp_info'):# 执行代码
dict.get(key)
方法在键不存在时返回 None
,而 None
在布尔上下文中为假,所以这种方式也能工作。
但是,这种方法有一个细微差别:如果 meter_config['bp_info']
的值是空列表 []
,它会被视为假,但如果是空字符串 ''
或 0
,也会被视为假。这与原始代码的行为一致。
3. 提取表盘信息
bp_info = meter_config['bp_info'][0]
-
假设
meter_config['bp_info']
是一个列表,提取其中的第一个元素 -
使用索引
[0]
表示我们只关心第一个表盘信息(可能有多个表盘,但通常只有一个) -
这行代码假设列表至少有一个元素,由于前面的条件检查,这是安全的
4. 检查并提取表盘中心
if 'bp_center' in bp_info:
-
检查
bp_info
字典中是否包含'bp_center'
键 -
只有当表盘中心信息存在时,才进行后续处理
5. 创建表盘中心数据结构
key_points['o'] = {'cx': bp_info['bp_center'][0],'cy': bp_info['bp_center'][1],'w': 0, # 表盘中心没有宽高'h': 0,'angle': 0,'value': None
}
这是代码的核心部分,创建了一个标准化的表盘中心数据结构:
-
键名
'o'
:使用字母 'o' 可能代表 "origin"(原点)或 "center"(中心) -
坐标提取:
-
'cx'
: 表盘中心的 x 坐标,从bp_info['bp_center'][0]
提取 -
'cy'
: 表盘中心的 y 坐标,从bp_info['bp_center'][1]
提取 -
假设
bp_center
是一个包含两个元素的列表或元组,格式为[x, y]
-
-
宽高设置:
-
'w': 0
和'h': 0
:表盘中心是一个点,没有宽度和高度,因此设为 0
-
-
角度设置:
-
'angle': 0
:表盘中心没有方向性,角度设为 0
-
-
值设置:
-
'value': None
:表盘中心本身没有测量值,设为 None
-
center_point = (key_points['o']['cx'], key_points['o']['cy']) if 'o' in key_points else None
这行代码的目的是从key_points
字典中安全地提取表盘中心的坐标,如果表盘中心信息不存在,则返回None
。
非线性表计的刻度点角度计算过程主要涉及以下函数和步骤:
-
extract_key_points_from_config
函数:从JSON配置中提取关键点信息,包括每个刻度点的坐标、宽高、旋转角度等,并计算每个刻度点的有效方向点(窄边中点)和相对于表盘中心的角度。 -
get_rotated_rect_vertices
函数:根据刻度框的中心坐标、宽高和旋转角度,计算旋转矩形的四个顶点。 -
determine_direction_point
函数:根据表盘中心和旋转矩形的顶点,确定刻度点的有效方向点(窄边中点)。 -
calculate_angle
函数:计算从表盘中心到有效方向点的角度(弧度),然后转换为度数。 -
normalize_angle
- 角度归一化处理
save_image
函数详细分析
这个函数用于在图像上绘制检测框和识别结果,是仪表读数识别系统的可视化部分。
函数签名
def save_image(img, reading_result, bbox_result):
参数说明
-
img
: 输入图像(numpy数组格式) -
reading_result
: 读数结果列表,包含每个检测到的仪表的读数 -
bbox_result
: 边界框列表,每个元素是一个包含四个坐标值的列表[xmin, ymin, xmax, ymax]
函数功能
-
遍历所有检测到的仪表边界框
-
在每个边界框上绘制矩形和读数结果
-
处理可能的异常情况,当无法获取有效读数时显示"no result"
代码详细分析
1. 边界框遍历
boxes = bbox_result
for i in range(len(boxes)):
-
将边界框列表赋值给变量
boxes
-
使用
for
循环遍历所有检测到的边界框
2. 正常情况处理
try: cv2.rectangle(img, (int(boxes[i][0]),int(boxes[i][1])), (int(boxes[i][2]),int(boxes[i][3])) , (255, 0, 0) , 3)cv2.putText(img, str(reading_result[i]), (int(boxes[i][0]),int(boxes[i][3])), cv2.FONT_HERSHEY_COMPLEX, 1, (255, 0, 0), 1,)
温湿度表处理修改代码
我现在在处理单指针表计bj_type-1-*-1-0这种表计时遇到一个问题,我有一个表盘是单指针类型,表盘中有一个表盘中心o-1,这个指针A以表盘中心为原点旋转,在模型模型检查指针式会检测到两个指针一个指针A一个指针B,指针A的位置关系是与指针B的关系是交叉关系,指针B的旋转中心o-2,o-1与o-2水平分布,相隔差不多四分之3个指针的长度,指针A与指针B的长度相同
我现在需要读数指针A的读数,但是指针B在表盘界面中会造成影响,会检测到指针B,指针B属于误检测,我现在想在指针A读数时把指针B排除出去,
我想了一下这种情况,可以采用当指针类型为bj_type-1-*-1-0(bj_type-1(代表指针表计)-*-1(代表表盘区域要读数的指针是一个)-0)时,(这个类型的表计也可能是非线性表计 ),检测到两个指针时,现在指针A与指针B的区别是指针A检测出来的OBB检测框,现在的指针计算逻辑,选取最远的窄边中点作为指针尖端,当前前面代码已经计算出来了两个检测框的距离圆心最远的窄边中点,现在如果这种表计类型bj_type-1-*-1-0(可能是非线性表计)本应该是检测一个指针,但是检测两个指针,
我现在想到选择正确选择指针A的逻辑是:当检测这种表计类型为bj_type-1-*-1-0,检测到两个OBB检测框,判断两个OBB检测框窄边中点距离圆心o的距离,这个距离最远的检测框就是检测出来的指针A,使用这个OBB检测框进行读数,帮我修改代码,实现这个功能
需要在仪表类型为bj_type-1-*-1-0(即单指针表计)时,如果检测到多个指针,则选择距离圆心最远的指针(通过窄边中点计算距离)作为正确的指针进行读数。
修改思路:
-
在handle_multiple_pointers函数中,当仪表类型为单指针(bj_type-1-*-1-0)且检测到多个指针时,选择距离圆心
-
注意:这里需要判断仪表配置中的指针数量(pointer_num)是否为1,并且仪表类型是指针式仪表(bj_type=1)。
-
另外,由于是非线性表计也可能出现这种情况,所以我们不能仅仅依据线性表计来判断,而是根据配置中的指针数量。
具体步骤:
在handle_multiple_pointers函数中,首先判断指针数量配置(pointer_num)是否为1,且检测到的指针数量大于1。
如果是,则计算每个指针的窄边中点到圆心的距离,选择距离最大的那个指针。
注意:圆心坐标可以从key_points['o']中获取。
由于在handle_multiple_pointers函数中已经根据指针数量进行了处理,我们可以在处理多指针之前加入这个特殊逻辑。
修改代码位置:在handle_multiple_pointers函数中,先判断是否是指针数量为1但检测到多个指针的情况,然后进行过滤。
同时,我们需要确保在计算指针读数时使用的是正确的指针。
我们将在handle_multiple_pointers函数中添加以下逻辑:
如果配置的指针数量为1(pointer_num == 1)且检测到的指针数量大于1,则选择窄边中点距离圆心最远的指针。
注意:这里我们假设圆心是key_points['o'],即表盘中心。
修改后的handle_multiple_pointers函数部分代码如下:
但是注意:原函数已经根据指针数量进行了处理,我们可以在按置信度排序后,在处理多个指针之前,先进行过滤。
由于原函数已经考虑了360度双指针等特殊情况,所以我们添加的逻辑只针对pointer_num==1且检测到多个指针的情况。
另外,注意原函数中对于360度双指针的特殊处理,我们添加的逻辑不能影响其他情况。
具体修改:
在handle_multiple_pointers函数中,获取到排序后的指针索引后,先判断是否是指针数量配置为1且检测到的指针数大于1。
如果是,则从所有检测到的指针中选取距离圆心最远的一个指针(通过窄边中点计算距离),然后只使用这个指针进行读数计算。
注意:我们需要计算每个指针的窄边中点到圆心的距离。圆心坐标从key_points['o']中获取。
def handle_multiple_pointers(pointers: List, scores: List, center: Dict, start: Dict, end: Dict,min_val: float, max_val: float, direction: str,meter_config: Dict,key_points: Dict) -> Optional[float]:
函数参数分析
-
pointers: 检测到的指针列表,每个元素包含指针的位置、尺寸和角度信息
-
scores: 对应的置信度分数列表,用于评估检测质量
-
center/start/end: 表盘中心、起始刻度和结束刻度的信息
-
min_val/max_val: 仪表的最小和最大量程值
-
direction: 刻度方向(顺时针或逆时针)
-
meter_config: 完整的仪表配置信息
-
key_points: 关键点信息,包含表盘中心和刻度点数据