Skip to content

Instantly share code, notes, and snippets.

@shekibobo
Last active May 26, 2023 14:03
Show Gist options
  • Save shekibobo/b8e853bf70d787b55a87f7a4cf7ed248 to your computer and use it in GitHub Desktop.
Save shekibobo/b8e853bf70d787b55a87f7a4cf7ed248 to your computer and use it in GitHub Desktop.
organize_astro_data.rb

Organize Astrophotography Data

This is a Ruby script to help organize astrophotography data into folders using keywords that can then be used to help process in PixInsight using WeightedBatchPreProcessing.

This version of the script was written to organize or reorganize the data collected to match my current workflow, which I will outline below.

Camera

I currently use a Canon EOS 1500 (T7) DSLR for astrophotography, and attach it to one of my telescopes. Data capture is performed using the ASIAir Plus. In the ASIAir app, my camera's settings are configured to include ISO, Date, and Temp in the customized file name.

Note: If you are using a different camera, the paarameters included in your file name will likely be different, and you will need to change the FitsFile#initialize method to correctly match your file's properties based on the order they appear. You may also want to update your target directories to include those parameters, in whatever order you feel is appropriate for your workflow.

With the above settings, the files I generally capture are formatted as follows:

  • Lights: Light_M51_300.0s_Bin1_ISO800_20220309-024714_6.0C_0040.fit
    • Target: M51
    • Exposure: 300.0s
    • Binning: 1
    • ISO: 800
    • DateTime: 20220309-024714
    • CCD-TEMP: 6.0C
    • Image index: 0040
  • Lights: Light_10 Lacertae_1-1_150.0s_Bin1_ISO800_20220913-223831_22.0C_0006.fit
    • Target: 10 Lacerta
    • Pane: 1-1
    • Exposure: 150.0s
    • Binning: 1
    • ISO: 800
    • DateTime: 20220913-223831
    • CCD-TEMP: 22.0C
    • Image index: 0006
  • Darks: Dark_300.0s_Bin1_ISO800_20220517-152626_51.0C_0070.fit
    • Exposure: 300.0s
    • Binning: 1
    • ISO: 800
    • DateTime: 20220517-152626
    • CCD-TEMP: 51.0C
    • Image index: 0070
  • Flats: Flat_2.3s_Bin1_ISO800_20220603-052827_14.0C_0006.fit
    • Exposure: 2.3s
    • Binning: 1
    • ISO: 800
    • DateTime: 20220603-052827
    • CCD-TEMP: 14.0C
    • Image index: 0006

The goal of this script is to group these files in a way that works well with WBPP in a multi-step process, and to facilitate this file organization rather than taking all the time to do it manually.

Pre-ASIAir Image Data

With the Canon T7 data captured before I started using an ASIAir Plus, the images were captured in RAW format as CR2 files, with the name IMG_0001.CR2, which is pretty useless for AP photo organization. However, I've found that these RAW files do include most of the necessary data in EXIF tags to allow renaming to match my newer data generated by the ASIAir Plus, including exposure time, camera temperature, iso, etc. Using exiftool, these files can be renamed (with some extra parsing work in Ruby and some user input) to match the same file name pattern with actual data from the original source files.

To rename your older IMG_XXXX.CR2 files, you can use the Rename files with EXIF data option. You will then be prompted to choose which type of file you are organizing. If you are organizing a Light file, you'll also be prompted to enter the target name.

IMPORTANT you must have exiftool installed and in your system path in order to run this renaming process.

Exposure Time

When the EXIF data includes ExposureTime less than 1 second, the value is formatted as a fraction, e.g. 1/250, which then gets interpreted by most file systems as a directory separator. In order to handle this appropriately to match the decimal exposure formatting that the ASIAir generates, we need to do a few workarounds. First, we need to replace the / character with - so that the files don't get misplaced in a new directory. Second, we need to take the file that exiftool generates and parse it to recalculate that fraction value as a decimal at an appropriate time scale. So we parse the 1-250, convert that to a Rational in Ruby, Rational(1, 250), and then change the scale from seconds to milliseconds to nanoseconds until we have the exposure time represented as a number equal to or greater than 1.0.

Renaming Previously Renamed Files

This operation also lets you rename files that you renamed with an older naming format and convert it automatically to use the consistent naming pattern. If the script finds files that are not named IMG_XXXX.CR2, it will prompt you to choose whether to skip or rename them. It will then rename them all to IMG_XXXX.CR2, where XXXX is the last 4 characters of the filename (usually the sequence number). It will then run the script as normal on the now normallized files.

Once all of the files are renamed, they can then be organized into folders just as we do with the FITS files that we get from the ASIAir.

Darks

Darks will be grouped in a folder by CCD-TEMP, ISO, EXP and MONTH (e.g. 2022-06). This lets me get a good idea what temperatures I might be missing while shooting with an uncooled camera, and allows me to create master darks with combined temperatures of +/- 1°C if I need more darks at a certain temperature. If I don't have enough for a temperature, I can just copy some from another nearby temperature into that directory for the purpose of generating the master.

Flat Darks

This script will also check for possible flat darks, and will ask for confirmation when the exposure time is 10.0s or less. In the case of a flat dark set, it will organize them into a folder with ISO, EXP, and FLATSET (the date of the darks and flats). These don't take CCD-TEMP into account, assuming the flats and flat darks are taken at roughly the same time and under the same conditions. 1°C variation is not going to make enough of a difference for me to care, and I only want one master flat from this FLATSET.

These files will be grouped into a folder prefixed with DarkFlat instead of just Dark. When moving off the ASIAir, I put this folder inside a FLATSET_<date> folder with the accompanying flats folder so I can load all the necessary files by directory in one shot in WBPP.

Flats

Similar to flat darks, flats will be grouped using the FLATSET keyword, as well as ISO, EXP, and also TELESCOPE and FILTER. Since the ASIAir doesn't keep track of those parameters in the file name, this script prompts you to select from a list of your telescopes and filters to fill in those names for the grouping directory.

All of the aforementioned keywords are used in WBPP when doing the lights calibration and integration.

Lights

Similar to flats, organizing lights will prompt to select the telescope and filter used for this data set, and will organize into a folder with vary similar keywords as the flats set.

PixInsight - WBPP

All of this organization is to facilitate a standardized workflow in PixInsight using WBPP with predefined process icons for generating each of master darks, master flats, and the master lights.

WBPP_Darks

This process icon is preloaded with appropriate master biases, uses the following grouping keywords on the Calibration tab:

Keyword Pre Post
CCD-TEMP x x
ISO x x
EXP x x
MONTH x x

The generated darks are then able to be used in the WBPP_Integration process icon.

WBPP_Flats

This process icon is also preloaded with appropriate master biases, and uses the following grouping keywords on the Calibration tab:

Keyword Pre Post
FLATSET x x
BIN x x
EXP x
CCD-TEMP
ISO x x

Since my flats and darkflats are together in the same directory, I can load them into WBPP using the Directory button in one step, and then click the run button. One important manual step after this is to remove the EXP_* segment from the new master flat's file name. If you don't do this, the next step, WBPP_Integration will not automatically match your flats to your lights, since EXP is a required grouping keyword in that step to automatically match darks to lights. If the property exists on the filename, they must match. If keywors on one file don't exist on another, they are ignored in keyword grouping in WBPP.

WBPP_Integration

This process icon is preloaded with appropriate biases, but shouldn't be necessary at this point if you followed the process described so far. This step uses the following grouping keywords on the Calibration tab:

Keyword Pre Post
FLATSET x
BIN x x
EXP x
CCD-TEMP x
ISO x x
LIGHT x
PANE x x

Note that the LIGHT and PANE keywords are optional, but are important if you are working with multiple targets at the same time, e.g. for a multi-panel mosaic with each target named differently, or if using the new ASIAir mosaic helper in your plans. If you are working on multiple targets, you'll want to make sure you choose the Registration Reference Image -> Mode -> auto by LIGHT (or PANE) setting under the Calibration tab.

This final step is relatively easy. Simply load your master darks (not darkflats), your master flats, and all your lights. The script should automatically detect and group all the files for calibration. You may have to manually select a few of the darks and flats for calibration if you don't have the right temperature of darks for some lights, or if you reuse the same FLATSET for multiple nights. Other than that, just check your other settings and output directory for this run and you should be good to go.

System Requirements

This script is written in Ruby, so you'll need to have a modern version of that language installed on your computer. If you're on a Mac like I am, you might already have that installed, but if not, you can use HomeBrew to install ruby-installer, and then use that to install an appropriately recent version of Ruby. I developed this script using ruby 3.1.2p20 (2022-04-12 revision 4491bb740a) [x86_64-darwin21]. You can install that with ruby-install ruby 3.1.2 in your terminal.

You will also need to install the gem highline to run this script. This is as simple as running the command gem install highline from the terminal after you have Ruby installed. Aside from that library, everything else is part of the standard language library.

If you are using the script to rename old IMG_XXXX.CR2 files, you must install exiftool, which can also be installed on a Mac with Homebrew using brew install exiftool from the command line.

Running the Organizer

This script MUST be run from the directory containing the files you want to group. The script file itself can live anywhere, but I keep it in my home directory.

Here is an example script run:

$ cd /Volumes/TF Images/ASIAIR/Autorun/Dark
$ ruby ~/organize_astro_data.rb

You will then be led through a list of prompts depending on what data you are organizing. You also have the option of doing a dry-run for each organization task you can choose.

$ ruby ~/astrophotography/organize_astro_data.rb
1. Darks
2. Flats
3. Lights
4. Remove empty directories
5. Remove jpg thumbnails
6. Rename files from EXIF data
7. Quit
1
Preparing to move 1564 DARK files...
Is this a dry run? [y/n]: n

This menu will repeat after completing each task until you quit. If all the files are already in their target directories, there will be nothing to move, and the script will just complete and go back to the main menu.

If you choose a dry-run for a given task, it will not move anything, but will print out the source file and its destination path. If you don't choose a dry run, nothing will be printed, and things will actually be moved.

There are also options to remove empty directories, which is useful if you've reorganized your files from a previous organization structure, and an option to remove all the jpg thumbnails to reduce the data that you'll be migrating off the ASIAir.

Disclaimer

I make no claims about the reliability of this script under your circumstances. Please test and verify the code and the conditions you will run this script through before running it on your data. Dry runs are your friend. If you are modifying the script to work for your data, you can use puts file.inspect to get a good look at how the script parsed your file names. I am not responsible for lost or corrupted data or damaged devices resulting from the use of this script, although under my specific conditions it has been working very well. Just be careful, make backups, test things out before you rely on this fully.

Clear skies!

# Copyright 2022 Joshua Kovach
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of this
# software and associated documentation files (the "Software"), to deal in the Software
# without restriction, including without limitation the rights to use, copy, modify,
# merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
# persons to whom the Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all copies or
# substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
# INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
# FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.
#
require 'fileutils'
require 'date_core'
require 'highline'
require 'mini_exiftool'
# Add your telescopes here. You will be prompted to choose one of them when organizing flats and lights.
class Telescope
ALL = [
REDCAT51 = 'RedCat51',
Z130 = 'ZhumellZ130',
AD8 = 'AperturaAD8',
DS90 = 'MeadeDS90',
]
end
# Add your filters here. You will be prompted to choose one of them when organizing flats and lights.
class Filter
ALL = [
BAADER_MOON = 'BaaderMoon',
NBZ = "NBZ",
NONE = 'NoFilter',
]
end
# Add your cameras here. If there is no camera chosen, it will prompt you to choose one.
class Camera
ALL = [
CANON_T7 = "T7",
ASI183MC = "183MC",
]
end
DT_FORMAT = '%Y%m%d-%H%M%S'
# Class describing the properties of the file that we can determine from the filename generated
# by the ASIAir. Depending on your camera and your filter setup, the file structure may be different.
# This script was written for use with the ASIAir Plus version 1.9, using a Canon EOS 1500 (T7) DSLR
# camera with all the filename metadata turned on. You may have more metadata, or a different order of
# metadata depending on which camera setup you have, or if you have an EFW (electronic filter wheel).
# In that case, you will need to change the order or add more properties in the initialize method so
# that your data is properly parsed. You will also likely want to change your `target_dir` for each
# type so that it organizes your data properly.
class Astrophoto
attr_accessor :type, :exposure, :bin, :camera, :gain, :iso, :created_at, :ccd_temp, :image_index, :path, :filename, :telescope,
:filter, :target, :dark_flat, :mosaic_pane
TYPES = [
DARK = 'Dark',
FLAT = 'Flat',
LIGHT = 'Light',
BIAS = 'Bias'
]
def initialize(path)
self.path = path
self.filename = path.split('/').last
parts = filename.gsub('.fit', '').gsub('.cr2', '').split('_')
puts "PARTS: #{parts}"
self.type = parts.shift
puts "TYPE: #{type}"
self.target = parts.shift if type == LIGHT
puts "TARGET: #{target}"
self.mosaic_pane = parts.shift if parts.first.match(/\A\d+-\d+\z/)
puts "PANE: #{mosaic_pane}"
# If the file is already organized somewhere, get the information from its path.
self.telescope = path.match(%r{TELESCOPE_([^_/]+).*})&.captures&.first
puts "TELESCOPE: #{telescope}"
self.filter = path.match(%r{FILTER_([^_/]+).*})&.captures&.first
puts "FILTER: #{filter}"
self.dark_flat = path.include?('DarkFlat')
puts "DarkFlat?: #{dark_flat}"
self.exposure = parts.shift
puts "EXP: #{exposure}"
self.bin = parts.shift.gsub('Bin', '') if parts.first.start_with?('Bin')
puts "BIN: #{bin}"
self.camera = parts.shift if Camera::ALL.include?(parts.first)
puts "CAMERA: #{camera}"
self.iso = parts.shift.gsub('ISO', '') if parts.first.start_with?('ISO')
puts "ISO: #{iso}"
self.gain = parts.shift.gsub('gain', '') if parts.first.start_with?('gain')
puts "GAIN: #{gain}"
self.created_at = DateTime.strptime(parts.shift, DT_FORMAT)
puts "CREATED_AT: #{created_at}"
self.ccd_temp = parts.shift
puts "CCD_TEMP: #{ccd_temp}"
self.image_index = parts.shift
puts "IMAGE_INDEX: #{image_index}"
end
def dark_flat?
dark_flat
end
# True if the dark is likely a dark flat and hasn't already been organized as dark flat.
def maybe_flat_dark?
exp_val = exposure.to_f
exp_units = exposure.gsub(exp_val.to_s, '')
exp_in_seconds = case exp_units
when 's'
exp_val
when 'ms'
exp_val / 1000.0
when 'us'
exp_val / 1_000_000.0
end
type == DARK && exp_in_seconds <= 10.0 && !dark_flat?
end
# The date formatted like '20220508'. If the pictures are taken in the latter half of the
# day, we are assuming that we'll use the flatset that will be generated the next day.
def flatset_id
if type == LIGHT && created_at.hour >= 12
created_at.next_day.strftime('%Y%m%d')
else
created_at.strftime('%Y%m%d')
end
end
# The Year-Month in which the image was taken. Useful for grouping darks by season.
def month
created_at.strftime('%Y-%m')
end
# The directory structure used to group and categorize the files, which will include useful
# grouping keywords for PixInsight's WeightedBatchPreProcessing script.
def target_dir
iso_or_gain = if iso != nil
"ISO_#{iso}"
elsif gain != nil
"GAIN_#{gain}"
end
case type
when DARK
if dark_flat?
"DarkFlat_FLATSET_#{flatset_id}_#{iso_or_gain}_EXP_#{exposure}_Bin_#{bin}_CAMERA_#{camera}"
else
"Dark_#{iso_or_gain}_EXP_#{exposure}_CCD-TEMP_#{ccd_temp}_CAMERA_#{camera}_MONTH_#{month}"
end
when FLAT
"Flat_FLATSET_#{flatset_id}_#{iso_or_gain}_EXP_#{exposure}_Bin_#{bin}_TELESCOPE_#{telescope}_FILTER_#{filter}_CAMERA_#{camera}"
when LIGHT
pane_id = "_PANE_#{mosaic_pane}" if mosaic_pane
if filename.downcase.end_with?(".fit")
"Light_#{target}#{pane_id}_FLATSET_#{flatset_id}_#{iso_or_gain}_EXP_#{exposure}_Bin_#{bin}_TELESCOPE_#{telescope}_FILTER_#{filter}_CAMERA_#{camera}"
elsif filename.downcase.end_with?(".cr2")
"Light_#{target}#{pane_id}_FLATSET_#{flatset_id}_#{iso_or_gain}_EXP_#{exposure}_Bin_#{bin}_CCD-TEMP_#{ccd_temp.gsub("0C", "")}_TELESCOPE_#{telescope}_FILTER_#{filter}_CAMERA_#{camera}"
end
when BIAS
"Bias_#{iso_or_gain}_EXP_#{exposure}_Bin_#{bin}_CAMERA_#{camera}_MONTH_#{month}"
end
end
# The full path where this file will be moved.
def target_path
File.join(target_dir, filename)
end
# The current directory of the file. If this is different from the target directory,
# you will be asked whether you want to move it or not.
def current_dir
segments = File.split(path) - [filename]
File.join(*segments)
end
# True if the path is already at the target destination. We don't need to move or ask
# anything about these files.
def already_moved?
path == target_path
end
# Performs the move. If `is_dry_run` is true, it will not move the files, but will output
# the file's current location and target location so you can verify it is correct before
# performing the actual move.
def move(is_dry_run)
FileUtils.mkdir target_dir, noop: is_dry_run unless File.exist? target_dir
if File.exist? target_path
puts "File already exists #{target_path}. Skipping..."
else
FileUtils.move path, target_path, verbose: is_dry_run, noop: is_dry_run
print "." unless is_dry_run
end
end
end
class FitsOrganizer
private attr_accessor :cli
def initialize
self.cli = HighLine.new
end
def fits_files
Dir['**/*.fit', '**/*.FIT', '**/*.cr2', '**/*.CR2'].uniq.map { |it| Astrophoto.new(it) }
end
# Organizes dark files by ISO, BIN, CCD-TEMP, EXPOSURE, and MONTH to facilitate the creation of
# master darks that may have varying temperatures. This organization can be changed by updating
# Astrophoto#target_dir for the DARK type.
#
# If the file has an exposure of less than 10 seconds, you will be asked if it is a flat dark.
# If so, it will be organized into a folder that will match your corresponding flat files so that
# you can run WBPP with just your biases, flat darks, and flats using the grouping keywords
# FLATSET, BIN, EXP, and ISO. CCD-TEMP will be ignored for the purposes of these files, as it is
# assumed they will be taken under roughly the same conditions as the flats are taken.
#
# If the files are normal dark files, they will be organized by ISO, EXPOSURE, BIN, CCD-TEMP, and MONTH.
# With this, you can run WBPP with just bias and darks using the grouping keywords CCD-TEMP, ISO, EXP,
# and MONTH (optional).
def organize_darks
dark_files = fits_files.filter { |it| it.type == Astrophoto::DARK }.sort_by { |it| it.path }
puts "Preparing to move #{dark_files.size} DARK files..."
is_dry_run = is_dry_run?
dark_files.slice_when { |a, b| a.image_index.to_i > b.image_index.to_i }.each do |darkset|
next if darkset.all? { |it| it.already_moved? }
if darkset.all? { |it| it.path != it.target_path }
move = cli.ask("Do you want to move the darkset in #{darkset.first.current_dir} to #{darkset.first.target_dir}? [y/n] ").downcase == 'y'
next unless move
end
if darkset.all? { |it| it.maybe_flat_dark? } &&
cli.ask("Is this a flat dark set (size #{darkset.size})? [y/n] #{darkset.first.filename}: ").downcase == 'y'
puts "Cool, we'll move that set to a FLATSET directory..."
darkset.each { |it| it.dark_flat = true }
end
cameras = darkset.map { |it| it.camera }.compact.uniq
camera = if cameras.empty?
puts "[WARNING] Camera not detected."
select_camera
elsif cameras.size > 1
puts "[WARNING] Multiple cameras detected: #{cameras}"
else
cameras.first
end
darkset.each do |file|
if file.camera.nil?
puts "Camera not detected. Using #{camera}."
file.camera = camera
end
end
darkset.each { |it| it.move(is_dry_run) }
puts "Done\n"
end
end
def organize_biases
bias_files = fits_files.filter { |it| it.type == Astrophoto::BIAS }.sort_by { |it| it.path }
puts "Preparing to move #{bias_files.size} BIAS files..."
is_dry_run = is_dry_run?
bias_files.slice_when { |a, b| a.image_index.to_i > b.image_index.to_i }.each do |biases|
next if biases.all? { |it| it.already_moved? }
if biases.all? { |it| it.path != it.target_path }
move = cli.ask("Do you want to move the bias set in #{biases.first.current_dir} to #{biases.first.target_dir}? [y/n] ").downcase == 'y'
next unless move
end
cameras = biases.map { |it| it.camera }.uniq
camera = if cameras.empty?
puts "[WARNING] Camera not detected."
select_camera
elsif cameras.size > 1
puts "[WARNING] Multiple cameras detected: #{cameras}"
else
cameras.first
end
biases.each do |file|
if file.camera.nil?
puts "Camera not detected. Using #{camera}."
file.camera = camera
end
end
biases.each { |it| it.move(is_dry_run) }
puts "Done\n"
end
end
# Organizes flat files by FLATSET, ISO, BIN, EXP (EXPOSURE), TELESCOPE, and FILTER. To change these
# properties, update Astrophoto#target_dir for the FLAT type. The TELESCOPE and FILTER keywords are
# for matching LIGHTS which will have the same keywords set when organized using this script.
#
# You can run WBPP with just your biases, flat darks, and flats using the grouping keywords
# FLATSET, BIN, EXP, and ISO. CCD-TEMP will be ignored for the purposes of these files, as it is
# assumed they will be taken under roughly the same conditions as the flat darks are taken.
#
# After running WBPP, you should delete the `EXP` keyword from the master flat file name (if present)
# before using that master flat in a WBPP integration run, since exposure time should not be considered
# when grouping flats to lights.
def organize_flats
flat_files = fits_files.filter { |it| it.type == Astrophoto::FLAT }.sort_by { |it| it.path }
puts "Preparing to move #{flat_files.size} FLAT files..."
is_dry_run = is_dry_run?
flat_sets = flat_files.slice_when { |a, b| a.image_index.to_i > b.image_index.to_i }
flat_sets.each do |flatset|
next if flatset.all? { |it| it.already_moved? }
if flatset.all? { |it| it.path != it.target_path }
move = cli.ask("Do you want to move the flatset in #{flatset.first.current_dir} to #{flatset.first.target_dir}? [y/n] ").downcase == 'y'
next unless move
end
puts "For FLATSET #{flatset.first.filename}..#{flatset.last.filename}:"
telescope = select_telescope
filter = select_filter
cameras = flatset.map { |it| it.camera }.uniq
camera = if cameras.empty?
puts "[WARNING] Camera not detected."
select_camera
elsif cameras.size > 1
puts "[WARNING] Multiple cameras detected: #{cameras}"
else
cameras.first
end
flatset.each do |file|
file.telescope = telescope
file.filter = filter
if file.camera.nil?
puts "Camera not detected. Using #{camera}."
file.camera = camera
end
end
flatset.each { |it| it.move(is_dry_run) }
puts "Done\n"
end
end
# Organizes light files by FLATSET, ISO, BIN, EXP (EXPOSURE), TELESCOPE, and FILTER. To change these
# properties, update Astrophoto#target_dir for the LIGHT type. The TELESCOPE and FILTER keywords are
# for matching LIGHTS which will have the same keywords set when organized using this script.
#
# CCD-TEMP is ignored in the group naming because each individual fits file contains that information
# in its fits header.
#
# You can run WBPP with just your master biases, master darks, and master flats using the grouping
# keywords FLATSET, BIN, EXP, CCD-TEMP, and ISO.
#
# If you are running WBPP on multiple targets using this data, e.g. for a mosaic, you should make sure
# to use LIGHT as a post-processing keyword and register files using `auto by LIGHT`.
def organize_lights
light_files = fits_files.filter { |it| it.type == Astrophoto::LIGHT }.sort_by { |it| it.path }
puts "Preparing to move #{light_files.size} LIGHT files..."
is_dry_run = is_dry_run?
light_sets = light_files.slice_when { |a, b| a.image_index.to_i > b.image_index.to_i }
light_sets.each do |lightset|
next if lightset.all? { |it| it.already_moved? }
if lightset.all? { |it| it.path != it.target_path }
move = cli.ask("Do you want to move the light set in #{lightset.first.current_dir} to #{lightset.first.target_dir}? [y/n] ").downcase == 'y'
next unless move
end
puts "For LIGHTS #{lightset.first.filename}..#{lightset.last.filename}:"
telescope = select_telescope
filter = select_filter
cameras = lightset.map { |it| it.camera }.uniq
camera = if cameras.empty?
puts "[WARNING] Camera not detected."
select_camera
elsif cameras.size > 1
puts "[WARNING] Multiple cameras detected: #{cameras}"
else
cameras.first
end
lightset.each do |file|
file.telescope = telescope
file.filter = filter
if file.camera.nil?
puts "Camera not detected. Using #{camera}."
file.camera = camera
end
end
lightset.each { |it| it.move(is_dry_run) }
puts "Done\n"
end
end
private def select_telescope
cli.choose do |menu|
menu.prompt = 'What telescope is this set for?'
Telescope::ALL.each do |scope|
menu.choice(scope)
end
menu.default = Telescope::REDCAT51
end
end
private def select_filter
cli.choose do |menu|
menu.prompt = 'What filter is used with this set?'
Filter::ALL.each do |filter|
menu.choice(filter)
end
menu.default = Filter::BAADER_MOON
end
end
private def select_camera
cli.choose do |menu|
menu.prompt = 'What camera is used with this set?'
Camera::ALL.each do |camera|
menu.choice(camera)
end
menu.default = Camera::CANON_T7
end
end
# TODO: Add menu to select for barlow/flatteners
private def select_accessories; end
# Checks for empty directories. Run this option after performing a move of previously
# organized data.
def remove_empty_directories
puts 'Cleaning up empty directories...'
is_dry_run = is_dry_run?
Dir['**/*/.DS_Store'].each { |ds_store| FileUtils.rm ds_store, verbose: true, noop: is_dry_run }
Dir['**/*/'].reverse_each { |d| FileUtils.rmdir d, verbose: true, noop: is_dry_run if (Dir.entries(d) - [".", ".."]).empty? }
end
# Removes all the jpg thumbnails under this directory.
def remove_jpg_thumbnails
puts 'Removing jpg thumbnails...'
is_dry_run = is_dry_run?
Dir['**/*_thn.jpg'].each { |jpg| FileUtils.rm jpg, verbose: true, noop: is_dry_run }
end
# Renames CR2 Raw files to match the same name pattern as ASIAir does based on EXIF data.
def rename_from_exif
type = cli.choose do |menu|
menu.prompt = 'What is the file type?'
Astrophoto::TYPES.each do |t|
menu.choice(t)
end
end
target = cli.ask('What is the target name?') if type == Astrophoto::LIGHT
is_dry_run = is_dry_run?
files = Dir['*.cr2', '*.CR2'].uniq
if files.none? { |cr2| cr2.start_with?('IMG_') }
cli.choose do |menu|
menu.prompt = "Files (#{files.size}) are already named, e.g. #{files.first&.split(File::SEPARATOR)&.last}. What do?"
menu.choice('Skip') { return }
menu.choice('Proceed with rename (this cannot be undone) and continue') do
# rename_to_img(files, is_dry_run)
end
menu.choice('Only rename back to IMG_****.cr2') do
rename_to_img(files, is_dry_run)
return
end
end
end
Dir['*.cr2', '*.CR2'].uniq.each do |cr2|
exif = MiniExiftool.new(cr2)
exif["SequenceNumber"] = exif.filename.split("_").last.split(".").first.to_i if exif["SequenceNumber"] == 0
exif["Artist"] = "Joshua Kovach"
exif.save
exif.reload
data = exif.to_hash
exp_time = data["ExposureTime"]
exp_unit = 's'
if exp_time < 1.0
exp_time *= 1000
exp_unit = 'ms'
end
if exp_time < 1.0
exp_time *= 1000
exp_unit = 'us'
end
exp_time_str = format("%.1f%s", exp_time, exp_unit)
created_at = data["DateTimeOriginal"].strftime(DT_FORMAT)
ccd_temp = "%.1fC" % data["CameraTemperature"].to_f
seq_num = data["SequenceNumber"].to_s.rjust(4, "0")
cam_model = data["Model"]
camera = Camera::ALL.find { |it| cam_model.include?(it) }
if camera.nil?
puts "Camera #{cam_model} did not match any of the expected models."
camera = cli.choose do |menu|
menu.prompt = "Choose an identifier for this camera:"
cam_model.split(" ").each do |id|
menu.choice(id)
end
end
end
target_file = "#{type}_#{target&.append("_")}#{exp_time_str}_Bin1_#{camera}_ISO#{data["ISO"]}_#{created_at}_#{ccd_temp}_#{seq_num}.CR2"
FileUtils.move cr2, target_file, verbose: is_dry_run, noop: is_dry_run unless File.exist?(target_file)
print "." unless is_dry_run
end
puts "Done\n"
end
def is_dry_run?
cli.ask('Is this a dry run? [y/n]: ').downcase == 'y'
end
def rename_to_img(files, is_dry_run)
files.each_with_index do |file, index|
idx = (file.split(/[_-]/).last.to_i || index).to_s.rjust(4, "0")
target_file = "IMG_#{idx}.CR2"
puts "Renaming to #{target_file}"
FileUtils.move file, target_file, verbose: is_dry_run, noop: is_dry_run unless File.exist?(target_file)
end
end
# Prompts the user to choose which organizing task to run. This is the main entry point of
# this script.
def organize
cli.choose do |menu|
menu.prompt = 'What are we organizing?'
menu.choice('Darks') do
organize_darks
organize
end
menu.choice('Flats') do
organize_flats
organize
end
menu.choice('Lights') do
organize_lights
organize
end
menu.choice('Biases') do
organize_biases
organize
end
menu.choice('Remove empty directories') do
remove_empty_directories
organize
end
menu.choice('Remove jpg thumbnails') do
remove_jpg_thumbnails
organize
end
menu.choice('Rename files from EXIF data') do
rename_from_exif
organize
end
menu.choice('Quit')
end
end
end
organizer = FitsOrganizer.new
organizer.organize
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment