Last active
August 14, 2025 08:15
-
-
Save habi/32475efafa02b2871bfe301a70fa5323 to your computer and use it in GitHub Desktop.
Updated montage script, with *lots* of help from and back and forth with ChatGPT
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| from selenium import webdriver | |
| from selenium.webdriver.chrome.service import Service | |
| from selenium.webdriver.chrome.options import Options | |
| from selenium.webdriver.common.by import By | |
| from webdriver_manager.chrome import ChromeDriverManager | |
| from PIL import Image | |
| import os | |
| import io | |
| import re | |
| import requests | |
| import time | |
| # Setup headless Chrome | |
| chrome_options = Options() | |
| chrome_options.add_argument("--headless") | |
| chrome_options.add_argument("--no-sandbox") | |
| chrome_options.add_argument("--disable-dev-shm-usage") | |
| driver = webdriver.Chrome( | |
| service=Service(ChromeDriverManager().install()), | |
| options=chrome_options | |
| ) | |
| try: | |
| url = "https://www.ana.unibe.ch/ueber_uns/team/index_ger.html" | |
| driver.get(url) | |
| time.sleep(5) # wait for JS to load images | |
| # Grab all <img> tags | |
| img_elements = driver.find_elements(By.TAG_NAME, "img") | |
| headshots = [] | |
| for img in img_elements: | |
| src = img.get_attribute("src") | |
| alt = img.get_attribute("alt") | |
| if src and "kopf.jpg" in src and alt: | |
| # clean name for filename | |
| name_clean = re.sub(r"[^a-zA-Z0-9]+", "_", alt.strip().lower()).strip("_") | |
| headshots.append((src, name_clean)) | |
| finally: | |
| driver.quit() | |
| # Download, filter, and resize images | |
| output_dir = "headshots" | |
| os.makedirs(output_dir, exist_ok=True) | |
| def is_grayscale(img: Image.Image) -> bool: | |
| """Return True if the image is purely grayscale (all R=G=B).""" | |
| if img.mode != "RGB": | |
| img = img.convert("RGB") | |
| # Sample pixels to speed up | |
| for pixel in img.getdata(): | |
| r, g, b = pixel | |
| if r != g or g != b: | |
| return False | |
| return True | |
| for src, name in headshots: | |
| filename = f"kopf_{name}.jpg" | |
| filepath = os.path.join(output_dir, filename) | |
| try: | |
| r = requests.get(src) | |
| r.raise_for_status() | |
| img = Image.open(io.BytesIO(r.content)) | |
| if is_grayscale(img): | |
| print(f"Skipped grayscale placeholder: {filename}") | |
| continue | |
| img.thumbnail((400, 400), Image.LANCZOS) | |
| img.save(filepath) | |
| print(f"Downloaded & resized: {filename}") | |
| except Exception as e: | |
| print(f"Failed {src}: {e}") | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| set -e | |
| # ===== Settings ===== | |
| HEADSHOT_DIR="./headshots" | |
| OUTPUT="team_a4_polaroid_variable_rows.png" | |
| DPI=300 | |
| # A4 size in pixels at 300 DPI | |
| A4_WIDTH=2480 | |
| A4_HEIGHT=3508 | |
| MARGIN=30 | |
| # Polaroid/placement parameters | |
| ROT_MAX=10 # max rotation in degrees | |
| SHADOW_OFFSET=10 | |
| SHADOW_BLUR=10 | |
| BORDER_SIZE=10 # white border | |
| OFFSET_MAX=15 # random placement offset | |
| ROW_PADDING=20 # extra vertical space for shadows/borders | |
| # ===== Temporary folder ===== | |
| TMP_DIR=$(mktemp -d) | |
| echo "Using temporary folder: $TMP_DIR" | |
| # ===== Gather and shuffle images ===== | |
| IMAGES=("$HEADSHOT_DIR"/kopf_*.jpg) | |
| NUM_IMAGES=${#IMAGES[@]} | |
| echo "Number of images: $NUM_IMAGES" | |
| if [ "$NUM_IMAGES" -eq 0 ]; then | |
| echo "No images found in $HEADSHOT_DIR" | |
| exit 1 | |
| fi | |
| # Shuffle array | |
| IMAGES=($(printf "%s\n" "${IMAGES[@]}" | shuf)) | |
| # ===== Compute optimal number of rows ===== | |
| ROWS=$(python3 -c "import math; n=$NUM_IMAGES; h=$A4_HEIGHT; w=$A4_WIDTH; r=int(round(math.sqrt(n*h/w))); print(max(1,r))") | |
| ROWS=$(( ROWS > NUM_IMAGES ? NUM_IMAGES : ROWS )) # max rows = num images | |
| echo "Number of rows: $ROWS" | |
| # ===== Compute images per row (alternating pattern) ===== | |
| IMAGES_PER_ROW=() | |
| TOGGLE=0 # 0 = ceil, 1 = floor | |
| # Compute base values | |
| BASE=$((NUM_IMAGES / ROWS)) | |
| CEIL=$(( (NUM_IMAGES + ROWS - 1) / ROWS )) # ceil division | |
| FLOOR=$BASE | |
| for ((i=0;i<ROWS;i++)); do | |
| if [ $TOGGLE -eq 0 ]; then | |
| IMAGES_PER_ROW+=($CEIL) | |
| TOGGLE=1 | |
| else | |
| IMAGES_PER_ROW+=($FLOOR) | |
| TOGGLE=0 | |
| fi | |
| done | |
| # Adjust last row to match total images | |
| SUM=0 | |
| for n in "${IMAGES_PER_ROW[@]}"; do SUM=$((SUM + n)); done | |
| DIFF=$((NUM_IMAGES - SUM)) | |
| IMAGES_PER_ROW[-1]=$((IMAGES_PER_ROW[-1] + DIFF)) | |
| echo "Images per row (alternating): ${IMAGES_PER_ROW[*]}" | |
| # ===== Prepare canvas (transparent) ===== | |
| convert -size ${A4_WIDTH}x${A4_HEIGHT} xc:none "$TMP_DIR/canvas.png" | |
| # ===== Process and place images ===== | |
| i=0 | |
| Y=$MARGIN | |
| for ROW in "${IMAGES_PER_ROW[@]}"; do | |
| CELL_WIDTH=$(( (A4_WIDTH - 2*MARGIN) / ROW )) | |
| CELL_HEIGHT=$(( (A4_HEIGHT - 2*MARGIN - ROWS*ROW_PADDING) / ROWS )) | |
| X=$MARGIN | |
| for ((j=0;j<ROW;j++)); do | |
| IMG="${IMAGES[i]}" | |
| BASENAME=$(printf "%03d" $i) | |
| # Random rotation and offsets | |
| ROT=$((RANDOM % (2*ROT_MAX + 1) - ROT_MAX)) | |
| OFFSET_X=$((RANDOM % (2*OFFSET_MAX + 1) - OFFSET_MAX)) | |
| OFFSET_Y=$((RANDOM % (2*OFFSET_MAX + 1) - OFFSET_MAX)) | |
| # Resize to fit cell, preserving aspect ratio | |
| POLAROID_WIDTH=$((CELL_WIDTH - 2*BORDER_SIZE)) | |
| POLAROID_HEIGHT=$((CELL_HEIGHT - 2*BORDER_SIZE - ROW_PADDING)) | |
| convert "$IMG" \ | |
| -resize "${POLAROID_WIDTH}x${POLAROID_HEIGHT}"\> \ | |
| -bordercolor white -border $BORDER_SIZE \ | |
| "$TMP_DIR/polaroid_$BASENAME.png" | |
| # Add shadow and rotate | |
| convert "$TMP_DIR/polaroid_$BASENAME.png" \ | |
| \( +clone -background black -shadow 60x$SHADOW_BLUR+$SHADOW_OFFSET+$SHADOW_OFFSET \) \ | |
| +swap -background none -layers merge +repage \ | |
| -background none -rotate $ROT \ | |
| "$TMP_DIR/polaroid_$BASENAME.png" | |
| # Composite onto canvas with random offset | |
| POS_X=$((X + OFFSET_X)) | |
| POS_Y=$((Y + OFFSET_Y)) | |
| convert "$TMP_DIR/canvas.png" "$TMP_DIR/polaroid_$BASENAME.png" \ | |
| -geometry +${POS_X}+${POS_Y} -composite "$TMP_DIR/canvas.png" | |
| X=$((X + CELL_WIDTH)) | |
| i=$((i+1)) | |
| done | |
| Y=$((Y + CELL_HEIGHT + ROW_PADDING)) | |
| done | |
| # ===== Save final output ===== | |
| mv "$TMP_DIR/canvas.png" "$OUTPUT" | |
| rm -r "$TMP_DIR" | |
| echo "Collage created: $OUTPUT" |
Author
habi
commented
Aug 14, 2025
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment