# 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/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 "; 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 [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 "; 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 [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 [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 [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" }