# photos -- photography and astrophotography aliases and functions
# Sources: dvrs-brad/image-cli-tools, oPromessa gist, rzbrk/astrocam, community
# ============================================================================
# EXIFTOOL
# ============================================================================
alias exif-strip-gps='exiftool -gps:all= -xmp:geotag= -overwrite_original'
alias exif-strip-all='exiftool -all= -overwrite_original'
alias exif-copy='exiftool -TagsFromFile'
alias exif-shot='exiftool -DateTimeOriginal -ExposureTime -FNumber -ISO -FocalLength -LensModel -Model -s'
alias exif-gps='exiftool -gpslatitude -gpslongitude -gpsaltitude -n -s'
alias exif-dump='exiftool -a -u -g1'
alias exif-rename='exiftool -r "-FileName<DateTimeOriginal" -d "%Y-%m-%d-%H%M%S%%-c.%%le" .'
alias exif-sort='exiftool -r -d "%Y/%m/%Y-%m-%d" "-directory<DateTimeOriginal" .'
# ============================================================================
# IMAGEMAGICK
# ============================================================================
alias imageSize='identify -format "%wx%h" '
alias imageInfo='identify -verbose '
alias img2jpg='mogrify -format jpg '
alias img2png='mogrify -format png '
alias img2webp='mogrify -format webp '
# ============================================================================
# IMAGE OPTIMIZATION
# ============================================================================
alias jpgopt='jpegoptim --max=90 --strip-all --all-progressive'
alias pngopt='optipng -o2 -quiet'
# ============================================================================
# COLOR MANAGEMENT
# ============================================================================
alias to-srgb='convert -colorspace sRGB -profile /usr/share/color/icc/colord/sRGB.icc'
alias load-icc='dispwin -d1 ~/.local/share/icc/display_profile.icc'
# ============================================================================
# TIMELAPSE
# ============================================================================
alias timelapse='ffmpeg -framerate 30 -pattern_type glob -i "*.jpg" -c:v libx264 -pix_fmt yuv420p -crf 18 timelapse.mp4'
# ============================================================================
# INDI (ASTROPHOTOGRAPHY)
# ============================================================================
alias indi-stop='pkill indiserver'
alias indi-status='pgrep -a indiserver'
# ============================================================================
# SIRIL
# ============================================================================
alias siril='siril-cli'
# ============================================================================
# FUNCTIONS
# ============================================================================
# sort photos into YYYY/MM/YYYY-MM-DD folders by EXIF date
photosort() {
dir="${1:-.}"
for file in "$dir"/*.jpg "$dir"/*.JPG "$dir"/*.jpeg "$dir"/*.png "$dir"/*.CR2 "$dir"/*.NEF "$dir"/*.ARW "$dir"/*.DNG; do
[ -f "$file" ] || continue
DATE=$(exiftool -d "%Y/%m/%Y-%m-%d" -DateTimeOriginal -S -s "$file" 2>/dev/null)
[ -z "$DATE" ] && DATE=$(stat -c %y "$file" 2>/dev/null | cut -d' ' -f1 | sed 's/-/\//;s/\(.*\)\//\1\//')
[ -z "$DATE" ] && continue
mkdir -p "$DATE"
mv -v "$file" "$DATE/"
done
}
# import from SD card with date-based organization
photoimport() {
sdcard="${1:-/media/$USER/SDCARD}"
dest="${2:-$HOME/Pictures/Import}"
[ ! -d "$sdcard/DCIM" ] && { echo "No DCIM found at $sdcard"; return 1; }
find "$sdcard/DCIM" -type f \( -iname '*.jpg' -o -iname '*.cr2' -o -iname '*.arw' -o -iname '*.nef' -o -iname '*.raf' -o -iname '*.dng' \) | while read -r f; do
DATE=$(exiftool -d "%Y/%m/%d" -DateTimeOriginal -S -s "$f" 2>/dev/null)
FNAME=$(exiftool -d "%Y%m%d-%H%M%S" -DateTimeOriginal -S -s "$f" 2>/dev/null)
EXT="${f##*.}"
mkdir -p "$dest/$DATE"
cp -n "$f" "$dest/$DATE/${FNAME}.${EXT}"
done
echo "[+] Import complete to $dest"
}
# stamp copyright on all images
exif_copyright() {
name="${1:-$(whoami)}"; year="${2:-$(date +%Y)}"
exiftool -r -overwrite_original \
-Copyright="Copyright $year $name. All rights reserved." \
-Artist="$name" -Creator="$name" \
-ext jpg -ext jpeg -ext png -ext cr2 -ext nef -ext arw .
}
# strip GPS for privacy before sharing
exif_privacy() {
dir="${1:-.}"
exiftool -r -overwrite_original \
-gps:all= -GPSLatitude= -GPSLongitude= -GPSAltitude= \
-GPSTimeStamp= -GPSDateStamp= -Location= "$dir"
}
# shift timestamps (timezone/clock correction)
exif_shift_time() {
offset="$1"; dir="${2:-.}"
[ -z "$offset" ] && { echo "Usage: exif_shift_time <+H:MM:SS> [dir]"; return 1; }
exiftool -r -overwrite_original -AllDates"${offset}" "$dir"
}
# add IPTC keyword tags
exif_tag() {
keyword="$1"; shift
[ -z "$keyword" ] && { echo "Usage: exif_tag <keyword> <files...>"; return 1; }
exiftool -overwrite_original -Keywords+="$keyword" -Subject+="$keyword" "$@"
}
# darktable batch convert RAW to JPEG
dt_batch() {
rawdir="${1:-.}"; outdir="${2:-./jpeg}"; quality="${3:-90}"
mkdir -p "$outdir"
for raw in "$rawdir"/*.CR2 "$rawdir"/*.NEF "$rawdir"/*.ARW "$rawdir"/*.RAF "$rawdir"/*.DNG "$rawdir"/*.RW2; do
[ -f "$raw" ] || continue
base=$(basename "${raw%.*}")
darktable-cli "$raw" "$outdir/${base}.jpg" \
--width 4000 --height 4000 \
--core --conf plugins/imageio/format/jpeg/quality="$quality"
done
}
# RawTherapee batch with preset
rt_batch() {
profile="$1"; outdir="${2:-./processed}"
[ -z "$profile" ] && { echo "Usage: rt_batch <profile.pp3> [outdir]"; return 1; }
mkdir -p "$outdir"
for raw in *.CR2 *.NEF *.ARW *.DNG *.RAF; do
[ -f "$raw" ] || continue
rawtherapee-cli -o "$outdir" -p "$profile" -j90 -js3 -Y -c "$raw"
done
}
# resize preserving aspect ratio
imageResize() {
width="${2:-1920}"
convert "$1" -resize "${width}x" "${1%.*}_${width}w.${1##*.}"
}
# watermark with text
watermark() {
text="${1:-$(whoami) $(date +%Y)}"; shift
for img in "$@"; do
convert "$img" -gravity SouthEast \
-fill "rgba(255,255,255,0.30)" -pointsize 24 \
-annotate +10+10 "$text" "wm_${img}"
done
}
# contact sheet / proof sheet
contact_sheet() {
dir="${1:-.}"; output="${2:-contact_sheet.jpg}"
montage "$dir"/*.jpg -geometry "300x300+5+5" -tile 5x \
-title "Contact Sheet - $(date +%Y-%m-%d)" \
-shadow -background "#f0f0f0" "$output"
}
# resize for social media platforms
resize_for() {
platform="$1"; img="$2"
[ -z "$img" ] && { echo "Usage: resize_for <instagram|facebook|twitter|web> <image>"; return 1; }
case "$platform" in
instagram) convert "$img" -resize 1080x1080^ -gravity center -extent 1080x1080 "ig_$img" ;;
facebook) convert "$img" -resize 1200x630^ -gravity center -extent 1200x630 "fb_$img" ;;
twitter) convert "$img" -resize 1200x675^ -gravity center -extent 1200x675 "tw_$img" ;;
web) convert "$img" -resize 2048x2048\> -quality 85 -strip "web_$img" ;;
esac
}
# panorama stitching with Hugin
auto_pano() {
outname="${1:-panorama}"; shift
pto_gen -o "$outname.pto" "$@"
cpfind --multirow -o "$outname.pto" "$outname.pto"
cpclean -o "$outname.pto" "$outname.pto"
linefind -o "$outname.pto" "$outname.pto"
autooptimiser -a -m -l -s -o "$outname.pto" "$outname.pto"
pano_modify --canvas=AUTO --crop=AUTO -o "$outname.pto" "$outname.pto"
hugin_executor --stitching --prefix="$outname" "$outname.pto"
}
# HDR exposure bracket merge
hdr_merge() {
output="${1:-hdr_merged.tif}"; shift
align_image_stack -m -a aligned_ "$@"
enfuse --exposure-weight=1 --saturation-weight=0.2 \
--contrast-weight=0 --hard-mask -o "$output" aligned_*.tif
rm -f aligned_*.tif
echo "HDR merged: $output"
}
# batch optimize JPEGs and PNGs
optimize_images() {
dir="${1:-.}"; quality="${2:-85}"
find "$dir" -iname "*.jpg" -o -iname "*.jpeg" | while read -r f; do
jpegoptim --max="$quality" --strip-all --all-progressive "$f"
done
find "$dir" -iname "*.png" | while read -r f; do
pngquant --quality=65-80 --skip-if-larger --force --ext .png "$f"
optipng -o2 -quiet "$f"
done
}
# web-optimized export
webexport() {
src="$1"; maxwidth="${2:-2048}"; quality="${3:-85}"
[ -z "$src" ] && { echo "Usage: webexport <image> [maxwidth] [quality]"; return 1; }
convert "$src" -resize "${maxwidth}x${maxwidth}>" -strip \
-interlace Plane -quality "$quality" -sampling-factor 4:2:0 \
-colorspace sRGB "${src%.*}_web.jpg"
}
# generate thumbnails at multiple sizes
gen_thumbs() {
img="$1"; base="${img%.*}"; ext="${img##*.}"
for size in 150 300 600 1200; do
convert "$img" -resize "${size}x${size}>" -quality 85 -strip "${base}_${size}.${ext}"
done
}
# capture light and dark frames with gphoto2
astro_capture() {
exposure="${1:-30}"; count="${2:-50}"; darks="${3:-10}"; iso="${4:-1600}"
outdir="./capture_$(date +%Y%m%d_%H%M%S)"
mkdir -p "$outdir/lights" "$outdir/darks"
gphoto2 --set-config iso="$iso" --set-config shutterspeed=bulb
echo "[*] Capturing $count lights at ${exposure}s ISO${iso}..."
i=1; while [ "$i" -le "$count" ]; do
gphoto2 --capture-image-and-download --bulb "$exposure" \
--filename "$outdir/lights/light_$(printf '%04d' $i).%C"
echo " Light $i/$count"
i=$((i + 1))
done
echo "[*] Cover lens for darks. Press Enter..."
read _dummy
i=1; while [ "$i" -le "$darks" ]; do
gphoto2 --capture-image-and-download --bulb "$exposure" \
--filename "$outdir/darks/dark_$(printf '%04d' $i).%C"
i=$((i + 1))
done
echo "[+] Capture complete: $outdir"
}
# organize calibration frames
organize_calibration() {
srcdir="${1:-.}"
mkdir -p lights darks flats biases
for f in "$srcdir"/*.fits "$srcdir"/*.FITS "$srcdir"/*.cr2 "$srcdir"/*.CR2 "$srcdir"/*.nef "$srcdir"/*.NEF; do
[ -f "$f" ] || continue
base=$(basename "$f" | tr '[:upper:]' '[:lower:]')
case "$base" in
*dark*) mv "$f" darks/ ;; *flat*) mv "$f" flats/ ;;
*bias*) mv "$f" biases/ ;; *) mv "$f" lights/ ;;
esac
done
echo "Organized: $(ls lights/ 2>/dev/null | wc -l) lights, $(ls darks/ 2>/dev/null | wc -l) darks, $(ls flats/ 2>/dev/null | wc -l) flats, $(ls biases/ 2>/dev/null | wc -l) biases"
}
# plate solve with astrometry.net
plate_solve() {
image="$1"; ra="${2:-}"; dec="${3:-}"; radius="${4:-5}"
[ -z "$image" ] && { echo "Usage: plate_solve <image> [ra] [dec] [radius]"; return 1; }
opts="--overwrite --no-plots --downsample 2"
[ -n "$ra" ] && [ -n "$dec" ] && opts="$opts --ra $ra --dec $dec --radius $radius"
solve-field $opts "$image"
}
# SiriL processing wrapper
siril_process() {
workdir="$1"; script="${2:-OSC_Preprocessing.ssf}"
[ -z "$workdir" ] && { echo "Usage: siril_process <workdir> [script]"; return 1; }
logfile="$workdir/siril_$(date +%Y%m%d_%H%M%S).log"
siril-cli -d "$workdir" -s "$script" 2>&1 | tee "$logfile"
}
# FITS to PNG conversion
fits2png() {
fits="$1"; out="${2:-${fits%.fits}.png}"
convert "$fits" -normalize -depth 8 "$out"
}
# star trails composite (lighten blend)
star_trails() {
dir="${1:-.}"; output="${2:-star_trails.tif}"
convert "$dir"/*.jpg -evaluate-sequence Max "$output"
echo "Star trails: $output"
}
# start INDI server with common drivers
start_indi() {
case "${1:-full}" in
mount) drivers="indi_eqmod_telescope" ;;
camera) drivers="indi_asi_ccd" ;;
guide) drivers="indi_asi_ccd indi_gpsd" ;;
full) drivers="indi_eqmod_telescope indi_asi_ccd indi_asi_focuser indi_asi_wheel" ;;
*) drivers="$*" ;;
esac
echo "Starting INDI: $drivers"
indiserver -v $drivers &
echo "PID: $!"
}
# ISS pass prediction
iss_passes() {
lat="${1:-40.7128}"; lon="${2:--74.0060}"
python3 -c "
from skyfield.api import load, wgs84
ts = load.timescale()
sats = load.tle_file('https://celestrak.org/NORAD/elements/gp.php?CATNR=25544&FORMAT=TLE')
iss = sats[0]
loc = wgs84.latlon($lat, $lon)
t0 = ts.now(); t1 = ts.tt_jd(t0.tt + 7)
times, events = iss.find_events(loc, t0, t1, altitude_degrees=10.0)
for ti, event in zip(times, events):
name = ('rise', 'culminate', 'set')[event]
print(f'{ti.utc_strftime(\"%Y-%m-%d %H:%M:%S\")} {name}')
" 2>/dev/null
}
# astronomy weather check
astro_weather() {
lat="${1:-40.71}"; lon="${2:--74.01}"
echo "=== Moon Phase ==="
curl -s "wttr.in/Moon" | head -25
echo ""
echo "=== Conditions ==="
curl -s "wttr.in/${lat},${lon}?format=Humidity:+%h+|+Wind:+%w+|+Cloud:+%C+|+Pressure:+%P" 2>/dev/null
echo ""
echo "Clear Outside: https://clearoutside.com/forecast/${lat}/${lon}"
}
# create photo session folder structure
new_session() {
name="${1:-$(date +%Y%m%d)_session}"; base="${2:-$HOME/Pictures}"
dir="$base/$name"
mkdir -p "$dir"/{RAW,JPEG,Selection,Edit,Export,Web}
echo "Session: $name\nDate: $(date +%Y-%m-%d)\nPhotographer: $(whoami)\nLocation:\nSubject:\nCamera:\nLens:\nNotes:" > "$dir/session_info.txt"
echo "Session created: $dir"
}
# create astrophotography session folder
new_astro_session() {
target="${1:-unknown}"; date="${2:-$(date +%Y%m%d)}"
dir="$HOME/Astrophotography/${date}_${target}"
mkdir -p "$dir"/{lights,darks,flats,biases,processed,masters,result}
echo "Target: $target\nDate: $date\nTelescope:\nCamera:\nMount:\nFilter:\nExposure:\nISO/Gain:\nFrames:\nBortle:\nSeeing:\nNotes:" > "$dir/session_log.txt"
echo "Astro session: $dir"
}
# rsync photo backup
photo_backup() {
src="${1:-$HOME/Pictures}"; dest="${2:-/mnt/nas/photos}"
logfile="$HOME/.photo_backup_$(date +%Y%m%d).log"
rsync -avhP --delete \
--include='*.jpg' --include='*.jpeg' --include='*.png' \
--include='*.cr2' --include='*.nef' --include='*.arw' \
--include='*.dng' --include='*.tiff' --include='*.xmp' \
--include='*.mp4' --include='*.mov' \
--include='*/' --exclude='*' \
"$src/" "$dest/" 2>&1 | tee "$logfile"
}
# calculate print size from image dimensions
print_size() {
img="$1"; dpi="${2:-300}"
dims=$(identify -format "%wx%h" "$img")
w=$(echo "$dims" | cut -dx -f1); h=$(echo "$dims" | cut -dx -f2)
pw=$(echo "scale=2; $w / $dpi" | bc); ph=$(echo "scale=2; $h / $dpi" | bc)
echo "${img}: ${w}x${h}px at ${dpi}dpi = ${pw}x${ph} inches"
}