Skip to content

Instantly share code, notes, and snippets.

@habi
Last active August 14, 2025 08:15
Show Gist options
  • Select an option

  • Save habi/32475efafa02b2871bfe301a70fa5323 to your computer and use it in GitHub Desktop.

Select an option

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
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}")
#!/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"
@habi
Copy link
Author

habi commented Aug 14, 2025

team_a4_polaroid_variable_rows

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment