Skip to content
Open
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
121 changes: 58 additions & 63 deletions Chapter_5/blink_comparator.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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()
120 changes: 73 additions & 47 deletions Chapter_5/transient_detector.py
Original file line number Diff line number Diff line change
@@ -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()