mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-07-04 13:03:13 +02:00
feat: shell scripts
This commit is contained in:
parent
265488a478
commit
f4f25821e5
@ -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$)
|
||||
|
||||
# ===========================================================================
|
||||
|
||||
@ -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
147
phone_focus_mode/README.md
Normal 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
131
phone_focus_mode/config.sh
Executable 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
219
phone_focus_mode/deploy.sh
Executable 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
241
phone_focus_mode/focus_ctl.sh
Executable 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
213
phone_focus_mode/focus_daemon.sh
Executable 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 "$@"
|
||||
21
phone_focus_mode/magisk_service.sh
Executable file
21
phone_focus_mode/magisk_service.sh
Executable 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
|
||||
1
python_pkg/praca_magisterska_video/__init__.py
Normal file
1
python_pkg/praca_magisterska_video/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Thesis video visualization scripts."""
|
||||
563
python_pkg/praca_magisterska_video/answers/pytanie_23.md
Normal file
563
python_pkg/praca_magisterska_video/answers/pytanie_23.md
Normal 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.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
**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 aż 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).
|
||||
|
||||

|
||||
|
||||
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ń).
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
**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ź!
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
**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.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
**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
|
||||
|
||||

|
||||
|
||||
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
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
**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.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
**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:**
|
||||
|
||||

|
||||
|
||||
| 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.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
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 (2015–2018); „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 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
|
||||
1045
python_pkg/praca_magisterska_video/answers/pytanie_24.md
Normal file
1045
python_pkg/praca_magisterska_video/answers/pytanie_24.md
Normal file
File diff suppressed because it is too large
Load Diff
530
python_pkg/praca_magisterska_video/visualize_q02.py
Normal file
530
python_pkg/praca_magisterska_video/visualize_q02.py
Normal 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()
|
||||
1627
python_pkg/praca_magisterska_video/visualize_q23.py
Normal file
1627
python_pkg/praca_magisterska_video/visualize_q23.py
Normal file
File diff suppressed because it is too large
Load Diff
1891
python_pkg/praca_magisterska_video/visualize_q24.py
Normal file
1891
python_pkg/praca_magisterska_video/visualize_q24.py
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user