chore: 更新最新代码
1
.idea/vcs.xml
generated
@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/ultralytics" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
3
image/.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
8
image/.idea/image.iml
generated
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="jdk" jdkName="ailai" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
6
image/.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
||||
7
image/.idea/misc.xml
generated
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Black">
|
||||
<option name="sdkName" value="image" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="ailai" project-jdk-type="Python SDK" />
|
||||
</project>
|
||||
8
image/.idea/modules.xml
generated
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/image.iml" filepath="$PROJECT_DIR$/.idea/image.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
6
image/.idea/vcs.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
0
image/__init__.py
Normal file
90
image/del_photo/change.py
Normal file
@ -0,0 +1,90 @@
|
||||
import os
|
||||
from PIL import Image
|
||||
import numpy as np
|
||||
|
||||
def is_large_gray(image, gray_lower_threshold=70, gray_upper_threshold=230, gray_ratio_threshold=0.7):
|
||||
"""
|
||||
判断图片是否大面积为灰色(基于像素的颜色值)
|
||||
|
||||
参数:
|
||||
- image: 图片对象(PIL Image)
|
||||
- gray_lower_threshold: 灰色下限(低于此值不算“灰色”)
|
||||
- gray_upper_threshold: 灰色上限(高于此值不算“灰色”)
|
||||
- gray_ratio_threshold: 灰色像素占比阈值(>70% 算大面积灰色)
|
||||
|
||||
返回:True 表示是大面积灰色,应删除
|
||||
"""
|
||||
# 将图片转换为 numpy 数组
|
||||
img_array = np.array(image)
|
||||
|
||||
# 获取图片的尺寸
|
||||
height, width, _ = img_array.shape
|
||||
total_pixels = height * width
|
||||
|
||||
# 判断是否为灰色像素(R、G、B 值都在 gray_lower_threshold 和 gray_upper_threshold 之间)
|
||||
gray_pixels = np.sum(
|
||||
(img_array[:, :, 0] >= gray_lower_threshold) &
|
||||
(img_array[:, :, 0] <= gray_upper_threshold) &
|
||||
(img_array[:, :, 1] >= gray_lower_threshold) &
|
||||
(img_array[:, :, 1] <= gray_upper_threshold) &
|
||||
(img_array[:, :, 2] >= gray_lower_threshold) &
|
||||
(img_array[:, :, 2] <= gray_upper_threshold)
|
||||
)
|
||||
|
||||
gray_ratio = gray_pixels / total_pixels
|
||||
|
||||
return gray_ratio > gray_ratio_threshold
|
||||
|
||||
|
||||
def process_images_in_folder(input_folder, output_folder):
|
||||
"""
|
||||
遍历文件夹,旋转图片并根据条件保存到输出文件夹
|
||||
"""
|
||||
# 创建输出文件夹(如果不存在)
|
||||
if not os.path.exists(output_folder):
|
||||
os.makedirs(output_folder)
|
||||
|
||||
# 支持的图片格式
|
||||
supported_formats = ('.jpg', '.jpeg', '.png', '.bmp', '.tiff', '.tif', '.webp')
|
||||
|
||||
for filename in os.listdir(input_folder):
|
||||
file_path = os.path.join(input_folder, filename)
|
||||
|
||||
if not os.path.isfile(file_path):
|
||||
continue
|
||||
|
||||
if not filename.lower().endswith(supported_formats):
|
||||
continue
|
||||
|
||||
print(f"处理: {filename}")
|
||||
|
||||
try:
|
||||
with Image.open(file_path) as img:
|
||||
# 判断是否为大面积灰色
|
||||
if is_large_gray(img):
|
||||
print(f" 🔴 不保存大面积灰色图片: {filename}")
|
||||
continue # 不保存该图片
|
||||
|
||||
# 否则:打开并旋转 180 度
|
||||
rotated_img = img.rotate(180, expand=False)
|
||||
|
||||
# 构建新的保存路径
|
||||
save_path = os.path.join(output_folder, filename)
|
||||
|
||||
# 保持原格式保存(覆盖原图)
|
||||
rotated_img.save(save_path, format=img.format)
|
||||
print(f" ✅ 已旋转并保存至: {save_path}")
|
||||
except Exception as e:
|
||||
print(f" ❌ 处理失败 {filename}: {e}")
|
||||
|
||||
|
||||
# ================ 使用示例 ================
|
||||
if __name__ == "__main__":
|
||||
folder = "/media/hx/disk/folder_5"
|
||||
output_folder = "/media/hx/disk/folder_5"
|
||||
|
||||
if not os.path.exists(folder):
|
||||
print("❌ 输入文件夹不存在!")
|
||||
else:
|
||||
process_images_in_folder(folder, output_folder)
|
||||
print("✅ 所有图片处理完成!")
|
||||
118
image/del_photo/del_image_gray.py
Normal file
@ -0,0 +1,118 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
from PIL import Image
|
||||
import numpy as np
|
||||
|
||||
|
||||
def is_grayscale_image(image_path, saturation_threshold=0.05, gray_intensity_threshold=200):
|
||||
"""
|
||||
判断图像是否为“灰色图片”(低饱和度或接近灰度)
|
||||
|
||||
:param image_path: 图像路径
|
||||
:param saturation_threshold: 饱和度阈值(0~1),越低越可能是灰色
|
||||
:param gray_intensity_threshold: 亮度阈值,过滤纯白/纯黑
|
||||
:return: True 表示是灰色图,应删除
|
||||
"""
|
||||
try:
|
||||
img = Image.open(image_path)
|
||||
|
||||
# 转为 RGB(处理灰度图自动转为 3 通道)
|
||||
if img.mode != 'RGB':
|
||||
img = img.convert('RGB')
|
||||
|
||||
# 转为 numpy 数组
|
||||
rgb = np.array(img).astype(np.float32) # (H, W, 3)
|
||||
H, W, _ = rgb.shape
|
||||
|
||||
if H * W == 0:
|
||||
return True # 空图
|
||||
|
||||
# 转为 HSV(手动计算避免 PIL 的 hsv 转换问题)
|
||||
r, g, b = rgb[..., 0], rgb[..., 1], rgb[..., 2]
|
||||
|
||||
max_c = np.maximum(np.maximum(r, g), b)
|
||||
min_c = np.minimum(np.minimum(r, g), b)
|
||||
delta = max_c - min_c
|
||||
|
||||
# 饱和度 S = delta / max_c
|
||||
with np.errstate(divide='ignore', invalid='ignore'):
|
||||
s = np.where(max_c == 0, 0, delta / max_c)
|
||||
|
||||
# 只取非纯黑区域的饱和度(避免纯黑区域干扰)
|
||||
valid_s = s[(max_c > 10) & (max_c < gray_intensity_threshold)] # 忽略极暗和极亮
|
||||
|
||||
if len(valid_s) == 0:
|
||||
return True # 全黑或全白
|
||||
|
||||
# 计算平均饱和度
|
||||
avg_saturation = valid_s.mean()
|
||||
|
||||
# 如果平均饱和度很低,认为是灰色图
|
||||
return avg_saturation < saturation_threshold
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ 无法读取图像 {image_path}: {e}")
|
||||
return True # 出错的图也删除(可选)
|
||||
|
||||
|
||||
def delete_gray_images(folder_path, extensions=None, dry_run=False):
|
||||
"""
|
||||
删除文件夹中的灰色图片
|
||||
|
||||
:param folder_path: 图片文件夹路径
|
||||
:param extensions: 支持的图片格式
|
||||
:param dry_run: 如果为 True,只打印不删除
|
||||
"""
|
||||
if extensions is None:
|
||||
extensions = ['.jpg', '.jpeg', '.png', '.bmp', '.tiff']
|
||||
|
||||
folder = Path(folder_path)
|
||||
if not folder.exists():
|
||||
print(f"❌ 文件夹不存在: {folder_path}")
|
||||
return
|
||||
|
||||
image_files = []
|
||||
for ext in extensions:
|
||||
image_files.extend(folder.glob(f'*{ext}'))
|
||||
image_files.extend(folder.glob(f'*{ext.upper()}'))
|
||||
|
||||
if not image_files:
|
||||
print(f"🔍 文件夹中没有找到图片: {folder_path}")
|
||||
return
|
||||
|
||||
print(f"🔍 扫描到 {len(image_files)} 张图片...")
|
||||
deleted_count = 0
|
||||
|
||||
for img_path in image_files:
|
||||
if is_grayscale_image(img_path):
|
||||
print(f"🗑️ 灰色图: {img_path.name}")
|
||||
if not dry_run:
|
||||
try:
|
||||
img_path.unlink() # 删除文件
|
||||
print(f"✅ 已删除: {img_path.name}")
|
||||
deleted_count += 1
|
||||
except Exception as e:
|
||||
print(f"❌ 删除失败 {img_path.name}: {e}")
|
||||
else:
|
||||
print(f"✅ 彩色图: {img_path.name} (保留)")
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
if dry_run:
|
||||
print(f"🧪 模拟完成,共发现 {deleted_count} 张灰色图将被删除")
|
||||
else:
|
||||
print(f"✅ 删除完成!共删除 {deleted_count} 张灰色图片")
|
||||
print(f"📁 保留图片数: {len(image_files) - deleted_count}")
|
||||
print("=" * 50)
|
||||
|
||||
|
||||
# ================== 用户配置 ==================
|
||||
FOLDER_PATH = "/media/hx/disk/folder_5" # 修改为你的图片文件夹
|
||||
DRY_RUN = False # 先设为 True 测试,确认无误后再改为 False
|
||||
|
||||
# ================== 执行 ==================
|
||||
if __name__ == "__main__":
|
||||
print(f"🚀 开始检测并删除灰色图片...")
|
||||
delete_gray_images(
|
||||
folder_path=FOLDER_PATH,
|
||||
dry_run=DRY_RUN
|
||||
)
|
||||
103
image/del_photo/del_image_ssim.py
Normal file
@ -0,0 +1,103 @@
|
||||
import os
|
||||
import cv2
|
||||
from skimage.metrics import structural_similarity as ssim
|
||||
|
||||
def calculate_ssim(image_path1, image_path2):
|
||||
"""
|
||||
计算两张图片的 SSIM 相似度
|
||||
"""
|
||||
# 读取图像
|
||||
img1 = cv2.imread(image_path1)
|
||||
img2 = cv2.imread(image_path2)
|
||||
|
||||
if img1 is None:
|
||||
print(f"❌ 无法读取图片1: {image_path1}")
|
||||
return None
|
||||
if img2 is None:
|
||||
print(f"❌ 无法读取图片2: {image_path2}")
|
||||
return None
|
||||
|
||||
# 转为灰度图
|
||||
gray1 = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY)
|
||||
gray2 = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY)
|
||||
|
||||
# 确保尺寸一致
|
||||
if gray1.shape != gray2.shape:
|
||||
print("⚠️ 图像尺寸不一致,正在调整...")
|
||||
h, w = min(gray1.shape[0], gray2.shape[0]), min(gray1.shape[1], gray2.shape[1])
|
||||
gray1 = cv2.resize(gray1, (w, h))
|
||||
gray2 = cv2.resize(gray2, (w, h))
|
||||
|
||||
# 计算 SSIM
|
||||
try:
|
||||
similarity = ssim(gray1, gray2)
|
||||
return similarity
|
||||
except Exception as e:
|
||||
print(f"❌ SSIM 计算失败: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def delete_similar_consecutive_images(folder_path, threshold=0.95, extensions=None):
|
||||
"""
|
||||
删除相似度高于阈值的连续图片
|
||||
|
||||
:param folder_path: 图片所在的文件夹路径
|
||||
:param threshold: SSIM 阈值,默认为 0.95
|
||||
:param extensions: 支持的图片格式列表,默认为 ['.jpg', '.jpeg', '.png']
|
||||
"""
|
||||
if extensions is None:
|
||||
extensions = ['.jpg', '.jpeg', '.png']
|
||||
|
||||
folder = os.path.abspath(folder_path)
|
||||
if not os.path.exists(folder):
|
||||
print(f"❌ 文件夹不存在: {folder_path}")
|
||||
return
|
||||
|
||||
# 获取所有图片文件路径
|
||||
image_files = []
|
||||
for ext in extensions:
|
||||
image_files.extend([os.path.join(folder, f) for f in os.listdir(folder) if f.lower().endswith(ext)])
|
||||
|
||||
if not image_files:
|
||||
print(f"🔍 文件夹中没有找到图片: {folder_path}")
|
||||
return
|
||||
|
||||
# 按文件名排序以确保顺序正确
|
||||
image_files.sort()
|
||||
|
||||
print(f"🔍 扫描到 {len(image_files)} 张图片...")
|
||||
deleted_count = 0
|
||||
|
||||
# 遍历每一对连续的图片
|
||||
for i in range(len(image_files) - 1):
|
||||
img_path1 = image_files[i]
|
||||
img_path2 = image_files[i + 1]
|
||||
|
||||
similarity = calculate_ssim(img_path1, img_path2)
|
||||
if similarity is not None and similarity > threshold:
|
||||
print(f"🗑️ 删除相似图片: {img_path2} (SSIM: {similarity:.4f})")
|
||||
try:
|
||||
os.remove(img_path2)
|
||||
deleted_count += 1
|
||||
except Exception as e:
|
||||
print(f"❌ 删除失败 {img_path2}: {e}")
|
||||
else:
|
||||
print(f"✅ 保留图片: {img_path2}")
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
print(f"✅ 删除完成!共删除 {deleted_count} 张相似图片")
|
||||
print(f"📁 保留图片数: {len(image_files) - deleted_count}")
|
||||
print("=" * 50)
|
||||
|
||||
|
||||
# ================== 用户配置 ==================
|
||||
FOLDER_PATH = "/media/hx/disk/folder_5" # 修改为你的图片文件夹路径
|
||||
THRESHOLD = 0.90 # SSIM 阈值
|
||||
|
||||
# ================== 执行 ==================
|
||||
if __name__ == "__main__":
|
||||
print(f"🚀 开始检测并删除相似图片...")
|
||||
delete_similar_consecutive_images(
|
||||
folder_path=FOLDER_PATH,
|
||||
threshold=THRESHOLD
|
||||
)
|
||||
63
image/del_photo/ssim.py
Normal file
@ -0,0 +1,63 @@
|
||||
import cv2
|
||||
from skimage.metrics import structural_similarity as ssim
|
||||
import os
|
||||
|
||||
def calculate_ssim(image_path1, image_path2):
|
||||
"""
|
||||
计算两张图片的 SSIM 相似度
|
||||
"""
|
||||
# 读取图像
|
||||
img1 = cv2.imread(image_path1)
|
||||
img2 = cv2.imread(image_path2)
|
||||
|
||||
if img1 is None:
|
||||
print(f"❌ 无法读取图片1: {image_path1}")
|
||||
return None
|
||||
if img2 is None:
|
||||
print(f"❌ 无法读取图片2: {image_path2}")
|
||||
return None
|
||||
|
||||
# 转为灰度图
|
||||
gray1 = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY)
|
||||
gray2 = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY)
|
||||
|
||||
# 确保尺寸一致
|
||||
if gray1.shape != gray2.shape:
|
||||
print("⚠️ 图像尺寸不一致,正在调整...")
|
||||
h, w = min(gray1.shape[0], gray2.shape[0]), min(gray1.shape[1], gray2.shape[1])
|
||||
gray1 = cv2.resize(gray1, (w, h))
|
||||
gray2 = cv2.resize(gray2, (w, h))
|
||||
|
||||
# 计算 SSIM
|
||||
try:
|
||||
similarity = ssim(gray1, gray2)
|
||||
return similarity
|
||||
except Exception as e:
|
||||
print(f"❌ SSIM 计算失败: {e}")
|
||||
return None
|
||||
|
||||
|
||||
# ==================== 使用示例 ====================
|
||||
if __name__ == "__main__":
|
||||
# 替换成你本地的两张图片路径
|
||||
path1 = "/home/hx/桌面/image/image/frame_20250805_120334_585.jpg"
|
||||
path2 = "/home/hx/桌面/image/image/frame_20250805_120334_570.jpg"
|
||||
|
||||
# 检查文件是否存在
|
||||
if not os.path.exists(path1):
|
||||
print(f"文件不存在: {path1}")
|
||||
print("请修改 path1 为实际存在的图片路径")
|
||||
elif not os.path.exists(path2):
|
||||
print(f"文件不存在: {path2}")
|
||||
print("请修改 path2 为实际存在的图片路径")
|
||||
else:
|
||||
print("正在计算 SSIM...")
|
||||
sim = calculate_ssim(path1, path2)
|
||||
if sim is not None:
|
||||
print(f"✅ SSIM 相似度: {sim:.4f}")
|
||||
if sim > 0.9:
|
||||
print("🔴 太相似(>0.9),应跳过重复帧")
|
||||
elif sim > 0.7:
|
||||
print("🟡 较相似,内容变化不大")
|
||||
else:
|
||||
print("🟢 差异明显,建议保存")
|
||||
144
image/image.py
Normal file
@ -0,0 +1,144 @@
|
||||
import cv2
|
||||
import time
|
||||
import os
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
from skimage.metrics import structural_similarity as ssim
|
||||
|
||||
# ================== 配置参数 ==================
|
||||
url = "rtsp://admin:XJ123456@192.168.1.51:554/streaming/channels/101"
|
||||
save_interval = 15 # 每隔 N 帧处理一次(可调)
|
||||
SSIM_THRESHOLD = 0.9 # SSIM 相似度阈值,>0.9 认为太像
|
||||
output_dir = os.path.join("userdata", "image") # 固定路径:userdata/image
|
||||
|
||||
# 灰色判断参数
|
||||
GRAY_LOWER = 70
|
||||
GRAY_UPPER = 230
|
||||
GRAY_RATIO_THRESHOLD = 0.7
|
||||
|
||||
# 创建输出目录
|
||||
if not os.path.exists(output_dir):
|
||||
os.makedirs(output_dir)
|
||||
print(f"已创建目录: {output_dir}")
|
||||
|
||||
def is_large_gray(image, gray_lower=GRAY_LOWER, gray_upper=GRAY_UPPER, ratio_thresh=GRAY_RATIO_THRESHOLD):
|
||||
"""
|
||||
判断图片是否大面积为灰色(R/G/B 都在 [gray_lower, gray_upper] 区间)
|
||||
"""
|
||||
img_array = np.array(image)
|
||||
if len(img_array.shape) != 3 or img_array.shape[2] != 3:
|
||||
return True # 非三通道图视为无效/灰色
|
||||
|
||||
h, w, _ = img_array.shape
|
||||
total = h * w
|
||||
|
||||
gray_mask = (
|
||||
(img_array[:, :, 0] >= gray_lower) & (img_array[:, :, 0] <= gray_upper) &
|
||||
(img_array[:, :, 1] >= gray_lower) & (img_array[:, :, 1] <= gray_upper) &
|
||||
(img_array[:, :, 2] >= gray_lower) & (img_array[:, :, 2] <= gray_upper)
|
||||
)
|
||||
gray_pixels = np.sum(gray_mask)
|
||||
gray_ratio = gray_pixels / total
|
||||
|
||||
return gray_ratio > ratio_thresh
|
||||
|
||||
max_retry_seconds = 10 # 最大重试时间为10秒
|
||||
retry_interval_seconds = 1 # 每隔1秒尝试重新连接一次
|
||||
|
||||
while True: # 外层循环用于处理重新连接逻辑
|
||||
cap = cv2.VideoCapture(url)
|
||||
start_time = time.time() # 记录开始尝试连接的时间
|
||||
|
||||
while not cap.isOpened(): # 如果无法打开摄像头,则进入重试逻辑
|
||||
if time.time() - start_time >= max_retry_seconds:
|
||||
print(f"已尝试重新连接 {max_retry_seconds} 秒,但仍无法获取视频流。")
|
||||
exit()
|
||||
|
||||
print("无法打开摄像头,正在尝试重新连接...")
|
||||
time.sleep(retry_interval_seconds) # 等待一段时间后再次尝试
|
||||
cap = cv2.VideoCapture(url)
|
||||
|
||||
print("✅ 开始读取视频流...")
|
||||
|
||||
frame_count = 0
|
||||
last_gray = None # 用于 SSIM 去重
|
||||
|
||||
try:
|
||||
while True:
|
||||
ret, frame = cap.read()
|
||||
if not ret:
|
||||
print("读取帧失败,可能是流中断或摄像头断开")
|
||||
cap.release() # 释放资源以便重新连接
|
||||
break # 跳出内层循环尝试重新连接
|
||||
|
||||
frame_count += 1
|
||||
|
||||
# 仅在指定间隔处理保存逻辑
|
||||
if frame_count % save_interval != 0:
|
||||
cv2.imshow('Camera Stream (Live)', frame)
|
||||
if cv2.waitKey(1) == ord('q'):
|
||||
raise KeyboardInterrupt
|
||||
continue
|
||||
|
||||
print(f"处理帧 {frame_count}")
|
||||
|
||||
# 转为 PIL 图像(用于后续判断和旋转)
|
||||
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
||||
pil_image = Image.fromarray(rgb_frame)
|
||||
|
||||
# STEP 1: 判断是否为大面积灰色(优先级最高)
|
||||
if is_large_gray(pil_image):
|
||||
print(f"跳过:大面积灰色图像 (frame_{frame_count})")
|
||||
cv2.imshow('Camera Stream (Live)', frame)
|
||||
if cv2.waitKey(1) == ord('q'):
|
||||
raise KeyboardInterrupt
|
||||
continue
|
||||
|
||||
# STEP 2: 判断是否为重复帧(基于 SSIM)
|
||||
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
|
||||
if last_gray is not None:
|
||||
try:
|
||||
similarity = ssim(gray, last_gray)
|
||||
if similarity > SSIM_THRESHOLD:
|
||||
print(f"跳过:与上一帧太相似 (SSIM={similarity:.3f})")
|
||||
cv2.imshow('Camera Stream (Live)', frame)
|
||||
if cv2.waitKey(1) == ord('q'):
|
||||
raise KeyboardInterrupt
|
||||
continue
|
||||
except Exception as e:
|
||||
print(f"SSIM 计算异常: {e}")
|
||||
|
||||
# 更新 last_gray 用于下一帧比较
|
||||
last_gray = gray.copy()
|
||||
|
||||
# STEP 3: 旋转 180 度
|
||||
rotated_pil = pil_image.rotate(180, expand=False)
|
||||
|
||||
# 生成文件名(时间戳 + 毫秒防重),使用 .png 扩展名
|
||||
timestamp = time.strftime("%Y%m%d_%H%M%S")
|
||||
ms = int((time.time() % 1) * 1000)
|
||||
filename = f"frame_{timestamp}_{ms:03d}.png" # ✅ 确保是 .png
|
||||
filepath = os.path.join(output_dir, filename)
|
||||
|
||||
# 保存图像为 PNG 格式(无损)
|
||||
try:
|
||||
rotated_pil.save(filepath, format='PNG') # ✅ PNG 不需要 quality 参数
|
||||
print(f"已保存: {filepath}")
|
||||
except Exception as e:
|
||||
print(f"保存失败 {filename}: {e}")
|
||||
|
||||
# 显示画面
|
||||
cv2.imshow('Camera Stream (Live)', frame)
|
||||
if cv2.waitKey(1) == ord('q'):
|
||||
raise KeyboardInterrupt
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n用户中断")
|
||||
break # 跳出外层循环并退出程序
|
||||
|
||||
finally:
|
||||
cap.release()
|
||||
cv2.destroyAllWindows()
|
||||
print(f"视频流已关闭,共处理 {frame_count} 帧。")
|
||||
|
||||
print("程序结束")
|
||||
165
image/image_upload.py
Normal file
@ -0,0 +1,165 @@
|
||||
import cv2
|
||||
import time
|
||||
import os
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
from skimage.metrics import structural_similarity as ssim
|
||||
import threading
|
||||
import transport_client
|
||||
from transport_client import send_images # 导入上传函数
|
||||
# ================== 配置数 ==================
|
||||
url = "rtsp://admin:XJ123456@192.168.1.51:554/streaming/channels/101"
|
||||
save_interval = 15 # 每隔 N 帧处理一次(可调)
|
||||
SSIM_THRESHOLD = 0.9 # SSIM 相似度阈值,>0.9 认为太像
|
||||
output_dir = os.path.join("userdata", "image") # 固定路径:userdata/image
|
||||
# 灰色判断参数
|
||||
GRAY_LOWER = 70
|
||||
GRAY_UPPER = 230
|
||||
GRAY_RATIO_THRESHOLD = 0.7
|
||||
|
||||
# 上传配置(新增)
|
||||
UPLOAD_SERVER_URL = "http://www.xj-robot.com:6000/upload"
|
||||
UPLOAD_SITE_NAME = "FactoryA"
|
||||
UPLOAD_LINE_ID = "Line1"
|
||||
UPLOAD_PURPOSE = "DET"
|
||||
|
||||
# 创建输出目录
|
||||
if not os.path.exists(output_dir):
|
||||
os.makedirs(output_dir)
|
||||
print(f"已创建目录: {output_dir}")
|
||||
|
||||
def is_large_gray(image, gray_lower=GRAY_LOWER, gray_upper=GRAY_UPPER, ratio_thresh=GRAY_RATIO_THRESHOLD):
|
||||
"""
|
||||
判断图片是否大面积为灰色(R/G/B 都在 [gray_lower, gray_upper] 区间)
|
||||
"""
|
||||
img_array = np.array(image)
|
||||
if len(img_array.shape) != 3 or img_array.shape[2] != 3:
|
||||
return True # 非三通道图视为无效/灰色
|
||||
|
||||
h, w, _ = img_array.shape
|
||||
total = h * w
|
||||
|
||||
gray_mask = (
|
||||
(img_array[:, :, 0] >= gray_lower) & (img_array[:, :, 0] <= gray_upper) &
|
||||
(img_array[:, :, 1] >= gray_lower) & (img_array[:, :, 1] <= gray_upper) &
|
||||
(img_array[:, :, 2] >= gray_lower) & (img_array[:, :, 2] <= gray_upper)
|
||||
)
|
||||
gray_pixels = np.sum(gray_mask)
|
||||
gray_ratio = gray_pixels / total
|
||||
|
||||
return gray_ratio > ratio_thresh
|
||||
|
||||
max_retry_seconds = 10 # 最大重试时间为10秒
|
||||
retry_interval_seconds = 1 # 每隔1秒尝试重新连接一次
|
||||
|
||||
while True: # 外层循环用于处理重新连接逻辑
|
||||
cap = cv2.VideoCapture(url)
|
||||
start_time = time.time() # 记录开始尝试连接的时间
|
||||
|
||||
while not cap.isOpened(): # 如果无法打开摄像头,则进入重试逻辑
|
||||
if time.time() - start_time >= max_retry_seconds:
|
||||
print(f"已尝试重新连接 {max_retry_seconds} 秒,但仍无法获取视频流。")
|
||||
exit()
|
||||
|
||||
print("无法打开摄像头,正在尝试重新连接...")
|
||||
time.sleep(retry_interval_seconds) # 等待一段时间后再次尝试
|
||||
cap = cv2.VideoCapture(url)
|
||||
|
||||
print("✅ 开始读取视频流...")
|
||||
|
||||
frame_count = 0
|
||||
last_gray = None # 用于 SSIM 去重
|
||||
|
||||
try:
|
||||
while True:
|
||||
ret, frame = cap.read()
|
||||
if not ret:
|
||||
print("读取帧失败,可能是流中断或摄像头断开")
|
||||
cap.release() # 释放资源以便重新连接
|
||||
break # 跳出内层循环尝试重新连接
|
||||
|
||||
frame_count += 1
|
||||
|
||||
# 仅在指定间隔处理保存逻辑
|
||||
if frame_count % save_interval != 0:
|
||||
cv2.imshow('Camera Stream (Live)', frame)
|
||||
if cv2.waitKey(1) == ord('q'):
|
||||
raise KeyboardInterrupt
|
||||
continue
|
||||
|
||||
print(f"处理帧 {frame_count}")
|
||||
|
||||
# 转为 PIL 图像(用于后续判断和旋转)
|
||||
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
||||
pil_image = Image.fromarray(rgb_frame)
|
||||
|
||||
# STEP 1: 判断是否为大面积灰色(优先级最高)
|
||||
if is_large_gray(pil_image):
|
||||
print(f"跳过:大面积灰色图像 (frame_{frame_count})")
|
||||
cv2.imshow('Camera Stream (Live)', frame)
|
||||
if cv2.waitKey(1) == ord('q'):
|
||||
raise KeyboardInterrupt
|
||||
continue
|
||||
|
||||
# STEP 2: 判断是否为重复帧(基于 SSIM)
|
||||
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
|
||||
if last_gray is not None:
|
||||
try:
|
||||
similarity = ssim(gray, last_gray)
|
||||
if similarity > SSIM_THRESHOLD:
|
||||
print(f"跳过:与上一帧太相似 (SSIM={similarity:.3f})")
|
||||
cv2.imshow('Camera Stream (Live)', frame)
|
||||
if cv2.waitKey(1) == ord('q'):
|
||||
raise KeyboardInterrupt
|
||||
continue
|
||||
except Exception as e:
|
||||
print(f"SSIM 计算异常: {e}")
|
||||
|
||||
# 更新 last_gray 用于下一帧比较
|
||||
last_gray = gray.copy()
|
||||
|
||||
# STEP 3: 旋转 180 度
|
||||
rotated_pil = pil_image.rotate(180, expand=False)
|
||||
|
||||
# 生成文件名(时间戳 + 毫秒防重),使用 .png 扩展名
|
||||
timestamp = time.strftime("%Y%m%d_%H%M%S")
|
||||
ms = int((time.time() % 1) * 1000)
|
||||
filename = f"frame_{timestamp}_{ms:03d}.png" # ✅ 确保是 .png
|
||||
filepath = os.path.join(output_dir, filename)
|
||||
|
||||
# 保存图像为 PNG 格式(无损)
|
||||
try:
|
||||
rotated_pil.save(filepath, format='PNG') # ✅ PNG 不需要 quality 参数
|
||||
print(f"已保存: {filepath}")
|
||||
|
||||
# ✅ 新增:异步上传
|
||||
def upload_task():
|
||||
send_images(
|
||||
folder_path=output_dir,
|
||||
server_url=UPLOAD_SERVER_URL,
|
||||
site_name=UPLOAD_SITE_NAME,
|
||||
line_id=UPLOAD_LINE_ID,
|
||||
purpose=UPLOAD_PURPOSE
|
||||
)
|
||||
|
||||
threading.Thread(target=upload_task, daemon=True).start()
|
||||
print(f"📤 已提交上传任务: {filename}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"保存失败 {filename}: {e}")
|
||||
|
||||
# 显示画面
|
||||
cv2.imshow('Camera Stream (Live)', frame)
|
||||
if cv2.waitKey(1) == ord('q'):
|
||||
raise KeyboardInterrupt
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n用户中断")
|
||||
break # 跳出外层循环并退出程序
|
||||
|
||||
finally:
|
||||
cap.release()
|
||||
cv2.destroyAllWindows()
|
||||
print(f"视频流已关闭,共处理 {frame_count} 帧。")
|
||||
|
||||
print("程序结束")
|
||||
194
image/transport_client.py
Normal file
@ -0,0 +1,194 @@
|
||||
import os
|
||||
import time
|
||||
import hashlib
|
||||
import requests
|
||||
import json
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Tuple, Optional
|
||||
|
||||
STATE_FILE = ".transfer_state.json"
|
||||
MAX_CHUNK_SIZE = 1024 * 1024 # 1MB chunks
|
||||
VALID_IMAGE_EXTENSIONS = {'.jpg', '.jpeg', '.png', '.bmp', '.gif'}
|
||||
MAX_RETRIES = 5
|
||||
BACKOFF_BASE = 2 # Exponential backoff base
|
||||
|
||||
def send_images(
|
||||
folder_path: str,
|
||||
server_url: str,
|
||||
site_name: str,
|
||||
line_id: str,
|
||||
purpose: str,
|
||||
max_retries: int = MAX_RETRIES
|
||||
) -> None:
|
||||
"""
|
||||
发送图像文件到服务器,支持断点续传和重试机制
|
||||
|
||||
Args:
|
||||
folder_path: 本地图像文件夹路径
|
||||
server_url: 服务器URL (e.g., "http://localhost:5000/upload")
|
||||
site_name: 现场名称
|
||||
line_id: 现场线号
|
||||
purpose: 图像用途 (DET/SEG等)
|
||||
max_retries: 最大重试次数
|
||||
"""
|
||||
folder = Path(folder_path)
|
||||
if not folder.is_dir():
|
||||
raise ValueError(f"Invalid folder path: {folder_path}")
|
||||
|
||||
# 初始化状态文件
|
||||
state_path = folder / STATE_FILE
|
||||
transfer_state = _load_transfer_state(state_path)
|
||||
|
||||
# 获取待发送文件列表 (过滤状态文件和非图像文件)
|
||||
files_to_send = [
|
||||
f for f in folder.iterdir()
|
||||
if f.is_file()
|
||||
and f.name != STATE_FILE
|
||||
and f.suffix.lower() in VALID_IMAGE_EXTENSIONS
|
||||
]
|
||||
|
||||
for file_path in files_to_send:
|
||||
file_id = _get_file_id(file_path)
|
||||
file_size = file_path.stat().st_size
|
||||
|
||||
# 初始化文件传输状态
|
||||
if file_id not in transfer_state:
|
||||
transfer_state[file_id] = {
|
||||
"file_path": str(file_path),
|
||||
"total_size": file_size,
|
||||
"sent": 0,
|
||||
"retry_count": 0
|
||||
}
|
||||
|
||||
file_state = transfer_state[file_id]
|
||||
retry_count = 0
|
||||
last_error = None
|
||||
|
||||
while file_state["sent"] < file_size and retry_count < max_retries:
|
||||
try:
|
||||
# 发送当前块
|
||||
chunk_start = file_state["sent"]
|
||||
chunk_end = min(chunk_start + MAX_CHUNK_SIZE, file_size)
|
||||
chunk_size = chunk_end - chunk_start
|
||||
|
||||
with open(file_path, 'rb') as f:
|
||||
f.seek(chunk_start)
|
||||
chunk_data = f.read(chunk_size)
|
||||
|
||||
# 准备请求
|
||||
params = {
|
||||
"site_name": site_name,
|
||||
"line_id": line_id,
|
||||
"purpose": purpose,
|
||||
"file_id": file_id,
|
||||
"start_byte": chunk_start,
|
||||
"total_size": file_size if chunk_start == 0 else None
|
||||
}
|
||||
|
||||
files = {"chunk": (file_path.name, chunk_data, "application/octet-stream")}
|
||||
|
||||
# 发送请求
|
||||
response = requests.post(
|
||||
server_url,
|
||||
params=params,
|
||||
files=files,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
# 处理响应
|
||||
if response.status_code == 200:
|
||||
# 更新已发送字节数
|
||||
file_state["sent"] = chunk_end
|
||||
transfer_state[file_id] = file_state
|
||||
_save_transfer_state(state_path, transfer_state)
|
||||
|
||||
# 传输完成
|
||||
if file_state["sent"] >= file_size:
|
||||
_cleanup_after_success(file_path, state_path, transfer_state, file_id)
|
||||
retry_count = 0 # 重置重试计数器
|
||||
else:
|
||||
raise Exception(f"Server error: {response.status_code}, {response.text}")
|
||||
|
||||
except Exception as e:
|
||||
last_error = str(e)
|
||||
retry_count += 1
|
||||
file_state["retry_count"] = retry_count
|
||||
transfer_state[file_id] = file_state
|
||||
_save_transfer_state(state_path, transfer_state)
|
||||
|
||||
# 指数退避重试
|
||||
wait_time = BACKOFF_BASE ** retry_count
|
||||
print(f"Retry {retry_count}/{max_retries} for {file_path.name} in {wait_time}s: {last_error}")
|
||||
time.sleep(wait_time)
|
||||
|
||||
# 处理传输失败
|
||||
if file_state["sent"] < file_size:
|
||||
print(f"Failed to send {file_path.name} after {max_retries} attempts")
|
||||
# 验证服务器连接
|
||||
if _check_server_health(server_url):
|
||||
print("Server is reachable - skipping this file")
|
||||
else:
|
||||
print("Server unreachable - will retry later")
|
||||
# 重置重试计数器以便下次尝试
|
||||
file_state["retry_count"] = 0
|
||||
transfer_state[file_id] = file_state
|
||||
_save_transfer_state(state_path, transfer_state)
|
||||
|
||||
def _load_transfer_state(state_path: Path) -> Dict:
|
||||
"""加载传输状态"""
|
||||
if state_path.exists():
|
||||
try:
|
||||
with open(state_path, 'r') as f:
|
||||
return json.load(f)
|
||||
except:
|
||||
return {}
|
||||
return {}
|
||||
|
||||
def _save_transfer_state(state_path: Path, state: Dict) -> None:
|
||||
"""原子化保存传输状态"""
|
||||
temp_path = state_path.with_suffix('.tmp')
|
||||
with open(temp_path, 'w') as f:
|
||||
json.dump(state, f, indent=2)
|
||||
shutil.move(str(temp_path), str(state_path))
|
||||
|
||||
def _get_file_id(file_path: Path) -> str:
|
||||
"""生成文件唯一ID (路径+修改时间+大小)"""
|
||||
stat = file_path.stat()
|
||||
unique_str = f"{file_path.resolve()}|{stat.st_mtime}|{stat.st_size}"
|
||||
return hashlib.sha256(unique_str.encode()).hexdigest()[:16]
|
||||
|
||||
def _cleanup_after_success(
|
||||
file_path: Path,
|
||||
state_path: Path,
|
||||
transfer_state: Dict,
|
||||
file_id: str
|
||||
) -> None:
|
||||
"""传输成功后清理"""
|
||||
# 删除本地文件
|
||||
file_path.unlink()
|
||||
# 清除状态记录
|
||||
if file_id in transfer_state:
|
||||
del transfer_state[file_id]
|
||||
_save_transfer_state(state_path, transfer_state)
|
||||
print(f"Successfully sent and deleted {file_path.name}")
|
||||
|
||||
def _check_server_health(server_url: str) -> bool:
|
||||
"""检查服务器健康状态"""
|
||||
try:
|
||||
# 提取基础URL (移除/upload部分)
|
||||
base_url = server_url.rsplit('/', 1)[0]
|
||||
response = requests.get(f"{base_url}/health", timeout=5)
|
||||
return response.status_code == 200
|
||||
except:
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 示例用法
|
||||
send_images(
|
||||
folder_path=r"C:\Users\chuyi\Pictures\test",
|
||||
server_url="http://www.xj-robot.com:6000/upload",
|
||||
site_name="FactoryA",
|
||||
line_id="Line1",
|
||||
purpose="DET"
|
||||
)
|
||||
|
Before Width: | Height: | Size: 1012 KiB |
|
Before Width: | Height: | Size: 863 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 1009 KiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 869 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 23 KiB |
@ -61,4 +61,3 @@ for i, cnt in enumerate(contours):
|
||||
cv2.imwrite(result_image_path, vis_image)
|
||||
print(f"✅ 旋转矩形可视化已保存至: {result_image_path}")
|
||||
|
||||
# 如果你想把结果保存为 JSON 或 CSV,也可以扩展
|
||||
|
Before Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 1008 KiB |
|
Before Width: | Height: | Size: 982 KiB |
|
Before Width: | Height: | Size: 989 KiB |
|
Before Width: | Height: | Size: 1018 KiB |
|
Before Width: | Height: | Size: 1009 KiB |
|
Before Width: | Height: | Size: 934 KiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |