From 66a2ea7ef9cac678877096004fdc1d169bfcfa19 Mon Sep 17 00:00:00 2001 From: Prakriti Gupta Date: Fri, 30 Aug 2024 14:19:54 -0700 Subject: [PATCH 1/2] COO-24 Add ability to view histograms --- fits_viewer.py | 50 ++++++++++++--------- fits_viewer_ui.py | 4 ++ histogram.py | 111 ++++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 1 + viewfinder.py | 2 +- 5 files changed, 147 insertions(+), 21 deletions(-) create mode 100644 histogram.py diff --git a/fits_viewer.py b/fits_viewer.py index 747bd69..29a65bc 100644 --- a/fits_viewer.py +++ b/fits_viewer.py @@ -9,15 +9,14 @@ # Standard Library Imports import os -import sys -import glob from fits_viewer_ui import FITSViewerUI from image_utils import normalize_image, convert_to_qimage from viewfinder import ViewfinderPopup +from histogram import Histogram # Third-Party Library Imports -from PyQt5.QtWidgets import QMainWindow, QLabel, QVBoxLayout, QHBoxLayout, QPushButton, QSlider, QTextEdit, QFileDialog, QDesktopWidget +from PyQt5.QtWidgets import QFileDialog, QDesktopWidget from PyQt5.QtCore import Qt, QSize, QEvent, QRect from PyQt5.QtGui import QPixmap, QImage from astropy.io import fits @@ -43,16 +42,6 @@ def __init__(self): self.reset_image = None self.update_subtraction = False self.image_dir = None - self.setup_connections() - - def setup_connections(self): - self.match_mode_action.toggled.connect(self.toggle_match_mode) - self.show_header_action.triggered.connect(self.show_header_tab) - self.slider.valueChanged.connect(self.adjust_contrast) - self.subtract_signal_action.triggered.connect(self.subtract_from_images) - - # Add a connection to open the viewfinder popup - self.viewfinder_action.triggered.connect(self.open_viewfinder_popup) def open_fits_image(self): """ @@ -162,13 +151,13 @@ def update_result_label_size(self): """ Adjust result_label to match the size of image_label1 and image_label2 """ - #if self.image_label1.pixmap(): - size = self.image_label1.pixmap().size() - self.result_label.setFixedSize(size) - self.result_label.setPixmap(self.result_label.pixmap().scaled(size, Qt.KeepAspectRatio, Qt.SmoothTransformation)) - # else: - # # Default size if image_label1 has no pixmap - # self.result_label.setFixedSize(1000, 500) # or any default size you prefer + if self.image_label1.pixmap(): + size = self.image_label1.pixmap().size() + self.result_label.setFixedSize(size) + self.result_label.setPixmap(self.result_label.pixmap().scaled(size, Qt.KeepAspectRatio, Qt.SmoothTransformation)) + else: + # Default size if image_label1 has no pixmap + self.result_label.setFixedSize(1000, 500) # or any default size you prefer def display_image(self, pixmap): """ @@ -612,3 +601,24 @@ def update_viewfinder(self): # Update the viewfinder popup with the cropped pixmap self.viewfinder_popup.set_image(cropped_pixmap) + + def show_histogram(self): + """ + Opens a histogram dialog for the cached images and result image. + """ + if None in self.cached_images and self.result_image is None: + print("No images available for histogram.") + return + + # Combine cached images and result image into one list + images_to_display = [img for img in self.cached_images if img is not None] + if self.result_image is not None: + images_to_display.append(QPixmap.fromImage(self.result_image)) + + if not images_to_display: + print("No valid images available for histogram.") + return + + # Create and show the histogram dialog + histogram_dialog = Histogram(images_to_display, self) + histogram_dialog.exec_() diff --git a/fits_viewer_ui.py b/fits_viewer_ui.py index d251562..cf41e7e 100644 --- a/fits_viewer_ui.py +++ b/fits_viewer_ui.py @@ -230,6 +230,10 @@ def create_tools_menu(self): self.show_header_action.triggered.connect(self.show_header_tab) self.tools_menu.addAction(self.show_header_action) + self.show_histogram_action = QAction("Histogram", self) + self.show_histogram_action.triggered.connect(self.show_histogram) + self.tools_menu.addAction(self.show_histogram_action) + def create_view_menu(self): """ Creates and configures the View menu. diff --git a/histogram.py b/histogram.py new file mode 100644 index 0000000..da863e3 --- /dev/null +++ b/histogram.py @@ -0,0 +1,111 @@ +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas +from PyQt5.QtWidgets import QDialog, QVBoxLayout, QSlider, QLabel, QPushButton, QHBoxLayout +from PyQt5.QtCore import Qt + +class Histogram(QDialog): + """ + A class for displaying histograms of images with interactive bin count adjustment and image navigation. + """ + def __init__(self, images, parent=None): + super().__init__(parent) + self.images = images # List of QPixmap objects + self.current_image_index = 0 # Start with the first image + + self.setWindowTitle("Image Histograms") + + # Create layout + layout = QVBoxLayout() + self.setLayout(layout) + + # Create a figure and axis for Matplotlib + self.figure, self.ax = plt.subplots(figsize=(8, 6)) + self.canvas = FigureCanvas(self.figure) + layout.addWidget(self.canvas) + + # Create a slider for adjusting the number of bins + self.slider = QSlider(Qt.Horizontal) + self.slider.setMinimum(1) + self.slider.setMaximum(256) + self.slider.setValue(256) + self.slider.setTickInterval(1) + self.slider.setTickPosition(QSlider.TicksBelow) + self.slider.valueChanged.connect(self.update_histogram) + layout.addWidget(self.slider) + + # Create a label to display current bin count + self.bin_label = QLabel() + layout.addWidget(self.bin_label) + + # Create navigation buttons + nav_layout = QHBoxLayout() + self.prev_button = QPushButton("Previous") + self.next_button = QPushButton("Next") + self.prev_button.clicked.connect(self.show_prev_image) + self.next_button.clicked.connect(self.show_next_image) + nav_layout.addWidget(self.prev_button) + nav_layout.addWidget(self.next_button) + layout.addLayout(nav_layout) + + # Update initial histogram + self.update_histogram() + + def update_histogram(self): + """ + Updates the histogram plot based on the current bin count from the slider. + """ + self.ax.clear() + + # Get the current image data + image_data = self.pixmap_to_array(self.images[self.current_image_index]) + + # Flatten the image data to 1D + image_data_flat = image_data.flatten() + + # Compute the histogram with the current number of bins + bin_count = self.slider.value() + histogram, bin_edges = np.histogram(image_data_flat, bins=bin_count, range=(np.min(image_data_flat), np.max(image_data_flat))) + + # Plot the histogram with color-coded bins + colors = plt.cm.viridis(np.linspace(0, 1, bin_count)) # Use a colormap for colors + self.ax.bar(bin_edges[:-1], histogram, width=np.diff(bin_edges), color=colors, edgecolor='black') + + # Set labels and title + self.ax.set_title('Histogram of Pixel Intensities') + self.ax.set_xlabel('Pixel Intensity') + self.ax.set_ylabel('Frequency') + self.ax.grid(True) + + # Update the bin count label + self.bin_label.setText(f'Number of Bins: {bin_count}') + + # Draw the canvas + self.canvas.draw() + + def pixmap_to_array(self, pixmap): + """ + Converts a QPixmap to a numpy array. + """ + image = pixmap.toImage() + width, height = image.width(), image.height() + ptr = image.bits() + ptr.setsize(height * width * 4) # 4 bytes per pixel (RGBA) + arr = np.array(ptr).reshape((height, width, 4)) # RGBA image + return arr[:, :, 0] # Use the red channel or convert to grayscale if needed + + def show_prev_image(self): + """ + Shows the previous image in the list. + """ + if self.current_image_index > 0: + self.current_image_index -= 1 + self.update_histogram() + + def show_next_image(self): + """ + Shows the next image in the list. + """ + if self.current_image_index < len(self.images) - 1: + self.current_image_index += 1 + self.update_histogram() diff --git a/requirements.txt b/requirements.txt index a11b0a1..79b094b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ astropy numpy PyQt5 +matplotlib \ No newline at end of file diff --git a/viewfinder.py b/viewfinder.py index 48f09fe..405e6ca 100644 --- a/viewfinder.py +++ b/viewfinder.py @@ -1,5 +1,5 @@ # ----------------------------------------------------------------------------- -# @file fits_viewer_ui.py +# @file viewfinder.py # @brief A dialog window for displaying and selecting between different image views. # @author Prakriti Gupta # ----------------------------------------------------------------------------- From c5c5596779e19c9f3069d8345360d1b0c305c633 Mon Sep 17 00:00:00 2001 From: Prakriti Gupta Date: Fri, 30 Aug 2024 14:48:27 -0700 Subject: [PATCH 2/2] Enhance pixmap to array --- histogram.py | 39 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/histogram.py b/histogram.py index da863e3..f81cf60 100644 --- a/histogram.py +++ b/histogram.py @@ -1,8 +1,15 @@ +# ----------------------------------------------------------------------------- +# @file histogram.py +# @brief A class for displaying histograms of images with interactive bin count adjustment and image navigation. +# @author Prakriti Gupta +# ----------------------------------------------------------------------------- + import numpy as np import matplotlib.pyplot as plt from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas from PyQt5.QtWidgets import QDialog, QVBoxLayout, QSlider, QLabel, QPushButton, QHBoxLayout from PyQt5.QtCore import Qt +from PyQt5.QtGui import QImage class Histogram(QDialog): """ @@ -86,13 +93,39 @@ def update_histogram(self): def pixmap_to_array(self, pixmap): """ Converts a QPixmap to a numpy array. + Handles both monochrome (grayscale), RGB, RGBA, and multispectral images. """ + # Convert QPixmap to QImage image = pixmap.toImage() width, height = image.width(), image.height() + + # Convert to numpy array ptr = image.bits() - ptr.setsize(height * width * 4) # 4 bytes per pixel (RGBA) - arr = np.array(ptr).reshape((height, width, 4)) # RGBA image - return arr[:, :, 0] # Use the red channel or convert to grayscale if needed + if image.format() == QImage.Format_Grayscale8: + # Monochrome image (8-bit grayscale) + ptr.setsize(width * height) # 1 byte per pixel + arr = np.array(ptr).reshape((height, width)) + else: + # RGB or RGBA image (8-bit per channel) + ptr.setsize(width * height * 4) # 4 bytes per pixel (RGBA) + arr = np.array(ptr).reshape((height, width, 4)) # RGBA image + + # Handle different formats + if image.format() == QImage.Format_RGB888: + arr = arr[:, :, :3] # Extract RGB channels + elif image.format() == QImage.Format_RGBA8888: + arr = arr[:, :, :3] # Use RGB channels only + else: + # Handle multispectral images + if arr.shape[2] > 3: + # Multispectral image with more than 3 channels + # You can choose how to handle this, e.g., extract specific bands or process all bands + # For demonstration, we will use all bands + pass + else: + raise ValueError("Unsupported image format") + + return arr def show_prev_image(self): """