feat: shell scripts

This commit is contained in:
Krzysztof kuhy Rudnicki 2026-02-22 16:57:36 +01:00
parent 265488a478
commit f4f25821e5
15 changed files with 6632 additions and 3 deletions

View File

@ -162,7 +162,7 @@ repos:
- id: codespell
args:
- --skip=*.json,*.lock,*.min.js,*.min.css,.git,__pycache__,.venv,*.txt
- --ignore-words-list=als,ans,ect,nd,som,sur,te,nam,numer,lew,sie,wil,postion,clen,ther,folow,derrive,ony,tje,noe,theses,crate,doubleclick,wile,tabel,pary,blok,proces,serwer,parametr,adres,hart,dout
- --ignore-words-list=als,ans,ect,nd,som,sur,te,nam,numer,lew,sie,wil,postion,clen,ther,folow,derrive,ony,tje,noe,theses,crate,doubleclick,wile,tabel,pary,blok,proces,serwer,parametr,adres,hart,dout,metod,tekst,synonim,grup,mosty,lokal,skalar,milion,nowe
exclude: ^(Bash/ffmpeg-build/|LaTeX/|CPP/|.*\.geojson$)
# ===========================================================================

View File

@ -235,7 +235,7 @@ create_media_archive() {
# Check readability first to provide a clearer error
if [[ ! -r $file ]]; then
log "WARNING: Cannot read $file (permission denied?)"
((copy_errors++))
((copy_errors++)) || true
continue
fi
@ -250,7 +250,7 @@ create_media_archive() {
if echo "$cp_err" | grep -qi "No space left on device"; then
log "HINT: Not enough free space to stage files. Using $TEMP_DIR. Free up space or change TEMP_DIR."
fi
((copy_errors++))
((copy_errors++)) || true
fi
fi
done

147
phone_focus_mode/README.md Normal file
View File

@ -0,0 +1,147 @@
# Phone Focus Mode
Location-based app restriction for a rooted Android phone using wireless ADB.
When within ~500m of home: only whitelisted productive apps remain usable.
When outside that radius: all apps work normally.
## Requirements
- Rooted phone with **Magisk** installed
- Wireless ADB enabled (`Settings → Developer options → Wireless debugging`)
- `adb` installed on your PC (`sudo apt install adb` on Debian/Ubuntu)
- GPS/Location enabled on the phone
## Setup (first time)
### 1. Find your home coordinates
Open Google Maps, right-click your apartment → copy the coordinates shown.
### 2. Edit `config.sh`
```sh
HOME_LAT="52.123456" # your latitude
HOME_LON="21.098765" # your longitude
RADIUS=500 # meters
```
### 3. (Optional) Adjust the whitelist in `config.sh`
To find the exact package name of any app:
```bash
./deploy.sh <phone_ip> --find-pkg stronglift
./deploy.sh <phone_ip> --find-pkg anki
./deploy.sh <phone_ip> --find-pkg pomodoro
```
Then add the correct package name to `WHITELIST` in `config.sh`.
### 4. Deploy
```bash
chmod +x deploy.sh
./deploy.sh 192.168.1.42 # replace with your phone's IP
```
This:
1. Pushes all scripts to `/data/local/tmp/focus_mode/` on the device
2. Installs a Magisk `service.d` script so the daemon auto-starts on boot
3. Starts the daemon immediately
## Usage
```bash
./deploy.sh <ip> --status # Current mode, location, distance from home
./deploy.sh <ip> --log # View recent daemon log
./deploy.sh <ip> --list # List all apps + whitelist status
./deploy.sh <ip> --enable # Force focus mode ON (for testing)
./deploy.sh <ip> --disable # Force focus mode OFF
./deploy.sh <ip> --stop # Stop daemon entirely (restores all apps)
./deploy.sh <ip> --start # Start daemon
./deploy.sh <ip> --restart # Restart daemon (picks up config changes)
./deploy.sh <ip> --pull-log # Download log file to your PC
```
## How it works
```
Every 60 seconds:
get_location() ─── dumpsys location ──► lat,lon
calc_distance() ─── Haversine formula ──► meters
├── within radius? ──► enable_focus_mode()
│ pm disable-user all non-whitelisted apps
│ record which apps were disabled
└── outside radius? ──► disable_focus_mode()
pm enable each app in the disabled list
```
**Hysteresis:** 50m buffer prevents rapid toggling at the boundary. You must travel
`radius - 50m` inward to trigger lock, and `radius + 50m` outward to unlock.
**Fail-safe:** If location is unavailable for 5 consecutive checks (~5 minutes),
focus mode is automatically disabled so you can't be locked out.
**State persistence:** The daemon records exactly which apps _it_ disabled
(in `/data/local/tmp/focus_mode/disabled_by_focus.txt`), so it never accidentally
re-enables apps that were already disabled by the user before focus mode ran.
## On-device control (without PC)
From a root terminal app (e.g. Termux + tsu):
```sh
su -c 'sh /data/local/tmp/focus_mode/focus_ctl.sh status'
su -c 'sh /data/local/tmp/focus_mode/focus_ctl.sh disable'
```
## File layout
| File | Purpose |
| ------------------- | --------------------------------------------- |
| `config.sh` | Coordinates, radius, whitelist, constants |
| `focus_daemon.sh` | Main daemon — runs on device, loops every 60s |
| `focus_ctl.sh` | Control utility — runs on device |
| `magisk_service.sh` | Magisk boot hook → auto-starts daemon |
| `deploy.sh` | PC-side ADB deployment and control script |
## Updating
After editing `config.sh` (e.g. changing whitelist):
```bash
./deploy.sh <ip> # re-pushes all files
# or just the config:
adb push config.sh /data/local/tmp/focus_mode/config.sh
./deploy.sh <ip> --restart
```
## Troubleshooting
**Location always unavailable:**
- Enable GPS and network location on the phone
- Open Google Maps once to warm up the GPS provider
- The daemon logs every attempt; check with `--log`
**App won't disable:**
- Some system apps can't be disabled even as root; they're silently skipped
- Check log for "Failed to disable" warnings
**Daemon not starting on boot:**
- Verify Magisk is installed and `service.d` is supported
- Check `/data/adb/service.d/99-focus-mode.sh` exists and is executable
- Some Magisk versions use `/data/adb/post-fs-data.d/` instead; try both
**Wrong package name in whitelist:**
- Use `./deploy.sh <ip> --find-pkg <keyword>` to find the exact package name
- Package names are case-sensitive

131
phone_focus_mode/config.sh Executable file
View File

@ -0,0 +1,131 @@
#!/system/bin/sh
# ============================================================
# Focus Mode Configuration
# ============================================================
# IMPORTANT: You MUST set HOME_LAT and HOME_LON to your
# apartment's coordinates before deploying.
# Get them from Google Maps: right-click your apartment → coords
# ============================================================
# --- Home location (Warsaw, auto-detected via GPS on 2026-02-22) ---
export HOME_LAT="REDACTED_LAT"
export HOME_LON="REDACTED_LON"
# --- Radius in meters ---
export RADIUS=500
# --- Hysteresis buffer in meters (prevents rapid toggling at boundary) ---
export HYSTERESIS=50
# --- Location check interval in seconds ---
export CHECK_INTERVAL=60
# --- Fail-safe: if location unavailable for this many consecutive checks,
# switch to unrestricted mode to avoid locking user out ---
export MAX_LOCATION_FAILS=5
# --- Log file ---
export LOG_FILE="/data/local/tmp/focus_mode/focus_mode.log"
export LOG_MAX_LINES=500
# --- State file (tracks which apps were disabled by focus mode) ---
STATE_DIR="/data/local/tmp/focus_mode"
export DISABLED_APPS_FILE="$STATE_DIR/disabled_by_focus.txt"
export MODE_FILE="$STATE_DIR/current_mode.txt"
# ============================================================
# WHITELISTED APPS
# These apps will ALWAYS remain enabled, even in focus mode.
# Package names verified against installed packages on 2026-02-22.
# ============================================================
export WHITELIST="
# --- User-requested productive apps ---
com.stronglifts.app
com.ichi2.anki
com.metrolist.music
com.kuhy.pomodoro_app
# --- Google system apps (add by name even though they show as system) ---
com.google.android.apps.maps
com.google.android.calendar
# --- Notes & productivity ---
net.cozic.joplin
# --- Navigation & transit (needed when going out) ---
net.osmand
de.schildbach.oeffi
com.kolejeslaskie.mss
# --- Banking (must always work) ---
pl.mbank
pl.pkobp.iko
# --- Security & root tools (must always work) ---
com.topjohnwu.magisk
moe.shizuku.privileged.api
me.phh.superuser
com.beemdevelopment.aegis
com.azure.authenticator
oracle.idm.mobile.authenticator
com.kunzisoft.keepass.libre
# --- Email & communication ---
com.microsoft.office.outlook
com.google.android.gm
ch.protonmail.android
com.microsoft.teams
"
# --- System / essential packages that must NEVER be disabled ---
# These are matched as prefixes (startswith).
# You generally don't need to edit this list.
export SYSTEM_NEVER_DISABLE="
com.android.launcher
com.android.launcher3
com.android.settings
com.android.systemui
com.android.phone
com.android.dialer
com.android.contacts
com.android.mms
com.android.messaging
com.android.providers
com.android.inputmethod
com.android.shell
com.android.packageinstaller
com.android.permissioncontroller
com.android.bluetooth
com.android.nfc
com.android.wifi
com.android.certinstaller
com.android.vpndialogs
com.android.se
com.android.emergency
com.android.camera
com.android.camera2
com.android.documentsui
com.android.externalstorage
com.android.keychain
com.android.location
com.android.networkstack
com.android.captiveportallogin
com.google.android.gms
com.google.android.gsf
com.google.android.ext.services
com.google.android.ext.shared
com.google.android.webview
com.google.android.trichromelibrary
com.google.android.inputmethod.latin
com.google.android.setupwizard
com.google.android.packageinstaller
com.google.android.permissioncontroller
com.google.android.deskclock
com.google.android.dialer
com.google.android.contacts
com.google.android.apps.messaging
android
com.mediatek
com.qualcomm
"

219
phone_focus_mode/deploy.sh Executable file
View File

@ -0,0 +1,219 @@
#!/bin/bash
# ============================================================
# Focus Mode Deployment Script
# Deploys focus mode to your rooted BL-9000 via wireless ADB
#
# Usage:
# ./deploy.sh [phone_ip] - Full deploy (first time or update)
# ./deploy.sh [phone_ip] --status - Check status
# ./deploy.sh [phone_ip] --log - View log
# ./deploy.sh [phone_ip] --stop - Stop daemon
# ./deploy.sh [phone_ip] --enable - Force focus mode on
# ./deploy.sh [phone_ip] --disable - Force focus mode off
# ============================================================
set -euo pipefail
PHONE_IP="${1:-}"
ACTION="${2:---deploy}"
REMOTE_DIR="/data/local/tmp/focus_mode"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
usage() {
echo "Usage: $0 <phone_ip> [action]"
echo ""
echo "Actions:"
echo " (none) Full deploy"
echo " --status Show daemon status and current mode"
echo " --log Tail the daemon log"
echo " --stop Stop daemon (re-enables all apps)"
echo " --start Start daemon"
echo " --restart Restart daemon"
echo " --enable Force focus mode on"
echo " --disable Force focus mode off"
echo " --list List all third-party apps and whitelist status"
echo " --pull-log Download log file locally"
echo " --find-pkg Show installed packages matching a filter (e.g. --find-pkg pomodoro)"
echo ""
echo "Examples:"
echo " $0 192.168.1.42"
echo " $0 192.168.1.42 --status"
echo " $0 192.168.1.42 --find-pkg stronglift"
exit 1
}
# ---- Pre-flight checks ----
check_adb() {
if ! command -v adb >/dev/null 2>&1; then
echo "ERROR: adb not found. Install Android platform-tools first."
echo " Ubuntu/Debian: sudo apt install adb"
echo " Arch: sudo pacman -S android-tools"
exit 1
fi
}
check_coords() {
local lat lon
lat="$(grep '^HOME_LAT=' "$SCRIPT_DIR/config.sh" | cut -d'"' -f2)"
lon="$(grep '^HOME_LON=' "$SCRIPT_DIR/config.sh" | cut -d'"' -f2)"
if [ "$lat" = "0.000000" ] && [ "$lon" = "0.000000" ]; then
echo "ERROR: You must set your home coordinates in config.sh before deploying!"
echo ""
echo " 1. Find your coords on Google Maps (right-click your apartment)"
echo " 2. Edit phone_focus_mode/config.sh:"
echo " HOME_LAT=\"52.123456\""
echo " HOME_LON=\"21.098765\""
exit 1
fi
echo " Home location: $lat, $lon"
}
check_ip() {
if [ -z "$PHONE_IP" ]; then
echo "ERROR: Phone IP not provided."
echo ""
usage
fi
}
connect_adb() {
echo "Connecting to $PHONE_IP:5555 ..."
adb connect "$PHONE_IP:5555"
sleep 1
if ! adb devices | grep -q "$PHONE_IP"; then
echo "ERROR: Could not connect to $PHONE_IP:5555"
echo "Make sure wireless ADB is enabled and the phone is reachable."
exit 1
fi
echo "Connected."
}
# Wrapper: run a root shell command on the phone
adb_root() {
adb -s "$PHONE_IP:5555" shell su -c "$1"
}
# ============================================================
# DEPLOY
# ============================================================
do_deploy() {
echo "=== Focus Mode Deployer ==="
echo ""
check_coords
echo ""
echo "[1/6] Connecting to phone..."
connect_adb
echo "[2/6] Verifying root access..."
if ! adb_root "id" | grep -q "uid=0"; then
echo "ERROR: Could not get root shell. Is Magisk installed?"
exit 1
fi
echo " Root confirmed."
echo "[3/6] Creating directories on device..."
# Use world-writable staging dir so non-root adb push works
adb -s "$PHONE_IP:5555" shell "mkdir -p /data/local/tmp/focus_stage"
adb_root "mkdir -p $REMOTE_DIR /data/adb/service.d"
adb_root "chmod 777 /data/local/tmp/focus_stage"
echo "[4/6] Uploading scripts..."
adb -s "$PHONE_IP:5555" push "$SCRIPT_DIR/config.sh" "/data/local/tmp/focus_stage/config.sh"
adb -s "$PHONE_IP:5555" push "$SCRIPT_DIR/focus_daemon.sh" "/data/local/tmp/focus_stage/focus_daemon.sh"
adb -s "$PHONE_IP:5555" push "$SCRIPT_DIR/focus_ctl.sh" "/data/local/tmp/focus_stage/focus_ctl.sh"
adb -s "$PHONE_IP:5555" push "$SCRIPT_DIR/magisk_service.sh" "/data/local/tmp/focus_stage/99-focus-mode.sh"
# Move staged files into place with root
adb_root "cp /data/local/tmp/focus_stage/config.sh $REMOTE_DIR/config.sh"
adb_root "cp /data/local/tmp/focus_stage/focus_daemon.sh $REMOTE_DIR/focus_daemon.sh"
adb_root "cp /data/local/tmp/focus_stage/focus_ctl.sh $REMOTE_DIR/focus_ctl.sh"
adb_root "cp /data/local/tmp/focus_stage/99-focus-mode.sh /data/adb/service.d/99-focus-mode.sh"
adb_root "rm -rf /data/local/tmp/focus_stage"
echo "[5/6] Setting permissions..."
adb_root "chmod 755 $REMOTE_DIR/config.sh $REMOTE_DIR/focus_daemon.sh $REMOTE_DIR/focus_ctl.sh"
adb_root "chmod 755 /data/adb/service.d/99-focus-mode.sh"
adb_root "touch $REMOTE_DIR/disabled_by_focus.txt"
adb_root "touch $REMOTE_DIR/focus_mode.log"
echo "[6/6] Starting daemon..."
# Kill existing daemon via pidfile to avoid hitting the ADB shell process
adb_root "
PIDFILE=$REMOTE_DIR/daemon.pid
if [ -f \"\$PIDFILE\" ]; then
OLD_PID=\$(cat \"\$PIDFILE\")
kill -9 \"\$OLD_PID\" 2>/dev/null
rm -f \"\$PIDFILE\"
fi
# Also kill any stray instances
for p in \$(pgrep -f focus_daemon.sh 2>/dev/null); do kill -9 \$p 2>/dev/null; done
sleep 1
setsid sh $REMOTE_DIR/focus_daemon.sh </dev/null >/dev/null 2>&1 &
echo \$!
"
sleep 3
echo ""
echo "=== Deploy complete! ==="
echo ""
echo "Checking status..."
adb_root "sh $REMOTE_DIR/focus_ctl.sh status"
echo ""
echo "The daemon will auto-start on every boot via Magisk service.d."
echo ""
echo "Useful commands:"
echo " $0 $PHONE_IP --status # Check mode and location"
echo " $0 $PHONE_IP --log # View daemon log"
echo " $0 $PHONE_IP --list # See all apps and whitelist status"
echo " $0 $PHONE_IP --enable # Force focus mode on for testing"
echo " $0 $PHONE_IP --disable # Force focus mode off"
}
# ============================================================
# Control actions (post-deploy)
# ============================================================
do_control() {
local ctl_cmd="$1"
connect_adb
adb_root "sh $REMOTE_DIR/focus_ctl.sh $ctl_cmd"
}
do_pull_log() {
connect_adb
echo "Downloading log..."
adb -s "$PHONE_IP:5555" pull "$REMOTE_DIR/focus_mode.log" "./focus_mode_$(date +%Y%m%d_%H%M%S).log"
echo "Done."
}
do_find_pkg() {
local filter="${3:-}"
if [ -z "$filter" ]; then
echo "Usage: $0 <ip> --find-pkg <search_term>"
exit 1
fi
connect_adb
echo "Packages matching '$filter':"
adb -s "$PHONE_IP:5555" shell pm list packages | grep -i "$filter" | sed 's/^package:/ /'
}
# ============================================================
# Entry point
# ============================================================
check_adb
check_ip
case "$ACTION" in
--deploy|"") do_deploy ;;
--status) do_control "status" ;;
--log) connect_adb; adb_root "sh $REMOTE_DIR/focus_ctl.sh log 100" ;;
--stop) do_control "stop" ;;
--start) do_control "start" ;;
--restart) do_control "restart" ;;
--enable) do_control "enable" ;;
--disable) do_control "disable" ;;
--list) do_control "list-apps" ;;
--pull-log) do_pull_log ;;
--find-pkg) do_find_pkg "$@" ;;
*) echo "Unknown action: $ACTION"; usage ;;
esac

241
phone_focus_mode/focus_ctl.sh Executable file
View File

@ -0,0 +1,241 @@
#!/system/bin/sh
# shellcheck shell=ash
# ============================================================
# Focus Mode Control Utility
# Run on the phone via: su -c /data/local/tmp/focus_mode/focus_ctl.sh <command>
# Or from PC via: adb shell su -c '/data/local/tmp/focus_mode/focus_ctl.sh <command>'
# ============================================================
SCRIPT_DIR="/data/local/tmp/focus_mode"
. "$SCRIPT_DIR/config.sh"
PIDFILE="$STATE_DIR/daemon.pid"
# ---- Logging ----
log() {
local ts
ts="$(date '+%Y-%m-%d %H:%M:%S')"
echo "[$ts] $1" >> "$LOG_FILE"
}
usage() {
echo "Usage: focus_ctl.sh <command>"
echo ""
echo "Commands:"
echo " start - Start the focus mode daemon"
echo " stop - Stop the daemon and re-enable all apps"
echo " status - Show current mode, location and disabled apps"
echo " enable - Force focus mode on (regardless of location)"
echo " disable - Force focus mode off (regardless of location)"
echo " log - Show daemon log"
echo " list-apps - List all non-whitelisted third-party apps"
echo " whitelist - List currently whitelisted packages"
echo " restart - Restart the daemon"
echo ""
}
# Helper to check if daemon is running
daemon_pid() {
if [ -f "$PIDFILE" ]; then
local pid
pid="$(cat "$PIDFILE")"
if kill -0 "$pid" 2>/dev/null; then
echo "$pid"
fi
fi
}
cmd_start() {
local pid
pid="$(daemon_pid)"
if [ -n "$pid" ]; then
echo "Daemon already running (PID $pid)"
return
fi
setsid sh "$SCRIPT_DIR/focus_daemon.sh" </dev/null >/dev/null 2>&1 &
sleep 2
pid="$(daemon_pid)"
if [ -n "$pid" ]; then
echo "Daemon started (PID $pid)"
else
echo "ERROR: Daemon failed to start. Check log: $LOG_FILE"
fi
}
cmd_stop() {
local pid
pid="$(daemon_pid)"
if [ -z "$pid" ]; then
echo "Daemon not running"
# Clean up stale pidfile if present
rm -f "$PIDFILE"
else
kill -TERM "$pid"
echo "Daemon stopped (sent SIGTERM to PID $pid)"
fi
}
cmd_status() {
local pid
pid="$(daemon_pid)"
local mode="unknown"
[ -f "$MODE_FILE" ] && mode="$(cat "$MODE_FILE")"
echo "=== Focus Mode Status ==="
if [ -n "$pid" ]; then
echo "Daemon: RUNNING (PID $pid)"
else
echo "Daemon: STOPPED"
fi
echo "Mode: $mode"
echo "Home: $HOME_LAT, $HOME_LON (radius: ${RADIUS}m)"
echo ""
# Show current location if available
location="$(dumpsys location 2>/dev/null \
| grep -oE 'Location\[.*[-]?[0-9]{1,3}\.[0-9]+,[-]?[0-9]{1,3}\.[0-9]+' \
| grep -oE '[-]?[0-9]{1,3}\.[0-9]+,[-]?[0-9]{1,3}\.[0-9]+' \
| head -1)"
if [ -n "$location" ]; then
lat="$(echo "$location" | cut -d',' -f1)"
lon="$(echo "$location" | cut -d',' -f2)"
dist="$(echo "$lat $lon $HOME_LAT $HOME_LON" | awk '{
PI=3.14159265358979; R=6371000
a1=$1*PI/180; o1=$2*PI/180
a2=$3*PI/180; o2=$4*PI/180
da=a2-a1; dlon=o2-o1
x=sin(da/2)^2+cos(a1)*cos(a2)*sin(dlon/2)^2
printf "%d", R*2*atan2(sqrt(x),sqrt(1-x))
}')"
echo "Location: $lat, $lon"
echo "Distance: ${dist}m from home"
else
echo "Location: unavailable"
fi
echo ""
if [ -f "$DISABLED_APPS_FILE" ] && [ -s "$DISABLED_APPS_FILE" ]; then
echo "=== Apps disabled by focus mode ==="
cat "$DISABLED_APPS_FILE"
else
echo "No apps currently disabled by focus mode"
fi
}
cmd_enable() {
# Disable daemon temporarily, force focus
echo "Forcing focus mode ON..."
. "$SCRIPT_DIR/config.sh"
# Source common functions - inline here for standalone use
: > "$STATE_DIR/disabled_by_focus.txt"
local count=0
for pkg in $(pm list packages -3 2>/dev/null | sed 's/^package://'); do
# Check whitelist
whitelisted=0
for w in $WHITELIST; do
w_clean="$(echo "$w" | tr -d '[:space:]')"
[ -z "$w_clean" ] && continue
[ "$pkg" = "$w_clean" ] && { whitelisted=1; break; }
done
[ "$whitelisted" -eq 1 ] && continue
# Check system protection
protected=0
for prefix in $SYSTEM_NEVER_DISABLE; do
prefix_clean="$(echo "$prefix" | tr -d '[:space:]')"
[ -z "$prefix_clean" ] && continue
case "$pkg" in
"$prefix_clean"*) protected=1; break ;;
esac
done
[ "$protected" -eq 1 ] && continue
if pm disable-user --user 0 "$pkg" >/dev/null 2>&1; then
echo "$pkg" >> "$STATE_DIR/disabled_by_focus.txt"
count=$((count + 1))
fi
done
echo "focus" > "$MODE_FILE"
echo "Done: disabled $count apps"
}
cmd_disable() {
echo "Forcing focus mode OFF..."
if [ -f "$DISABLED_APPS_FILE" ] && [ -s "$DISABLED_APPS_FILE" ]; then
local count=0
while IFS= read -r pkg; do
[ -z "$pkg" ] && continue
pm enable "$pkg" >/dev/null 2>&1 && count=$((count + 1))
done < "$DISABLED_APPS_FILE"
: > "$DISABLED_APPS_FILE"
echo "Done: re-enabled $count apps"
else
echo "No apps to re-enable"
fi
echo "normal" > "$MODE_FILE"
}
cmd_log() {
local lines="${1:-50}"
if [ -f "$LOG_FILE" ]; then
tail -n "$lines" "$LOG_FILE"
else
echo "Log file not found: $LOG_FILE"
fi
}
cmd_list_apps() {
echo "=== Third-party apps NOT in whitelist ==="
for pkg in $(pm list packages -3 2>/dev/null | sed 's/^package://'); do
whitelisted=0
for w in $WHITELIST; do
w="$(echo "$w" | tr -d '[:space:]')"
[ -z "$w" ] && continue
[ "$pkg" = "$w" ] && { whitelisted=1; break; }
done
if [ "$whitelisted" -eq 0 ]; then
# Check if currently disabled by focus mode
if grep -qF "$pkg" "$DISABLED_APPS_FILE" 2>/dev/null; then
echo " [BLOCKED] $pkg"
else
echo " [active] $pkg"
fi
fi
done
echo ""
echo "=== Whitelisted apps ==="
for w in $WHITELIST; do
w="$(echo "$w" | tr -d '[:space:]')"
[ -z "$w" ] && continue
echo " [allowed] $w"
done
}
cmd_whitelist() {
echo "=== Whitelisted packages ==="
for w in $WHITELIST; do
w="$(echo "$w" | tr -d '[:space:]')"
[ -z "$w" ] && continue
# Check if installed
if pm list packages "$w" 2>/dev/null | grep -qF "$w"; then
echo " [installed] $w"
else
echo " [not found] $w"
fi
done
}
case "$1" in
start) cmd_start ;;
stop) cmd_stop ;;
status) cmd_status ;;
enable) cmd_enable ;;
disable) cmd_disable ;;
log) cmd_log "${2:-50}" ;;
list-apps) cmd_list_apps ;;
whitelist) cmd_whitelist ;;
restart) cmd_stop; sleep 2; cmd_start ;;
*) usage ;;
esac

213
phone_focus_mode/focus_daemon.sh Executable file
View File

@ -0,0 +1,213 @@
#!/system/bin/sh
# shellcheck shell=ash
# ============================================================
# Focus Mode Daemon
# Runs on rooted Android device. Periodically checks GPS
# location and restricts non-whitelisted apps when near home.
# ============================================================
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
. "$SCRIPT_DIR/config.sh"
PIDFILE="$STATE_DIR/daemon.pid"
# ---- PID lock: exit if already running ----
acquire_lock() {
mkdir -p "$STATE_DIR"
if [ -f "$PIDFILE" ]; then
local old_pid
old_pid="$(cat "$PIDFILE")"
if kill -0 "$old_pid" 2>/dev/null; then
echo "Daemon already running (PID $old_pid), exiting."
exit 0
fi
# Stale pidfile
rm -f "$PIDFILE"
fi
echo $$ > "$PIDFILE"
}
# ---- Logging ----
log() {
local ts
ts="$(date '+%Y-%m-%d %H:%M:%S')"
echo "[$ts] $1" >> "$LOG_FILE"
}
# ---- Build helper files for fast package checks ----
build_whitelist_file() {
echo "$WHITELIST" | grep -v '^[[:space:]]*#' | grep -v '^[[:space:]]*$' \
| sed 's/^[[:space:]]*//;s/[[:space:]]*$//' > "$STATE_DIR/whitelist.txt"
}
build_sysprotect_file() {
echo "$SYSTEM_NEVER_DISABLE" | grep -v '^[[:space:]]*$' \
| sed 's/^[[:space:]]*//;s/[[:space:]]*$//' > "$STATE_DIR/sysprotect.txt"
}
# ---- Initialization ----
init() {
mkdir -p "$STATE_DIR"
touch "$LOG_FILE"
touch "$DISABLED_APPS_FILE"
if [ "$HOME_LAT" = "0.000000" ] && [ "$HOME_LON" = "0.000000" ]; then
log "ERROR: Home coordinates not set! Edit config.sh first."
exit 1
fi
build_whitelist_file
build_sysprotect_file
if [ -f "$MODE_FILE" ]; then
CURRENT_MODE="$(cat "$MODE_FILE")"
else
CURRENT_MODE="normal"
fi
LOCATION_FAIL_COUNT=0
log "Focus mode daemon started (PID=$$, mode=$CURRENT_MODE, home=$HOME_LAT,$HOME_LON, radius=${RADIUS}m)"
}
# ---- Location ----
get_location() {
dumpsys location 2>/dev/null \
| grep -oE '[-]?[0-9]{1,3}\.[0-9]{4,},[-]?[0-9]{1,3}\.[0-9]{4,}' \
| head -1
}
# ---- Distance Calculation (Haversine via awk) ----
calc_distance() {
echo "$1 $2 $3 $4" | awk '{
PI = 3.14159265358979323846
R = 6371000.0
lat1 = $1 * PI / 180.0
lon1 = $2 * PI / 180.0
lat2 = $3 * PI / 180.0
lon2 = $4 * PI / 180.0
dlat = lat2 - lat1
dlon = lon2 - lon1
sdlat = sin(dlat / 2.0)
sdlon = sin(dlon / 2.0)
a = sdlat * sdlat + cos(lat1) * cos(lat2) * sdlon * sdlon
c = 2.0 * atan2(sqrt(a), sqrt(1.0 - a))
printf "%d\n", R * c
}'
}
# ---- Check if package is allowed (whitelist or system-protected) ----
is_allowed() {
local pkg="$1"
# Exact match against whitelist file
if grep -qxF "$pkg" "$STATE_DIR/whitelist.txt" 2>/dev/null; then
return 0
fi
# Prefix match against system-protect file
while IFS= read -r prefix; do
[ -z "$prefix" ] && continue
case "$pkg" in
"$prefix"*) return 0 ;;
esac
done < "$STATE_DIR/sysprotect.txt"
return 1
}
# ---- Focus Mode Control ----
enable_focus_mode() {
[ "$CURRENT_MODE" = "focus" ] && return
log "ENABLING focus mode - restricting non-whitelisted apps"
: > "$DISABLED_APPS_FILE"
local tmp_pkgs="$STATE_DIR/pkg_list.txt"
pm list packages -3 2>/dev/null | sed 's/^package://' > "$tmp_pkgs"
while IFS= read -r pkg; do
[ -z "$pkg" ] && continue
is_allowed "$pkg" && continue
if pm disable-user --user 0 "$pkg" >/dev/null 2>&1; then
echo "$pkg" >> "$DISABLED_APPS_FILE"
fi
done < "$tmp_pkgs"
rm -f "$tmp_pkgs"
local count
count=$(wc -l < "$DISABLED_APPS_FILE" 2>/dev/null || echo 0)
CURRENT_MODE="focus"
echo "focus" > "$MODE_FILE"
log "Focus mode enabled - disabled $count apps"
}
disable_focus_mode() {
[ "$CURRENT_MODE" = "normal" ] && return
log "DISABLING focus mode - re-enabling apps"
local count=0
if [ -f "$DISABLED_APPS_FILE" ] && [ -s "$DISABLED_APPS_FILE" ]; then
while IFS= read -r pkg; do
[ -z "$pkg" ] && continue
pm enable "$pkg" >/dev/null 2>&1 && count=$((count + 1))
done < "$DISABLED_APPS_FILE"
: > "$DISABLED_APPS_FILE"
fi
CURRENT_MODE="normal"
echo "normal" > "$MODE_FILE"
log "Focus mode disabled - re-enabled $count apps"
}
# ---- Signal handlers ----
cleanup() {
log "Daemon shutting down - re-enabling all apps"
disable_focus_mode
rm -f "$PIDFILE"
exit 0
}
# HUP is intentionally NOT trapped so the daemon survives ADB disconnects.
# Only SIGTERM/SIGINT trigger a clean shutdown.
trap cleanup INT TERM
# ---- Main Loop ----
main() {
acquire_lock
init
while true; do
location="$(get_location)"
if [ -n "$location" ]; then
lat="$(echo "$location" | cut -d',' -f1)"
lon="$(echo "$location" | cut -d',' -f2)"
distance="$(calc_distance "$lat" "$lon" "$HOME_LAT" "$HOME_LON")"
if [ "$CURRENT_MODE" = "focus" ]; then
threshold=$((RADIUS + HYSTERESIS))
else
threshold=$((RADIUS - HYSTERESIS))
fi
if [ "$distance" -le "$threshold" ] 2>/dev/null; then
enable_focus_mode
else
disable_focus_mode
fi
LOCATION_FAIL_COUNT=0
log "Location: $lat,$lon | Distance: ${distance}m | Threshold: ${threshold}m | Mode: $CURRENT_MODE"
else
LOCATION_FAIL_COUNT=$((LOCATION_FAIL_COUNT + 1))
log "Location unavailable (attempt $LOCATION_FAIL_COUNT/$MAX_LOCATION_FAILS)"
if [ "$LOCATION_FAIL_COUNT" -ge "$MAX_LOCATION_FAILS" ]; then
log "FAIL-SAFE: Location unavailable too long, switching to normal mode"
disable_focus_mode
fi
fi
sleep "$CHECK_INTERVAL"
done
}
main "$@"

View File

@ -0,0 +1,21 @@
#!/bin/sh
# ============================================================
# Magisk service.d autostart script
# This file is placed on the device at:
# /data/adb/service.d/99-focus-mode.sh
# Magisk executes everything in service.d on boot with root.
# ============================================================
# Wait for system to be fully booted before starting daemon
sleep 120
SCRIPT_DIR="/data/local/tmp/focus_mode"
# Ensure scripts are executable
chmod +x "$SCRIPT_DIR/focus_daemon.sh"
chmod +x "$SCRIPT_DIR/focus_ctl.sh"
# Start focus daemon in a new session (detached from any controlling terminal)
setsid sh "$SCRIPT_DIR/focus_daemon.sh" </dev/null >/dev/null 2>&1 &
exit 0

View File

@ -0,0 +1 @@
"""Thesis video visualization scripts."""

View File

@ -0,0 +1,563 @@
## PYTANIE 23: Segmentacja obrazu
**Problem, strategie klasyczne i sieci neuronowe.**
---
### Tło pojęciowe — słowniczek
**Obraz cyfrowy (digital image)** — macierz pikseli. Obraz 1920×1080 = ~2 mln pikseli. Każdy piksel ma wartość (grayscale: 0-255) lub kanały RGB (3 × 0-255). Segmentacja operuje na tej macierzy.
**Piksel (pixel)** — najmniejsza jednostka obrazu. „Picture element." Segmentacja = przypisanie etykiety KAŻDEMU pikselowi.
**Segmentacja obrazu (image segmentation)** — podział obrazu na regiony, gdzie każdy piksel dostaje etykietę klasy (np. „samochód", „droga", „niebo"). Różni się od klasyfikacji (cały obraz → 1 etykieta) i detekcji (bounding box + etykieta).
**Czy naprawdę KAŻDY piksel?** Tak, w semantic segmentation wynik to mapa o IDENTYCZNYM rozmiarze jak obraz wejściowy. Obraz 640×480 → mapa 640×480, w której KAŻDY z 307 200 pikseli ma etykietę klasy. Żaden piksel nie jest pominięty. Nawet piksele „tła" dostają etykietę (np. klasa „background" lub „void"). W instance segmentation dodatkowo piksele tego samego obiektu dostają ten sam ID instancji.
Obraz wejściowy: 640 × 480 pikseli (RGB, 3 kanały)
Mapa segmentacji: 640 × 480 pikseli (1 kanał — numer klasy)
Piksel (100, 200): RGB=(134, 178, 210) → klasa 3 ("niebo")
Piksel (320, 400): RGB=(82, 79, 73) → klasa 7 ("droga")
KAŻDY piksel ma etykietę — nawet ten "nudny" fragment tła.
---
**Over-segmentation (nad-segmentacja)** — sytuacja, gdy algorytm segmentacji generuje ZBYT WIELE regionów — więcej niż jest obiektów/klas na obrazie. Jeden obiekt zostaje podzielony na kilka-kilkadziesiąt fragmentów. Problem typowy dla metod klasycznych (watershed, region growing).
Obraz: jeden kubek na stole
Idealna segmentacja: 2 regiony (kubek, tło)
Over-segmentation: 47 regionów! (kubek podzielony na 12 kawałków,
stół na 20, tło na 15)
Dlaczego to się dzieje?
- Watershed: każde lokalne minimum jasności → osobny region → setki regionów
- Region Growing: drobne różnice w intensywności → osobne regiony
- Szum (noise) w obrazie → fałszywe granice
Jak sobie z tym radzić?
- **Markers/seeds:** zamiast automatycznych minimów → podaj ręczne punkty startowe
- **Superpixels:** celowa nad-segmentacja na ~100-500 jednorodnych "superpikseli"
(np. SLIC), potem GRUPOWANIE superpikseli w klasy → szybsze i stabilniejsze
- **Hierarchiczne:** wielopoziomowa segmentacja → scalanie regionów bottom-up
- **Deep learning:** sieci neuronowe uczą się "co jest obiektem" z danych → nie mają
problemu z over-segmentation (bo wiedzą, że kubek to jeden obiekt)
**Under-segmentation (pod-segmentacja)** — przeciwieństwo: zbyt mało regionów, różne obiekty zlane w jeden region. Mniej typowy problem.
---
**Typy segmentacji:**
**Semantic segmentation** — każdy piksel → klasa, ale NIE rozróżnia instancji. Wszystkie samochody = jedna klasa „samochód".
[samochód][samochód][droga][droga][pieszo][niebo]
Dwa samochody = ta sama etykieta "samochód"
**Instance segmentation** — rozróżnia instancje tego samego obiektu. Samochód#1 i Samochód#2 mają różne etykiety.
**Panoptic segmentation** — łączy semantic + instance. Obiekty „things" (samochody, ludzie) mają instancje; „stuff" (niebo, droga) — tylko klasy.
---
#### Pojęcia kluczowe dla progowania i Otsu
**Wariancja (variance, σ²)** — miara tego, jak bardzo wartości RÓŻNIĄ SIĘ od swojej średniej. Im większa wariancja, tym bardziej „rozrzucone" są dane. Wzór: σ² = Σ(xᵢ - μ)² / n, gdzie μ to średnia.
Przykład 1 — MAŁA wariancja (dane skupione):
wartości: [48, 50, 52, 49, 51] średnia μ = 50
σ² = ((48-50)² + (50-50)² + (52-50)² + (49-50)² + (51-50)²) / 5
= (4 + 0 + 4 + 1 + 1) / 5 = 2.0
Przykład 2 — DUŻA wariancja (dane rozrzucone):
wartości: [10, 90, 30, 80, 50] średnia μ = 52
σ² = ((10-52)² + (90-52)² + (30-52)² + (80-52)² + (50-52)²) / 5
= (1764 + 1444 + 484 + 784 + 4) / 5 = 896.0
Mała σ² = punkty blisko średniej = dane JEDNORODNE
Duża σ² = punkty daleko od średniej = dane RÓŻNORODNE
**Wewnątrzklasowa (within-class)** — „wewnątrz klasy" oznacza, że mierzymy wariancję OSOBNO dla każdej grupy (klasy), a potem ważymy wynik proporcją pikseli w grupie. Jeśli klasa 0 ma piksele [30, 50, 45] a klasa 1 ma piksele [180, 200, 190], to σ²_wewnątrz = (udział_kl0 × σ²_kl0) + (udział_kl1 × σ²_kl1).
**Wariancja wewnątrzklasowa (within-class variance)** — obliczasz wariancję KAŻDEJ klasy osobno, ważysz przez udział pikseli w tej klasie, sumujesz. Jeśli σ²_wewnątrz jest MAŁA → klasy są „jednorodne" (piksele w klasie 0 mają podobne jasności, piksele w klasie 1 też).
**Co to znaczy „klasy jednorodne"?** — jednorodna klasa to taka, w której WSZYSTKIE piksele mają podobne wartości. Np. klasa „tło" ma jasności [195, 200, 198, 205] → jednorodna (σ² mała). Klasa mieszająca tło i obiekt [30, 200, 50, 190] → niejednorodna (σ² duża). Otsu szuka progu T, który daje NAJBARDZIEJ jednorodne klasy.
**Histogram bimodalny (bimodal histogram)** — histogram z DWOMA wyraźnymi „garbami" (pikami). „Bi" = dwa, „modal" = moda (najczęstsza wartość). Typowy dla obrazów z jednym obiektem na tle — garb 1 odpowiada ciemnym pikselom (obiekt), garb 2 jasnym (tło). Otsu działa TYLKO gdy histogram jest bimodalny — bo szuka progu MIĘDZY garbami.
Garb 1 (ciemne~60): piksele obiektu
Garb 2 (jasne~190): piksele tła
Dolina między garbami → tu Otsu stawia próg T!
Gdyby histogram miał JEDEN garb (unimodalny) → brak naturalnego
podziału → Otsu wybierze losowy próg → słaby wynik.
![Histogram bimodalny, wariancja wewnątrzklasowa i jednorodność klas — Otsu](img/q23_otsu_bimodal.png)
---
**Thresholding (progowanie)** — najprostsza metoda segmentacji. Pomysł: każdy piksel ma wartość jasności (0=czarny, 255=biały). Wybierz PRÓG T: piksel > T → klasa 1 (obiekt), piksel ≤ T → klasa 0 (tło). Działa lepiej niż się wydaje na prostych obrazach (tekst na kartce, RTG, dokumenty).
Obraz (jasność pikseli): [50][200][180][30][220][190]
Próg T=128:
50 ≤ 128 → 0 (tło)
200 > 128 → 1 (obiekt)
180 > 128 → 1
30 ≤ 128 → 0
Wynik: [ 0 ][ 1 ][ 1 ][ 0][ 1 ][ 1 ]
Problem: JAK wybrać T? Ręcznie → subiektywne. Rozwiązanie → Otsu.
Mnemonik: „PRÓG na bramce" — jak bramkarz, przepuszcza piksele jaśniejsze od T,
blokuje ciemniejsze.
**Otsu** — automatyczny dobór progu. Algorytm: przetestuj WSZYSTKIE progi T=0..255, dla każdego oblicz wariancję wewnątrzklasową (jak „różnorodne" są piksele w klasie 0 i klasie 1). Wybierz T minimalizujące tę wariancję = klasy jak najbardziej jednorodne. Złożoność: O(n·L) gdzie n=piksele, L=poziomy jasności (256). Ograniczenie: działa TYLKO dla 2 klas i zakłada bimodalny histogram jasności (dwa „garby"). Patrz diagram powyżej.
Pseudokod Otsu:
best_T = 0
min_var = ∞
for T in 0..255:
c0 = piksele z jasność ≤ T
c1 = piksele z jasność > T
w0 = len(c0) / len(all_pixels)
w1 = len(c1) / len(all_pixels)
var = w0 * variance(c0) + w1 * variance(c1)
if var < min_var:
min_var = var
best_T = T
return best_T
Mnemonik: „AUTO-bramkarz Otsu" — sam sprawdza 256 progów i wybiera najlepszy.
---
#### Pojęcia kluczowe dla Region Growing
**Region Growing (rozrastanie regionu)** — zaczynasz od jednego piksela „ziarna" (seed) wybranego ręcznie lub automatycznie. Sprawdzasz sąsiadów: jeśli sąsiad jest PODOBNY (np. |jasność_sąsiada - jasność_regionu| < próg), dodaj go do regionu. Powtarzaj nie ma więcej podobnych sąsiadów. Następnie nowy seed nowy region.
**Dlaczego seed „ręcznie LUB automatycznie"?** — to dwa różne scenariusze użycia:
RĘCZNY seed:
- Użytkownik klika myszką na obraz: „tu jest obiekt"
- Użycie: segmentacja interaktywna (Photoshop „magic wand",
narzędzia medyczne do zaznaczania guzów na RTG)
- Zaleta: precyzyjny, użytkownik wie co chce segmentować
- Wada: wymaga człowieka → nie skaluje się do 10 000 obrazów
AUTOMATYCZNY seed — metody:
1. Siatka (grid): seed co N pikseli (np. co 50 px na obrazie 500×500 → 100 seedów)
2. Lokalne ekstrema histogramu: znajdź najczęstszą jasność → seed tam
3. Losowanie: wylosuj K punktów jako seedy
4. Analiza gradientu: piksele w „płaskich" regionach (brak krawędzi) → dobre seedy
Dlaczego OR a nie AND?
Bo to ALTERNATYWNE podejścia — albo człowiek wybiera (mało i precyzyjnie),
albo algorytm wybiera (dużo i szybko, ale mniej precyzyjnie).
![Region Growing: seed ręczny vs automatyczny, krok po kroku, fale BFS](img/q23_region_growing.png)
Pseudokod Region Growing:
region = {seed}
queue = [seed]
while queue not empty:
pixel = queue.pop()
for neighbor in pixel.neighbors(): # 4 lub 8 sąsiadów
if neighbor not visited AND similar(neighbor, region):
region.add(neighbor)
queue.append(neighbor)
Mnemonik: „PLAMA atramentu" — seed to kropla atramentu na papierze,
rozlewa się na podobne (jasne) miejsca, zatrzymuje się na granicach.
---
#### Pojęcia kluczowe dla Watershed
**Watershed (metoda zlewiska)** — traktuje obraz jak mapę topograficzną: wartość jasności piksela = wysokość terenu. Ciemne piksele = doliny, jasne = szczyty. Algorytm „zalewa" mapę wodą od najniższych punktów (minimów). Gdy woda z dwóch dolin się spotyka — tam jest GRANICA segmentu (grań).
![Watershed: obraz jako mapa topograficzna, zalewanie, over-segmentation i marker-controlled watershed](img/q23_watershed.png)
Algorytm:
1. Zamień obraz na „mapę wysokości" (jasność = wysokość)
2. Znajdź wszystkie lokalne minima (najciemniejsze punkty)
3. „Zalewaj" od minimów — woda rośnie równomiernie
4. Gdy woda z dwóch dolin się spotyka → postaw TAMĘ (granicę segmentu)
5. Kontynuuj aż cały obraz zalany
Problem: MASYWNA over-segmentation — każde lokalne minimum (nawet szum!) → osobna dolina
Rozwiązanie: marker-controlled watershed — użytkownik podaje markery (seedy),
zalewamy TYLKO od tych markerów
Mnemonik: „ZALEWANIE terenu" — wyobraź sobie model terenu z plasteliny w wannie.
Powoli nalewasz wodę → doliny się wypełniają → granie gór = granice segmentów.
---
#### Pojęcia kluczowe dla Mean Shift
**Okno (window) / jądro (kernel)** — w kontekście Mean Shift to koło (lub kula w wielowymiarowej przestrzeni) o ustalonej szerokości (bandwidth = promień h) wokół aktualnego punktu. Wewnątrz okna algorytm oblicza „średnią ważoną" pozycji pikseli. Okno = jądro — to synonim. Nazwa „jądro" pochodzi od estymacji jądrowej gęstości (kernel density estimation, KDE).
Okno o promieniu h = 30 wokół punktu (100, 150):
Bierze WSZYSTKIE piksele, których cechy (jasność, x, y)
są w odległości ≤ 30 od (100, 150).
Oblicza ich średnią → przesuwa okno NA TĘ ŚREDNIĄ.
Powtarza aż okno się „zatrzyma" (przesunięcie < ε).
**Najwyższa gęstość (density peak)** — punkt w przestrzeni cech, gdzie jest NAJWIĘKSZE skupisko pikseli. Jak najwyższy szczyt góry w 3D. Mean Shift = „przesuń w kierunku średniej" → iteracyjnie zbliża się do szczytu gęstości.
**Przestrzeń cech (feature space)** — każdy piksel jest opisany nie tylko pozycją (x, y) ale też cechami koloru (jasność, R, G, B). Przestrzeń cech to przestrzeń wielowymiarowa, np. (R, G, B, x, y) = 5 wymiarów. Piksele o podobnych kolorach i blisko siebie będą blisko w przestrzeni cech → tworzą klastry (skupiska).
Piksel A: (x=100, y=200, R=30, G=25, B=35) → punkt w 5D
Piksel B: (x=102, y=201, R=32, G=27, B=33) → BLISKO A w 5D
Piksel C: (x=105, y=198, R=200, G=210, B=220) → DALEKO od A w 5D (inny kolor!)
→ A i B w jednym segmencie, C w innym
**Dlaczego Mean Shift NIE wymaga podania liczby segmentów?** — W K-means musisz podać K=3 (trzy klastry) ZANIM uruchomisz algorytm. Mean Shift działa inaczej: każdy piksel startuje i „toczy się" do najbliższego szczytu gęstości. Ile jest szczytów = tyle segmentów. Algorytm sam ODKRYWA liczbę klastrów. Parametrem jest tylko bandwidth (szerokość okna h): duże h → mało szczytów → mało segmentów; małe h → dużo szczytów → dużo segmentów.
![Mean Shift: przestrzeń cech, jądro przesuwane do max gęstości, dlaczego bez K](img/q23_mean_shift.png)
Pseudokod Mean Shift:
for each pixel p:
x = p.features # np. (R, G, B, pos_x, pos_y)
repeat:
window = all pixels within distance h from x
x_new = weighted_mean(window)
if |x_new - x| < epsilon:
break
x = x_new
p.cluster = x # zbieżny punkt = ID klastra
Mnemonik: „KULKI toczą się do dołków" — rozsyp kulki na nierównym stole,
każda toczy się do najbliższego zagłębienia. Ile dołków = tyle segmentów.
---
#### Pojęcia kluczowe dla Normalized Cuts
**Cięcie grafu (graph cut)** — graf to zbiór węzłów (pikseli) połączonych krawędziami (z wagami = podobieństwo). „Ciąć graf" to znaleźć LINIĘ dzielącą węzły na grupy, tak aby krawędzie „przecięte" tą linią miały niską wagę (= łączyły niepodobne piksele), a krawędzie wewnątrz grup miały wysoką wagę (= łączyły podobne piksele).
**Jak szukamy cięcia?** — Naiwnie: sprawdź WSZYSTKIE możliwe podziały → wykładnicza złożoność. Normalized Cuts zamienia problem na rozwiązanie „problemu wartości własnych" (eigenvalue problem) macierzy Laplacianu grafu. Drugi najmniejszy wektor własny wskazuje, które piksele należą do grupy A (wartości dodatnie) a które do B (wartości ujemne).
**Dlaczego „znormalizowane" (normalized)?** — Zwykłe cięcie (min-cut) ma wadę: preferuje odcinanie MALUTKICH grup (1 piksel odcięty = małe cięcie). Normalizowanie dzieli koszt cięcia przez rozmiar grup → duże, zrównoważone segmenty.
![Normalized Cuts: obraz jako graf, cięcie, algorytm krok po kroku](img/q23_normalized_cuts.png)
Pseudokod Normalized Cuts (uproszczony):
# 1. Zbuduj macierz podobieństwa W
for each pair of pixels (i, j):
W[i,j] = exp(-|color_i - color_j|^2 / sigma^2) # jeśli sąsiedzi
W[i,j] = 0 # jeśli odlegli
# 2. Macierz stopni D
D = diag(sum(W, axis=1)) # D[i,i] = suma wiersza i
# 3. Rozwiąż problem wartości własnych
(D - W) * y = lambda * D * y
# Weź DRUGI najm. wektor własny y (pierwszy = trywialny)
# 4. Podziel piksele
segment_A = {i : y[i] > 0}
segment_B = {i : y[i] <= 0}
Mnemonik: „CIĘCIE sznurków" — piksele połączone sznurkami (mocne = podobne).
Tnij SŁABE sznurki → dwie grupy. Normalizacja = nie odcinaj samotnych pikseli.
---
#### Pojęcia kluczowe dla sieci neuronowych
**ReLU (Rectified Linear Unit)** — najpopularniejsza funkcja aktywacji w sieciach neuronowych. Wzór: ReLU(x) = max(0, x). Jeśli wejście jest ujemne → wynik = 0 (neuron „milczy"). Jeśli wejście jest dodatnie → wynik = x (neuron „przepuszcza" sygnał bez zmiany). Prosta, ale bardzo skuteczna — szybsza od starszych funkcji (sigmoid, tanh), bo nie wymaga obliczania exp().
ReLU(-3) = max(0, -3) = 0 ← neuron „wyłączony"
ReLU(0) = max(0, 0) = 0 ← na granicy
ReLU(2.5) = max(0, 2.5) = 2.5 ← neuron „włączony", przekazuje 2.5
Dlaczego nie po prostu f(x) = x (bez progu)?
Bo liniowość → cała sieć = jedna warstwa liniowa (tracisz głębokość).
ReLU jest NIELINIOWA (ma „zakręt" w 0) → pozwala sieci uczyć się
skomplikowanych wzorców.
![ReLU: wykres funkcji, dlaczego ReLU, przykład numeryczny](img/q23_relu.png)
**Iloczyn skalarny (dot product)** — operacja na dwóch wektorach (listach liczb) dająca JEDNĄ liczbę. Mnożysz odpowiednie elementy parami i sumujesz wyniki. W CNN konwolucja = iloczyn skalarny filtra × fragment obrazu. Duży wynik = wektory „podobne" (filtr pasuje do fragmentu).
a = [1, 3, -2] b = [4, -1, 5]
a · b = 1·4 + 3·(-1) + (-2)·5 = 4 - 3 - 10 = -9
W konwolucji:
filtr = [-1, 0, 1, -1, 0, 1, -1, 0, 1] (spłaszczony 3×3)
fragment = [50, 50, 200, 50, 50, 200, 50, 50, 200]
dot = (-1)·50 + 0·50 + 1·200 + ... = 450 → duży = krawędź!
![Iloczyn skalarny: definicja, geometryczna interpretacja, użycie w konwolucji](img/q23_dot_product.png)
---
**Warstwa Fully Connected (FC, gęsta, dense)** — warstwa, w której KAŻDY neuron jest połączony z KAŻDYM wejściem. Obraz 7×7×512 (po konwolucjach) = 25 088 wartości. FC z 4096 neuronami = 25 088 × 4 096 = **~103 miliony wag**. Wady: (1) wymaga STAŁEGO rozmiaru wejścia (zawsze 7×7×512), (2) traci informację GDZIE coś jest (spłaszcza przestrzeń na wektor 1D).
**Konwolucja (convolution)** — operacja przesuwania małego filtra (np. 3×3) po obrazie. W każdej pozycji oblicza iloczyn skalarny filtra × fragment obrazu → jedną liczbę. TE SAME wagi filtra użyte w KAŻDEJ pozycji → dzielenie parametrów. Zachowuje informację przestrzenną (GDZIE coś jest).
**Conv 1×1 (konwolucja punktowa)** — filtr o rozmiarze 1×1 pikseli. „Patrzy" na JEDEN piksel, ale WSZYSTKIE kanały (np. 512). Działa jak FC, ale OSOBNO dla KAŻDEGO piksela → zachowuje mapę H×W. FCN zamienia FC na Conv 1×1: zamiast spłaszczyć 7×7×512 → 25 088 → FC, robi Conv1×1 na KAŻDYM z 7×7 pikseli × 512 kanałów → mapa 7×7×C (C = liczba klas).
**Jak FCN zamienia FC na Conv 1×1?** — Klasyczny CNN: ostatnia mapa cech 7×7×512 → FLATTEN → wektor 25 088 → FC → 1000 klas → „to jest kot". FCN: ostatnia mapa cech H×W×512 → Conv1×1(512→C) → mapa H×W×C → upsample do pełnej rozdzielczości. Kluczowa różnica: NIE spłaszczamy → możemy przetwarzać obraz o DOWOLNYM rozmiarze.
**Skip connections z encodera** — w encoder-decoder encoder zmniejsza obraz (pooling): 224→112→56→28→14. W tym procesie traci DETALE przestrzenne (dokładne krawędzie). Skip connections = „drogi na skróty" — cechy z wczesnych warstw encodera (pełne detali) są przekazywane WPROST do odpowiednich warstw decodera. Decoder wie CO i GDZIE.
![FCN: warstwa FC vs Conv 1×1, konwolucja, skip connections](img/q23_fc_vs_conv1x1.png)
---
**U-Net — dlaczego kształt „U"?** — Narysuj architekturę: encoder zmniejsza rozdzielczość (bloki idą w DÓŁ po lewej stronie), bottleneck jest na dole, decoder zwiększa rozdzielczość (bloki idą W GÓRĘ po prawej stronie). Wizualnie tworzy literę „U". „Encoder schodzi w dół" = każda warstwa encodera ma MNIEJSZĄ rozdzielczość (224→112→56→28), wizualizowane jako bloki o malejącym rozmiarze ułożone jeden pod drugim.
**Concatenation (konkatenacja, złączenie)** — operacja „sklejania" dwóch tensorów wzdłuż osi kanałów. Jeśli encoder na poziomie 2 daje mapę 128×128×64 kanałów, a decoder na poziomie 2 daje mapę 128×128×64 kanałów, to concatenation = 128×128×**128** kanałów (64+64). Różni się od DODAWANIA (addition), które daje 128×128×64 (element-wise sum). Concatenation zachowuje WIĘCEJ informacji — sieć sama wybiera, które kanały wykorzystać.
Dodawanie (ResNet-style):
encoder [a, b, c] + decoder [x, y, z] = [a+x, b+y, c+z] → 3 kanały
Concatenation (U-Net-style):
encoder [a, b, c] ++ decoder [x, y, z] = [a, b, c, x, y, z] → 6 kanałów!
→ więcej informacji, sieć sama zdecyduje co ważne
![U-Net: architektura w kształcie U, skip connections z concatenation, encoder ↓ decoder ↑](img/q23_unet_arch.png)
Mnemonik U-Net: „Litera U — w dół i w górę" — encoder schodzi ↓ (zmniejsza),
decoder wraca ↑ (zwiększa), między nimi mosty (skip = concat).
---
**Receptive field (pole widzenia, pole recepcyjne)** — ile pikseli WEJŚCIOWYCH wpływa na JEDEN piksel wyjściowy. Konwolucja 3×3 → RF = 3×3. Dwie konwolucje 3×3 pod rząd → RF = 5×5 (druga widzi 3×3 fragmenty, z których każdy widział 3×3 → efektywnie 5×5). Większe RF = neuron widzi większy kontekst = lepiej rozumie co to za piksel.
**Dlaczego większe RF jest lepsze?** — Pojedynczy piksel o jasności 150 może być fragmentem nieba LUB samochodu. Patrząc na otoczenie 3×3 → nadal nie wiesz. Patrząc na otoczenie 50×50 → widzisz budynki obok → „to droga!". Segmentacja wymaga KONTEKSTU globalnego.
**Rate (współczynnik dylatacji)** — parametr atrous (dilated) convolution. Rate=1 = zwykła konwolucja (filtr dotyka sąsiadów). Rate=2 = filtr próbkuje co DRUGI piksel → RF rośnie z 3×3 do 5×5 przy TYCH SAMYCH 9 wagach. Rate=3 → RF = 7×7. Większy kontekst za darmo (bez dodatkowych parametrów).
**Global Average Pooling (GAP)** — operacja redukcji: mapa cech H×W×C → 1×1×C. Dla KAŻDEGO kanału oblicza ŚREDNIĄ ze wszystkich H×W pikseli. Wynik: jeden wektor o wymiarze C, reprezentujący „średnią informację" z całego obrazu. RF = nieskończone (cały obraz). Używane w ASPP DeepLab jako jedna z równoległych gałęzi.
Mapa cech 7×7×512:
Kanał 0: macierz 7×7 wartości → średnia → jedna liczba
Kanał 1: macierz 7×7 wartości → średnia → jedna liczba
...
Kanał 511: macierz 7×7 wartości → średnia → jedna liczba
Wynik: wektor [avg₀, avg₁, ..., avg₅₁₁] → 1×1×512
![Receptive field: zwykła vs dilated konwolucja, rate, global average pooling](img/q23_receptive_field.png)
---
**Transformer** — architektura sieci neuronowej zaproponowana w 2017 (Vaswani et al., „Attention Is All You Need"). Oryginalnie dla NLP (tłumaczenie), od 2020 (ViT — Vision Transformer) stosowana w wizji komputerowej. Kluczowy mechanizm: **self-attention** — każdy element (piksel/token) „pyta" WSZYSTKIE inne elementy: „jak bardzo jesteś ze mną powiązany?". Każdy element tworzy trzy wektory: Q (Query — czego szukam?), K (Key — co oferuję), V (Value — moja wartość). Attention = softmax(Q·Kᵀ / √d) · V. Koszt: O(n²) pamięci (n = liczba elementów).
**SOTA (State Of The Art)** — najlepszy znany wynik na danym benchmarku (zbiorze testowym) w danym momencie. Np. „Mask2Former osiąga mIoU 57.8% na ADE20K — to aktualny SOTA". SOTA ciągle się zmienia — każdy nowy paper może pobić poprzedni rekord.
![Transformer: CNN lokalny vs Transformer globalny, self-attention Q/K/V, SOTA](img/q23_transformer_attention.png)
---
**mIoU (mean Intersection over Union)** — standardowa metryka segmentacji. Dla każdej klasy: IoU = (piksele poprawne ∩ ground truth) / (piksele poprawne ground truth). Potem średnia z klas.
Klasa "samochód": predykcja=100 pikseli, GT=120, wspólne=80
IoU = 80 / (100+120-80) = 80/140 = 0.571 = 57.1%
**Dice Loss** — funkcja kosztu powiązana z IoU: 2·|A∩B| / (|A|+|B|). Popularna w segmentacji medycznej (dobrze radzi sobie z class imbalance).
**Focal Loss** — modyfikacja cross-entropy redukująca wpływ łatwych przykładów, skupiająca uczenie na trudnych. Kluczowa przy class imbalance (np. 99% tła, 1% obiekt).
---
### Problem: czym jest segmentacja obrazu?
Segmentacja obrazu to **przypisanie etykiety klasy KAŻDEMU pikselowi** obrazu. Wynik: mapa segmentacji o tym samym rozmiarze co obraz wejściowy, gdzie każdy piksel ma etykietę (np. „samochód", „droga", „niebo").
Wejście: obraz 640×480 (RGB) = 307 200 pikseli
Wynik: mapa 640×480, każdy piksel → etykieta = 307 200 etykiet
Obraz: [niebo niebo niebo niebo]
[niebo drzewo drzewo niebo]
[droga droga samochód droga]
[droga droga droga droga]
**Czym segmentacja NIE jest:**
Zadanie Wynik Granulacja
──────────────────────────────────────────────────────────────
Klasyfikacja 1 etykieta na cały obraz obraz
Detekcja bounding box + klasa prostokąt
Segmentacja etykieta per piksel piksel
**3 warianty segmentacji:**
![Typy segmentacji obrazu](img/segmentation_types.png)
| Wariant | Co robi | Przykład |
| ------------ | -------------------------------------------- | ------------------------------------------- |
| **Semantic** | klasa per piksel, bez rozróżniania instancji | wszystkie samochody = „samochód" |
| **Instance** | rozróżnia instancje tego samego obiektu | samochód#1, samochód#2 |
| **Panoptic** | semantic + instance razem | „stuff" (niebo) + „things" (samochód#1, #2) |
---
### Strategie klasyczne
Metody niewymagające uczenia maszynowego — oparte na ręcznie zdefiniowanych regułach (próg, podobieństwo, struktura grafu).
| Metoda | Idea | Wada | Złożoność | Mnemonik |
| ------------------- | -------------------------------------------------------- | ---------------------------------- | ---------- | ------------------ |
| **Thresholding** | piksel > T → klasa 1, else → klasa 0 | tylko 2 klasy, proste sceny | O(n) | „PRÓG na bramce" |
| **Otsu** | automatyczny próg (min wariancja wewnątrzklasowa) | j.w. ale dobiera T sam | O(n·L) | „AUTO-bramkarz" |
| **Region Growing** | dodawaj sąsiednie piksele o podobnej wartości | over-segmentation, zależy od seeda | O(n) | „PLAMA atramentu" |
| **Watershed** | obraz = mapa wysokości, granice = granie gór | over-segmentation | O(n log n) | „ZALEWANIE terenu" |
| **Mean Shift** | iteracyjnie przesuwaj jądro do max gęstości | wolny | O(n²) | „KULKI toczą się" |
| **Normalized Cuts** | piksele = węzły grafu, minimalizuj znormalizowane cięcie | bardzo wolny | O(n³) | „CIĘCIE sznurków" |
#### DIY Przykład — Thresholding (Otsu) krok po kroku
Poniższy diagram pokazuje CAŁY pipeline progowania Otsu od obrazu wejściowego do wyniku. Obraz syntetyczny 64×64 z ciemnym kołem na jasnym tle — typowy przypadek bimodalny.
![DIY Thresholding + Otsu: obraz → histogram bimodalny → progowanie → szukanie min σ² → pseudokod → wynik](img/q23_diy_thresholding.png)
Pseudokod Otsu (Python-style):
best_T, min_var = 0, float('inf')
for T in range(256):
c0 = pixels[pixels <= T] # piksele ciemne
c1 = pixels[pixels > T] # piksele jasne
if len(c0) == 0 or len(c1) == 0:
continue
w0 = len(c0) / len(pixels) # udział klasy 0
w1 = len(c1) / len(pixels) # udział klasy 1
var = w0 * variance(c0) + w1 * variance(c1) # σ² wewnątrzklasowa
if var < min_var:
min_var = var
best_T = T
# best_T = optymalny próg (np. 128)
result = (pixels > best_T).astype(int) # binaryzacja
**Wspólna wada klasycznych metod:** wymagają ręcznego doboru parametrów (próg, seed, kernel), nie uczą się cech z danych, słabe na złożonych obrazach naturalnych.
---
### Sieci neuronowe (deep learning)
Metody uczące się automatycznie rozpoznawać cechy z danych treningowych. Wszystkie oparte na architekturze **encoder-decoder** z wariacjami.
**Wspólna idea encoder-decoder:**
Encoder: obraz [224×224] → [112] → [56] → [28] → [14] (wyciąga CECHY)
Decoder: cechy [14] → [28] → [56] → [112] → [224×224] (odtwarza MAPĘ)
bottleneck
| Sieć | Rok | Kluczowa innowacja | Use case | Mnemonik |
| --------------- | ---- | ----------------------------------------- | -------------------- | ----------------------- |
| **FCN** | 2015 | w pełni konwolucyjna + skip connections | pierwsza end-to-end | „FC → Conv 1×1" |
| **U-Net** | 2015 | U-shape + skip concat + data augmentation | segmentacja medyczna | „Litera U + mosty" |
| **DeepLab v3+** | 2018 | atrous (dilated) conv + ASPP | general-purpose | „DZIURY w filtrze" |
| **SegFormer** | 2021 | transformer encoder (self-attention) | SOTA lightweight | „WSZYSCY ze WSZYSTKIMI" |
| **Mask2Former** | 2022 | masked attention + unified architecture | SOTA universal | „WSZYSCY ze WSZYSTKIMI" |
**FCN (Fully Convolutional Network):**
Mnemonik: „FC → Conv 1×1 = otwieramy bramkę dla DOWOLNEGO rozmiaru"
Zwykły CNN: Conv → Conv → Pool → ... → FC → FC → "kot"
FCN: Conv → Conv → Pool → ... → Conv1×1 → Upsample → mapa pikseli
Innowacja: zamiana FC na Conv1×1 → wejście dowolnego rozmiaru
Skip connections: łączą cechy z encodera → zachowują detale przestrzenne
**U-Net:**
Mnemonik: „Litera U + mosty" — schodzisz w dół, wracasz w górę,
po drodze mosty (skip connections z concat) przenoszą detale.
Encoder (↓) Decoder (↑)
[64]────skip────→[64] ← skip connections = concatenation
[128]───skip───→[128] (przenosi detale z encodera do decodera)
[256]──skip──→[256]
[512]─skip─→[512]
[1024] ← bottleneck
Dlaczego medycyna? Działa dobrze z MAŁYMI zbiorami danych (data augmentation)
**DeepLab v3+:**
Mnemonik: „DZIURY w filtrze" — filtr dosłownie ma dziury (à trous),
przez co widzi dalej bez dodatkowych parametrów.
Zwykła konwolucja 3×3: [x][x][x] receptive field = 3
Dilated (rate=2): [x][ ][x][ ][x] receptive field = 5, te same parametry!
ASPP: równolegle rate=6,12,18 → multi-scale features → łączenie
Efekt: widzi kontekst globalny BEZ zwiększania parametrów
**Transformery (SegFormer, Mask2Former):**
Mnemonik: „WSZYSCY ze WSZYSTKIMI" — każdy piksel rozmawia z KAŻDYM innym.
CNN: filtr 3×3 widzi LOKALNY kontekst (sąsiadów)
Transformer: self-attention widzi CAŁY obraz naraz
Cena: O(n²) pamięci (n = piksele), ale lepsze wyniki
#### DIY Przykład — U-Net krok po kroku
Poniższy diagram pokazuje CAŁY pipeline U-Net od obrazu wejściowego do mapy segmentacji. Obraz syntetyczny 64×64 z dwoma obiektami (koła) na jasnym tle.
![DIY U-Net: obraz → encoder zmniejsza → bottleneck → decoder zwiększa + skip → mapa segmentacji → pseudokod](img/q23_diy_unet.png)
Pseudokod U-Net (PyTorch-style):
# ENCODER — zmniejsza rozdzielczość, wyciąga cechy
e1 = conv_block(input, filters=64) # [64×64×64]
e2 = conv_block(maxpool(e1), filters=128) # [32×32×128]
e3 = conv_block(maxpool(e2), filters=256) # [16×16×256]
# BOTTLENECK — najgłębsza warstwa
b = conv_block(maxpool(e3), filters=512) # [8×8×512]
# DECODER — zwiększa rozdzielczość + skip connections (concat!)
d3 = conv_block(concat(upconv(b), e3), filters=256) # [16×16×256]
d2 = conv_block(concat(upconv(d3), e2), filters=128) # [32×32×128]
d1 = conv_block(concat(upconv(d2), e1), filters=64) # [64×64×64]
# WYNIK — Conv 1×1 → mapa klas
output = conv_1x1(d1, n_classes=3) # [64×64×3] → argmax → [64×64] etykiety
---
### Metryki i funkcje kosztu
| Metryka/Loss | Wzór | Kiedy użyć |
| ------------------ | ----------------------------- | ------------------------------------ |
| **mIoU** | mean(IoU per klasa) | standardowy benchmark |
| **Pixel Accuracy** | poprawne / wszystkie | prosta, ale zła przy class imbalance |
| **Dice Loss** | 1 - 2·\|A∩B\| / (\|A\|+\|B\|) | segmentacja medyczna |
| **Focal Loss** | -α(1-p)^γ · log(p) | class imbalance (99% tła) |
### Etymologia
**Segmentacja** — łac. „segmentum" = odcięty kawałek; podział obrazu na regiony. **Otsu** — Nobuyuki Otsu (1979); automatyczny dobór progu. **Watershed** — metafora: woda spływająca z grani do dolin (z geografii). **U-Net** — Ronneberger et al. (Freiburg, 2015); „U" od kształtu architektury. **FCN** — Fully Convolutional Network (Long, Shelhamer, Darrell, 2015). **DeepLab** — Google (20152018); „Atrous" z fr. „à trous" = „z dziurami" (dilated convolutions). **mIoU** — mean Intersection over Union.
### Jak zapamiętać
**Super-mnemonik na kolejność algorytmów:**
„Turyści Oglądają Rzekę, Wodospad, Morze, Nurt — Fotografują Uroczy Dwór Tajemnic"
Klasyczne: Thresholding → Otsu → Region growing → Watershed → Mean shift → Normalized cuts
Neuronowe: FCN → U-Net → DeepLab → Transformer
![Mnemoniki: karty z algorytmami segmentacji i ich skojarzeniami](img/q23_mnemonics.png)
**Mnemoniki per algorytm — STRATEGIE KLASYCZNE:**
| Algorytm | Mnemonik | Skojarzenie |
| ------------------- | --------------------------- | ------------------------------------------------------- |
| **Thresholding** | „PRÓG na bramce" | Bramkarz przepuszcza piksele > T, blokuje ≤ T |
| **Otsu** | „AUTO-bramkarz" | Sam sprawdza 256 progów, wybiera najlepszy (min σ²) |
| **Region Growing** | „PLAMA atramentu" | Kropla atramentu rozlewa się na podobne piksele (BFS) |
| **Watershed** | „ZALEWANIE terenu" | Woda zalewa doliny, granie gór = granice segmentów |
| **Mean Shift** | „KULKI toczą się do dołków" | Każda kulka → max gęstości, ile dołków = tyle segmentów |
| **Normalized Cuts** | „CIĘCIE sznurków" | Tnij słabe sznurki (krawędzie grafu), zachowaj silne |
**Mnemoniki per algorytm — SIECI NEURONOWE:**
| Sieć | Mnemonik | Skojarzenie |
| --------------- | ----------------------- | ------------------------------------------------------------ |
| **FCN** | „FC → Conv 1×1" | Otwiera bramkę dla dowolnego rozmiaru wejścia |
| **U-Net** | „Litera U + mosty" | Schodzisz ↓, wracasz ↑, mosty (skip concat) przenoszą detale |
| **DeepLab** | „DZIURY w filtrze" | Filtr ma dziury (à trous) → widzi dalej bez dodatkowych wag |
| **Transformer** | „WSZYSCY ze WSZYSTKIMI" | Każdy piksel pyta każdy inny (self-attention, O(n²)) |
**Mnemoniki per metrykę:**
- **mIoU** = „Nakładka / Suma" → intersection / union, uśrednione per klasa
- **Dice** = „Dwie nakładki / Razem" → 2·|A∩B| / (|A|+|B|)
- **Focal** = „Fokus na TRUDNYCH" → trudne piksele ważą więcej

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,530 @@
"""MoviePy visualization for PYTANIE 2: Shortest path algorithms.
Creates an animated video walking through Dijkstra, Bellman-Ford, and A*
on a small example graph, rendering each algorithm step by step.
"""
from __future__ import annotations
import os
from pathlib import Path
import numpy as np
os.environ["FFMPEG_BINARY"] = "/usr/bin/ffmpeg"
from moviepy import (
ColorClip,
CompositeVideoClip,
TextClip,
VideoClip,
concatenate_videoclips,
)
from moviepy.video.fx import FadeIn, FadeOut
# ── Constants ─────────────────────────────────────────────────────
W, H = 1280, 720
FPS = 24
STEP_DUR = 8.0
HEADER_DUR = 5.0
FONT_B = "/usr/share/fonts/TTF/DejaVuSans-Bold.ttf"
FONT_R = "/usr/share/fonts/TTF/DejaVuSans.ttf"
OUTPUT_DIR = Path(__file__).resolve().parent / "videos"
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
OUTPUT = str(OUTPUT_DIR / "q02_shortest_path.mp4")
# Graph definition
NODE_POS = {"S": (250, 280), "A": (550, 180), "B": (550, 450), "C": (850, 320)}
EDGES_DIJKSTRA = [
("S", "A", 2),
("S", "B", 5),
("A", "C", 3),
("B", "A", 1),
("B", "C", 6),
]
EDGES_BF = [("S", "A", 2), ("A", "C", 3), ("S", "B", 5), ("B", "A", -4)]
# Colors
BG = (20, 20, 40)
COL_DEFAULT = (70, 130, 200)
COL_CURRENT = (255, 200, 50)
COL_VISITED = (80, 200, 100)
COL_EDGE = (100, 100, 130)
COL_EDGE_ACT = (255, 100, 80)
INF = "inf"
def _tc(**kwargs: object) -> TextClip:
"""TextClip wrapper that adds enough bottom margin to prevent clipping."""
fs = kwargs.get("font_size", 24)
m = int(fs) // 3 + 2
kwargs["margin"] = (0, m)
return TextClip(**kwargs)
def _make_header(
title: str, subtitle: str, duration: float = HEADER_DUR
) -> CompositeVideoClip:
bg = ColorClip(size=(W, H), color=BG).with_duration(duration)
t = (
_tc(
text=title,
font_size=52,
color="white",
font=FONT_B,
)
.with_duration(duration)
.with_position(("center", 250))
)
s = (
_tc(
text=subtitle,
font_size=28,
color="#AABBCC",
font=FONT_R,
)
.with_duration(duration)
.with_position(("center", 340))
)
return CompositeVideoClip([bg, t, s], size=(W, H)).with_effects(
[FadeIn(0.5), FadeOut(0.5)]
)
def _draw_circle(
frame: np.ndarray, cx: int, cy: int, r: int, color: tuple[int, ...]
) -> None:
yy, xx = np.ogrid[:H, :W]
mask = ((xx - cx) ** 2 + (yy - cy) ** 2) <= r**2
frame[mask] = color
def _draw_line(
frame: np.ndarray,
x1: int,
y1: int,
x2: int,
y2: int,
color: tuple[int, ...],
thickness: int = 2,
) -> None:
length = max(int(np.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)), 1)
for i in range(length):
frac = i / length
px = int(x1 + frac * (x2 - x1))
py = int(y1 + frac * (y2 - y1))
for dx in range(-thickness, thickness + 1):
for dy in range(-thickness, thickness + 1):
nx, ny = px + dx, py + dy
if 0 <= nx < W and 0 <= ny < H:
frame[ny, nx] = color
def _draw_arrow(
frame: np.ndarray,
x1: int,
y1: int,
x2: int,
y2: int,
color: tuple[int, ...],
thickness: int = 2,
) -> None:
r = 32
length = max(np.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2), 1)
ddx = (x2 - x1) / length
ddy = (y2 - y1) / length
sx = int(x1 + ddx * r)
sy = int(y1 + ddy * r)
ex = int(x2 - ddx * r)
ey = int(y2 - ddy * r)
_draw_line(frame, sx, sy, ex, ey, color, thickness)
angle = np.arctan2(ey - sy, ex - sx)
arrow_len = 12
for side in [-1, 1]:
a = angle + np.pi + side * 0.4
ax = int(ex + arrow_len * np.cos(a))
ay = int(ey + arrow_len * np.sin(a))
_draw_line(frame, ex, ey, ax, ay, color, thickness)
def _render_graph(
nodes: dict[str, tuple[int, int]],
edges: list[tuple[str, str, int]],
_distances: dict[str, str],
current: str | None = None,
visited: set[str] | None = None,
active_edge: tuple[str, str] | None = None,
) -> np.ndarray:
if visited is None:
visited = set()
frame = np.full((H, W, 3), BG, dtype=np.uint8)
for src, dst, _w in edges:
sx, sy = nodes[src]
dx, dy = nodes[dst]
ec = COL_EDGE_ACT if active_edge == (src, dst) else COL_EDGE
_draw_arrow(frame, sx, sy, dx, dy, ec, thickness=2)
for name, (x, y) in nodes.items():
if name == current:
nc = COL_CURRENT
elif name in visited:
nc = COL_VISITED
else:
nc = COL_DEFAULT
_draw_circle(frame, x, y, 30, nc)
# Border ring
border = tuple(max(c - 40, 0) for c in nc)
yy, xx = np.ogrid[:H, :W]
ring = (((xx - x) ** 2 + (yy - y) ** 2) <= 30**2) & (
((xx - x) ** 2 + (yy - y) ** 2) > 27**2
)
frame[ring] = border
return frame
def _make_step(
nodes: dict[str, tuple[int, int]],
edges: list[tuple[str, str, int]],
distances: dict[str, str],
current: str | None = None,
visited: set[str] | None = None,
active_edge: tuple[str, str] | None = None,
step_text: str = "",
algo_name: str = "",
duration: float = STEP_DUR,
) -> CompositeVideoClip:
if visited is None:
visited = set()
graph_frame = _render_graph(nodes, edges, distances, current, visited, active_edge)
def make_frame(_t: float) -> np.ndarray:
return graph_frame.copy()
bg_clip = VideoClip(make_frame, duration=duration).with_fps(FPS)
overlays: list[VideoClip] = [bg_clip]
if algo_name:
overlays.append(
_tc(
text=algo_name,
font_size=28,
color="#64B5F6",
font=FONT_B,
)
.with_duration(duration)
.with_position((40, 20))
)
dist_items = [f"{k}: {v}" for k, v in distances.items()]
table_text = "dist = { " + ", ".join(dist_items) + " }"
overlays.append(
_tc(
text=table_text,
font_size=18,
color="#B0BEC5",
font=FONT_R,
)
.with_duration(duration)
.with_position((40, 60))
)
visited_text = f"visited = {{ {', '.join(sorted(visited))} }}"
overlays.append(
_tc(
text=visited_text,
font_size=18,
color="#A5D6A7",
font=FONT_R,
)
.with_duration(duration)
.with_position((40, 90))
)
for src, dst, w in edges:
sx, sy = nodes[src]
dx, dy = nodes[dst]
mx = (sx + dx) // 2 - 6
my = (sy + dy) // 2 - 12
wcol = "#FF8A65" if active_edge == (src, dst) else "#90A4AE"
overlays.append(
_tc(
text=str(w),
font_size=16,
color=wcol,
font=FONT_B,
)
.with_duration(duration)
.with_position((mx, my))
)
for name, (x, y) in nodes.items():
overlays.append(
_tc(
text=name,
font_size=20,
color="white",
font=FONT_B,
)
.with_duration(duration)
.with_position((x - 7, y - 12))
)
d = distances.get(name, INF)
overlays.append(
_tc(
text=f"d={d}",
font_size=14,
color="#FFE082",
font=FONT_R,
)
.with_duration(duration)
.with_position((x - 16, y + 35))
)
if step_text:
overlays.append(
_tc(
text=step_text,
font_size=18,
color="#E0E0E0",
font=FONT_R,
)
.with_duration(duration)
.with_position((40, 600))
)
return CompositeVideoClip(overlays, size=(W, H)).with_effects(
[FadeIn(0.3), FadeOut(0.3)]
)
def _dijkstra_steps() -> list[CompositeVideoClip]:
n = NODE_POS
e = EDGES_DIJKSTRA
return [
_make_step(
n,
e,
{"S": "0", "A": INF, "B": INF, "C": INF},
current="S",
step_text="Inicjalizacja: d[S]=0, reszta=∞. Wybierz S (min d).",
algo_name="Algorytm Dijkstry",
),
_make_step(
n,
e,
{"S": "0", "A": "2", "B": "5", "C": INF},
current="S",
active_edge=("S", "A"),
step_text="Relaksacja S→A: d[A]=0+2=2. S→B: d[B]=0+5=5.",
algo_name="Algorytm Dijkstry",
),
_make_step(
n,
e,
{"S": "0", "A": "2", "B": "5", "C": "5"},
current="A",
visited={"S"},
active_edge=("A", "C"),
step_text="Zamknij S. Min=A(2). Relaksacja A→C: d[C]=2+3=5.",
algo_name="Algorytm Dijkstry",
),
_make_step(
n,
e,
{"S": "0", "A": "2", "B": "5", "C": "5"},
current="B",
visited={"S", "A"},
active_edge=("B", "A"),
step_text="Zamknij A. Min=B(5). B→A: 5+1=6>2, nie zmieniaj. B→C: 5+6=11>5.",
algo_name="Algorytm Dijkstry",
),
_make_step(
n,
e,
{"S": "0", "A": "2", "B": "5", "C": "5"},
current="C",
visited={"S", "A", "B"},
step_text="Zamknij B. Min=C(5). Koniec! Wynik: d={S:0, A:2, B:5, C:5}.",
algo_name="Dijkstra -- WYNIK",
),
]
def _bellman_ford_steps() -> list[CompositeVideoClip]:
n = NODE_POS
e = EDGES_BF
return [
_make_step(
n,
e,
{"S": "0", "A": INF, "B": INF, "C": INF},
step_text="Bellman-Ford: relaksuj WSZYSTKIE krawędzie V-1=3 razy. Ujemne wagi OK!",
algo_name="Algorytm Bellmana-Forda",
),
_make_step(
n,
e,
{"S": "0", "A": "2", "B": "5", "C": "5"},
active_edge=("S", "A"),
step_text="Iteracja 1: S→A:2, A→C:5, S→B:5. Potem B→A: 5+(-4)=1 < 2 → A=1!",
algo_name="Bellman-Ford -- iteracja 1",
),
_make_step(
n,
e,
{"S": "0", "A": "1", "B": "5", "C": "5"},
active_edge=("B", "A"),
step_text="B→A z ujemną wagą -4: d[A] poprawione z 2 na 1! (Dijkstra by to pominął!)",
algo_name="Bellman-Ford -- ujemna waga",
),
_make_step(
n,
e,
{"S": "0", "A": "1", "B": "5", "C": "4"},
active_edge=("A", "C"),
step_text="Iteracja 2: A→C: 1+3=4 < 5 → C=4. Propagacja poprawionego A.",
algo_name="Bellman-Ford -- iteracja 2",
),
_make_step(
n,
e,
{"S": "0", "A": "1", "B": "5", "C": "4"},
step_text="Iteracja 3: brak zmian. V-ta iteracja: brak popraw → brak cyklu ujemnego.",
algo_name="Bellman-Ford -- WYNIK, O(V*E)",
),
]
def _astar_steps() -> list[CompositeVideoClip]:
n = NODE_POS
e = EDGES_DIJKSTRA
return [
_make_step(
n,
e,
{"S": "0", "A": INF, "B": INF, "C": INF},
current="S",
step_text="A*: f(n)=g(n)+h(n). Cel=C. h(S)=5, h(A)=3, h(B)=4, h(C)=0. f(S)=0+5=5.",
algo_name="Algorytm A*",
),
_make_step(
n,
e,
{"S": "0", "A": "2", "B": "5", "C": INF},
current="S",
active_edge=("S", "A"),
step_text="Relaksuj S: A(g=2,f=2+3=5), B(g=5,f=5+4=9). Min f → A(5).",
algo_name="A* -- rozwijanie S",
),
_make_step(
n,
e,
{"S": "0", "A": "2", "B": "5", "C": "5"},
current="A",
visited={"S"},
active_edge=("A", "C"),
step_text="Rozwiń A(f=5): A→C: g=2+3=5, f=5+0=5. Min f → C(5) = CEL!",
algo_name="A* -- rozwijanie A",
),
_make_step(
n,
e,
{"S": "0", "A": "2", "B": "5", "C": "5"},
current="C",
visited={"S", "A"},
step_text="Dotarliśmy do C! Koszt=5. A* NIE przetwarza B (3 vs 4 w Dijkstrze).",
algo_name="A* -- cel osiągnięty!",
),
]
def _comparison_slide() -> CompositeVideoClip:
bg = ColorClip(size=(W, H), color=BG).with_duration(12.0)
title = (
_tc(
text="Porównanie algorytmów",
font_size=40,
color="white",
font=FONT_B,
)
.with_duration(12.0)
.with_position(("center", 40))
)
rows = [
("Cecha", "Dijkstra", "Bellman-Ford", "A*"),
("Typ", "Zachłanny", "Prog. dynamiczne", "Heurystyczny"),
("Problem", "SSSP", "SSSP", "Single-pair"),
("Ujemne wagi", "NIE", "TAK", "NIE"),
("Cykl ujemny", "NIE wykrywa", "TAK wykrywa", "NIE"),
("Złożoność", "O((V+E)log V)", "O(V*E)", "Zależy od h(n)"),
]
clips: list[VideoClip] = [bg, title]
for i, row in enumerate(rows):
y_pos = 120 + i * 85
for j, cell in enumerate(row):
x_pos = 60 + j * 300
fs = 18 if i > 0 else 22
color = "#64B5F6" if i == 0 else "#CFD8DC"
tc = (
_tc(
text=cell,
font_size=fs,
color=color,
font=FONT_B if i == 0 else FONT_R,
)
.with_duration(12.0)
.with_position((x_pos, y_pos))
)
clips.append(tc)
return CompositeVideoClip(clips, size=(W, H)).with_effects(
[FadeIn(0.5), FadeOut(0.5)]
)
def main() -> None:
"""Generate the Q02 shortest path visualization video."""
sections: list[VideoClip] = []
sections.append(
_make_header(
"Pytanie 2: Algorytmy najkrótszej ścieżki",
"Dijkstra * Bellman-Ford * A*",
duration=8.0,
)
)
sections.append(_make_header("Algorytm Dijkstry", "Zachłanny, SSSP, wagi ≥ 0"))
sections.extend(_dijkstra_steps())
sections.append(
_make_header("Algorytm Bellmana-Forda", "Prog. dynamiczne, ujemne wagi, O(V·E)")
)
sections.extend(_bellman_ford_steps())
sections.append(
_make_header("Algorytm A*", "Heurystyczny, f(n)=g(n)+h(n), Single-pair")
)
sections.extend(_astar_steps())
sections.append(_comparison_slide())
sections.append(
_make_header(
"Podsumowanie",
"Dijkstra=chciwy | Bellman-Ford=brute force x(V-1) | A*=Dijkstra+GPS",
duration=8.0,
)
)
final = concatenate_videoclips(sections, method="compose")
final.write_videofile(
OUTPUT, fps=FPS, codec="libx264", audio=False, preset="medium", threads=4
)
print(f"Video saved to: {OUTPUT}")
if __name__ == "__main__":
main()

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff