370 lines
15 KiB
Python
370 lines
15 KiB
Python
import sys
|
|
import os
|
|
import datetime
|
|
import subprocess
|
|
import re
|
|
import math
|
|
import time
|
|
from PyQt6.QtWidgets import (
|
|
QApplication, QWidget, QVBoxLayout, QHBoxLayout,
|
|
QPushButton, QLabel, QLineEdit, QFileDialog,
|
|
QProgressBar, QTextEdit, QMessageBox, QCheckBox
|
|
)
|
|
from PyQt6.QtCore import Qt, QTimer
|
|
from PIL import Image
|
|
|
|
class ImageSlicer(QWidget):
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.is_slicing = False
|
|
self.progress_target = 0
|
|
self.progress_timer = QTimer(self)
|
|
self.progress_timer.timeout.connect(self.update_progress_bar)
|
|
self.initUI()
|
|
|
|
def initUI(self):
|
|
self.setWindowTitle('Image Slicer')
|
|
|
|
# Layouts
|
|
main_layout = QVBoxLayout()
|
|
form_layout = QVBoxLayout()
|
|
button_layout = QHBoxLayout()
|
|
|
|
# Folder selection
|
|
folder_layout = QHBoxLayout()
|
|
self.folder_label = QLabel('Image Folder:')
|
|
self.folder_path = QLineEdit()
|
|
self.folder_button = QPushButton('Browse')
|
|
self.folder_button.clicked.connect(self.browse_folder)
|
|
folder_layout.addWidget(self.folder_label)
|
|
folder_layout.addWidget(self.folder_path)
|
|
folder_layout.addWidget(self.folder_button)
|
|
form_layout.addLayout(folder_layout)
|
|
|
|
# Dimensions
|
|
dimensions_layout = QHBoxLayout()
|
|
self.width_label = QLabel('Width (cm):')
|
|
self.width_input = QLineEdit('0')
|
|
self.height_label = QLabel('Height (cm):')
|
|
self.height_input = QLineEdit()
|
|
dimensions_layout.addWidget(self.width_label)
|
|
dimensions_layout.addWidget(self.width_input)
|
|
dimensions_layout.addWidget(self.height_label)
|
|
dimensions_layout.addWidget(self.height_input)
|
|
form_layout.addLayout(dimensions_layout)
|
|
|
|
# Checkboxes
|
|
checkbox_layout = QHBoxLayout()
|
|
self.save_combined_checkbox = QCheckBox("Save Combined Image")
|
|
self.save_combined_checkbox.setChecked(False)
|
|
self.skip_merge_checkbox = QCheckBox("Skip Image Merging")
|
|
checkbox_layout.addWidget(self.save_combined_checkbox)
|
|
checkbox_layout.addWidget(self.skip_merge_checkbox)
|
|
form_layout.addLayout(checkbox_layout)
|
|
|
|
# Progress bar
|
|
self.progress_bar = QProgressBar()
|
|
self.progress_bar.setStyleSheet("""
|
|
QProgressBar::chunk {
|
|
background-color: orange;
|
|
}
|
|
""")
|
|
form_layout.addWidget(self.progress_bar)
|
|
|
|
# Log screen
|
|
self.log_screen = QTextEdit()
|
|
self.log_screen.setReadOnly(True)
|
|
form_layout.addWidget(self.log_screen)
|
|
|
|
# Action buttons
|
|
self.start_button = QPushButton('Start')
|
|
self.start_button.clicked.connect(self.toggle_slicing)
|
|
self.save_log_button = QPushButton('Save Log')
|
|
self.save_log_button.clicked.connect(self.save_log)
|
|
button_layout.addWidget(self.start_button)
|
|
button_layout.addWidget(self.save_log_button)
|
|
|
|
main_layout.addLayout(form_layout)
|
|
main_layout.addLayout(button_layout)
|
|
self.setLayout(main_layout)
|
|
|
|
def browse_folder(self):
|
|
if self.is_slicing: return
|
|
folder = QFileDialog.getExistingDirectory(self, 'Select Folder')
|
|
if folder:
|
|
self.folder_path.setText(folder)
|
|
self.log(f"Selected folder: {folder}")
|
|
|
|
def log(self, message):
|
|
self.log_screen.append(message)
|
|
QApplication.processEvents()
|
|
|
|
def cm_to_pixels(self, cm):
|
|
return int(cm * 96 / 2.54)
|
|
|
|
def open_folder(self, path):
|
|
if sys.platform == "win32":
|
|
os.startfile(path)
|
|
elif sys.platform == "darwin":
|
|
subprocess.Popen(["open", path])
|
|
else:
|
|
subprocess.Popen(["xdg-open", path])
|
|
|
|
def toggle_slicing(self, checked=False):
|
|
if self.is_slicing:
|
|
self.stop_slicing()
|
|
else:
|
|
self.start_slicing()
|
|
|
|
def stop_slicing(self):
|
|
self.is_slicing = False
|
|
self.log("Stopping process...")
|
|
|
|
def update_progress_bar(self):
|
|
current_value = self.progress_bar.value()
|
|
if current_value < self.progress_target:
|
|
self.progress_bar.setValue(current_value + 1)
|
|
elif current_value > self.progress_target:
|
|
self.progress_bar.setValue(self.progress_target)
|
|
|
|
def save_folder_paths_to_info_txt(self, folder_path):
|
|
self.log("Finding all subdirectories with images...")
|
|
self.progress_target = 20
|
|
folders_with_images = []
|
|
for root, dirs, files in os.walk(folder_path):
|
|
QApplication.processEvents() # Keep UI responsive
|
|
# Exclude _combined and _log folders
|
|
if os.path.basename(root).endswith(('_combined', '_log')):
|
|
continue
|
|
if any(f.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp', '.gif')) for f in files):
|
|
folders_with_images.append(root)
|
|
|
|
if not folders_with_images:
|
|
self.log("No folders with images found.")
|
|
self.progress_target = 100
|
|
return
|
|
|
|
self.log(f"Found {len(folders_with_images)} folders. Saving to info.txt...")
|
|
self.progress_target = 60
|
|
|
|
info_file_path = os.path.join(folder_path, "info.txt")
|
|
try:
|
|
with open(info_file_path, 'w', encoding='utf-8') as f:
|
|
for folder in sorted(folders_with_images):
|
|
f.write(f"{folder}\n")
|
|
self.log(f"Successfully saved paths to {info_file_path}")
|
|
except Exception as e:
|
|
self.log(f"Error saving info.txt: {e}")
|
|
self.progress_target = 100
|
|
|
|
def merge_and_slice_images(self, folder_path, slice_width_cm, slice_height_cm, save_combined):
|
|
self.log("Finding folders to process...")
|
|
self.progress_target = 5
|
|
QApplication.processEvents()
|
|
|
|
folders_to_process = []
|
|
processed_result_folders = [] # New list to store result folder paths
|
|
if any(f.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp', '.gif')) for f in os.listdir(folder_path)):
|
|
folders_to_process.append(folder_path)
|
|
for item in os.listdir(folder_path):
|
|
item_path = os.path.join(folder_path, item)
|
|
if os.path.isdir(item_path):
|
|
if any(f.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp', '.gif')) for f in os.listdir(item_path)):
|
|
folders_to_process.append(item_path)
|
|
|
|
self.progress_target = 10
|
|
if not folders_to_process:
|
|
self.log("No folders with images found to process.")
|
|
self.progress_target = 100
|
|
return
|
|
|
|
total_folders = len(folders_to_process)
|
|
progress_per_folder = 90 / total_folders
|
|
|
|
try:
|
|
for i, current_folder in enumerate(folders_to_process):
|
|
if not self.is_slicing: break
|
|
|
|
base_progress = 10 + (i * progress_per_folder)
|
|
self.log(f"--- Processing folder: {os.path.basename(current_folder)} ---")
|
|
self.progress_target = int(base_progress)
|
|
|
|
image_files = [os.path.join(current_folder, f) for f in os.listdir(current_folder) if f.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp', '.gif'))]
|
|
def natural_sort_key(s): return [int(text) if text.isdigit() else text.lower() for text in re.split('([0-9]+)', s)]
|
|
image_files.sort(key=natural_sort_key)
|
|
|
|
if not image_files: continue
|
|
|
|
self.log(f"Merging {len(image_files)} images...")
|
|
self.progress_target = int(base_progress + (progress_per_folder * 0.2))
|
|
|
|
slice_height_px = self.cm_to_pixels(slice_height_cm)
|
|
target_width_px = self.cm_to_pixels(slice_width_cm) if slice_width_cm > 0 else 0
|
|
|
|
processed_images = []
|
|
first_image_width = 0
|
|
for image_path in image_files:
|
|
if not self.is_slicing: break
|
|
img = Image.open(image_path)
|
|
if target_width_px == 0:
|
|
if first_image_width == 0: first_image_width = img.width
|
|
current_target_width = first_image_width
|
|
else:
|
|
current_target_width = target_width_px
|
|
if img.width != current_target_width:
|
|
aspect_ratio = img.height / img.width
|
|
new_height = int(current_target_width * aspect_ratio)
|
|
img = img.resize((current_target_width, new_height), Image.Resampling.LANCZOS)
|
|
processed_images.append(img)
|
|
QApplication.processEvents()
|
|
if not self.is_slicing: break
|
|
|
|
self.progress_target = int(base_progress + (progress_per_folder * 0.6))
|
|
|
|
total_height = sum(p_img.height for p_img in processed_images)
|
|
combined_image = Image.new('RGB', (processed_images[0].width, total_height))
|
|
current_height = 0
|
|
for p_img in processed_images:
|
|
combined_image.paste(p_img, (0, current_height))
|
|
current_height += p_img.height
|
|
|
|
result_folder_name = f"{os.path.basename(current_folder)}_result"
|
|
output_dir = os.path.join(current_folder, result_folder_name)
|
|
os.makedirs(output_dir, exist_ok=True)
|
|
processed_result_folders.append(output_dir) # Add to list
|
|
|
|
if save_combined:
|
|
combined_folder_name = f"{os.path.basename(current_folder)}_combined"
|
|
combined_dir = os.path.join(os.path.dirname(output_dir), combined_folder_name)
|
|
os.makedirs(combined_dir, exist_ok=True)
|
|
combined_image_path = os.path.join(combined_dir, "combined_image.png")
|
|
combined_image.save(combined_image_path, 'PNG')
|
|
self.log(f"Saved combined image to: {combined_image_path}")
|
|
|
|
num_slices = math.ceil(combined_image.height / slice_height_px)
|
|
for j in range(num_slices):
|
|
if not self.is_slicing: break
|
|
top = j * slice_height_px
|
|
bottom = min((j + 1) * slice_height_px, combined_image.height)
|
|
box = (0, top, combined_image.width, bottom)
|
|
if top >= bottom: continue
|
|
cropped_img = combined_image.crop(box)
|
|
slice_filename = f"result_{j+1}.png"
|
|
slice_path = os.path.join(output_dir, slice_filename)
|
|
cropped_img.save(slice_path, 'PNG')
|
|
self.log(f"Saved {num_slices} slices in {result_folder_name}")
|
|
self.progress_target = int(base_progress + progress_per_folder)
|
|
|
|
# Save processed result folders to info.txt
|
|
if processed_result_folders:
|
|
info_file_path = os.path.join(folder_path, "info.txt")
|
|
try:
|
|
with open(info_file_path, 'w', encoding='utf-8') as f:
|
|
for result_folder in sorted(processed_result_folders):
|
|
f.write(f"{result_folder}\n")
|
|
self.log(f"Successfully saved processed folder paths to {info_file_path}")
|
|
except Exception as e:
|
|
self.log(f"Error saving info.txt for processed folders: {e}")
|
|
|
|
except Exception as e:
|
|
raise # Re-raise the exception to be caught by start_slicing
|
|
|
|
def start_slicing(self):
|
|
self.log_screen.clear()
|
|
self.progress_bar.setValue(0)
|
|
self.progress_target = 0
|
|
folder_path = self.folder_path.text()
|
|
slice_width_cm_str = self.width_input.text()
|
|
slice_height_cm_str = self.height_input.text()
|
|
|
|
if not os.path.isdir(folder_path):
|
|
QMessageBox.warning(self, "Warning", "Please select a valid folder.")
|
|
self.log("Error: Invalid folder path.")
|
|
return
|
|
|
|
try:
|
|
slice_width_cm = float(slice_width_cm_str) if slice_width_cm_str else 0
|
|
slice_height_cm = float(slice_height_cm_str) if slice_height_cm_str else 0
|
|
if slice_width_cm < 0 or (not self.skip_merge_checkbox.isChecked() and slice_height_cm <= 0):
|
|
raise ValueError
|
|
except (ValueError, TypeError):
|
|
QMessageBox.warning(self, "Warning", "Please enter valid positive numbers for height (width can be 0).")
|
|
self.log("Error: Invalid input.")
|
|
return
|
|
|
|
self.is_slicing = True
|
|
self.start_button.setText("Stop")
|
|
self.progress_timer.start(10)
|
|
|
|
save_combined = self.save_combined_checkbox.isChecked()
|
|
skip_merge = self.skip_merge_checkbox.isChecked()
|
|
|
|
try:
|
|
if skip_merge:
|
|
self.log("--- Skipping Merge: Saving folder paths to info.txt ---")
|
|
self.save_folder_paths_to_info_txt(folder_path)
|
|
else:
|
|
self.log("--- Starting Image Merge and Slice Process ---")
|
|
self.merge_and_slice_images(folder_path, slice_width_cm, slice_height_cm, save_combined)
|
|
|
|
if self.is_slicing:
|
|
self.progress_target = 100
|
|
while self.progress_bar.value() < 99:
|
|
time.sleep(0.01)
|
|
QApplication.processEvents()
|
|
self.log("--- All tasks completed ---")
|
|
QMessageBox.information(self, "Success", "Image processing completed for all tasks.")
|
|
|
|
except Exception as e:
|
|
self.log(f"An unexpected error occurred: {e}")
|
|
QMessageBox.critical(self, "Error", f"An unexpected error occurred: {e}")
|
|
finally:
|
|
self.stop_slicing_ui()
|
|
|
|
def stop_slicing_ui(self):
|
|
self.is_slicing = False
|
|
self.start_button.setText("Start")
|
|
self.progress_timer.stop()
|
|
self.progress_bar.setValue(0)
|
|
|
|
|
|
def save_log(self):
|
|
if self.is_slicing:
|
|
QMessageBox.warning(self, "Warning", "Cannot save log while process is running.")
|
|
return
|
|
|
|
folder_path = self.folder_path.text()
|
|
if not folder_path or not os.path.isdir(folder_path):
|
|
QMessageBox.warning(self, "Warning", "Please select a valid base folder first.")
|
|
return
|
|
|
|
log_content = self.log_screen.toPlainText()
|
|
if not log_content:
|
|
QMessageBox.warning(self, "Warning", "Log is empty.")
|
|
return
|
|
|
|
try:
|
|
log_dir_name = f"{os.path.basename(folder_path)}_log"
|
|
log_dir_path = os.path.join(folder_path, log_dir_name)
|
|
os.makedirs(log_dir_path, exist_ok=True)
|
|
|
|
now = datetime.datetime.now()
|
|
log_filename = f"log_{now.strftime('%H%M%S')}.log"
|
|
save_path = os.path.join(log_dir_path, log_filename)
|
|
|
|
with open(save_path, 'w', encoding='utf-8') as f:
|
|
f.write(log_content)
|
|
|
|
self.log(f"Log saved to {save_path}")
|
|
QMessageBox.information(self, "Success", f"Log successfully saved to\n{save_path}")
|
|
|
|
except Exception as e:
|
|
self.log(f"Error saving log: {e}")
|
|
QMessageBox.critical(self, "Error", f"Failed to save log file: {e}")
|
|
|
|
|
|
if __name__ == '__main__':
|
|
app = QApplication(sys.argv)
|
|
ex = ImageSlicer()
|
|
ex.show()
|
|
sys.exit(app.exec()) |