import sys
import os
import numpy as np
from PySide6.QtWidgets import (QApplication, QMainWindow, QVBoxLayout, QWidget, 
                              QLabel, QPushButton, QFileDialog, QHBoxLayout,
                              QSlider, QSpinBox, QDoubleSpinBox, QComboBox,
                              QTabWidget, QMessageBox, QCheckBox, QGroupBox,
                              QLineEdit, QFormLayout, QSizePolicy, QToolButton,
                              QButtonGroup, QRadioButton, QColorDialog,QPlainTextEdit,)
from PySide6.QtGui import (QPixmap, QImage, QPainter, QColor, QCursor, QPen, QTextOption,
                          QBrush, QPainterPath, QDoubleValidator, QIntValidator,QFont,QShortcut,QKeySequence)
from PySide6.QtCore import Qt, QPoint, QRect, Signal, QSize, QRectF, QPointF
from PySide6.QtGui import QPainterPath, QPainterPathStroker
import matplotlib.pyplot as plt
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
from skimage import filters, exposure, color
from collections import defaultdict
from sklearn.cluster import KMeans
from PIL import Image
import numpy as np
from collections import Counter
from qt_material import apply_stylesheet, QtStyleTools

class DrawingCommand:
    """表示一个绘图命令，保存路径状态"""
    def __init__(self, path,parent_processor=None):
        self.path = QPainterPath(path)  # 深拷贝路径
        
class ImageDisplay(QLabel):
    mouseMoved = Signal(QPoint)
    mousePressed = Signal(QPoint)
    mouseReleased = Signal(QPoint)
    selectionComplete = Signal(QRect)
    
    def __init__(self, parent=None,parent_processor=None):
        super().__init__(parent)
        self.parent_processor = parent_processor  # 保存上层引用
        self.setMouseTracking(True)
        self.selection_start = None
        self.selection_end = None
        self.selection_rect = None
        self.drawing = False
        self.drawing_path = QPainterPath()
        self.committed_path = QPainterPath()  # 已确认的路径
        self.mode = "select"  # "select", "draw", "color_pick", "calibrate"
        self.pen_color = QColor(255, 0, 0, 128)  # 淡黄色，透明度50%
        self.pen_width = 5
        self.calibration_points = {
            'x1': {'pos': None, 'color': QColor(0, 255, 0), 'label': "X1"},
            'x2': {'pos': None, 'color': QColor(0, 255, 0), 'label': "X2"},
            'y1': {'pos': None, 'color': QColor(0, 0, 255), 'label': "Y1"},
            'y2': {'pos': None, 'color': QColor(0, 0, 255), 'label': "Y2"}
        }
        
        self.extracted_points = []
        self.font = QFont()
        self.font.setPointSize(12)
        self.font.setBold(True)
        self.pixels = None
        
        # 撤销/重做栈
        self.undo_stack = []
        self.redo_stack = []
        
        # 设置快捷键
        self.undo_shortcut = QShortcut(QKeySequence("Ctrl+Z"), self)
        self.undo_shortcut.activated.connect(self.undo)
        self.redo_shortcut = QShortcut(QKeySequence("Ctrl+Y"), self)
        self.redo_shortcut.activated.connect(self.redo)
    
    def mouseMoveEvent(self, event):
        self.mouseMoved.emit(event.position().toPoint())
        if self.drawing and self.mode == "draw":
            self.drawing_path.lineTo(event.position())
            self.update()
        if self.drawing and self.mode == "select":
            self.selection_end = event.position().toPoint()
            self.update()
        if self.mode == "manual":
            if self.rect().contains(event.pos()):
                self.setCursor(Qt.CrossCursor)
        super().mouseMoveEvent(event)
    
    def mousePressEvent(self, event):
        if event.button() == Qt.LeftButton:
            self.mousePressed.emit(event.position().toPoint())
            self.drawing = True
            if self.mode == "draw":
                # 开始新路径时保存当前状态到撤销栈
                self.pen_color = QColor(255, 255, 0, 128)  # 淡黄色，透明度50%
                self.pen_width = 20
                if self.drawing_path.elementCount() == 0:
                    self.save_to_undo_stack()
                self.drawing_path.moveTo(event.position())
                self.update()
            elif self.mode == "select":
                self.pen_color = QColor(128,128,128, 128)  # 灰色，透明度50%
                self.pen_width = 4
                self.selection_start = event.position().toPoint()
                self.selection_end = event.position().toPoint()
                self.update()
            elif self.mode == "manual":
                self.extracted_points.insert(0, event.position().toPoint())
                self.parent_processor.update_extracted_points_display(self.parent_processor.transform_points(self.extracted_points))
                self.update()
        
        super().mousePressEvent(event)
    
    def mouseReleaseEvent(self, event):
        if event.button() == Qt.LeftButton and self.drawing:
            self.mouseReleased.emit(event.position().toPoint())
            self.drawing = False
            
            if self.mode == "select" and self.selection_start and self.selection_end:
                self.selection_rect = QRect(
                    min(self.selection_start.x(), self.selection_end.x()),
                    min(self.selection_start.y(), self.selection_end.y()),
                    abs(self.selection_start.x() - self.selection_end.x()),
                    abs(self.selection_start.y() - self.selection_end.y())
                )
                self.selectionComplete.emit(self.selection_rect)
                self.update()
            
            elif self.mode == "draw":
                # 鼠标释放时提交当前绘图路径
                self.commit_drawing()
        
        super().mouseReleaseEvent(event)
    
    def paintEvent(self, event):
        super().paintEvent(event)
        
        painter = QPainter(self)
        painter.setFont(self.font)
        
         # Draw freehand drawing
        if self.drawing_path and self.mode == "draw":
            # 绘制已确认的路径
            painter.setPen(QPen(self.pen_color, self.pen_width))
            painter.drawPath(self.committed_path)
            # 绘制当前正在绘制的路径
            if self.drawing_path.elementCount() > 0:
                painter.setPen(QPen(self.pen_color, self.pen_width))
                painter.drawPath(self.drawing_path)
        
        # 绘制选择矩形
        if self.selection_rect and self.mode == "select":
            painter.setPen(QPen(QColor(128,128,128, 128), self.pen_width, Qt.DashLine))
            painter.drawRect(self.selection_rect)
        
        # 绘制临时选择矩形
        if self.drawing and self.mode == "select" and self.selection_start and self.selection_end:
            rect = QRect(
                min(self.selection_start.x(), self.selection_end.x()),
                min(self.selection_start.y(), self.selection_end.y()),
                abs(self.selection_start.x() - self.selection_end.x()),
                abs(self.selection_start.y() - self.selection_end.y())
            )
            painter.setPen(QPen(QColor(128,128,128, 128), self.pen_width, Qt.DashLine))
            painter.drawRect(rect)
        
        # 绘制校准点和提取点（保持不变）
        for point_type, point in self.calibration_points.items():
            if point['pos']:
                painter.setPen(QPen(point['color'], 8))
                painter.drawPoint(point['pos'])
                label_pos = QPoint(point['pos'].x() + 10, point['pos'].y() - 10)
                painter.setPen(QPen(point['color'], 2))
                painter.drawText(label_pos, point['label'])
        
        painter.setPen(QPen(QColor(255, 0, 0), 2))
        for point in self.extracted_points:
            if point:
                painter.drawEllipse(point, 1, 1)
    
    def save_to_undo_stack(self):
        """保存当前绘图状态到撤销栈"""
        # 保存当前已确认路径和正在绘制的路径的组合
        combined_path = QPainterPath(self.committed_path)
        combined_path.addPath(self.drawing_path)
        self.undo_stack.append(DrawingCommand(combined_path))
        self.redo_stack.clear()  # 新操作后清空重做栈
    
    def commit_drawing(self):
        """提交当前绘图路径到已确认路径"""
        if self.drawing_path.elementCount() > 0:
            self.save_to_undo_stack()
            temp = QPainterPath(self.drawing_path)
            self.committed_path.addPath(temp)
            self.drawing_path = QPainterPath()
            self.update()
    
    def undo(self):
        """撤销上一次操作"""
        if self.undo_stack:
            # 保存当前状态到重做栈
            combined_path = QPainterPath(self.committed_path)
            combined_path.addPath(self.drawing_path)
            self.redo_stack.append(DrawingCommand(combined_path))
            
            # 恢复上一个状态
            command = self.undo_stack.pop()
            self.committed_path = QPainterPath(command.path)
            self.drawing_path = QPainterPath()
            self.update()
    
    def redo(self):
        """重做上一次撤销的操作"""
        if self.redo_stack:
            # 保存当前状态到撤销栈
            combined_path = QPainterPath(self.committed_path)
            combined_path.addPath(self.drawing_path)
            self.undo_stack.append(DrawingCommand(combined_path))
            
            # 恢复重做状态
            command = self.redo_stack.pop()
            self.committed_path = QPainterPath(command.path)
            self.drawing_path = QPainterPath()
            self.update()
    
    def clear_drawing(self):
        """清空所有绘图"""
        self.save_to_undo_stack()
        self.committed_path = QPainterPath()
        self.drawing_path = QPainterPath()
        self.update()
    
    def clear_calibration_point(self, point_type):
        """Clear a specific calibration point"""
        if point_type in self.calibration_points:
            self.calibration_points[point_type]['pos'] = None
            self.update()
    
    def set_calibration_point(self, point_type, pos, color=None, label=None):
        """Set a calibration point position"""
        if point_type in self.calibration_points:
            self.calibration_points[point_type]['pos'] = pos
            if color:
                self.calibration_points[point_type]['color'] = color
            if label:
                self.calibration_points[point_type]['label'] = label
            self.update()
    
    def set_extracted_points(self, points):
        """Set extracted points to be displayed"""
        self.extracted_points = points
        self.update()

class ImageProcessor(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Advanced Image Analysis Tool")
        self.setGeometry(100, 100, 1600, 1000)
        
        # Image data
        self.original_pixmap = None
        self.current_pixmap = None
        self.image_path = None
        self.scaled_pixmap = None
        self.image_data = None
        
        # Zoom and coordinate correction
        self.zoom_factor = 5
        self.cursor_pos = None
        self.calibration_values = {
            'x1': 0, 'x2': 1, 
            'y1': 0, 'y2': 1
        }
        
        # Data extraction
        self.selected_colors = []
        self.dominant_colors = []
        self.x_params = [1,0]
        self.y_params = [1,0]
        # Create UI
        self.create_ui()
        
    def create_ui(self):
        main_widget = QWidget()
        self.setCentralWidget(main_widget)
        layout = QHBoxLayout()
        main_widget.setLayout(layout)
        
        # Left panel (main image and controls)
        left_panel = QWidget()
        left_layout = QVBoxLayout()
        left_panel.setLayout(left_layout)
        
        # File controls
        file_group = QGroupBox("File Operations")
        file_layout = QHBoxLayout()
        
        self.load_btn = QPushButton("Load Image")
        self.load_btn.clicked.connect(self.load_image)
        file_layout.addWidget(self.load_btn)
        
        self.save_btn = QPushButton("Save Image")
        self.save_btn.clicked.connect(self.save_image)
        self.save_btn.setEnabled(False)
        file_layout.addWidget(self.save_btn)
        
        self.reset_btn = QPushButton("Reset")
        self.reset_btn.clicked.connect(self.reset_image)
        self.reset_btn.setEnabled(False)
        file_layout.addWidget(self.reset_btn)
        file_group.setLayout(file_layout)
        file_group.setMaximumHeight(120)
        left_layout.addWidget(file_group)
        
        # Main image display
        self.main_display = ImageDisplay(parent_processor=self)
        self.main_display.setAlignment(Qt.AlignCenter)
        # self.main_display.setMaximumHeight(500)
        self.main_display.setStyleSheet("")
    #     self.main_display.setStyleSheet(f"""
    #     QToolButton {{
    #         background-color: {color.name()};
    #         border: none;
    #         border-radius: 4px;
    #         padding: 5px;
    #     }}
    # """)
        self.main_display.setStyleSheet("background-color: gray;")
        self.main_display.mouseMoved.connect(self.update_magnifier)
        self.main_display.mousePressed.connect(self.handle_calibration_point)
        self.main_display.selectionComplete.connect(self.handle_selection)
        left_layout.addWidget(self.main_display, 1)
        
        # Coordinate correction controls - 所有控件在同一行
        coord_group = QGroupBox("Coordinate Correction")
        coord_layout = QHBoxLayout()  # 改用水平布局

        # X1 input with pick button
        x1_widget = QWidget()  # 用 QWidget 包装 x1_layout
        x1_layout = QHBoxLayout(x1_widget)
        self.x1_input = QLineEdit("0")
        self.x1_input.setValidator(QDoubleValidator())
        self.x1_input.editingFinished.connect(self.update_calibration_values)
        self.pick_x1_btn = QPushButton("Pick X1")
        self.pick_x1_btn.clicked.connect(lambda: self.set_calibration_mode("x1"))
        x1_layout.addWidget(QLabel("X1:"))  # 添加标签
        x1_layout.addWidget(self.x1_input)
        x1_layout.addSpacing(5)  # 添加间距
        x1_layout.addWidget(self.pick_x1_btn)
        coord_layout.addWidget(x1_widget)  # 添加到主水平布局

        # X2 input with pick button
        x2_widget = QWidget()
        x2_layout = QHBoxLayout(x2_widget)
        self.x2_input = QLineEdit("1")
        self.x2_input.setValidator(QDoubleValidator())
        self.x2_input.editingFinished.connect(self.update_calibration_values)
        self.pick_x2_btn = QPushButton("Pick X2")
        self.pick_x2_btn.clicked.connect(lambda: self.set_calibration_mode("x2"))
        x2_layout.addWidget(QLabel("X2:"))
        x2_layout.addWidget(self.x2_input)
        x2_layout.addSpacing(5)
        x2_layout.addWidget(self.pick_x2_btn)
        coord_layout.addWidget(x2_widget)

        # Y1 input with pick button
        y1_widget = QWidget()
        y1_layout = QHBoxLayout(y1_widget)
        self.y1_input = QLineEdit("0")
        self.y1_input.setValidator(QDoubleValidator())
        self.y1_input.editingFinished.connect(self.update_calibration_values)
        self.pick_y1_btn = QPushButton("Pick Y1")
        self.pick_y1_btn.clicked.connect(lambda: self.set_calibration_mode("y1"))
        y1_layout.addWidget(QLabel("Y1:"))
        y1_layout.addWidget(self.y1_input)
        y1_layout.addSpacing(5)
        y1_layout.addWidget(self.pick_y1_btn)
        coord_layout.addWidget(y1_widget)

        # Y2 input with pick button
        y2_widget = QWidget()
        y2_layout = QHBoxLayout(y2_widget)
        self.y2_input = QLineEdit("1")
        self.y2_input.setValidator(QDoubleValidator())
        self.y2_input.editingFinished.connect(self.update_calibration_values)
        self.pick_y2_btn = QPushButton("Pick Y2")
        self.pick_y2_btn.clicked.connect(lambda: self.set_calibration_mode("y2"))
        y2_layout.addWidget(QLabel("Y2:"))
        y2_layout.addWidget(self.y2_input)
        y2_layout.addSpacing(5)
        y2_layout.addWidget(self.pick_y2_btn)
        coord_layout.addWidget(y2_widget)

        coord_group.setLayout(coord_layout)
        left_layout.addWidget(coord_group)
        
        # Add left panel to main layout
        layout.addWidget(left_panel, 3)
        
        # Right panel (magnifier, color tools, and data extraction)
        right_panel = QWidget()
        right_layout = QVBoxLayout()
        right_panel.setLayout(right_layout)
        right_panel.setMaximumWidth(450)
        
        # Magnifier group
        magnifier_group = QGroupBox("Magnifier")
        mag_layout = QVBoxLayout()
        
        # Zoom factor control
        zoom_layout = QHBoxLayout()
        zoom_layout.addWidget(QLabel("Zoom:"))
        self.zoom_spin = QSpinBox()
        self.zoom_spin.setRange(1, 20)
        self.zoom_spin.setValue(5)
        self.zoom_spin.valueChanged.connect(self.update_zoom_factor)
        zoom_layout.addWidget(self.zoom_spin)
        mag_layout.addLayout(zoom_layout)
        
        # Magnifier display
        self.magnifier_display = QLabel()
        self.magnifier_display.setAlignment(Qt.AlignCenter)
        self.magnifier_display.setStyleSheet("background-color: black;")
        self.magnifier_display.setMinimumSize(200, 300)
        self.magnifier_display.setMaximumHeight(300)
        mag_layout.addWidget(self.magnifier_display)
        
        # Coordinate display below magnifier
        self.mag_coord_label = QLabel("Position: (0, 0)\nCalibrated: (0.00, 0.00)")
        self.mag_coord_label.setAlignment(Qt.AlignCenter)
        mag_layout.addWidget(self.mag_coord_label)
        
        magnifier_group.setLayout(mag_layout)
        right_layout.addWidget(magnifier_group)
        
        # Tool selection
        tool_group = QGroupBox("Tools")
        tool_layout = QHBoxLayout()
        
        # Tool mode selection
        mode_group = QButtonGroup()
        
        select_btn = QRadioButton("Box")
        select_btn.setChecked(True)
        select_btn.toggled.connect(lambda: self.set_tool_mode("select"))
        mode_group.addButton(select_btn)
        
        draw_btn = QRadioButton("Drawing")
        draw_btn.toggled.connect(lambda: self.set_tool_mode("draw"))
        mode_group.addButton(draw_btn)

        manual_btn = QRadioButton("manual")
        manual_btn.toggled.connect(lambda: self.set_tool_mode("manual"))
        mode_group.addButton(manual_btn)

        tool_layout.addWidget(select_btn)
        tool_layout.addWidget(draw_btn)
        tool_layout.addWidget(manual_btn)
        
        # Clear drawings button
        self.clear_draw_btn = QPushButton("Clear")
        self.clear_draw_btn.clicked.connect(self.clear_drawings)
        tool_layout.addWidget(self.clear_draw_btn)
        
        tool_group.setLayout(tool_layout)
        tool_group.setMaximumHeight(150)
        right_layout.addWidget(tool_group)
        
        # Color analysis group
        color_group = QGroupBox("Color Analysis")
        color_analysis_layout = QVBoxLayout()
                # Color selection
        color_layout = QHBoxLayout()
        color_layout.addWidget(QLabel("Color:"))
        
        self.color_btn = QToolButton()
        # 先完全清除主题样式
        self.color_btn.setStyleSheet("")
        # 然后应用自定义样式
        self.color_btn.setStyleSheet("""
            QToolButton {
                background-color: #FF0000;
                border: none;
                border-radius: 4px;
                padding: 5px;
            }
            /* 其他状态样式 */
        """)
        self.color_btn.clicked.connect(self.select_color)
        self.color_btn.setFixedSize(30, 30)
        color_layout.addWidget(self.color_btn)
        
        self.color_rgb = QLineEdit("255,0,0")
        self.color_rgb.editingFinished.connect(self.update_color_from_rgb)
        color_layout.addWidget(self.color_rgb)
        
        color_analysis_layout.addLayout(color_layout)

        # Dominant colors
        self.dominant_colors_label = QLabel("Dominant Colors:")
        color_analysis_layout.addWidget(self.dominant_colors_label)
        
        self.dominant_colors_layout = QHBoxLayout()
        color_analysis_layout.addLayout(self.dominant_colors_layout)
        
        # Extract data points button
        data_points_layout = QHBoxLayout()
        self.extract_btn = QPushButton("Extract Data Points")
        self.extract_btn.clicked.connect(self.extract_data_points)
        data_points_layout.addWidget(self.extract_btn)
        self.extract_save_btn = QPushButton("Save Data Points")
        self.extract_save_btn.clicked.connect(self.save_data_points)
        data_points_layout.addWidget(self.extract_save_btn)
        color_analysis_layout.addLayout(data_points_layout)
        
        
        # Resample controls
        resample_layout = QHBoxLayout()
        resample_layout.addWidget(QLabel("dx:"))
        self.resample_spin_x = QSpinBox()
        self.resample_spin_x.setRange(1, 10000)
        self.resample_spin_x.setValue(10)
        resample_layout.addWidget(self.resample_spin_x)
        resample_layout.addWidget(QLabel("dy:"))
        self.resample_spin_y = QSpinBox()
        self.resample_spin_y.setRange(1, 10000)
        self.resample_spin_y.setValue(10)
        resample_layout.addWidget(self.resample_spin_y)
        
        self.resample_btn = QPushButton("Resample")
        self.resample_btn.clicked.connect(self.resample_points)
        resample_layout.addWidget(self.resample_btn)
        color_analysis_layout.addLayout(resample_layout)
        
        # Extracted points display
        self.extracted_points_label = QLabel("Extracted Points:")
        color_analysis_layout.addWidget(self.extracted_points_label)
        
        self.extracted_points_display = QPlainTextEdit()
        # self.extracted_points_display.setReadOnly(True)  # 如果不需要编辑，设为只读
        self.extracted_points_display.setLineWrapMode(QPlainTextEdit.LineWrapMode.WidgetWidth)  # 自动换行
        # self.extracted_points_display.setWordWrapMode(QTextOption.WrapMode.WordWrap)  # 按单词换行
        color_analysis_layout.addWidget(self.extracted_points_display)
        
        color_group.setLayout(color_analysis_layout)
        right_layout.addWidget(color_group)
        
        # Add right panel to main layout
        layout.addWidget(right_panel, 1)
        
        # Status bar
        self.statusBar().showMessage("Ready to load image")
        
        # Calibration state
        self.calibration_mode = None  # "x1", "x2", "y1", "y2"
    
    def load_image(self):
        """Load an image file"""
        file_path, _ = QFileDialog.getOpenFileName(
            self, "Open Image File", "", 
            "Image Files (*.png *.jpg *.jpeg *.bmp *.tif *.tiff);;All Files (*)"
        )
        
        if file_path:
            self.image_path = file_path
            try:
                # Load with QPixmap for display
                self.original_pixmap = QPixmap(file_path)
                if self.original_pixmap.isNull():
                    raise ValueError("Failed to load image")
                
                self.current_pixmap = self.original_pixmap.copy()
                
                # Load image data for processing
                self.image_data = self.pixmap_to_numpy(self.original_pixmap)
                
                # Display images
                self.display_main_image()
                
                # Analyze dominant colors
                self.analyze_dominant_colors()
                
                # Enable controls
                self.save_btn.setEnabled(True)
                self.reset_btn.setEnabled(True)
                self.extract_btn.setEnabled(True)
                
                self.statusBar().showMessage(f"Loaded: {os.path.basename(file_path)}")
                
            except Exception as e:
                QMessageBox.warning(self, "Error", f"Failed to load image: {str(e)}")
                self.statusBar().showMessage("Error loading image")
    def pixmap_to_numpy(self, pixmap):
        """Convert QPixmap to numpy array"""
        image = pixmap.toImage()
        width = image.width()
        height = image.height()
        
        buffer = bytes(image.constBits())
        arr = np.frombuffer(buffer, dtype=np.uint8).reshape((height, width, 4))
        return arr[:, :, :3]  # Return RGB only
    
    def display_main_image(self):
        """Display the main image with proper scaling"""
        if not self.current_pixmap or self.current_pixmap.isNull():
            return
        
        TOLERANCE = 0  # 误差范围
        # Calculate available space
        margins = self.main_display.contentsMargins()
        label_width = self.main_display.width() - margins.left() - margins.right()
        label_height = self.main_display.height() - margins.top() - margins.bottom() - TOLERANCE
        
        # Ensure minimum dimensions
        label_width = max(10, label_width)
        label_height = max(10, label_height)
        
        # Scale image maintaining aspect ratio
        self.scaled_pixmap = self.current_pixmap.scaled(
            label_width, label_height,
            Qt.KeepAspectRatio,
            Qt.SmoothTransformation
        )
        self.main_display.setPixmap(self.scaled_pixmap)
        # self.main_display.setScaledContents(True)
    def update_magnifier(self, pos):
        """Update the magnifier view based on cursor position"""
        if not self.scaled_pixmap or self.scaled_pixmap.isNull():
            return
        
        self.cursor_pos = pos
        pixmap = self.scaled_pixmap
        
        # Calculate position in original image coordinates
        img_rect = pixmap.rect()
        disp_rect = self.main_display.contentsRect()
        offset_x = (disp_rect.width() - img_rect.width()) // 2
        offset_y = (disp_rect.height() - img_rect.height()) // 2
        
        # Adjust for centering
        adj_pos = QPoint(pos.x() - offset_x, pos.y() - offset_y)
        
        # Only proceed if cursor is over the image
        if not img_rect.contains(adj_pos):
            self.magnifier_display.clear()
            return
        
        # Calculate zoom area
        size = 100  # Size of the area to magnify (in original display pixels)
        zoom_size = size * self.zoom_factor
        
        # Create magnified pixmap
        magnified = QPixmap(zoom_size, zoom_size)
        magnified.fill(Qt.transparent)
        
        painter = QPainter(magnified)
        
        # Draw the zoomed portion
        source_rect = QRect(adj_pos.x() - size//2, adj_pos.y() - size//2, size, size)
        target_rect = QRect(0, 0, zoom_size, zoom_size)
        painter.drawPixmap(target_rect, pixmap, source_rect)
        
        # Draw crosshair
        painter.setPen(QPen(Qt.red, 2))
        painter.drawLine(zoom_size//2, 0, zoom_size//2, zoom_size)
        painter.drawLine(0, zoom_size//2, zoom_size, zoom_size//2)
        
        painter.end()
        
        self.magnifier_display.setPixmap(magnified)
        
        # Update coordinate display
        if self.image_data is not None:
            # Get actual pixel coordinates in original image
            orig_width = self.current_pixmap.width()
            orig_height = self.current_pixmap.height()
            
            x = adj_pos.x() / img_rect.width() * orig_width
            y = adj_pos.y() / img_rect.height() * orig_height
            
            # Calculate calibrated coordinates
            cal_x, cal_y = self.calculate_calibrated_coords(x, y)
            
            self.mag_coord_label.setText(
                f"Position: ({int(x)}, {int(y)})\n"
                f"Calibrated: ({cal_x:.2f}, {cal_y:.2f})"
            )
    
    def calculate_calibrated_coords(self, x, y):
        """Calculate calibrated coordinates based on calibration points"""
        x1_point = self.main_display.calibration_points['x1']['pos']
        x2_point = self.main_display.calibration_points['x2']['pos']
        y1_point = self.main_display.calibration_points['y1']['pos']
        y2_point = self.main_display.calibration_points['y2']['pos']
        # self.calculate_transformation()
        # Default to simple scaling if not all points are set
        if None in [x1_point, x2_point, y1_point, y2_point]:
            orig_width = self.current_pixmap.width()
            orig_height = self.current_pixmap.height()
            
            cal_x = self.calibration_values['x1'] + (x / orig_width) * (
                self.calibration_values['x2'] - self.calibration_values['x1'])
            cal_y = self.calibration_values['y1'] + (y / orig_height) * (
                self.calibration_values['y2'] - self.calibration_values['y1'])
            return cal_x, cal_y
        
        # Convert display points to original image coordinates
        img_rect = self.scaled_pixmap.rect()
        disp_rect = self.main_display.contentsRect()
        offset_x = (disp_rect.width() - img_rect.width()) // 2
        offset_y = (disp_rect.height() - img_rect.height()) // 2
        
        def display_to_original(disp_point):
            if disp_point is None:
                return None
            orig_x = (disp_point.x() - offset_x) / img_rect.width() * self.current_pixmap.width()
            orig_y = (disp_point.y() - offset_y) / img_rect.height() * self.current_pixmap.height()
            return (orig_x, orig_y)
        
        x1_orig = display_to_original(x1_point)
        x2_orig = display_to_original(x2_point)
        y1_orig = display_to_original(y1_point)
        y2_orig = display_to_original(y2_point)
        
        # Calculate x coordinate
        if x1_orig and x2_orig and x1_orig[0] != x2_orig[0]:  # Avoid division by zero
            x_ratio = (x - x1_orig[0]) / (x2_orig[0] - x1_orig[0])
            cal_x = self.calibration_values['x1'] + x_ratio * (
                self.calibration_values['x2'] - self.calibration_values['x1'])
        else:
            cal_x = self.calibration_values['x1']
        
        # Calculate y coordinate
        if y1_orig and y2_orig and y1_orig[1] != y2_orig[1]:  # Avoid division by zero
            y_ratio = (y - y1_orig[1]) / (y2_orig[1] - y1_orig[1])
            cal_y = self.calibration_values['y1'] + y_ratio * (
                self.calibration_values['y2'] - self.calibration_values['y1'])
        else:
            cal_y = self.calibration_values['y1']
        
        return cal_x, cal_y
    def calculate_transformation(self):
        """计算坐标转换参数"""
        # 获取原始坐标
        x1_pixel = self.main_display.calibration_points['x1']['pos'].x()
        x2_pixel = self.main_display.calibration_points['x2']['pos'].x()
        y1_pixel = self.main_display.calibration_points['y1']['pos'].y()
        y2_pixel = self.main_display.calibration_points['y2']['pos'].y()
        
        # 计算x轴转换参数 (y = a*x_pixel + b)
        # 使用点1和点2
        A_x = np.array([[x1_pixel, 1], [x2_pixel, 1]])
        b_x = np.array([self.calibration_values['x1'], self.calibration_values['x2']])
        self.x_params = np.linalg.solve(A_x, b_x)
        
        # 计算y轴转换参数 (y = c*y_pixel + d)
        # 使用点3和点4
        A_y = np.array([[y1_pixel, 1], [y2_pixel, 1]])
        b_y = np.array([self.calibration_values['y1'], self.calibration_values['y2']])
        self.y_params = np.linalg.solve(A_y, b_y)
        
        # print("\n转换参数:")
        # print(f"x_new = {self.x_params[0]:.4f} * x_pixel + {self.x_params[1]:.4f}")
        # print(f"y_new = {self.y_params[0]:.4f} * y_pixel + {self.y_params[1]:.4f}")
    
    def transform_points(self,points):
        """转换所有点到新坐标系"""
        self.transformed_points = []
        if self.x_params is None or self.y_params is None:
            return points
        for qpoint in points:
            x_new = self.x_params[0] * qpoint.x() + self.x_params[1]
            y_new = self.y_params[0] * qpoint.y() + self.y_params[1]
            self.transformed_points.append((x_new, y_new))
        return self.transformed_points

    def set_calibration_mode(self, mode):
        """Set the calibration mode for picking points"""
        self.calibration_mode = mode
        self.main_display.mode = "calibrate"
        self.statusBar().showMessage(f"Click on image to select {mode} point")
    
    def handle_calibration_point(self, pos):
        """Handle calibration point selection"""
        if self.calibration_mode is None:
            return
        
        if not self.scaled_pixmap or self.scaled_pixmap.isNull():
            return
        
        # Calculate position in original image coordinates
        img_rect = self.scaled_pixmap.rect()
        disp_rect = self.main_display.contentsRect()
        offset_x = (disp_rect.width() - img_rect.width()) // 2
        offset_y = (disp_rect.height() - img_rect.height()) // 2
        adj_pos = QPoint(pos.x() - offset_x, pos.y() - offset_y)
        if img_rect.contains(adj_pos):
            # Get the current value from the input field
            try:
                if self.calibration_mode == "x1":
                    value = float(self.x1_input.text())
                    self.calibration_values['x1'] = value
                    self.main_display.set_calibration_point('x1', pos)
                elif self.calibration_mode == "x2":
                    value = float(self.x2_input.text())
                    self.calibration_values['x2'] = value
                    self.main_display.set_calibration_point('x2', pos)
                elif self.calibration_mode == "y1":
                    value = float(self.y1_input.text())
                    self.calibration_values['y1'] = value
                    self.main_display.set_calibration_point('y1', pos)
                elif self.calibration_mode == "y2":
                    value = float(self.y2_input.text())
                    self.calibration_values['y2'] = value
                    self.main_display.set_calibration_point('y2', pos)
            except ValueError:
                QMessageBox.warning(self, "Error", "Please enter a valid number for the coordinate")
                return
            # Reset calibration mode
            self.calibration_mode = None
            self.main_display.mode = "select"
            # self.statusBar().showMessage(f"{self.calibration_mode.upper()} point set")
    
    def update_calibration_values(self):
        """Update calibration values from input fields"""
        try:
            self.calibration_values['x1'] = float(self.x1_input.text())
            self.calibration_values['x2'] = float(self.x2_input.text())
            self.calibration_values['y1'] = float(self.y1_input.text())
            self.calibration_values['y2'] = float(self.y2_input.text())
        except ValueError:
            pass
        self.calculate_transformation()        
    def set_tool_mode(self, mode):
        """Set the current tool mode"""
        self.main_display.mode = mode
        if mode == "select":
            self.statusBar().showMessage("Selection tool: Drag to select area")
        elif mode == "draw":
            self.statusBar().showMessage("Drawing tool: Click and drag to draw")
        elif mode == "manual":
            self.statusBar().showMessage("Manual: Click on image to manual mark")
    
    def select_color(self):
        """Open color dialog to select a color"""
        color = QColorDialog.getColor()
        if color.isValid():
            self.main_display.pen_color = color
            self.color_btn.setStyleSheet("")
            self.color_btn.setStyleSheet(f"""
                    QToolButton {{
                        background-color: {color.name()};
                        border: none;
                        border-radius: 4px;
                        padding: 5px;
                    }}
                    QToolButton:hover {{
                        background-color: rgb({max(0, color.red()-30)},{max(0, color.green()-30)},{max(0, color.blue()-30)});
                    }}
                """)
            self.color_rgb.setText(f"{color.red()},{color.green()},{color.blue()}")
    
    def update_color_from_rgb(self):
        """Update color from RGB text input"""
        try:
            r, g, b = map(int, self.color_rgb.text().split(','))
            color = QColor(r, g, b)
            if color.isValid():
                self.main_display.pen_color = color
                self.color_btn.setStyleSheet("")
                self.color_btn.setStyleSheet(f"""
                    QToolButton {{
                        background-color: {color.name()};
                        border: none;
                        border-radius: 4px;
                        padding: 5px;
                    }}
                    QToolButton:hover {{
                        background-color: rgb({max(0, r-30)},{max(0, g-30)},{max(0, b-30)});
                    }}
                """)        
        except ValueError:
            pass
    
    def clear_drawings(self):
        """Clear all drawings"""
        self.main_display.clear_drawing()


    def analyze_dominant_colors(self, n_colors=5, white_threshold=240,step=8):
        """Analyze and display dominant colors in the image"""
        if self.image_data is None:
            return
        # Clear previous colors
        for i in reversed(range(self.dominant_colors_layout.count())): 
            self.dominant_colors_layout.itemAt(i).widget().setParent(None)
        # 读取图片并转换为NumPy数组
        img = Image.open(self.image_path).convert("RGB")
        self.pixels = np.array(img)  # shape: (H, W, 3)
        # 读取图片并转换为NumPy数组（通过QPixmap）
        # pixels = self.image_data
        # if pixels.ndim == 4:  # 若包含Alpha通道（RGBA格式）
        #     pixels = pixels[..., :3]  # 仅保留RGB通道
        # 1. 去除白色背景（向量化操作）
        not_white_mask = ~(self.pixels >= white_threshold).all(axis=-1)
        pixels =  self.pixels[not_white_mask]
        # 2. 按步长合并颜色（向量化操作）
        quantized_pixels = (pixels // step) * step
        # 3. 统计颜色频率（使用np.unique加速）
        colors, counts = np.unique(
            quantized_pixels, 
            axis=0, 
            return_counts=True
        )
        # 4. 按频率排序并返回前N名
        sorted_indices = np.argsort(-counts)
        top_colors = colors[sorted_indices][:n_colors]

        # Create color buttons
        for color in top_colors:
            r, g, b = color
            color_btn = QToolButton()
            color_btn.setStyleSheet('')
            color_btn.setStyleSheet(f"""
            QToolButton {{
                background-color: rgb({r},{g},{b});
                border: none;
                border-radius: 4px;
                padding: 5px;
            }}
            QToolButton:hover {{
                background-color: rgb({max(0, r-30)},{max(0, g-30)},{max(0, b-30)});
            }}
        """)
            color_btn.setFixedSize(30, 30)
            color_btn.clicked.connect(lambda _, c=color: self.select_dominant_color(c))
            self.dominant_colors_layout.addWidget(color_btn)
        
        self.dominant_colors_label.setText(f"Dominant Colors ({n_colors}):")
    
    def select_dominant_color(self, color):
        """Select a dominant color"""
        r, g, b = color
        qcolor = QColor(r, g, b)
        self.main_display.pen_color = qcolor
        self.color_btn.setStyleSheet(f"""
            QToolButton {{
                background-color: {qcolor.name()};
                border: none;
                border-radius: 4px;
                padding: 5px;
            }}
            QToolButton:hover {{
                background-color: rgb({max(0, r-30)},{max(0, g-30)},{max(0, b-30)});
            }}
        """)
        self.color_rgb.setText(f"{r},{g},{b}")
    
    def save_data_points(self):
        """Save the extracted data points to a file"""
        if not self.transformed_points:
            QMessageBox.warning(self, "Warning", "No data points to save.")
            return
        file_path, _ = QFileDialog.getSaveFileName(
            self, "Save Data Points", "", "Text Files (*.txt)"
        )
        if file_path:
            try:
                with open(file_path, 'w') as f:
                    for point in self.transformed_points:
                        f.write(f"{point[0]:.4f},{point[1]:.4f}\n")
                QMessageBox.information(self, "Success", "Data points saved successfully.")
            except Exception as e:
                QMessageBox.critical(self, "Error", f"Failed to save data points: {str(e)}")
    def extract_data_points(self,sample_num = 100):
        """Extract data points based on selected colors and regions"""
        if self.image_data is None:
            return
        
        # Get the current color to extract
        try:
            r, g, b = map(int, self.color_rgb.text().split(','))
            target_color = np.array([r, g, b])
        except ValueError:
            QMessageBox.warning(self, "Error", "Please select a valid color first")
            return
        
        # Convert image to numpy array if not already
        image_array = self.pixels
        
        # Calculate color distances
        color_distances = np.sqrt(
            np.sum((image_array - target_color) ** 2, axis=2)
        )
        
        # Find pixels within threshold
        threshold = 50  # Adjust as needed
        matches = color_distances < threshold
        
        # Get coordinates of matching pixels
        y_coords, x_coords = np.where(matches)
        x_coords,y_coords = self.random_downsample(x_coords,y_coords,target_count=10000)

        img_rect = self.scaled_pixmap.rect()
        disp_rect = self.main_display.contentsRect()
        offset_x = (disp_rect.width() - img_rect.width()) // 2
        offset_y = (disp_rect.height() - img_rect.height()) // 2
        pixel_scale_x = self.current_pixmap.width() / img_rect.width()
        pixel_scale_y = self.current_pixmap.height() / img_rect.height()
        # 均匀采样最多1000个点，优先保留颜色更接近的点
        x_coords, y_coords = self.uniform_sample_points(x_coords, y_coords, offset_x = offset_x,offset_y = offset_y,pixel_scale_x = pixel_scale_x,pixel_scale_y=pixel_scale_y,max_points=10000)
        # Convert to display coordinates for visualization

        display_points = []
        for x, y in zip(x_coords, y_coords):
            # Convert to display coordinates
            disp_x = (x / self.current_pixmap.width()) * img_rect.width() + offset_x
            disp_y = (y / self.current_pixmap.height()) * img_rect.height() + offset_y
            display_points.append(QPoint(int(disp_x), int(disp_y)))
        
        # Store pixel coordinates for coordinate calculation
        # self.extracted_points_pixels = list(zip(x_coords, y_coords))
        
        # Update display with extracted points
        self.main_display.set_extracted_points(display_points)
        self.update_extracted_points_display(self.transform_points(display_points))
    def uniform_sample_points(self, x_coords, y_coords,offset_x = 0,offset_y = 0, max_points=1000, dx=None, dy=None,rect = None,path = None,pixel_scale_x = 1,pixel_scale_y=1):
        """
        优化版采样函数 - 基于dx/dy间隔采样，支持矩形区域或绘制线条区域过滤
        
        参数:
            x_coords, y_coords: 匹配点的坐标（NumPy数组）
            max_points: 最大返回点数
            dx: 横坐标采样间隔（必须指定）
            dy: 纵坐标采样间隔（必须指定）
            rect: QRect，只采样矩形区域内的点（与path二选一）
            path: QPainterPath，只采样线条绘制区域内的点（与rect二选一）
            line_width: 路径采样宽度（像素）
        
        返回:
            sampled_x, sampled_y: 采样后的坐标
        """
        dx = self.resample_spin_x.value()
        dy = self.resample_spin_y.value()
        # 参数校验
        if dx is None or dy is None:
            raise ValueError("dx and dy must be specified")
        x_coords,y_coords = self.sample_by_dxdy(dx=dx,dy=dy,x_coords=x_coords,y_coords=y_coords)
        if self.main_display.mode =='select':
            return self.sample_in_rect(x_coords, y_coords, self.main_display.selection_rect,offset_x = offset_x,offset_y = offset_y,pixel_scale_x = pixel_scale_x,pixel_scale_y=pixel_scale_y)
        if self.main_display.mode =='draw':
            return self.sample_near_path(x_coords, y_coords, self.main_display.committed_path,line_width=self.main_display.pen_width,offset_x = offset_x,offset_y = offset_y,pixel_scale_x = pixel_scale_x,pixel_scale_y=pixel_scale_y)

    def random_downsample(self, x_coords, y_coords, target_count=1000):
        """
        高性能随机下采样函数
        
        参数:
            x_coords, y_coords: 原始坐标数组
            target_count: 目标点数
        
        返回:
            下采样后的坐标数组
        """
        if len(x_coords) <= target_count:
            return x_coords, y_coords
        
        # 使用numpy的随机选择功能
        indices = np.random.choice(len(x_coords), target_count, replace=False)
        return x_coords[indices], y_coords[indices]
    def sample_by_dxdy(self, x_coords, y_coords, dx, dy):
        """
        高性能dx/dy间隔采样函数
        
        参数:
            x_coords, y_coords: 坐标数组
            dx, dy: 采样间隔
            max_points: 最大返回点数(可选)
        
        返回:
            采样后的坐标数组
        """
        # 先合并坐标并转换为float32提高性能
        coords = np.column_stack((x_coords.astype(np.float32), 
                                y_coords.astype(np.float32)))
        
        # 生成网格中心点坐标(向量化操作)
        x_min, y_min = np.min(coords, axis=0)
        x_max, y_max = np.max(coords, axis=0)
        
        x_centers = np.arange(x_min + dx/2, x_max, dx)
        y_centers = np.arange(y_min + dy/2, y_max, dy)
        
        # 创建网格中心点矩阵
        grid_centers = np.array(np.meshgrid(x_centers, y_centers)).T.reshape(-1, 2)
        
        # 使用KDTree加速最近邻搜索
        from scipy.spatial import cKDTree
        tree = cKDTree(coords)
        _, indices = tree.query(grid_centers, k=1)
        
        # 处理结果
        valid_mask = indices < len(coords)
        unique_indices = np.unique(indices[valid_mask])
        sampled_coords = coords[unique_indices]
        return sampled_coords[:, 0], sampled_coords[:, 1]

    def sample_in_rect(self, x_coords, y_coords, rect, offset_x = 0,offset_y = 0,max_points=None,pixel_scale_x = 1,pixel_scale_y=1):
        """
        矩形区域内采样函数
        
        参数:
            x_coords, y_coords: 坐标数组
            rect: QRect区域
            max_points: 最大返回点数(可选)
        
        返回:
            矩形区域内的坐标数组
        """
        if rect is None:
            return x_coords,y_coords
        # 向量化矩形区域检查
        # 应用缩放比例
        left = (rect.left() - offset_x) * pixel_scale_x
        top = (rect.top() - offset_y) * pixel_scale_y
        right = (rect.right() - offset_x) * pixel_scale_x
        bottom = (rect.bottom() - offset_y) * pixel_scale_y
        
        # 使用布尔索引快速过滤
        mask = ((x_coords >= left) & (x_coords <= right) & 
                (y_coords >= top) & (y_coords <= bottom))
        return x_coords[mask],y_coords[mask]

    def sample_near_path(self, x_coords, y_coords, path, line_width,offset_x = 0,offset_y = 0, max_points=None,pixel_scale_x = 1,pixel_scale_y=1):
        """
        路径附近采样函数
        
        参数:
            x_coords, y_coords: 坐标数组
            path: QPainterPath
            line_width: 采样宽度
            max_points: 最大返回点数(可选)
        
        返回:
            路径附近的坐标数组
        """
        if path is None:
            return x_coords,y_coords
        # 2. 准备加宽后的路径用于精确检测
        stroke_path = QPainterPathStroker()
        stroke_path.setWidth(line_width)
        widened_path = stroke_path.createStroke(path)
        # 3. 批量检查点是否在加宽后的路径内
        # 转换为QPointF数组(分批处理避免内存问题)
        batch_size = 1000
        in_path_mask = np.zeros(len(x_coords), dtype=bool)
        x_s,y_s = (x_coords)/pixel_scale_x+offset_x,(y_coords)/pixel_scale_y+offset_y
        for i in range(0, len(x_coords), batch_size):
            batch_x = x_s[i:i+batch_size]
            batch_y = y_s[i:i+batch_size]
            points = [QPointF(x, y) for x, y in zip(batch_x, batch_y)]
            in_path_mask[i:i+batch_size] = [widened_path.contains(p) for p in points]
        
        return  x_coords[in_path_mask], y_coords[in_path_mask]


    def resample_points(self,cords,sample_num = 0,grid_size=50):
        """Resample the extracted points to a specified number"""
        if not self.main_display.extracted_points:
            QMessageBox.warning(self, "Warning", "No points to resample")
            return
        self.statusBar().showMessage(f"Resampled to {len(self.extracted_points)} points")
    
    def update_extracted_points_display(self, points):
        """Update the display of extracted points"""
        MAX_DIS_NUM = 1000
        if len(points) < MAX_DIS_NUM:
            points_text = "\n".join(f"({x:.2f}, {y:.2f})" for x, y in points)
        else:
            points_text = "\n".join(f"({x:.2f}, {y:.2f})" for x, y in points[:MAX_DIS_NUM]) + \
                        f"\n...and {len(points)-MAX_DIS_NUM} more"
        self.extracted_points_display.setPlainText(points_text)
    
    def handle_selection(self, rect):
        """Handle selection rectangle"""
        if self.main_display.mode == "select":
            self.statusBar().showMessage(f"Selected area: {rect.width()}x{rect.height()} pixels")
    
    def update_zoom_factor(self, value):
        """Update zoom factor for magnifier"""
        self.zoom_factor = value
    
    def save_image(self):
        """Save the processed image"""
        if not self.current_pixmap or not self.image_path:
            return
        
        file_path, _ = QFileDialog.getSaveFileName(
            self, "Save Image", self.image_path,
            "PNG Image (*.png);;JPEG Image (*.jpg *.jpeg);;BMP Image (*.bmp);;TIFF Image (*.tif *.tiff)"
        )
        
        if file_path:
            try:
                success = self.current_pixmap.save(file_path)
                if success:
                    self.statusBar().showMessage(f"Saved to: {file_path}")
                else:
                    QMessageBox.warning(self, "Error", "Failed to save image")
            except Exception as e:
                QMessageBox.warning(self, "Error", f"Failed to save image: {str(e)}")
    
    def reset_image(self):
        """Reset to original image"""
        if self.original_pixmap:
            self.current_pixmap = self.original_pixmap.copy()
            self.display_main_image()
            self.statusBar().showMessage("Image reset to original")
    
    def resizeEvent(self, event):
        """Handle window resize"""
        super().resizeEvent(event)
        self.display_main_image()
        if self.cursor_pos:
            self.update_magnifier(self.cursor_pos)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    theme = 'light_blue.xml'
    # self.ui.setWindowIcon(QIcon("resources/icons/logo.svg"))
    # Set theme on in itialization
    apply_stylesheet(
        app,
        theme ,
        invert_secondary=('light' in theme and 'dark' not in theme),
    )
    window = ImageProcessor()
    window.showMaximized()
    sys.exit(app.exec())