Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

COO-24 Add ability to view histograms #5

Merged
merged 2 commits into from
Sep 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 30 additions & 20 deletions fits_viewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
"""
Expand Down Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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_()
4 changes: 4 additions & 0 deletions fits_viewer_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
144 changes: 144 additions & 0 deletions histogram.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# -----------------------------------------------------------------------------
# @file histogram.py
# @brief A class for displaying histograms of images with interactive bin count adjustment and image navigation.
# @author Prakriti Gupta <[email protected]>
# -----------------------------------------------------------------------------

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):
"""
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.
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()
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):
"""
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()
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
astropy
numpy
PyQt5
matplotlib
2 changes: 1 addition & 1 deletion viewfinder.py
Original file line number Diff line number Diff line change
@@ -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 <[email protected]>
# -----------------------------------------------------------------------------
Expand Down
Loading