diff --git a/Chapter_5/blink_comparator.py b/Chapter_5/blink_comparator.py index 5de38d6..cc70511 100644 --- a/Chapter_5/blink_comparator.py +++ b/Chapter_5/blink_comparator.py @@ -1,3 +1,11 @@ +# -*- coding: utf-8 -*- +""" +Created on Tue Dec 30 09:42:19 2025 + +@author: mitoa +""" +# Edited to use f-string and solve the hang issue (the window does not end properly when run on Spyder6.) + import os from pathlib import Path import numpy as np @@ -6,92 +14,79 @@ MIN_NUM_KEYPOINT_MATCHES = 50 def main(): - """Loop through 2 folders with paired images, register and blink images.""" - night1_files = sorted(os.listdir('night_1')) - night2_files = sorted(os.listdir('night_2')) path1 = Path.cwd() / 'night_1' path2 = Path.cwd() / 'night_2' path3 = Path.cwd() / 'night_1_registered' + path3.mkdir(parents=True, exist_ok=True) - for i, _ in enumerate(night1_files): + night1_files = sorted([f for f in os.listdir(path1) if f.lower().endswith('.png')]) + night2_files = sorted([f for f in os.listdir(path2) if f.lower().endswith('.png')]) + + for i in range(len(night1_files)): img1 = cv.imread(str(path1 / night1_files[i]), cv.IMREAD_GRAYSCALE) img2 = cv.imread(str(path2 / night2_files[i]), cv.IMREAD_GRAYSCALE) - print("Comparing {} to {}.\n".format(night1_files[i], night2_files[i])) + if img1 is None or img2 is None: + continue + + print(f"Displaying and Processing: {night1_files[i]}") - # Find keypoints and best matches between them. + # 1. Find matches kp1, kp2, best_matches = find_best_matches(img1, img2) - img_match = cv.drawMatches(img1, kp1, img2, kp2, - best_matches, outImg=None) - # Draw a line between the two images. - height, width = img1.shape - cv.line(img_match, (width, 0), (width, height), (255, 255, 255), 1) - QC_best_matches(img_match) # Comment-out to ignore. + # 2. Show the matching lines on screen + img_match = cv.drawMatches(img1, kp1, img2, kp2, best_matches, outImg=None) + # --- DRAW THE VERTICAL SEPARATOR LINE --- + height, width = img1.shape[:2] + # Using Bright Green (0, 255, 0) and thickness of 3 so it's impossible to miss + cv.line(img_match, (width, 0), (width, height), (0, 255, 0), 3) + + cv.imshow('Keypoint Matches', img_match) + cv.waitKey(2000) # Shows the match lines for 2 seconds - # Register left-hand image using keypoints. + # 3. Register the image img1_registered = register_image(img1, img2, kp1, kp2, best_matches) - # QC registration and save registered image (Optional steps): - blink(img1, img1_registered, 'Check Registration', num_loops=5) - out_filename = '{}_registered.png'.format(night1_files[i][:-4]) - cv.imwrite(str(path3 / out_filename), img1_registered) # Will overwrite! + # 4. Save (Overwrites existing) + out_filename = f"{Path(night1_files[i]).stem}_registered.png" + cv.imwrite(str(path3 / out_filename), img1_registered) + # 5. Show the "Blink" on screen to check alignment + # This will toggle between img1_registered and img2 + blink(img1_registered, img2, 'Registration Check (Blink)', num_loops=10) + + # Cleanup for Spyder stability cv.destroyAllWindows() - - # Run the blink comparator - blink(img1_registered, img2, 'Blink Comparator', num_loops=15) + cv.waitKey(1) + + print("Processing complete. All windows closed.") def find_best_matches(img1, img2): - """Return list of keypoints and list of best matches for two images.""" - orb = cv.ORB_create(nfeatures=100) # Initiate ORB object. - - # Find the keypoints and descriptors with ORB. - kp1, desc1 = orb.detectAndCompute(img1, mask=None) - kp2, desc2 = orb.detectAndCompute(img2, mask=None) - - # Find keypoint matches using Brute Force Matcher. + orb = cv.ORB_create(nfeatures=1000) + kp1, desc1 = orb.detectAndCompute(img1, None) + kp2, desc2 = orb.detectAndCompute(img2, None) + if desc1 is None or desc2 is None: + return kp1, kp2, [] bf = cv.BFMatcher(cv.NORM_HAMMING, crossCheck=True) - matches = bf.match(desc1, desc2) - - # Sort matches in ascending order of distance and keep best n matches. - matches = sorted(matches, key=lambda x: x.distance) - best_matches = matches[:MIN_NUM_KEYPOINT_MATCHES] - - return kp1, kp2, best_matches - -def QC_best_matches(img_match): - """Draw best keypoint matches connected by colored lines.""" - cv.imshow('Best {} Matches'.format(MIN_NUM_KEYPOINT_MATCHES), img_match) - cv.waitKey(2500) # Keeps window active 2.5 seconds. - + matches = sorted(bf.match(desc1, desc2), key=lambda x: x.distance) + return kp1, kp2, matches[:MIN_NUM_KEYPOINT_MATCHES] + def register_image(img1, img2, kp1, kp2, best_matches): - """Return first image registered to second image.""" - if len(best_matches) >= MIN_NUM_KEYPOINT_MATCHES: - src_pts = np.zeros((len(best_matches), 2), dtype=np.float32) - dst_pts = np.zeros((len(best_matches), 2), dtype=np.float32) - for i, match in enumerate(best_matches): - src_pts[i, :] = kp1[match.queryIdx].pt - dst_pts[i, :] = kp2[match.trainIdx].pt - - h_array, mask = cv.findHomography(src_pts, dst_pts, cv.RANSAC) - height, width = img2.shape # Get dimensions of image 2. - img1_warped = cv.warpPerspective(img1, h_array, (width, height)) - - return img1_warped - - else: - print("WARNING: Number of keypoint matches < {}\n".format - (MIN_NUM_KEYPOINT_MATCHES)) - return img1 + if len(best_matches) >= 10: + src_pts = np.float32([kp1[m.queryIdx].pt for m in best_matches]).reshape(-1, 1, 2) + dst_pts = np.float32([kp2[m.trainIdx].pt for m in best_matches]).reshape(-1, 1, 2) + h_matrix, _ = cv.findHomography(src_pts, dst_pts, cv.RANSAC, 5.0) + return cv.warpPerspective(img1, h_matrix, (img2.shape[1], img2.shape[0])) + return img1 def blink(image_1, image_2, window_name, num_loops): - """Replicate blink comparator with two images.""" + """Toggles two images in the same window.""" for _ in range(num_loops): cv.imshow(window_name, image_1) - cv.waitKey(330) + if cv.waitKey(400) & 0xFF == ord('q'): break # Press 'q' to skip to next image cv.imshow(window_name, image_2) - cv.waitKey(330) - + if cv.waitKey(400) & 0xFF == ord('q'): break + cv.destroyWindow(window_name) + if __name__ == '__main__': main() diff --git a/Chapter_5/transient_detector.py b/Chapter_5/transient_detector.py index f0fcbed..2369d0b 100644 --- a/Chapter_5/transient_detector.py +++ b/Chapter_5/transient_detector.py @@ -1,66 +1,92 @@ -import os -from pathlib import Path +# -*- coding: utf-8 -*- +""" +Created on Fri Jan 16 14:24:23 2026 +Update on transient_detector. +This version uses f-strings, zip, +and solved hang issues. +Built on Spyder6 +January 16, 2026 +@author: Mito Akiyoshi +""" + import cv2 as cv +from pathlib import Path -PAD = 5 # Ignore pixels this distance from edge +# Configuration +PAD = 5 +THRESHOLD_VAL = 30 +NUM_TRANSIENTS = 2 -def find_transient(image, diff_image, pad): - """Finds and draws circle around transients moving against a star field.""" - transient = False +def find_transient(image_to_label, diff_image, pad): + """Finds brightest spot, labels it, and erases it from the search image.""" height, width = diff_image.shape - cv.rectangle(image, (PAD, PAD), (width - PAD, height - PAD), 255, 1) - minVal, maxVal, minLoc, maxLoc = cv.minMaxLoc(diff_image) - if pad < maxLoc[0] < width - pad and pad < maxLoc[1] < height - pad: - cv.circle(image, maxLoc, 10, 255, 0) - transient = True - return transient, maxLoc + _, maxVal, _, maxLoc = cv.minMaxLoc(diff_image) + + if (pad < maxLoc[0] < width - pad and + pad < maxLoc[1] < height - pad and + maxVal > THRESHOLD_VAL): + + # Draw the target circle on the display image (White outline) + cv.circle(image_to_label, maxLoc, 10, 255, 1) + # Eraser: Black out this area in the diff_image so we don't find it again + cv.circle(diff_image, maxLoc, 10, 0, -1) + return True + + return False def main(): - night1_files = sorted(os.listdir('night_1_registered_transients')) - night2_files = sorted(os.listdir('night_2')) path1 = Path.cwd() / 'night_1_registered_transients' path2 = Path.cwd() / 'night_2' - path3 = Path.cwd() / 'night_1_2_transients' - - # Images should all be the same size and similar exposures. - for i, _ in enumerate(night1_files[:-1]): # Leave off negative image - img1 = cv.imread(str(path1 / night1_files[i]), cv.IMREAD_GRAYSCALE) - img2 = cv.imread(str(path2 / night2_files[i]), cv.IMREAD_GRAYSCALE) + out_path = Path.cwd() / 'night_1_2_transients' + out_path.mkdir(exist_ok=True) - # Get absolute difference between images. - diff_imgs1_2 = cv.absdiff(img1, img2) - cv.imshow('Difference', diff_imgs1_2) - cv.waitKey(2000) + # Use glob to get files and sort them to ensure they match + night1_files = sorted(path1.glob('*.png')) + night2_files = sorted(path2.glob('*.png')) - # Copy difference image and find and circle brightest pixel. - temp = diff_imgs1_2.copy() - transient1, transient_loc1 = find_transient(img1, temp, PAD) + for p1, p2 in zip(night1_files, night2_files): + img1 = cv.imread(str(p1), cv.IMREAD_GRAYSCALE) + img2 = cv.imread(str(p2), cv.IMREAD_GRAYSCALE) + + if img1 is None or img2 is None: + continue - # Draw black circle on temporary image to obliterate brightest spot. - cv.circle(temp, transient_loc1, 10, 0, -1) - - # Get location of new brightest pixel and circle it on input image. - transient2, _ = find_transient(img1, temp, PAD) + # 1. Show raw difference for 2 seconds + diff_imgs1_2 = cv.absdiff(img1, img2) + cv.imshow('Difference', diff_imgs1_2) + cv.waitKey(2000) + + # 2. Detection (Working on a copy of the difference) + working_diff = diff_imgs1_2.copy() + detections_found = 0 + for _ in range(NUM_TRANSIENTS): + if find_transient(img1, working_diff, PAD): + detections_found += 1 - if transient1 or transient2: - print('\nTRANSIENT DETECTED between {} and {}\n' - .format(night1_files[i], night2_files[i])) + # 3. Handle Detections + if detections_found > 0: + # Console Log using f-strings + print(f"\nTRANSIENT DETECTED between {p1.name} and {p2.name}\n") + + # Text labels on the image font = cv.FONT_HERSHEY_COMPLEX_SMALL - cv.putText(img1, night1_files[i], (10, 25), - font, 1, (255, 255, 255), 1, cv.LINE_AA) - cv.putText(img1, night2_files[i], (10, 55), - font, 1, (255, 255, 255), 1, cv.LINE_AA) - + cv.putText(img1, p1.name, (10, 25), font, 1, (255, 255, 255), 1, cv.LINE_AA) + cv.putText(img1, p2.name, (10, 55), font, 1, (255, 255, 255), 1, cv.LINE_AA) + + # Create Blended Survey Image blended = cv.addWeighted(img1, 1, diff_imgs1_2, 1, 0) + + # Display result in the 'Surveyed' window cv.imshow('Surveyed', blended) - cv.waitKey(2500) - - out_filename = '{}_DECTECTED.png'.format(night1_files[i][:-4]) - cv.imwrite(str(path3 / out_filename), blended) # Will overwrite! - + cv.imwrite(str(out_path / f"{p1.stem}_DETECTED.png"), blended) + + # Pause to show results; press 'q' to quit early + if cv.waitKey(2500) & 0xFF == ord('q'): + break else: - print('\nNo transient detected between {} and {}\n' - .format(night1_files[i], night2_files[i])) + print(f"\nNo transient detected between {p1.name} and {p2.name}\n") + + cv.destroyAllWindows() if __name__ == '__main__': main()