diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..908b5d1 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "pixfuck" +version = "0.1.1" +edition = "2021" + +[dependencies] +image = "0.25" +rayon = "1.10" +palette = { version = "0.7", features = ["std"] } +clap = { version = "4.5", features = ["derive"] } +log = "0.4" +env_logger = "0.11" +dirs = "4.0" diff --git a/main.py b/main.py deleted file mode 100644 index 8e9c773..0000000 --- a/main.py +++ /dev/null @@ -1,56 +0,0 @@ -import sys -import os -import traceback -from PyQt6.QtWidgets import QApplication -from ui import PixelSortApp -from ui.logger import setup_logger, get_logger - -def log_environment_info(logger): - """Log relevant environment information for debugging.""" - logger.debug(f"Display: {os.environ.get('DISPLAY')}") - logger.debug(f"Wayland Display: {os.environ.get('WAYLAND_DISPLAY')}") - logger.debug(f"Qt Platform: {os.environ.get('QT_QPA_PLATFORM')}") - -def initialize_application(logger): - """Initialize and return the QApplication instance.""" - app = QApplication(sys.argv) - logger.info("QApplication created") - logger.debug(f"Available platforms: {QApplication.platformName()}") - return app - -def create_main_window(logger): - """Create and return the main application window.""" - window = PixelSortApp() - logger.info("Window created") - return window - -def handle_error(logger, error): - """Handle application errors and log them appropriately.""" - logger.error(f"Error: {str(error)}") - logger.error("Traceback:", exc_info=True) - sys.exit(1) - -def run_application(): - """Main application runner function.""" - logger = setup_logger() - app_logger = get_logger('main') - - try: - app_logger.info("Starting application...") - log_environment_info(app_logger) - - app = initialize_application(app_logger) - window = create_main_window(app_logger) - - window.show() - app_logger.info("Window shown") - exit_code = app.exec() - app_logger.info("Application exited successfully") - sys.exit(exit_code) - except Exception as e: - handle_error(app_logger, e) - -if __name__ == "__main__": - run_application() - -# coast was here :3 diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 28e7b33..0000000 --- a/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -PyQt6>=6.4.0 -Pillow>=9.0.0 -numpy>=1.20.0 -numba>=0.55.0 -scipy>=1.7.0 \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..a655408 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,20 @@ +pub mod sort; + +use image::{ImageFormat, ImageReader}; +use std::fs::File; +use std::path::Path; + +pub use sort::{SortMode, sort_pixels}; + +pub fn sort_image(input_path: &str, output_path: &str, mode: SortMode) -> image::ImageResult<()> { + let img = ImageReader::open(input_path)?.decode()?.to_rgb8(); + let sorted = sort_pixels(img, mode); + let ext = Path::new(output_path) + .extension() + .and_then(|s| s.to_str()) + .unwrap_or("png"); + + let mut file = File::create(output_path)?; + let format = ImageFormat::from_extension(ext).unwrap_or(ImageFormat::Png); + sorted.write_to(&mut file, format) +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..34a1138 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,33 @@ +use pixfuck::{sort_image, SortMode}; +use std::io::{self, Write}; + +fn main() { + println!("Welcome to Pixfuck! Please enter input file realpath:"); + let mut input = String::new(); + io::stdin().read_line(&mut input).unwrap(); + let input = input.trim(); + + println!("Enter output file path:"); + let mut output = String::new(); + io::stdin().read_line(&mut output).unwrap(); + let output = output.trim(); + + println!("Choose mode: 0=Hue, 1=Saturation, 2=Lightness, 3=Brightness"); + let mut mode_str = String::new(); + io::stdin().read_line(&mut mode_str).unwrap(); + let mode = match mode_str.trim() { + "0" => SortMode::Hue, + "1" => SortMode::Saturation, + "2" => SortMode::Lightness, + "3" => SortMode::Brightness, + _ => { + eprintln!("Invalid mode, defaulting to Hue"); + SortMode::Hue + } + }; + + match sort_image(input, output, mode) { + Ok(_) => println!("Image sorted and saved to this directory!"), + Err(e) => eprintln!("Error: {}", e), + } +} diff --git a/src/sort/brightness.rs b/src/sort/brightness.rs new file mode 100644 index 0000000..53447f2 --- /dev/null +++ b/src/sort/brightness.rs @@ -0,0 +1,26 @@ +use image::{Rgb, RgbImage}; +use rayon::prelude::*; + +pub fn sort(img: RgbImage) -> RgbImage { + let (w, h) = img.dimensions(); + let mut rows: Vec>> = (0..h) + .map(|y| (0..w).map(|x| *img.get_pixel(x, y)).collect()) + .collect(); + + rows.par_iter_mut().for_each(|row| { + row.sort_by(|a, b| brightness(a).total_cmp(&brightness(b))); + }); + + let mut out = RgbImage::new(w, h); + for (y, row) in rows.iter().enumerate() { + for (x, px) in row.iter().enumerate() { + out.put_pixel(x as u32, y as u32, *px); + } + } + + out +} + +fn brightness(rgb: &Rgb) -> f32 { + 0.299 * rgb[0] as f32 + 0.587 * rgb[1] as f32 + 0.114 * rgb[2] as f32 +} diff --git a/src/sort/hue.rs b/src/sort/hue.rs new file mode 100644 index 0000000..f8954ec --- /dev/null +++ b/src/sort/hue.rs @@ -0,0 +1,29 @@ +use image::{Rgb, RgbImage}; +use rayon::prelude::*; +use palette::{Srgb, Hsl, IntoColor}; + +pub fn sort(img: RgbImage) -> RgbImage { + let (w, h) = img.dimensions(); + let mut rows: Vec>> = (0..h) + .map(|y| (0..w).map(|x| *img.get_pixel(x, y)).collect()) + .collect(); + + rows.par_iter_mut().for_each(|row| { + row.sort_by(|a, b| hue(a).partial_cmp(&hue(b)).unwrap()); + }); + + let mut out = RgbImage::new(w, h); + for (y, row) in rows.iter().enumerate() { + for (x, px) in row.iter().enumerate() { + out.put_pixel(x as u32, y as u32, *px); + } + } + + out +} + +fn hue(rgb: &Rgb) -> f32 { + let Srgb { red, green, blue, .. } = Srgb::new(rgb[0], rgb[1], rgb[2]).into_format::(); + let hsl: Hsl = Srgb::new(red, green, blue).into_color(); + hsl.hue.into_degrees() +} diff --git a/src/sort/lightness.rs b/src/sort/lightness.rs new file mode 100644 index 0000000..84523e5 --- /dev/null +++ b/src/sort/lightness.rs @@ -0,0 +1,29 @@ +use image::{Rgb, RgbImage}; +use rayon::prelude::*; +use palette::{Srgb, Hsl, IntoColor}; + +pub fn sort(img: RgbImage) -> RgbImage { + let (w, h) = img.dimensions(); + let mut rows: Vec>> = (0..h) + .map(|y| (0..w).map(|x| *img.get_pixel(x, y)).collect()) + .collect(); + + rows.par_iter_mut().for_each(|row| { + row.sort_by(|a, b| light(a).partial_cmp(&light(b)).unwrap()); + }); + + let mut out = RgbImage::new(w, h); + for (y, row) in rows.iter().enumerate() { + for (x, px) in row.iter().enumerate() { + out.put_pixel(x as u32, y as u32, *px); + } + } + + out +} + +fn light(rgb: &Rgb) -> f32 { + let Srgb { red, green, blue, .. } = Srgb::new(rgb[0], rgb[1], rgb[2]).into_format::(); + let hsl: Hsl = Srgb::new(red, green, blue).into_color(); + hsl.lightness +} diff --git a/src/sort/mod.rs b/src/sort/mod.rs new file mode 100644 index 0000000..7c1b16a --- /dev/null +++ b/src/sort/mod.rs @@ -0,0 +1,23 @@ +pub mod brightness; +pub mod hue; +pub mod lightness; +pub mod saturation; + +use image::RgbImage; + +#[derive(Clone, Copy)] +pub enum SortMode { + Hue, + Saturation, + Lightness, + Brightness, +} + +pub fn sort_pixels(img: RgbImage, mode: SortMode) -> RgbImage { + match mode { + SortMode::Hue => hue::sort(img), + SortMode::Saturation => saturation::sort(img), + SortMode::Lightness => lightness::sort(img), + SortMode::Brightness => brightness::sort(img), + } +} diff --git a/src/sort/saturation.rs b/src/sort/saturation.rs new file mode 100644 index 0000000..2f12a65 --- /dev/null +++ b/src/sort/saturation.rs @@ -0,0 +1,29 @@ +use image::{Rgb, RgbImage}; +use rayon::prelude::*; +use palette::{Srgb, Hsl, IntoColor}; + +pub fn sort(img: RgbImage) -> RgbImage { + let (w, h) = img.dimensions(); + let mut rows: Vec>> = (0..h) + .map(|y| (0..w).map(|x| *img.get_pixel(x, y)).collect()) + .collect(); + + rows.par_iter_mut().for_each(|row| { + row.sort_by(|a, b| sat(a).partial_cmp(&sat(b)).unwrap()); + }); + + let mut out = RgbImage::new(w, h); + for (y, row) in rows.iter().enumerate() { + for (x, px) in row.iter().enumerate() { + out.put_pixel(x as u32, y as u32, *px); + } + } + + out +} + +fn sat(rgb: &Rgb) -> f32 { + let Srgb { red, green, blue, .. } = Srgb::new(rgb[0], rgb[1], rgb[2]).into_format::(); + let hsl: Hsl = Srgb::new(red, green, blue).into_color(); + hsl.saturation +} diff --git a/ui/__init__.py b/ui/__init__.py deleted file mode 100644 index 0eedc11..0000000 --- a/ui/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .pixel_sort_app import PixelSortApp -from .worker import PixelSortWorker - -__all__ = ['PixelSortApp', 'PixelSortWorker'] \ No newline at end of file diff --git a/ui/__pycache__/__init__.cpython-313.pyc b/ui/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index 67d471c..0000000 Binary files a/ui/__pycache__/__init__.cpython-313.pyc and /dev/null differ diff --git a/ui/__pycache__/pixel_sort_app.cpython-313.pyc b/ui/__pycache__/pixel_sort_app.cpython-313.pyc deleted file mode 100644 index ed98c0c..0000000 Binary files a/ui/__pycache__/pixel_sort_app.cpython-313.pyc and /dev/null differ diff --git a/ui/__pycache__/worker.cpython-313.pyc b/ui/__pycache__/worker.cpython-313.pyc deleted file mode 100644 index 215e0a4..0000000 Binary files a/ui/__pycache__/worker.cpython-313.pyc and /dev/null differ diff --git a/ui/logger.py b/ui/logger.py deleted file mode 100644 index efab41c..0000000 --- a/ui/logger.py +++ /dev/null @@ -1,49 +0,0 @@ -import logging -import os -from datetime import datetime - -def setup_logger(): - # Create logs directory if it doesn't exist - if not os.path.exists('logs'): - os.makedirs('logs') - - # Configure logging - log_file = os.path.join('logs', 'debug.log') - - # Clear previous log file if it exists - if os.path.exists(log_file): - try: - with open(log_file, 'w') as f: - f.write(f"=== New Session Started at {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} ===\n") - except Exception as e: - print(f"Warning: Could not clear previous log file: {e}") - - # Create formatter - formatter = logging.Formatter( - '%(asctime)s - %(name)s - %(levelname)s - %(message)s', - datefmt='%Y-%m-%d %H:%M:%S' - ) - - # Setup file handler - file_handler = logging.FileHandler(log_file) - file_handler.setLevel(logging.DEBUG) - file_handler.setFormatter(formatter) - - # Setup console handler - console_handler = logging.StreamHandler() - console_handler.setLevel(logging.INFO) - console_handler.setFormatter(formatter) - - # Get root logger - logger = logging.getLogger() - logger.setLevel(logging.DEBUG) - - # Add handlers - logger.addHandler(file_handler) - logger.addHandler(console_handler) - - return logger - -# Create a function to get a logger for a specific module -def get_logger(name): - return logging.getLogger(name) \ No newline at end of file diff --git a/ui/main_window.ui b/ui/main_window.ui deleted file mode 100644 index 18c354d..0000000 --- a/ui/main_window.ui +++ /dev/null @@ -1,302 +0,0 @@ - - - MainWindow - - - - 0 - 0 - 1400 - 740 - - - - Enhanced Pixel Sorting App - - - - - - - - 16777215 - 80 - - - - File Operations - - - - - - Load Image - - - - - - - false - - - Save Sorted Image - - - - - - - - - - - 16777215 - 140 - - - - Sorting Options - - - - - - Sort Criterion: - - - - - - - - Brightness - - - - - Hue - - - - - Saturation - - - - - Intensity - - - - - Minimum - - - - - - - - Pattern: - - - - - - - - Linear - - - - - Radial - - - - - Spiral - - - - - Wave - - - - - - - - Sort Angle: - - - - - - - 0 - - - 359 - - - 0 - - - Qt::Orientation::Horizontal - - - QSlider::TickPosition::TicksBelow - - - 30 - - - - - - - ° - - - 0 - - - 359 - - - 0 - - - - - - - Intensity: - - - - - - - 1 - - - 100 - - - 100 - - - Qt::Orientation::Horizontal - - - QSlider::TickPosition::TicksBelow - - - 10 - - - - - - - % - - - 1 - - - 100 - - - 100 - - - - - - - - - - - 16777215 - 100 - - - - Sort Control - - - - - - false - - - Sort Pixels - - - - - - - 0 - - - - - - - - - - - - - - Original Image, Load an image with the button above - - - Qt::AlignmentFlag::AlignCenter - - - - - - - - - - - Sorted Image will Render Here upon completion of Sorting. - - - Qt::AlignmentFlag::AlignCenter - - - - - - - - - - - - - 0 - 0 - 1400 - 23 - - - - - - - - diff --git a/ui/pixel_sort_app.py b/ui/pixel_sort_app.py deleted file mode 100644 index 8cb0591..0000000 --- a/ui/pixel_sort_app.py +++ /dev/null @@ -1,307 +0,0 @@ -import os -import psutil -from PyQt6.QtWidgets import QMainWindow, QFileDialog, QMessageBox, QLabel, QSpinBox, QProgressDialog -from PyQt6.QtGui import QPixmap, QImage -from PyQt6.QtCore import Qt -from PyQt6 import uic -from PIL import Image, UnidentifiedImageError -from .worker import PixelSortWorker -from .logger import get_logger - -class PixelSortApp(QMainWindow): - def __init__(self): - super().__init__() - self.logger = get_logger('PixelSortApp') - self.logger.info("Initializing PixelSortApp") - - # Load the UI file - uic.loadUi('ui/main_window.ui', self) - - # Initialize variables to hold images - self.original_image = None - self.sorted_image = None - - # Initialize worker thread - self.worker = None - - # Set up image labels - self.original_label.setAlignment(Qt.AlignmentFlag.AlignCenter) - self.sorted_label.setAlignment(Qt.AlignmentFlag.AlignCenter) - self.original_label.setMinimumSize(400, 300) - self.sorted_label.setMinimumSize(400, 300) - - # Connect signals - self.setup_connections() - self.logger.info("PixelSortApp initialization complete") - - def setup_connections(self): - self.logger.debug("Setting up signal connections") - self.load_button.clicked.connect(self.load_image) - self.save_button.clicked.connect(self.save_image) - self.sort_button.clicked.connect(self.sort_pixels) - self.angle_slider.valueChanged.connect(self.update_angle_spinbox) - self.intensity_slider.valueChanged.connect(self.update_intensity_spinbox) - self.angle_value_label.valueChanged.connect(self.update_angle_slider) - self.intensity_value_label.valueChanged.connect(self.update_intensity_slider) - - def update_intensity_spinbox(self, value): - self.logger.debug(f"Updating intensity spinbox to {value}%") - self.intensity_value_label.setValue(value) - - def update_angle_spinbox(self, value): - self.logger.debug(f"Updating angle spinbox to {value}°") - self.angle_value_label.setValue(value) - - def update_intensity_slider(self, value): - self.logger.debug(f"Updating intensity slider to {value}%") - self.intensity_slider.setValue(value) - - def update_angle_slider(self, value): - self.logger.debug(f"Updating angle slider to {value}°") - self.angle_slider.setValue(value) - - def validate_image_size(self, image_path): - """Validate if the image size is within acceptable limits.""" - try: - with Image.open(image_path) as img: - width, height = img.size - # Calculate approximate memory usage (3 bytes per pixel for RGB) - memory_usage = width * height * 3 - # Get available system memory - available_memory = psutil.virtual_memory().available - - # Check if image is too large (more than 50% of available memory) - if memory_usage > available_memory * 0.5: - return False, f"Image is too large ({width}x{height}). Please use a smaller image." - - # Check if dimensions are too large - if width > 10000 or height > 10000: - return False, f"Image dimensions ({width}x{height}) exceed maximum allowed size (10000x10000)." - - return True, None - except Exception as e: - return False, f"Error validating image: {str(e)}" - - def load_image(self): - """Load an image with enhanced error handling and validation.""" - self.logger.info("Opening file dialog to load image") - options = QFileDialog.Option.DontUseNativeDialog - file_name, _ = QFileDialog.getOpenFileName( - self, "Open Image File", "", - "Images (*.png *.xpm *.jpg *.jpeg *.bmp *.gif);;All Files (*)", - options=options - ) - - if not file_name: - return - - try: - # Validate file exists and is readable - if not os.path.isfile(file_name): - raise FileNotFoundError(f"File not found: {file_name}") - - if not os.access(file_name, os.R_OK): - raise PermissionError(f"Cannot read file: {file_name}") - - # Validate image size and memory requirements - is_valid, error_msg = self.validate_image_size(file_name) - if not is_valid: - QMessageBox.critical(self, "Error", error_msg) - return - - # Show loading progress dialog - progress = QProgressDialog("Loading image...", None, 0, 100, self) - progress.setWindowModality(Qt.WindowModality.WindowModal) - progress.setWindowTitle("Loading") - progress.setMinimumDuration(0) - progress.setValue(0) - progress.show() - - self.logger.info(f"Loading image from: {file_name}") - - # Load image with progress updates - with Image.open(file_name) as img: - # Convert to RGB if necessary - if img.mode != 'RGB': - image = img.convert('RGB') - else: - image = img.copy() - progress.setValue(30) - - # Store the original image - self.original_image = image - progress.setValue(60) - - # Display the image - self.display_image(image, self.original_label) - progress.setValue(90) - - # Reset UI state - self.sorted_label.clear() - self.sorted_label.setText("Sorted Image") - self.sort_button.setEnabled(True) - self.save_button.setEnabled(False) - self.progress_bar.setValue(0) - - progress.setValue(100) - self.logger.info("Image loaded successfully") - - except UnidentifiedImageError: - self.logger.error(f"Invalid image format: {file_name}") - QMessageBox.critical(self, "Error", "The selected file is not a valid image format.") - except FileNotFoundError as e: - self.logger.error(str(e)) - QMessageBox.critical(self, "Error", str(e)) - except PermissionError as e: - self.logger.error(str(e)) - QMessageBox.critical(self, "Error", str(e)) - except MemoryError: - self.logger.error("Not enough memory to load the image") - QMessageBox.critical(self, "Error", "Not enough memory to load the image. Please try a smaller image.") - except Exception as e: - self.logger.error(f"Failed to load image: {str(e)}", exc_info=True) - QMessageBox.critical(self, "Error", f"Failed to load image:\n{str(e)}") - finally: - if 'progress' in locals(): - progress.close() - - def display_image(self, image, label): - """Display an image in a label while maintaining aspect ratio.""" - self.logger.debug(f"Displaying image of size {image.size}") - try: - # Convert PIL Image to QImage - qimage = self.pil_to_qimage(image) - if qimage.isNull(): - self.logger.error("Failed to convert PIL image to QImage") - return - - # Create pixmap from QImage - pixmap = QPixmap.fromImage(qimage) - if pixmap.isNull(): - self.logger.error("Failed to create QPixmap from QImage") - return - - # Get label size - label_size = label.size() - - # Calculate scaling factors - w_scale = label_size.width() / pixmap.width() - h_scale = label_size.height() / pixmap.height() - - # Use the smaller scale to maintain aspect ratio - scale = min(w_scale, h_scale) - - # Calculate new dimensions - new_width = int(pixmap.width() * scale) - new_height = int(pixmap.height() * scale) - - # Scale the pixmap - scaled_pixmap = pixmap.scaled( - new_width, - new_height, - Qt.AspectRatioMode.KeepAspectRatio, - Qt.TransformationMode.SmoothTransformation - ) - - # Center the pixmap in the label - label.setAlignment(Qt.AlignmentFlag.AlignCenter) - label.setPixmap(scaled_pixmap) - - except Exception as e: - self.logger.error(f"Error displaying image: {str(e)}", exc_info=True) - - def pil_to_qimage(self, image): - """Convert PIL Image to QImage with proper format handling.""" - self.logger.debug("Converting PIL image to QImage") - try: - # Ensure image is in RGB mode - if image.mode != 'RGB': - image = image.convert('RGB') - - # Get image data - data = image.tobytes("raw", "RGB") - - # Create QImage with proper format - qimage = QImage( - data, - image.width, - image.height, - image.width * 3, # stride - QImage.Format.Format_RGB888 - ) - - # Create a deep copy to ensure data ownership - return qimage.copy() - - except Exception as e: - self.logger.error(f"Error converting PIL image to QImage: {str(e)}", exc_info=True) - return QImage() - - def resizeEvent(self, event): - self.logger.debug("Window resize event triggered") - # Refresh images on window resize - if self.original_image: - self.display_image(self.original_image, self.original_label) - if self.sorted_image: - self.display_image(self.sorted_image, self.sorted_label) - super().resizeEvent(event) - - def sort_pixels(self): - if self.original_image is None: - self.logger.warning("Attempted to sort pixels without loading an image") - QMessageBox.warning(self, "Warning", "No image loaded to sort.") - return - - self.logger.info("Starting pixel sorting operation") - # Disable buttons to prevent multiple operations - self.sort_button.setEnabled(False) - self.save_button.setEnabled(False) - - # Get sorting parameters - angle = self.angle_slider.value() - criterion = self.criteria_combo.currentText() - pattern = self.pattern_combo.currentText() - intensity = self.intensity_slider.value() / 100.0 - - # Start the worker thread - self.worker = PixelSortWorker(self.original_image, angle, criterion, pattern, intensity) - self.worker.progress.connect(self.update_progress) - self.worker.finished.connect(self.on_sort_finished) - self.worker.error.connect(self.on_sort_error) - self.worker.start() - - def update_progress(self, value): - self.progress_bar.setValue(value) - - def on_sort_finished(self, sorted_image): - self.logger.info("Sorting finished, displaying result") - self.sorted_image = sorted_image - self.display_image(sorted_image, self.sorted_label) - self.sort_button.setEnabled(True) - self.save_button.setEnabled(True) - self.worker = None - - def on_sort_error(self, error_message): - self.logger.error(f"Sorting error: {error_message}") - QMessageBox.critical(self, "Error", f"An error occurred during sorting:\n{error_message}") - self.sort_button.setEnabled(True) - self.worker = None - - def save_image(self): - if self.sorted_image is None: - QMessageBox.warning(self, "Warning", "No sorted image to save.") - return - options = QFileDialog.Option.DontUseNativeDialog - file_name, _ = QFileDialog.getSaveFileName( - self, "Save Image File", "", - "PNG Image (*.png);;JPEG Image (*.jpg);;BMP Image (*.bmp);;All Files (*)", - options=options - ) - if file_name: - try: - self.logger.info(f"Saving image to: {file_name}") - self.sorted_image.save(file_name) - self.logger.info("Image saved successfully") - except Exception as e: - self.logger.error(f"Failed to save image: {str(e)}", exc_info=True) - QMessageBox.critical(self, "Error", f"Failed to save image:\n{e}") \ No newline at end of file diff --git a/ui/worker.py b/ui/worker.py deleted file mode 100644 index 833dc58..0000000 --- a/ui/worker.py +++ /dev/null @@ -1,313 +0,0 @@ -import traceback -from PyQt6.QtCore import QThread, pyqtSignal -from PIL import Image -import numpy as np -from numba import njit -import scipy.ndimage -from .logger import get_logger - -logger = get_logger('PixelSortWorker') - -# Numba-compatible RGB to HSV conversion -@njit -def rgb_to_hsv_numba(r, g, b): - maxc = np.maximum(np.maximum(r, g), b) - minc = np.minimum(np.minimum(r, g), b) - delta = maxc - minc - h = np.zeros_like(maxc, dtype=np.float32) - s = np.zeros_like(maxc, dtype=np.float32) - v = maxc.astype(np.float32) - - mask = delta != 0 - s[mask] = delta[mask] / maxc[mask] - - rc = np.zeros_like(maxc, dtype=np.float32) - gc = np.zeros_like(maxc, dtype=np.float32) - bc = np.zeros_like(maxc, dtype=np.float32) - rc[mask] = (maxc[mask] - r[mask]) / delta[mask] - gc[mask] = (maxc[mask] - g[mask]) / delta[mask] - bc[mask] = (maxc[mask] - b[mask]) / delta[mask] - - cond_r = (r == maxc) & mask - cond_g = (g == maxc) & mask - cond_b = (b == maxc) & mask - - h[cond_r] = bc[cond_r] - gc[cond_r] - h[cond_g] = np.float32(2.0) + rc[cond_g] - bc[cond_g] - h[cond_b] = np.float32(4.0) + gc[cond_b] - rc[cond_b] - - h = (h / np.float32(6.0)) % np.float32(1.0) - h[~mask] = np.float32(0.0) - - return h, s, v - -@njit -def row_max(arr): - n_rows, n_cols = arr.shape - result = np.empty(n_rows, dtype=arr.dtype) - for i in range(n_rows): - max_val = arr[i, 0] - for j in range(1, n_cols): - if arr[i, j] > max_val: - max_val = arr[i, j] - result[i] = max_val - return result - -@njit -def row_min(arr): - n_rows, n_cols = arr.shape - result = np.empty(n_rows, dtype=arr.dtype) - for i in range(n_rows): - min_val = arr[i, 0] - for j in range(1, n_cols): - if arr[i, j] < min_val: - min_val = arr[i, j] - result[i] = min_val - return result - -@njit -def process_line(line, criterion_id): - length = line.shape[0] - key = np.empty(length, dtype=np.float32) - line = line.astype(np.float32) - if criterion_id == 0: - for j in range(length): - key[j] = (line[j, 0] + line[j, 1] + line[j, 2]) / np.float32(3.0) - elif criterion_id == 1: - r = line[:, 0] / np.float32(255.0) - g = line[:, 1] / np.float32(255.0) - b = line[:, 2] / np.float32(255.0) - h, s, v = rgb_to_hsv_numba(r, g, b) - key = h.astype(np.float32) - elif criterion_id == 2: - r = line[:, 0] / np.float32(255.0) - g = line[:, 1] / np.float32(255.0) - b = line[:, 2] / np.float32(255.0) - h, s, v = rgb_to_hsv_numba(r, g, b) - key = s.astype(np.float32) - elif criterion_id == 3: - max_vals = row_max(line) - min_vals = row_min(line) - key = (max_vals + min_vals) / np.float32(2.0) - elif criterion_id == 4: - key = row_min(line) - else: - for j in range(length): - key[j] = (line[j, 0] + line[j, 1] + line[j, 2]) / np.float32(3.0) - - sorted_indices = np.argsort(key) - sorted_line = line[sorted_indices].astype(np.uint8) - return sorted_line - -class PixelSortWorker(QThread): - progress = pyqtSignal(int) - finished = pyqtSignal(Image.Image) - error = pyqtSignal(str) - - def __init__(self, image, angle, criterion, pattern, intensity): - super().__init__() - self.logger = get_logger('PixelSortWorker') - self.image = image - self.angle = angle - self.criterion = criterion - self.pattern = pattern - self.intensity = intensity - self.logger.info(f"Initialized worker with angle={angle}, criterion={criterion}, pattern={pattern}, intensity={intensity}") - - def run(self): - try: - self.logger.info("Starting pixel sorting operation") - sorted_image = self.pixel_sort(self.image, self.angle, self.criterion, self.pattern, self.intensity) - self.logger.info("Pixel sorting completed successfully") - self.finished.emit(sorted_image) - except Exception as e: - self.logger.error(f"Error during pixel sorting: {str(e)}", exc_info=True) - tb = traceback.format_exc() - self.error.emit(f"{str(e)}\n{tb}") - - def pixel_sort(self, image, angle, criterion, pattern, intensity): - self.logger.debug(f"Starting pixel_sort with image size {image.size}") - # Convert image to NumPy array - img_array = np.array(image) - height, width, channels = img_array.shape - self.logger.debug(f"Original image shape: {img_array.shape}") - - # Map criterion to integer id - criterion_map = { - 'Brightness': 0, - 'Hue': 1, - 'Saturation': 2, - 'Intensity': 3, - 'Minimum': 4, - } - criterion_id = criterion_map.get(criterion, 0) - self.logger.debug(f"Using criterion ID: {criterion_id} ({criterion})") - self.logger.debug(f"Using pattern: {pattern}") - - # Convert angle to radians - angle_rad = np.radians(angle) - - # Normalize angle to -180 to 180 degrees - angle_rad = angle_rad % (2 * np.pi) - if angle_rad > np.pi: - angle_rad -= 2 * np.pi - - # Determine if we should shear horizontally or vertically - # For angles between -45 and 45 degrees, shear horizontally - # For angles between 45 and 135 degrees, shear vertically - if -np.pi/4 <= angle_rad <= np.pi/4: - # Shear horizontally - shear_factor = np.tan(angle_rad) - shear_matrix = np.array([ - [1, shear_factor, 0], - [0, 1, 0], - [0, 0, 1] - ]) - inverse_shear_matrix = np.array([ - [1, -shear_factor, 0], - [0, 1, 0], - [0, 0, 1] - ]) - # Sort horizontally - sort_axis = 1 - else: - # Shear vertically - shear_factor = 1.0 / np.tan(angle_rad) - shear_matrix = np.array([ - [1, 0, 0], - [shear_factor, 1, 0], - [0, 0, 1] - ]) - inverse_shear_matrix = np.array([ - [1, 0, 0], - [-shear_factor, 1, 0], - [0, 0, 1] - ]) - # Sort vertically - sort_axis = 0 - - # Apply shear transformation - self.logger.debug("Applying shear transformation") - sheared_array = scipy.ndimage.affine_transform( - img_array, - shear_matrix, - offset=[0, 0, 0], - order=1, - mode='wrap', - cval=0 - ) - - # Sort pixels - self.logger.debug("Sorting pixels") - sorted_array = sheared_array.copy() - - # Process each line along the appropriate axis - if sort_axis == 1: # Sort horizontally - for i in range(sheared_array.shape[0]): - line = sorted_array[i, :, :].copy() - sorted_line = process_line(line, criterion_id) - if intensity < 1.0: - mask = np.random.rand(line.shape[0]) < intensity - blended_line = line.copy() - blended_line[mask] = sorted_line[mask] - sorted_array[i, :, :] = blended_line - else: - sorted_array[i, :, :] = sorted_line - progress_percent = int(((i + 1) / sheared_array.shape[0]) * 100) - self.progress.emit(progress_percent) - else: # Sort vertically - for i in range(sheared_array.shape[1]): - line = sorted_array[:, i, :].copy() - sorted_line = process_line(line, criterion_id) - if intensity < 1.0: - mask = np.random.rand(line.shape[0]) < intensity - blended_line = line.copy() - blended_line[mask] = sorted_line[mask] - sorted_array[:, i, :] = blended_line - else: - sorted_array[:, i, :] = sorted_line - progress_percent = int(((i + 1) / sheared_array.shape[1]) * 100) - self.progress.emit(progress_percent) - - # Apply inverse shear transformation - self.logger.debug("Applying inverse shear transformation") - result_array = scipy.ndimage.affine_transform( - sorted_array, - inverse_shear_matrix, - offset=[0, 0, 0], - order=1, - mode='wrap', - cval=0 - ) - - # Ensure the result has the same shape as the input - if result_array.shape != img_array.shape: - result_array = self.maintain_aspect_ratio(result_array, img_array.shape) - - # Convert back to PIL Image - sorted_image = Image.fromarray(result_array.astype(np.uint8)) - self.logger.debug("Pixel sorting completed") - return sorted_image - - def get_line_points(self, x1, y1, x2, y2): - """Get all points along a line using Bresenham's line algorithm.""" - points = [] - dx = abs(x2 - x1) - dy = abs(y2 - y1) - x, y = x1, y1 - n = 1 + dx + dy - x_inc = 1 if x2 > x1 else -1 - y_inc = 1 if y2 > y1 else -1 - error = dx - dy - dx *= 2 - dy *= 2 - - for _ in range(n): - points.append((x, y)) - if error > 0: - x += x_inc - error -= dy - else: - y += y_inc - error += dx - - return points - - def maintain_aspect_ratio(self, img_array, target_shape): - """Maintain aspect ratio while resizing the image to match target shape.""" - self.logger.debug(f"Maintaining aspect ratio: current shape {img_array.shape} -> target shape {target_shape}") - - current_height, current_width = img_array.shape[:2] - target_height, target_width = target_shape[:2] - - # Calculate scaling factors - scale_h = target_height / current_height - scale_w = target_width / current_width - - # Use the smaller scaling factor to maintain aspect ratio - scale = min(scale_h, scale_w) - - # Calculate new dimensions - new_height = int(current_height * scale) - new_width = int(current_width * scale) - - # Resize the image - resized = scipy.ndimage.zoom( - img_array, - (scale, scale, 1) if len(img_array.shape) == 3 else (scale, scale), - order=1, - mode='constant', - cval=0 - ) - - # Create a new array with target shape - result = np.zeros(target_shape, dtype=img_array.dtype) - - # Calculate padding - pad_h = (target_height - new_height) // 2 - pad_w = (target_width - new_width) // 2 - - # Copy the resized image into the center of the result - result[pad_h:pad_h + new_height, pad_w:pad_w + new_width] = resized - - return result \ No newline at end of file