import cv2
import numpy as np
import os
import tkinter as tk
from tkinter import filedialog, ttk, messagebox
from PIL import Image, ImageTk
import re
from collections import deque
class FastImageBrowser:
def __init__(self, root):
self.root = root
self.root.title("去噪图")
self.root.geometry("1400x700")
# 初始化变量
self.image_folder = ""
self.image_files = []
self.current_index = 0
self.current_image = None
self.processed_image = None
self.is_playing = False
self.play_delay = 100 # 默认轮播速度
self.play_timer = None
self.scale_factor = 0.1
self.min_clip = tk.DoubleVar(value=0.0)
self.max_clip = tk.DoubleVar(value=1.0)
self.buffer_size = 3
self.frame_buffer = deque(maxlen=self.buffer_size) # 多帧缓冲
# 统一尺寸(默认为None,首次加载时确定)
self.fixed_size = None
# 参数
self.nlm_h = tk.IntVar(value=10)
self.min_clip = tk.DoubleVar(value=0.0)
self.max_clip = tk.DoubleVar(value=0.99)
self.gamma_value = tk.DoubleVar(value=1.0) # 固定gamma值,不自动调节
self.setup_ui()
def setup_ui(self):
controls_frame = tk.Frame(self.root, padx=10, pady=5)
controls_frame.pack(fill=tk.X)
tk.Button(controls_frame, text="选择文件夹", command=self.select_folder).grid(row=0, column=0, padx=5)
tk.Label(controls_frame, text="选择图片:").grid(row=0, column=1, padx=5)
self.image_combobox = ttk.Combobox(controls_frame, state="readonly", width=30)
self.image_combobox.grid(row=0, column=2, padx=5)
self.image_combobox.bind("<<ComboboxSelected>>", self.on_image_selected)
# 播放控制按钮
button_frame = tk.Frame(controls_frame)
button_frame.grid(row=0, column=3, padx=5)
self.play_button = tk.Button(button_frame, text="开始轮播", command=self.toggle_play)
self.play_button.pack(side=tk.LEFT, padx=2)
tk.Button(button_frame, text="上一张", command=self.prev_image).pack(side=tk.LEFT, padx=2)
tk.Button(button_frame, text="下一张", command=self.next_image).pack(side=tk.LEFT, padx=2)
# 轮播速度选择
tk.Label(controls_frame, text="轮播速度(ms):").grid(row=0, column=4, padx=5)
self.speed_var = tk.IntVar(value=1000)
speed_combobox = ttk.Combobox(controls_frame, textvariable=self.speed_var, width=8)
speed_combobox.grid(row=0, column=5, padx=5)
speed_combobox['values'] = (50,100,200,500,1000)
speed_combobox.state(["readonly"])
speed_combobox.bind("<<ComboboxSelected>>", self.update_play_speed)
# 图像缩放控制
tk.Label(controls_frame, text="图像缩放(%):").grid(row=0, column=6, padx=5)
self.scale_var = tk.DoubleVar(value=100)
scale_combo = ttk.Combobox(controls_frame, textvariable=self.scale_var, width=8)
scale_combo.grid(row=0, column=7, padx=5)
scale_combo['values'] = ("1", "5", "10", "20", "40", "60","80","100")
scale_combo.state(["readonly"])
scale_combo.bind("<<ComboboxSelected>>", self.update_scale_factor)
# 去噪参数控制
tk.Label(controls_frame, text="帧间去噪强度:").grid(row=0, column=8, padx=5)
tk.Scale(controls_frame, from_=0, to=30, orient=tk.HORIZONTAL, variable=self.nlm_h, length=150,
command=lambda e: self.process_image()).grid(row=0, column=9, padx=5)
# 对比度拉伸阈值控制
threshold_frame = tk.Frame(self.root, padx=10, pady=5)
threshold_frame.pack(fill=tk.X)
# 最小值阈值滑块 (0%-50%)
tk.Label(threshold_frame, text="最小值阈值(0-0.5):").pack(side=tk.LEFT, padx=5)
min_scale = tk.Scale(threshold_frame, from_=0, to=0.5, resolution=0.01,
orient=tk.HORIZONTAL, variable=self.min_clip,
length=200, command=lambda e: self.process_image())
min_scale.pack(side=tk.LEFT, padx=5)
# 最大值阈值滑块 (50%-100%)
tk.Label(threshold_frame, text="最大值阈值(0.5-1):").pack(side=tk.LEFT, padx=5)
max_scale = tk.Scale(threshold_frame, from_=0.5, to=1.0, resolution=0.01,
orient=tk.HORIZONTAL, variable=self.max_clip,
length=200, command=lambda e: self.process_image())
max_scale.pack(side=tk.LEFT, padx=5)
# Gamma控制
gamma_frame = tk.Frame(self.root, padx=10, pady=5)
gamma_frame.pack(fill=tk.X)
tk.Label(gamma_frame, text="Gamma校正:").pack(side=tk.LEFT, padx=5)
tk.Scale(gamma_frame, from_=0.1, to=3.0, resolution=0.1, orient=tk.HORIZONTAL,
variable=self.gamma_value, length=300,
command=lambda e: self.process_image()).pack(side=tk.LEFT)
# 图像显示区域
images_frame = tk.Frame(self.root, padx=10, pady=5)
images_frame.pack(fill=tk.BOTH, expand=True)
for title, attr in [("原始图像", "canvas_original"),
("处理结果", "canvas_processed")]:
frame = tk.Frame(images_frame)
frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
tk.Label(frame, text=title).pack()
canvas = tk.Canvas(frame, bg="lightgray", width=600, height=600)
canvas.pack(fill=tk.BOTH, expand=True)
setattr(self, attr, canvas)
def update_play_speed(self, event=None):
"""更新轮播速度"""
self.play_delay = self.speed_var.get()
if self.is_playing:
self.stop_play()
self.toggle_play()
def update_scale_factor(self, event=None):
new_scale = float(self.scale_var.get()) / 100
if abs(self.scale_factor - new_scale) < 1e-5:
return
self.scale_factor = new_scale
if self.image_files:
filename = self.image_files[self.current_index]
self.load_and_buffer_image(filename)
# 重新处理和显示当前图像
self.process_image()
self.display_image(self.current_image, self.canvas_original)
def select_folder(self):
folder = filedialog.askdirectory(title="选择图片文件夹")
if not folder:
return
self.stop_play()
self.image_folder = folder
exts = ('.tif', '.jpg', '.jpeg', '.png', '.bmp')
self.image_files = sorted([f for f in os.listdir(folder) if f.lower().endswith(exts)])
if not self.image_files:
messagebox.showwarning("警告", "文件夹中没有支持格式的图片")
return
# 按数字自然排序(有数字的文件名)
def natural_key(filename):
match = re.search(r'(\d+)', filename)
if match:
return int(match.group(1))
return filename
self.image_files.sort(key=natural_key)
self.image_combobox['values'] = self.image_files
self.image_combobox.current(0)
self.current_index = 0
self.frame_buffer.clear()
self.fixed_size = None # 重置尺寸
self.load_and_buffer_image(self.image_files[0])
self.display_image(self.current_image, self.canvas_original)
def on_image_selected(self, event):
selected = self.image_combobox.get()
if selected:
self.current_index = self.image_files.index(selected)
self.load_and_buffer_image(selected)
self.display_image(self.current_image, self.canvas_original)
self.process_image()
def load_and_buffer_image(self, filename):
path = os.path.join(self.image_folder, filename)
try:
img_data = np.fromfile(path, dtype=np.uint8)
img = cv2.imdecode(img_data, cv2.IMREAD_UNCHANGED)
if img is None:
raise ValueError("图像解码失败")
except Exception as e:
messagebox.showerror("错误", f"无法加载图像 {filename}:\n{e}")
return
# 应用缩放
if self.scale_factor != 1.0:
h, w = img.shape[:2]
new_w = max(10, int(w * self.scale_factor))
new_h = max(10, int(h * self.scale_factor))
img = cv2.resize(img, (new_w, new_h), interpolation=cv2.INTER_AREA)
# 转灰度并归一化成uint8
gray = self.to_grayscale(img)
if gray.dtype != np.uint8:
gray = cv2.normalize(gray, None, 0, 255, cv2.NORM_MINMAX)
gray = gray.astype(np.uint8)
# 首张图确定统一尺寸
if self.fixed_size is None:
self.fixed_size = gray.shape[::-1]
# 统一尺寸,resize成fixed_size
if (gray.shape[1], gray.shape[0]) != self.fixed_size:
gray = cv2.resize(gray, self.fixed_size, interpolation=cv2.INTER_AREA)
self.current_image = gray
# 更新缓冲
self.frame_buffer.append(gray)
# 缓冲不够补齐
while len(self.frame_buffer) < self.buffer_size:
self.frame_buffer.append(gray)
self.process_image()
def to_grayscale(self, img):
if len(img.shape) == 3:
return cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
return img
def gamma_correction(self, image, gamma=1.0):
if gamma <= 0:
gamma = 1.0
inv_gamma = 1.0 / gamma
table = np.array([((i / 255.0) ** inv_gamma) * 255 for i in range(256)]).astype(np.uint8)
if image.dtype != np.uint8:
image = cv2.normalize(image, None, 0, 255, cv2.NORM_MINMAX)
image = image.astype(np.uint8)
return cv2.LUT(image, table)
def contrast_stretch(self, image, min_clip, max_clip):
max_range = 255
min_val = np.percentile(image, min_clip * 100)
max_val = np.percentile(image, max_clip * 100)
result = np.zeros_like(image, dtype=float)
mask_low = image < min_val
mask_high = image > max_val
mask_mid = (image >= min_val) & (image <= max_val)
result[mask_low] = 0
result[mask_high] = max_range
if max_val > min_val:
normalized = (image[mask_mid] - min_val) / (max_val - min_val)
result[mask_mid] = normalized * max_range
else:
result[mask_mid] = max_range / 2
return result.clip(0, 255).astype(np.uint8)
def process_image(self):
if len(self.frame_buffer) < self.buffer_size:
return
frames = list(self.frame_buffer)
frames_uint8 = []
for f in frames:
if f.dtype != np.uint8:
nf = cv2.normalize(f, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)
frames_uint8.append(nf)
else:
frames_uint8.append(f)
# 多帧去噪
denoised = cv2.fastNlMeansDenoisingMulti(
frames_uint8,
imgToDenoiseIndex=self.buffer_size // 2,
temporalWindowSize=self.buffer_size,
h=self.nlm_h.get(),
templateWindowSize=3,
searchWindowSize=11
)
# Gamma校正
gamma_corrected = self.gamma_correction(denoised, self.gamma_value.get())
# 亮度裁剪 + 对比度拉伸
stretched = self.contrast_stretch(
gamma_corrected,
self.min_clip.get(),
self.max_clip.get()
)
# 中值滤波,去噪平滑
filtered = cv2.medianBlur(stretched, 3)
# CLAHE局部对比度增强
clahe = cv2.createCLAHE(clipLimit=4.0, tileGridSize=(4, 4))
enhanced = clahe.apply(filtered)
# Otsu二值化,用增强后的图像
_, binary = cv2.threshold(enhanced, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
# 开运算去噪
kernel_open = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (7, 7))
opened = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel_open)
# 闭运算补洞
kernel_close = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))
closed = cv2.morphologyEx(opened, cv2.MORPH_CLOSE, kernel_close)
# 找最大轮廓
contours, _ = cv2.findContours(closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if not contours:
messagebox.showwarning("警告", "未检测到任何轮廓")
return
largest_contour = max(contours, key=cv2.contourArea)
# 生成掩膜
mask = np.zeros_like(stretched, dtype=np.uint8)
cv2.drawContours(mask, [largest_contour], -1, 255, thickness=-1)
# 连通域面积筛选
contours_mask, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
mask_filtered = np.zeros_like(mask)
area_threshold = 5000 # 可根据目标大小调整
for cnt in contours_mask:
if cv2.contourArea(cnt) > area_threshold:
cv2.drawContours(mask_filtered, [cnt], -1, 255, thickness=-1)
mask = mask_filtered
kernel_dilate = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
mask = cv2.dilate(mask, kernel_dilate, iterations=1)
# 合成结果,掩膜区域用对比度拉伸后的图像,掩膜外设为0
result = denoised.copy()
result[mask == 255] = stretched[mask == 255]
result[mask == 0] = 0
# 彩色显示,绘制白色轮廓线
result_color = cv2.cvtColor(result, cv2.COLOR_GRAY2BGR)
cv2.drawContours(result_color, [largest_contour], -1, (255, 255, 255), 2)
self.processed_image = result_color
self.display_image(self.processed_image, self.canvas_processed)
def display_image(self, image, canvas):
img = image
# 16位转换8位
if img.dtype == np.uint16:
img = (img / 256).astype(np.uint8)
if len(img.shape) == 2:
display_img = cv2.cvtColor(img, cv2.COLOR_GRAY2RGB)
elif len(img.shape) == 3 and img.shape[2] == 3:
display_img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
else:
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
display_img = cv2.cvtColor(gray, cv2.COLOR_GRAY2RGB)
img_pil = Image.fromarray(display_img)
canvas_width = canvas.winfo_width() or 600
canvas_height = canvas.winfo_height() or 600
img_width, img_height = img_pil.size
ratio = min(canvas_width / img_width, canvas_height / img_height)
if ratio < 1:
new_size = (int(img_width * ratio), int(img_height * ratio))
img_pil = img_pil.resize(new_size, Image.LANCZOS)
img_tk = ImageTk.PhotoImage(img_pil)
canvas.delete("all")
canvas.create_image(canvas_width // 2, canvas_height // 2, anchor=tk.CENTER, image=img_tk)
canvas.image = img_tk
def toggle_play(self):
if not self.image_files:
messagebox.showwarning("警告", "请先选择包含图片的文件夹")
return
if self.is_playing:
self.stop_play()
self.play_button.config(text="开始轮播")
else:
self.play_delay = self.speed_var.get()
self.is_playing = True
self.play_button.config(text="停止轮播")
self.play_next()
def stop_play(self):
self.is_playing = False
if self.play_timer:
self.root.after_cancel(self.play_timer)
self.play_timer = None
def play_next(self):
if not self.is_playing or not self.image_files:
return
self.current_index = (self.current_index + 1) % len(self.image_files)
filename = self.image_files[self.current_index]
self.image_combobox.set(filename)
self.load_and_buffer_image(filename)
self.display_image(self.current_image, self.canvas_original)
self.process_image()
self.play_timer = self.root.after(self.play_delay, self.play_next)
def next_image(self):
if not self.image_files:
return
self.current_index = (self.current_index + 1) % len(self.image_files)
filename = self.image_files[self.current_index]
self.image_combobox.set(filename)
self.load_and_buffer_image(filename)
self.display_image(self.current_image, self.canvas_original)
self.process_image()
def prev_image(self):
if not self.image_files:
return
self.current_index = (self.current_index - 1) % len(self.image_files)
filename = self.image_files[self.current_index]
self.image_combobox.set(filename)
self.load_and_buffer_image(filename)
self.display_image(self.current_image, self.canvas_original)
self.process_image()
if __name__ == "__main__":
root = tk.Tk()
app = FastImageBrowser(root)
root.mainloop()
怎么改