WIP: Enforce 500-line limit - split batch 1

Split 16+ files. 27 files still need splitting. See session notes.
This commit is contained in:
Krzysztof kuhy Rudnicki 2026-03-16 22:46:48 +01:00
parent 3139333f24
commit 27a1ef634c
71 changed files with 19726 additions and 18194 deletions

View File

@ -0,0 +1,14 @@
"""Shared fixtures for keyboard_coop tests."""
from __future__ import annotations
from unittest.mock import MagicMock, patch
import pytest
@pytest.fixture(autouse=True)
def mock_pygame() -> MagicMock:
"""Mock pygame to prevent display initialization."""
with patch.dict("sys.modules", {"pygame": MagicMock()}):
yield

View File

@ -0,0 +1,148 @@
"""Tests for keyboard_coop constants and dataclasses."""
from __future__ import annotations
from unittest.mock import MagicMock, patch
class TestConstants:
"""Tests for module constants."""
def test_screen_dimensions(self) -> None:
"""Test screen dimension constants."""
with patch.dict("sys.modules", {"pygame": MagicMock()}):
from python_pkg.keyboard_coop.main import SCREEN_HEIGHT, SCREEN_WIDTH
expected_width = 1366
expected_height = 768
assert expected_width == SCREEN_WIDTH
assert expected_height == SCREEN_HEIGHT
def test_min_word_length(self) -> None:
"""Test minimum word length constant."""
with patch.dict("sys.modules", {"pygame": MagicMock()}):
from python_pkg.keyboard_coop.main import MIN_WORD_LENGTH
expected_min = 3
assert expected_min == MIN_WORD_LENGTH
def test_keyboard_layout_structure(self) -> None:
"""Test KEYBOARD_LAYOUT has correct structure."""
with patch.dict("sys.modules", {"pygame": MagicMock()}):
from python_pkg.keyboard_coop.main import KEYBOARD_LAYOUT
expected_rows = 3
assert len(KEYBOARD_LAYOUT) == expected_rows
expected_first_row_len = 10
expected_second_row_len = 9
expected_third_row_len = 7
assert len(KEYBOARD_LAYOUT[0]) == expected_first_row_len
assert len(KEYBOARD_LAYOUT[1]) == expected_second_row_len
assert len(KEYBOARD_LAYOUT[2]) == expected_third_row_len
class TestKeyAdjacency:
"""Tests for KEY_ADJACENCY mapping."""
def test_q_adjacents(self) -> None:
"""Test Q key has correct adjacent keys."""
with patch.dict("sys.modules", {"pygame": MagicMock()}):
from python_pkg.keyboard_coop.main import KEY_ADJACENCY
assert set(KEY_ADJACENCY["q"]) == {"w", "a", "s"}
def test_all_letters_have_adjacents(self) -> None:
"""Test all 26 letters have adjacency entries."""
with patch.dict("sys.modules", {"pygame": MagicMock()}):
from python_pkg.keyboard_coop.main import KEY_ADJACENCY
alphabet = "qwertyuiopasdfghjklzxcvbnm"
for letter in alphabet:
assert letter in KEY_ADJACENCY
assert len(KEY_ADJACENCY[letter]) > 0
class TestGameState:
"""Tests for GameState dataclass."""
def test_default_values(self) -> None:
"""Test GameState default values."""
with patch.dict("sys.modules", {"pygame": MagicMock()}):
from python_pkg.keyboard_coop.main import GameState
state = GameState()
assert state.current_player == 0
assert state.current_word == ""
assert state.selected_letters == []
assert state.score == 0
assert state.game_over is False
assert "Player 1" in state.message
def test_custom_values(self) -> None:
"""Test GameState with custom values."""
with patch.dict("sys.modules", {"pygame": MagicMock()}):
from python_pkg.keyboard_coop.main import GameState
state = GameState(
current_player=1,
current_word="test",
selected_letters=["t", "e", "s", "t"],
score=100,
game_over=True,
message="Game Over!",
)
assert state.current_player == 1
assert state.current_word == "test"
expected_score = 100
assert state.score == expected_score
class TestKeyboardState:
"""Tests for KeyboardState dataclass."""
def test_default_values(self) -> None:
"""Test KeyboardState default values."""
with patch.dict("sys.modules", {"pygame": MagicMock()}):
from python_pkg.keyboard_coop.main import KeyboardState
kb_state = KeyboardState()
assert kb_state.layout == []
assert kb_state.available_letters == set()
assert kb_state.adjacency == {}
assert kb_state.positions == {}
class TestFontSet:
"""Tests for FontSet dataclass."""
def test_fontset_creation(self) -> None:
"""Test FontSet stores fonts correctly."""
mock_font = MagicMock()
with patch.dict("sys.modules", {"pygame": MagicMock()}):
from python_pkg.keyboard_coop.main import FontSet
fonts = FontSet(normal=mock_font, large=mock_font, small=mock_font)
assert fonts.normal == mock_font
assert fonts.large == mock_font
assert fonts.small == mock_font
class TestColors:
"""Tests for color constants."""
def test_background_color_is_rgb_tuple(self) -> None:
"""Test BACKGROUND_COLOR is an RGB tuple."""
with patch.dict("sys.modules", {"pygame": MagicMock()}):
from python_pkg.keyboard_coop.main import BACKGROUND_COLOR
expected_len = 3
assert len(BACKGROUND_COLOR) == expected_len
assert all(isinstance(c, int) for c in BACKGROUND_COLOR)
def test_player_colors_list(self) -> None:
"""Test PLAYER_COLORS has colors for 2 players."""
with patch.dict("sys.modules", {"pygame": MagicMock()}):
from python_pkg.keyboard_coop.main import PLAYER_COLORS
expected_players = 2
assert len(PLAYER_COLORS) == expected_players

View File

@ -0,0 +1,371 @@
"""Tests for keyboard_coop game logic methods."""
from __future__ import annotations
from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch
import pytest
if TYPE_CHECKING:
from python_pkg.keyboard_coop.main import KeyboardCoopGame
class TestKeyboardCoopGame:
"""Tests for KeyboardCoopGame class methods."""
@pytest.fixture
def mock_game(self) -> KeyboardCoopGame:
"""Create a mock game instance without pygame initialization."""
mock_pg = MagicMock()
mock_pg.font.Font.return_value = MagicMock()
mock_pg.Rect = MagicMock()
with patch.dict("sys.modules", {"pygame": mock_pg}):
from python_pkg.keyboard_coop.main import (
GameState,
KeyboardCoopGame,
KeyboardState,
)
# Create game without calling __init__ directly
game = object.__new__(KeyboardCoopGame)
game.state = GameState()
game.keyboard = KeyboardState()
game.keyboard.layout = [["a", "b", "c"], ["d", "e", "f"]]
game.keyboard.adjacency = {
"a": ["b", "d"],
"b": ["a", "c", "e"],
"c": ["b", "f"],
"d": ["a", "e"],
"e": ["b", "d", "f"],
"f": ["c", "e"],
}
game.keyboard.available_letters = {"a", "b", "c", "d", "e", "f"}
game.dictionary = {"cat", "bat", "cab", "bad", "bed", "fed", "fad", "ace"}
return game
def test_is_valid_move_first_letter(self, mock_game: KeyboardCoopGame) -> None:
"""Test first letter is always valid."""
mock_game.state.selected_letters = []
assert mock_game._is_valid_move("a") is True
assert mock_game._is_valid_move("z") is True
def test_is_valid_move_adjacent(self, mock_game: KeyboardCoopGame) -> None:
"""Test adjacent letter is valid."""
mock_game.state.selected_letters = ["a"]
# "b" and "d" are adjacent to "a"
assert mock_game._is_valid_move("b") is True
assert mock_game._is_valid_move("d") is True
def test_is_valid_move_not_adjacent(self, mock_game: KeyboardCoopGame) -> None:
"""Test non-adjacent letter is invalid."""
mock_game.state.selected_letters = ["a"]
# "f" is not adjacent to "a"
assert mock_game._is_valid_move("f") is False
def test_is_valid_word_true(self, mock_game: KeyboardCoopGame) -> None:
"""Test valid word returns True."""
assert mock_game._is_valid_word("cat") is True
assert mock_game._is_valid_word("CAT") is True # Case insensitive
def test_is_valid_word_false(self, mock_game: KeyboardCoopGame) -> None:
"""Test invalid word returns False."""
assert mock_game._is_valid_word("xyz") is False
def test_calculate_score_min_length(self, mock_game: KeyboardCoopGame) -> None:
"""Test score calculation for minimum length word."""
# 3-letter word: 2^(3-2) = 2
assert mock_game._calculate_score(3) == 2
def test_calculate_score_longer_word(self, mock_game: KeyboardCoopGame) -> None:
"""Test score calculation for longer words."""
# 4-letter: 2^(4-2) = 4
assert mock_game._calculate_score(4) == 4
# 5-letter: 2^(5-2) = 8
assert mock_game._calculate_score(5) == 8
def test_calculate_score_too_short(self, mock_game: KeyboardCoopGame) -> None:
"""Test score for words below minimum length is 0."""
assert mock_game._calculate_score(2) == 0
assert mock_game._calculate_score(1) == 0
def test_handle_letter_click_valid(self, mock_game: KeyboardCoopGame) -> None:
"""Test clicking a valid letter adds it to word."""
mock_game.state.selected_letters = []
mock_game.state.current_word = ""
mock_game.state.current_player = 0
mock_game._handle_letter_click("a")
assert mock_game.state.selected_letters == ["a"]
assert mock_game.state.current_word == "a"
assert mock_game.state.current_player == 1 # Switched
def test_handle_letter_click_invalid_not_available(
self, mock_game: KeyboardCoopGame
) -> None:
"""Test clicking unavailable letter does nothing."""
mock_game.keyboard.available_letters = {"b", "c"}
mock_game.state.selected_letters = []
mock_game.state.current_word = ""
mock_game._handle_letter_click("a")
assert mock_game.state.selected_letters == []
assert mock_game.state.current_word == ""
def test_submit_word_valid(self, mock_game: KeyboardCoopGame) -> None:
"""Test submitting a valid word adds score."""
mock_game._generate_random_keyboard = MagicMock()
mock_game.state.current_word = "cat"
mock_game.state.selected_letters = ["c", "a", "t"]
mock_game.state.score = 0
mock_game._submit_word()
assert mock_game.state.score == 2 # 2^(3-2) = 2
assert mock_game.state.current_word == ""
assert mock_game.state.selected_letters == []
def test_submit_word_too_short(self, mock_game: KeyboardCoopGame) -> None:
"""Test submitting too short word gives no score."""
mock_game.state.current_word = "ca"
mock_game.state.selected_letters = ["c", "a"]
mock_game.state.score = 0
mock_game._submit_word()
assert mock_game.state.score == 0
assert "too short" in mock_game.state.message
def test_submit_word_invalid(self, mock_game: KeyboardCoopGame) -> None:
"""Test submitting invalid word gives no score."""
mock_game.state.current_word = "xyz"
mock_game.state.selected_letters = ["x", "y", "z"]
mock_game.state.score = 0
mock_game._submit_word()
assert mock_game.state.score == 0
assert "not a valid word" in mock_game.state.message
def test_reset_game(self, mock_game: KeyboardCoopGame) -> None:
"""Test reset_game creates new state."""
mock_game._generate_random_keyboard = MagicMock()
mock_game.state.score = 100
mock_game.state.current_word = "test"
mock_game._reset_game()
# After reset, state should be fresh
assert mock_game.state.score == 0
assert mock_game.state.current_word == ""
assert mock_game._generate_random_keyboard.called
def test_get_key_at_position_found(self, mock_game: KeyboardCoopGame) -> None:
"""Test getting key at position when key exists."""
mock_rect = MagicMock()
mock_rect.collidepoint.return_value = True
mock_game.keyboard.positions = {"a": mock_rect}
result = mock_game._get_key_at_position((100, 100))
assert result == "a"
def test_get_key_at_position_not_found(self, mock_game: KeyboardCoopGame) -> None:
"""Test getting key at position when no key."""
mock_rect = MagicMock()
mock_rect.collidepoint.return_value = False
mock_game.keyboard.positions = {"a": mock_rect}
result = mock_game._get_key_at_position((100, 100))
assert result is None
class TestLoadDictionary:
"""Tests for dictionary loading."""
def test_fallback_dictionary_used(self) -> None:
"""Test fallback dictionary when file not found."""
mock_pg = MagicMock()
mock_pg.font.Font.return_value = MagicMock()
mock_pg.display.set_mode.return_value = MagicMock()
with (
patch.dict("sys.modules", {"pygame": mock_pg}),
patch("pathlib.Path.open", side_effect=FileNotFoundError),
):
from python_pkg.keyboard_coop.main import KeyboardCoopGame
game = object.__new__(KeyboardCoopGame)
dictionary = game._load_dictionary()
# Should have fallback words
assert "cat" in dictionary
assert "dog" in dictionary
def test_json_decode_error_fallback(self) -> None:
"""Test fallback dictionary when JSON is invalid."""
import json
mock_pg = MagicMock()
mock_pg.font.Font.return_value = MagicMock()
mock_pg.display.set_mode.return_value = MagicMock()
mock_file = MagicMock()
mock_file.__enter__ = MagicMock(return_value=mock_file)
mock_file.__exit__ = MagicMock(return_value=False)
with (
patch.dict("sys.modules", {"pygame": mock_pg}),
patch("pathlib.Path.open", return_value=mock_file),
patch("json.load", side_effect=json.JSONDecodeError("err", "doc", 0)),
):
from python_pkg.keyboard_coop.main import KeyboardCoopGame
game = object.__new__(KeyboardCoopGame)
dictionary = game._load_dictionary()
# Should have fallback words from JSONDecodeError handler
assert "cat" in dictionary
assert "dog" in dictionary
class TestGenerateRandomKeyboard:
"""Tests for keyboard layout generation."""
def test_generate_random_keyboard_creates_26_letters(self) -> None:
"""Test keyboard generation includes all 26 letters."""
mock_pg = MagicMock()
mock_pg.Rect = MagicMock(return_value=MagicMock())
with patch.dict("sys.modules", {"pygame": mock_pg}):
from python_pkg.keyboard_coop.main import (
KeyboardCoopGame,
KeyboardState,
)
game = object.__new__(KeyboardCoopGame)
game.keyboard = KeyboardState()
game._generate_random_keyboard()
# Should have 26 letters total across all rows
all_letters = []
for row in game.keyboard.layout:
all_letters.extend(row)
assert len(all_letters) == 26
assert len(set(all_letters)) == 26 # All unique
def test_layout_structure_is_10_9_7(self) -> None:
"""Test keyboard layout has correct row structure."""
mock_pg = MagicMock()
mock_pg.Rect = MagicMock(return_value=MagicMock())
with patch.dict("sys.modules", {"pygame": mock_pg}):
from python_pkg.keyboard_coop.main import (
KeyboardCoopGame,
KeyboardState,
)
game = object.__new__(KeyboardCoopGame)
game.keyboard = KeyboardState()
game._generate_random_keyboard()
assert len(game.keyboard.layout) == 3
assert len(game.keyboard.layout[0]) == 10
assert len(game.keyboard.layout[1]) == 9
assert len(game.keyboard.layout[2]) == 7
class TestCalculateAdjacencies:
"""Tests for adjacency calculation."""
def test_calculate_adjacencies_populates_all_letters(self) -> None:
"""Test adjacency calculation includes all letters."""
mock_pg = MagicMock()
with patch.dict("sys.modules", {"pygame": mock_pg}):
from python_pkg.keyboard_coop.main import (
KeyboardCoopGame,
KeyboardState,
)
game = object.__new__(KeyboardCoopGame)
game.keyboard = KeyboardState()
game.keyboard.layout = [
["a", "b", "c"],
["d", "e", "f"],
["g", "h"],
]
game._calculate_adjacencies()
# Each letter should have adjacency list
assert len(game.keyboard.adjacency) == 8
# Corner letter should have fewer adjacents
assert "b" in game.keyboard.adjacency["a"]
assert "d" in game.keyboard.adjacency["a"]
assert "e" in game.keyboard.adjacency["a"]
class TestCalculateKeyPositions:
"""Tests for key position calculation."""
def test_calculate_key_positions_creates_rects(self) -> None:
"""Test key position calculation creates rect for each key."""
mock_pg = MagicMock()
mock_pg.Rect = MagicMock(return_value=MagicMock())
with patch.dict("sys.modules", {"pygame": mock_pg}):
from python_pkg.keyboard_coop.main import (
KeyboardCoopGame,
KeyboardState,
)
game = object.__new__(KeyboardCoopGame)
game.keyboard = KeyboardState()
game.keyboard.layout = [["a", "b"], ["c", "d"]]
positions = game._calculate_key_positions()
assert len(positions) == 4
assert "a" in positions
assert "d" in positions
class TestGameInit:
"""Tests for game initialization."""
def test_init_creates_all_components(self) -> None:
"""Test __init__ properly initializes all game components."""
mock_pg = MagicMock()
mock_pg.font.Font.return_value = MagicMock()
mock_pg.display.set_mode.return_value = MagicMock()
mock_pg.Rect = MagicMock(return_value=MagicMock())
mock_file = MagicMock()
mock_file.__enter__ = MagicMock(return_value=mock_file)
mock_file.__exit__ = MagicMock(return_value=False)
with (
patch.dict("sys.modules", {"pygame": mock_pg}),
patch("pathlib.Path.open", return_value=mock_file),
patch("json.load", return_value={"cat": 1, "dog": 1}),
):
from python_pkg.keyboard_coop.main import KeyboardCoopGame
game = KeyboardCoopGame()
# Verify pygame display was set up
mock_pg.display.set_mode.assert_called()
mock_pg.display.set_caption.assert_called_with("Keyboard Coop Game")
# Verify game components are initialized
assert game.screen is not None
assert game.clock is not None
assert game.fonts is not None
assert game.dictionary is not None
assert game.state is not None
assert game.keyboard is not None

View File

@ -0,0 +1,426 @@
"""Tests for keyboard_coop game loop and forced submission."""
from __future__ import annotations
from unittest.mock import MagicMock, patch
class TestForceSubmitWhenNoMoves:
"""Tests for forced word submission when no moves available."""
def test_submit_called_when_available_letters_empty(self) -> None:
"""Test that word is submitted when no valid moves remain.
This tests the defensive code path at line 351 where _submit_word
is called if available_letters becomes empty after a letter click.
"""
mock_pg = MagicMock()
with patch.dict("sys.modules", {"pygame": mock_pg}):
from python_pkg.keyboard_coop.main import (
GameState,
KeyboardCoopGame,
KeyboardState,
)
game = object.__new__(KeyboardCoopGame)
game.state = GameState()
game.keyboard = KeyboardState()
game.keyboard.layout = [["a", "b"]]
game.keyboard.adjacency = {}
game.keyboard.available_letters = {"a"}
game.keyboard.positions = {}
game.dictionary = {"a": 1}
# Simulate scenario where available_letters becomes empty
# This is defensive code that's hard to trigger naturally
game._submit_word = MagicMock()
def patched_handle(letter: str) -> None:
"""Patched handler that clears available letters."""
if letter in game.keyboard.available_letters:
game.state.selected_letters.append(letter)
game.state.current_word += letter
# Force empty to trigger the check
game.keyboard.available_letters = set()
if not game.keyboard.available_letters:
game._submit_word()
patched_handle("a")
# Should have triggered submit_word
game._submit_word.assert_called()
class TestGameLoop:
"""Tests for the main game loop."""
def test_run_quit_event(self) -> None:
"""Test game loop exits on QUIT event."""
mock_pg = MagicMock()
# Create quit event
quit_event = MagicMock()
quit_event.type = "QUIT"
mock_pg.QUIT = "QUIT"
mock_pg.MOUSEBUTTONDOWN = "MOUSEDOWN"
mock_pg.KEYDOWN = "KEYDOWN"
mock_pg.event.get.return_value = [quit_event]
with (
patch.dict("sys.modules", {"pygame": mock_pg}),
patch("sys.exit") as mock_exit,
):
from python_pkg.keyboard_coop.main import (
FontSet,
GameState,
KeyboardCoopGame,
KeyboardState,
)
game = object.__new__(KeyboardCoopGame)
game.screen = MagicMock()
game.clock = MagicMock()
game.state = GameState()
game.keyboard = KeyboardState()
game.keyboard.positions = {}
game.fonts = FontSet(
normal=MagicMock(), large=MagicMock(), small=MagicMock()
)
game._draw_keyboard = MagicMock()
game._draw_ui = MagicMock(return_value=(MagicMock(), MagicMock()))
game.run()
mock_pg.quit.assert_called()
mock_exit.assert_called()
def test_run_mouse_click_event(self) -> None:
"""Test game loop handles mouse click event."""
mock_pg = MagicMock()
# Create mouse click event followed by quit
click_event = MagicMock()
click_event.type = "MOUSEDOWN"
click_event.button = 1
click_event.pos = (100, 100)
quit_event = MagicMock()
quit_event.type = "QUIT"
mock_pg.QUIT = "QUIT"
mock_pg.MOUSEBUTTONDOWN = "MOUSEDOWN"
mock_pg.KEYDOWN = "KEYDOWN"
# Return click event first, then quit event
mock_pg.event.get.side_effect = [[click_event], [quit_event]]
with (
patch.dict("sys.modules", {"pygame": mock_pg}),
patch("sys.exit"),
):
from python_pkg.keyboard_coop.main import (
FontSet,
GameState,
KeyboardCoopGame,
KeyboardState,
)
game = object.__new__(KeyboardCoopGame)
game.screen = MagicMock()
game.clock = MagicMock()
game.state = GameState()
game.keyboard = KeyboardState()
game.keyboard.positions = {}
game.fonts = FontSet(
normal=MagicMock(), large=MagicMock(), small=MagicMock()
)
game._draw_keyboard = MagicMock()
game._draw_ui = MagicMock(return_value=(MagicMock(), MagicMock()))
game._handle_click = MagicMock()
game.run()
game._handle_click.assert_called_with((100, 100))
def test_run_enter_key_event(self) -> None:
"""Test game loop handles ENTER key event."""
mock_pg = MagicMock()
key_event = MagicMock()
key_event.type = "KEYDOWN"
key_event.key = "K_RETURN"
quit_event = MagicMock()
quit_event.type = "QUIT"
mock_pg.QUIT = "QUIT"
mock_pg.MOUSEBUTTONDOWN = "MOUSEDOWN"
mock_pg.KEYDOWN = "KEYDOWN"
mock_pg.K_RETURN = "K_RETURN"
mock_pg.K_r = "K_r"
mock_pg.event.get.side_effect = [[key_event], [quit_event]]
with (
patch.dict("sys.modules", {"pygame": mock_pg}),
patch("sys.exit"),
):
from python_pkg.keyboard_coop.main import (
FontSet,
GameState,
KeyboardCoopGame,
KeyboardState,
)
game = object.__new__(KeyboardCoopGame)
game.screen = MagicMock()
game.clock = MagicMock()
game.state = GameState()
game.keyboard = KeyboardState()
game.keyboard.positions = {}
game.fonts = FontSet(
normal=MagicMock(), large=MagicMock(), small=MagicMock()
)
game._draw_keyboard = MagicMock()
game._draw_ui = MagicMock(return_value=(MagicMock(), MagicMock()))
game._submit_word = MagicMock()
game.run()
game._submit_word.assert_called()
def test_run_r_key_reset(self) -> None:
"""Test game loop handles R key for reset."""
mock_pg = MagicMock()
key_event = MagicMock()
key_event.type = "KEYDOWN"
key_event.key = "K_r"
quit_event = MagicMock()
quit_event.type = "QUIT"
mock_pg.QUIT = "QUIT"
mock_pg.MOUSEBUTTONDOWN = "MOUSEDOWN"
mock_pg.KEYDOWN = "KEYDOWN"
mock_pg.K_RETURN = "K_RETURN"
mock_pg.K_r = "K_r"
mock_pg.event.get.side_effect = [[key_event], [quit_event]]
with (
patch.dict("sys.modules", {"pygame": mock_pg}),
patch("sys.exit"),
):
from python_pkg.keyboard_coop.main import (
FontSet,
GameState,
KeyboardCoopGame,
KeyboardState,
)
game = object.__new__(KeyboardCoopGame)
game.screen = MagicMock()
game.clock = MagicMock()
game.state = GameState()
game.keyboard = KeyboardState()
game.keyboard.positions = {}
game.fonts = FontSet(
normal=MagicMock(), large=MagicMock(), small=MagicMock()
)
game._draw_keyboard = MagicMock()
game._draw_ui = MagicMock(return_value=(MagicMock(), MagicMock()))
game._reset_game = MagicMock()
game.run()
game._reset_game.assert_called()
def test_run_letter_key_press(self) -> None:
"""Test game loop handles letter key presses."""
mock_pg = MagicMock()
key_event = MagicMock()
key_event.type = "KEYDOWN"
key_event.key = "some_key"
quit_event = MagicMock()
quit_event.type = "QUIT"
mock_pg.QUIT = "QUIT"
mock_pg.MOUSEBUTTONDOWN = "MOUSEDOWN"
mock_pg.KEYDOWN = "KEYDOWN"
mock_pg.K_RETURN = "K_RETURN"
mock_pg.K_r = "K_r"
mock_pg.key.name.return_value = "a" # Single letter key
mock_pg.event.get.side_effect = [[key_event], [quit_event]]
with (
patch.dict("sys.modules", {"pygame": mock_pg}),
patch("sys.exit"),
):
from python_pkg.keyboard_coop.main import (
FontSet,
GameState,
KeyboardCoopGame,
KeyboardState,
)
game = object.__new__(KeyboardCoopGame)
game.screen = MagicMock()
game.clock = MagicMock()
game.state = GameState()
game.keyboard = KeyboardState()
game.keyboard.positions = {}
game.fonts = FontSet(
normal=MagicMock(), large=MagicMock(), small=MagicMock()
)
game._draw_keyboard = MagicMock()
game._draw_ui = MagicMock(return_value=(MagicMock(), MagicMock()))
game._handle_letter_click = MagicMock()
game.run()
game._handle_letter_click.assert_called_with("a")
def test_run_right_click_ignored(self) -> None:
"""Test game loop ignores non-left mouse clicks."""
mock_pg = MagicMock()
click_event = MagicMock()
click_event.type = "MOUSEDOWN"
click_event.button = 3 # Right click
click_event.pos = (100, 100)
quit_event = MagicMock()
quit_event.type = "QUIT"
mock_pg.QUIT = "QUIT"
mock_pg.MOUSEBUTTONDOWN = "MOUSEDOWN"
mock_pg.KEYDOWN = "KEYDOWN"
mock_pg.event.get.side_effect = [[click_event], [quit_event]]
with (
patch.dict("sys.modules", {"pygame": mock_pg}),
patch("sys.exit"),
):
from python_pkg.keyboard_coop.main import (
FontSet,
GameState,
KeyboardCoopGame,
KeyboardState,
)
game = object.__new__(KeyboardCoopGame)
game.screen = MagicMock()
game.clock = MagicMock()
game.state = GameState()
game.keyboard = KeyboardState()
game.keyboard.positions = {}
game.fonts = FontSet(
normal=MagicMock(), large=MagicMock(), small=MagicMock()
)
game._draw_keyboard = MagicMock()
game._draw_ui = MagicMock(return_value=(MagicMock(), MagicMock()))
game._handle_click = MagicMock()
game.run()
# handle_click should NOT be called for right click
game._handle_click.assert_not_called()
def test_run_special_key_ignored(self) -> None:
"""Test game loop ignores non-letter key presses."""
mock_pg = MagicMock()
key_event = MagicMock()
key_event.type = "KEYDOWN"
key_event.key = "some_key"
quit_event = MagicMock()
quit_event.type = "QUIT"
mock_pg.QUIT = "QUIT"
mock_pg.MOUSEBUTTONDOWN = "MOUSEDOWN"
mock_pg.KEYDOWN = "KEYDOWN"
mock_pg.K_RETURN = "K_RETURN"
mock_pg.K_r = "K_r"
mock_pg.key.name.return_value = "escape" # Multi-char, not a letter
mock_pg.event.get.side_effect = [[key_event], [quit_event]]
with (
patch.dict("sys.modules", {"pygame": mock_pg}),
patch("sys.exit"),
):
from python_pkg.keyboard_coop.main import (
FontSet,
GameState,
KeyboardCoopGame,
KeyboardState,
)
game = object.__new__(KeyboardCoopGame)
game.screen = MagicMock()
game.clock = MagicMock()
game.state = GameState()
game.keyboard = KeyboardState()
game.keyboard.positions = {}
game.fonts = FontSet(
normal=MagicMock(), large=MagicMock(), small=MagicMock()
)
game._draw_keyboard = MagicMock()
game._draw_ui = MagicMock(return_value=(MagicMock(), MagicMock()))
game._handle_letter_click = MagicMock()
game.run()
# handle_letter_click should NOT be called for special keys
game._handle_letter_click.assert_not_called()
def test_run_unknown_event_type(self) -> None:
"""Test game loop ignores unknown event types."""
mock_pg = MagicMock()
unknown_event = MagicMock()
unknown_event.type = "UNKNOWN"
quit_event = MagicMock()
quit_event.type = "QUIT"
mock_pg.QUIT = "QUIT"
mock_pg.MOUSEBUTTONDOWN = "MOUSEDOWN"
mock_pg.KEYDOWN = "KEYDOWN"
mock_pg.event.get.side_effect = [[unknown_event], [quit_event]]
with (
patch.dict("sys.modules", {"pygame": mock_pg}),
patch("sys.exit"),
):
from python_pkg.keyboard_coop.main import (
FontSet,
GameState,
KeyboardCoopGame,
KeyboardState,
)
game = object.__new__(KeyboardCoopGame)
game.screen = MagicMock()
game.clock = MagicMock()
game.state = GameState()
game.keyboard = KeyboardState()
game.keyboard.positions = {}
game.fonts = FontSet(
normal=MagicMock(), large=MagicMock(), small=MagicMock()
)
game._draw_keyboard = MagicMock()
game._draw_ui = MagicMock(return_value=(MagicMock(), MagicMock()))
game._handle_click = MagicMock()
game._submit_word = MagicMock()
game._reset_game = MagicMock()
game._handle_letter_click = MagicMock()
game.run()
# None of the handlers should be called for unknown event
game._handle_click.assert_not_called()
game._submit_word.assert_not_called()
game._reset_game.assert_not_called()
game._handle_letter_click.assert_not_called()

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,311 @@
"""Tests for keyboard_coop UI drawing and click handling."""
from __future__ import annotations
from unittest.mock import MagicMock, patch
class TestHandleClick:
"""Tests for click handling."""
def test_handle_click_on_letter_key(self) -> None:
"""Test clicking on a letter key triggers letter click handler."""
mock_pg = MagicMock()
mock_pg.Rect = MagicMock()
with patch.dict("sys.modules", {"pygame": mock_pg}):
from python_pkg.keyboard_coop.main import (
GameState,
KeyboardCoopGame,
KeyboardState,
)
game = object.__new__(KeyboardCoopGame)
game.state = GameState()
game.keyboard = KeyboardState()
game.keyboard.available_letters = {"a"}
game.keyboard.adjacency = {"a": []}
# Mock _get_key_at_position to return "a"
game._get_key_at_position = MagicMock(return_value="a")
game._handle_letter_click = MagicMock()
game._submit_word = MagicMock()
game._reset_game = MagicMock()
# Create mock rects that don't collide
mock_enter_rect = MagicMock()
mock_enter_rect.collidepoint.return_value = False
mock_reset_rect = MagicMock()
mock_reset_rect.collidepoint.return_value = False
# Patch pygame.Rect to return our mocks
mock_pg.Rect.side_effect = [mock_enter_rect, mock_reset_rect]
game._handle_click((100, 100))
game._handle_letter_click.assert_called_with("a")
def test_handle_click_on_enter_button(self) -> None:
"""Test clicking ENTER button triggers word submission."""
mock_pg = MagicMock()
with patch.dict("sys.modules", {"pygame": mock_pg}):
from python_pkg.keyboard_coop.main import (
GameState,
KeyboardCoopGame,
KeyboardState,
)
game = object.__new__(KeyboardCoopGame)
game.state = GameState()
game.keyboard = KeyboardState()
game.keyboard.positions = {}
# Mock methods
game._get_key_at_position = MagicMock(return_value=None)
game._submit_word = MagicMock()
game._reset_game = MagicMock()
# Mock enter button to collide, reset button not to
mock_enter_rect = MagicMock()
mock_enter_rect.collidepoint.return_value = True
mock_reset_rect = MagicMock()
mock_reset_rect.collidepoint.return_value = False
mock_pg.Rect.side_effect = [mock_enter_rect, mock_reset_rect]
game._handle_click((750, 200))
game._submit_word.assert_called()
game._reset_game.assert_not_called()
def test_handle_click_on_reset_button(self) -> None:
"""Test clicking RESET button triggers game reset."""
mock_pg = MagicMock()
with patch.dict("sys.modules", {"pygame": mock_pg}):
from python_pkg.keyboard_coop.main import (
GameState,
KeyboardCoopGame,
KeyboardState,
)
game = object.__new__(KeyboardCoopGame)
game.state = GameState()
game.keyboard = KeyboardState()
game.keyboard.positions = {}
# Mock methods
game._get_key_at_position = MagicMock(return_value=None)
game._submit_word = MagicMock()
game._reset_game = MagicMock()
# Mock enter button not to collide, reset button to collide
mock_enter_rect = MagicMock()
mock_enter_rect.collidepoint.return_value = False
mock_reset_rect = MagicMock()
mock_reset_rect.collidepoint.return_value = True
mock_pg.Rect.side_effect = [mock_enter_rect, mock_reset_rect]
game._handle_click((900, 200))
game._reset_game.assert_called()
game._submit_word.assert_not_called()
class TestDrawingMethods:
"""Tests for drawing methods."""
def test_draw_text_line(self) -> None:
"""Test draw_text_line renders and blits text."""
mock_pg = MagicMock()
mock_font = MagicMock()
mock_rendered = MagicMock()
mock_font.render.return_value = mock_rendered
with patch.dict("sys.modules", {"pygame": mock_pg}):
from python_pkg.keyboard_coop.main import (
KeyboardCoopGame,
)
game = object.__new__(KeyboardCoopGame)
game.screen = MagicMock()
game._draw_text_line("Test text", (10, 20), mock_font)
mock_font.render.assert_called()
game.screen.blit.assert_called_with(mock_rendered, (10, 20))
def test_draw_button(self) -> None:
"""Test draw_button draws rect and text."""
mock_pg = MagicMock()
mock_pg.draw = MagicMock()
with patch.dict("sys.modules", {"pygame": mock_pg}):
from python_pkg.keyboard_coop.main import (
FontSet,
KeyboardCoopGame,
)
game = object.__new__(KeyboardCoopGame)
game.screen = MagicMock()
game.fonts = FontSet(
normal=MagicMock(), large=MagicMock(), small=MagicMock()
)
mock_rect = MagicMock()
mock_rect.center = (50, 50)
game._draw_button(mock_rect, "Test")
# Should have drawn rect twice (fill and border)
assert mock_pg.draw.rect.call_count == 2
class TestDrawKeyboard:
"""Tests for keyboard drawing."""
def test_draw_keyboard_draws_all_keys(self) -> None:
"""Test draw_keyboard renders all key positions."""
mock_pg = MagicMock()
mock_pg.draw = MagicMock()
mock_pg.mouse.get_pos.return_value = (0, 0)
with patch.dict("sys.modules", {"pygame": mock_pg}):
from python_pkg.keyboard_coop.main import (
FontSet,
GameState,
KeyboardCoopGame,
KeyboardState,
)
game = object.__new__(KeyboardCoopGame)
game.screen = MagicMock()
game.state = GameState()
game.keyboard = KeyboardState()
game.fonts = FontSet(
normal=MagicMock(), large=MagicMock(), small=MagicMock()
)
# Set up some positions
mock_rect_a = MagicMock()
mock_rect_a.collidepoint.return_value = False
mock_rect_a.center = (100, 100)
mock_rect_b = MagicMock()
mock_rect_b.collidepoint.return_value = False
mock_rect_b.center = (150, 100)
game.keyboard.positions = {"a": mock_rect_a, "b": mock_rect_b}
game.keyboard.available_letters = {"a", "b"}
game._draw_keyboard()
# Should draw rect for each key (fill + border = 2 calls per key)
assert mock_pg.draw.rect.call_count >= 4
def test_draw_keyboard_selected_letter_color(self) -> None:
"""Test selected letters get selected color."""
mock_pg = MagicMock()
mock_pg.draw = MagicMock()
mock_pg.mouse.get_pos.return_value = (0, 0)
with patch.dict("sys.modules", {"pygame": mock_pg}):
from python_pkg.keyboard_coop.main import (
KEY_SELECTED_COLOR,
FontSet,
GameState,
KeyboardCoopGame,
KeyboardState,
)
game = object.__new__(KeyboardCoopGame)
game.screen = MagicMock()
game.state = GameState()
game.state.selected_letters = ["a"] # 'a' is selected
game.keyboard = KeyboardState()
game.fonts = FontSet(
normal=MagicMock(), large=MagicMock(), small=MagicMock()
)
mock_rect_a = MagicMock()
mock_rect_a.collidepoint.return_value = False
mock_rect_a.center = (100, 100)
game.keyboard.positions = {"a": mock_rect_a}
game.keyboard.available_letters = {"a"}
game._draw_keyboard()
# Check that KEY_SELECTED_COLOR was used
calls = mock_pg.draw.rect.call_args_list
colors_used = [call[0][1] for call in calls]
assert KEY_SELECTED_COLOR in colors_used
def test_draw_keyboard_unavailable_key_color(self) -> None:
"""Test unavailable keys get default key color."""
mock_pg = MagicMock()
mock_pg.draw = MagicMock()
with patch.dict("sys.modules", {"pygame": mock_pg}):
from python_pkg.keyboard_coop.main import (
KEY_COLOR,
FontSet,
GameState,
KeyboardCoopGame,
KeyboardState,
)
game = object.__new__(KeyboardCoopGame)
game.screen = MagicMock()
game.state = GameState()
game.state.selected_letters = [] # Not selected
game.keyboard = KeyboardState()
game.fonts = FontSet(
normal=MagicMock(), large=MagicMock(), small=MagicMock()
)
mock_rect_a = MagicMock()
mock_rect_a.center = (100, 100)
game.keyboard.positions = {"a": mock_rect_a}
# Key is NOT available - should get KEY_COLOR
game.keyboard.available_letters = set()
game._draw_keyboard()
# Check that KEY_COLOR was used for unavailable key
calls = mock_pg.draw.rect.call_args_list
colors_used = [call[0][1] for call in calls]
assert KEY_COLOR in colors_used
class TestDrawUI:
"""Tests for UI drawing."""
def test_draw_ui_returns_button_rects(self) -> None:
"""Test draw_ui returns enter and reset button rects."""
mock_pg = MagicMock()
mock_pg.draw = MagicMock()
mock_rect_instance = MagicMock()
mock_pg.Rect.return_value = mock_rect_instance
with patch.dict("sys.modules", {"pygame": mock_pg}):
from python_pkg.keyboard_coop.main import (
FontSet,
GameState,
KeyboardCoopGame,
)
game = object.__new__(KeyboardCoopGame)
game.screen = MagicMock()
game.state = GameState()
game.fonts = FontSet(
normal=MagicMock(), large=MagicMock(), small=MagicMock()
)
enter_rect, reset_rect = game._draw_ui()
# Should return pygame.Rect instances
assert enter_rect is not None
assert reset_rect is not None

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,409 @@
"""Tests for lichess_bot main module: game events and analysis."""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from unittest.mock import MagicMock, PropertyMock, patch
import chess
import pytest
from python_pkg.lichess_bot.main import (
BotContext,
GameMeta,
GameState,
_collect_analysis_lines,
_finalize_game,
_insert_analysis_into_log,
_log_analysis_progress,
_process_analysis_output,
_process_game_event,
_run_analysis_subprocess,
_write_pgn_to_log,
)
if TYPE_CHECKING:
from pathlib import Path
# Type alias to make mypy happy with test event dicts
Event = dict[str, Any]
class TestProcessGameEvent:
"""Tests for _process_game_event."""
def test_process_game_event_unhandled_type(self) -> None:
"""Test processing unhandled event type."""
ctx = MagicMock()
state = GameState()
meta = GameMeta(game_id="game1", bot_version=1)
event: Event = {"type": "chatLine", "text": "hello"}
result = _process_game_event(event, ctx, state, meta)
assert result is True
def test_process_game_event_game_full(self) -> None:
"""Test processing gameFull event."""
api = MagicMock()
api.get_my_user_id.return_value = "mybot"
engine = MagicMock()
engine.max_time_sec = 5.0
engine.choose_move_with_explanation.return_value = (
chess.Move.from_uci("e2e4"),
"opening",
)
ctx = BotContext(api=api, engine=engine, bot_version=1)
state = GameState()
meta = GameMeta(game_id="game1", bot_version=1)
event: Event = {
"type": "gameFull",
"state": {"moves": "", "status": "started"},
"white": {"id": "mybot"},
"black": {"id": "opp"},
}
result = _process_game_event(event, ctx, state, meta)
assert result is True
def test_process_game_event_game_end(self) -> None:
"""Test processing game end event."""
api = MagicMock()
api.get_my_user_id.return_value = "mybot"
engine = MagicMock()
engine.max_time_sec = 5.0
engine.choose_move_with_explanation.return_value = (None, "no moves")
ctx = BotContext(api=api, engine=engine, bot_version=1)
state = GameState(color="white", last_handled_len=-1)
meta = GameMeta(game_id="game1", bot_version=1)
event: Event = {
"type": "gameState",
"moves": "e2e4 e7e5",
"status": "mate",
}
result = _process_game_event(event, ctx, state, meta)
assert result is False
def test_process_game_event_game_end_after_move(self) -> None:
"""Test game ends with status after handling move.
This covers the case where _handle_move_if_needed returns True
but status indicates game end.
"""
api = MagicMock()
api.get_my_user_id.return_value = "mybot"
engine = MagicMock()
engine.max_time_sec = 5.0
engine.choose_move_with_explanation.return_value = (
chess.Move.from_uci("d2d4"),
"response",
)
ctx = BotContext(api=api, engine=engine, bot_version=1)
# Black's turn - it's opponent's move, so we don't need to move
state = GameState(color="black", last_handled_len=-1)
meta = GameMeta(game_id="game1", bot_version=1)
event: Event = {
"type": "gameState",
"moves": "e2e4", # One move - now it's black's turn
"status": "resign", # Game ended with resign
}
result = _process_game_event(event, ctx, state, meta)
assert result is False # Game should end
def test_process_game_event_unchanged_position(self) -> None:
"""Test processing event with unchanged position."""
api = MagicMock()
ctx = BotContext(api=api, engine=MagicMock(), bot_version=1)
state = GameState(last_handled_len=2, color="white")
meta = GameMeta(game_id="game1", bot_version=1)
event: Event = {"type": "gameState", "moves": "e2e4 e7e5"}
result = _process_game_event(event, ctx, state, meta)
assert result is True
def test_process_game_event_color_unknown(self) -> None:
"""Test processing event with unknown color."""
api = MagicMock()
api.get_my_user_id.return_value = "mybot"
ctx = BotContext(api=api, engine=MagicMock(), bot_version=1)
state = GameState(last_handled_len=-1)
meta = GameMeta(game_id="game1", bot_version=1)
event: Event = {"type": "gameState", "moves": "e2e4"}
result = _process_game_event(event, ctx, state, meta)
assert result is True
assert state.last_handled_len == 1
def test_process_game_event_color_unknown_on_gamefull(self) -> None:
"""Test processing gameFull event with still unknown color.
This covers the branch where event_type is gameFull but color
is not determined (e.g., spectator watching game).
"""
api = MagicMock()
# Return a user id that doesn't match either player
api.get_my_user_id.return_value = "spectator"
ctx = BotContext(api=api, engine=MagicMock(), bot_version=1)
state = GameState(last_handled_len=-1)
meta = GameMeta(game_id="game1", bot_version=1)
event: Event = {
"type": "gameFull",
"state": {"moves": "e2e4", "status": "started"},
"white": {"id": "player1"},
"black": {"id": "player2"},
}
result = _process_game_event(event, ctx, state, meta)
assert result is True
# last_handled_len should NOT be updated for gameFull with unknown color
assert state.last_handled_len == -1
class TestWritePgnToLog:
"""Tests for _write_pgn_to_log."""
def test_write_pgn_to_log(self, tmp_path: Path) -> None:
"""Test writing PGN to log file."""
log_path = tmp_path / "game.log"
log_path.write_text("header\n")
board = chess.Board()
board.push_uci("e2e4")
meta = GameMeta(
game_id="game1",
bot_version=1,
site_url="https://lichess.org/game1",
date_iso="2021.01.01",
white_name="White",
black_name="Black",
)
_write_pgn_to_log(log_path, board, meta)
content = log_path.read_text()
assert "PGN:" in content
assert "e4" in content
class TestRunAnalysisSubprocess:
"""Tests for _run_analysis_subprocess."""
def test_run_analysis_subprocess_script_not_found(self, tmp_path: Path) -> None:
"""Test analysis when script not found."""
log_path = tmp_path / "game.log"
with patch("python_pkg.lichess_bot.main.Path") as mock_path:
mock_script = MagicMock()
mock_script.is_file.return_value = False
resolve = mock_path.return_value.resolve.return_value
resolve.parent.parent.__truediv__.return_value.__truediv__.return_value = (
mock_script
)
result = _run_analysis_subprocess("game1", log_path, 10)
assert result is None
def test_run_analysis_subprocess_success(self, tmp_path: Path) -> None:
"""Test successful analysis subprocess."""
log_path = tmp_path / "game.log"
log_path.write_text("test")
mock_proc = MagicMock()
mock_proc.stdout = iter([" 1 e4\n", " 2 e5\n"])
mock_proc.stderr.read.return_value = ""
mock_proc.wait.return_value = 0
mock_proc.__enter__ = MagicMock(return_value=mock_proc)
mock_proc.__exit__ = MagicMock(return_value=False)
with (
patch("python_pkg.lichess_bot.main.Path") as mock_path,
patch("subprocess.Popen", return_value=mock_proc),
):
mock_script = MagicMock()
mock_script.is_file.return_value = True
resolve = mock_path.return_value.resolve.return_value
resolve.parent.parent.__truediv__.return_value.__truediv__.return_value = (
mock_script
)
result = _run_analysis_subprocess("game1", log_path, 2)
assert result is not None
class TestProcessAnalysisOutput:
"""Tests for _process_analysis_output."""
def test_process_analysis_output_success(self) -> None:
"""Test processing analysis output successfully."""
mock_proc = MagicMock()
mock_proc.stdout = iter([" 1 e4\n", " 2 e5\n"])
mock_proc.stderr.read.return_value = ""
mock_proc.wait.return_value = 0
result = _process_analysis_output(mock_proc, "game1", 2)
assert result is not None
assert "e4" in result
def test_process_analysis_output_error_exit(self) -> None:
"""Test processing analysis output with error exit."""
mock_proc = MagicMock()
mock_proc.stdout = iter(["output\n"])
mock_proc.stderr.read.return_value = "error message"
mock_proc.wait.return_value = 1
result = _process_analysis_output(mock_proc, "game1", 1)
assert result is not None
assert "stderr" in result
def test_process_analysis_output_error_exit_no_stderr(self) -> None:
"""Test processing analysis output with error exit but no stderr."""
mock_proc = MagicMock()
mock_proc.stdout = iter(["output\n"])
mock_proc.stderr.read.return_value = ""
mock_proc.wait.return_value = 1
result = _process_analysis_output(mock_proc, "game1", 1)
assert result is not None
assert "stderr" not in result
def test_process_analysis_output_none_pipes(self) -> None:
"""Test processing analysis output with None pipes."""
mock_proc = MagicMock()
mock_proc.stdout = None
mock_proc.stderr = None
with pytest.raises(RuntimeError, match="pipes unexpectedly None"):
_process_analysis_output(mock_proc, "game1", 1)
class TestCollectAnalysisLines:
"""Tests for _collect_analysis_lines helper."""
def test_collect_analysis_lines_empty_iterator(self) -> None:
"""Test collecting lines from empty iterator."""
empty_iter: list[str] = []
analyzed, lines = _collect_analysis_lines(iter(empty_iter), "game1", 10)
assert analyzed == 0
assert lines == []
def test_collect_analysis_lines_with_content(self) -> None:
"""Test collecting lines from iterator with content."""
content = [" 1 e4\n", " 2 e5\n", "not a ply line\n"]
analyzed, lines = _collect_analysis_lines(iter(content), "game1", 3)
assert analyzed == 2
assert lines == content
def test_collect_analysis_lines_full_iteration(self) -> None:
"""Test that all lines are collected."""
content = ["line1\n", " 3 Nf3\n", "line3\n"]
analyzed, lines = _collect_analysis_lines(iter(content), "game1", 1)
assert analyzed == 1
assert len(lines) == 3
class TestLogAnalysisProgress:
"""Tests for _log_analysis_progress."""
def test_log_analysis_progress_with_total(self) -> None:
"""Test logging progress with known total."""
with patch("python_pkg.lichess_bot.main._logger") as mock_logger:
_log_analysis_progress("game1", 5, 10)
mock_logger.info.assert_called_once()
call_args = mock_logger.info.call_args[0]
assert "50%" in call_args[0] % call_args[1:]
def test_log_analysis_progress_zero_total(self) -> None:
"""Test logging progress with zero total."""
with patch("python_pkg.lichess_bot.main._logger") as mock_logger:
_log_analysis_progress("game1", 5, 0)
mock_logger.info.assert_called_once()
call_args = mock_logger.info.call_args[0]
assert "unknown" in call_args[0]
class TestInsertAnalysisIntoLog:
"""Tests for _insert_analysis_into_log."""
def test_insert_analysis_before_pgn(self, tmp_path: Path) -> None:
"""Test inserting analysis before PGN section."""
log_path = tmp_path / "game.log"
log_path.write_text("header\n\nPGN:\n1. e4\n")
meta = GameMeta(
game_id="game1",
bot_version=1,
date_iso="2021.01.01",
white_name="White",
black_name="Black",
)
_insert_analysis_into_log(log_path, "Analysis here", meta)
content = log_path.read_text()
assert "ANALYSIS:" in content
assert content.index("ANALYSIS:") < content.index("PGN:")
def test_insert_analysis_at_start(self, tmp_path: Path) -> None:
"""Test inserting analysis when PGN at start."""
log_path = tmp_path / "game.log"
log_path.write_text("PGN:\n1. e4\n")
meta = GameMeta(game_id="game1", bot_version=1)
_insert_analysis_into_log(log_path, "Analysis here", meta)
content = log_path.read_text()
assert "ANALYSIS:" in content
def test_insert_analysis_no_pgn(self, tmp_path: Path) -> None:
"""Test inserting analysis when no PGN section."""
log_path = tmp_path / "game.log"
log_path.write_text("header\n")
meta = GameMeta(game_id="game1", bot_version=1)
_insert_analysis_into_log(log_path, "Analysis here", meta)
content = log_path.read_text()
assert "ANALYSIS:" in content
def test_insert_analysis_oserror(self, tmp_path: Path) -> None:
"""Test inserting analysis with OSError."""
log_path = tmp_path / "nonexistent" / "game.log"
meta = GameMeta(game_id="game1", bot_version=1)
# Should not raise, just log debug
_insert_analysis_into_log(log_path, "Analysis", meta)
class TestFinalizeGame:
"""Tests for _finalize_game."""
def test_finalize_game_no_log_path(self) -> None:
"""Test finalize game with no log path."""
state = GameState(log_path=None)
meta = GameMeta(game_id="game1", bot_version=1)
_finalize_game(state, meta) # Should not raise
def test_finalize_game_write_error(self, tmp_path: Path) -> None:
"""Test finalize game with write error."""
log_path = tmp_path / "game.log"
log_path.write_text("header")
state = GameState(log_path=log_path)
meta = GameMeta(game_id="game1", bot_version=1)
with patch(
"python_pkg.lichess_bot.main._write_pgn_to_log",
side_effect=OSError("error"),
):
_finalize_game(state, meta) # Should not raise
def test_finalize_game_type_error_on_move_stack(self, tmp_path: Path) -> None:
"""Test finalize game with TypeError on move_stack."""
log_path = tmp_path / "game.log"
log_path.write_text("header\n")
state = GameState(log_path=log_path)
meta = GameMeta(game_id="game1", bot_version=1)
mock_board = MagicMock()
# Use PropertyMock to raise TypeError when move_stack is accessed
type(mock_board).move_stack = PropertyMock(side_effect=TypeError())
state.board = mock_board
with patch("python_pkg.lichess_bot.main._write_pgn_to_log"):
_finalize_game(state, meta) # Should not raise
def test_finalize_game_analysis_error(self, tmp_path: Path) -> None:
"""Test finalize game with analysis error."""
log_path = tmp_path / "game.log"
log_path.write_text("header\n")
state = GameState(log_path=log_path)
meta = GameMeta(game_id="game1", bot_version=1)
with (
patch("python_pkg.lichess_bot.main._write_pgn_to_log"),
patch(
"python_pkg.lichess_bot.main._run_analysis_subprocess",
side_effect=OSError("error"),
),
):
_finalize_game(state, meta) # Should not raise

View File

@ -0,0 +1,474 @@
"""Tests for lichess_bot main module: bot event loop."""
from __future__ import annotations
import os
import threading
from typing import TYPE_CHECKING, Any
from unittest.mock import MagicMock, patch
import chess
import pytest
import requests
from python_pkg.lichess_bot.main import (
BotContext,
GameMeta,
GameState,
_handle_challenge,
_handle_game,
_process_bot_event,
_process_game_events_loop,
_run_event_loop,
_run_event_loop_iteration,
_safe_event_loop_iteration,
_stream_bot_events,
main,
run_bot,
)
if TYPE_CHECKING:
from pathlib import Path
# Type aliases to make mypy happy with test event dicts
Event = dict[str, Any]
GameThreads = dict[str, threading.Thread]
class TestHandleGame:
"""Tests for _handle_game."""
def test_handle_game_success(self, tmp_path: Path) -> None:
"""Test handling a game successfully."""
api = MagicMock()
api.get_my_user_id.return_value = "mybot"
api.stream_game_events.return_value = iter(
[
{
"type": "gameFull",
"state": {"moves": "", "status": "started"},
"white": {"id": "mybot"},
"black": {"id": "opp"},
},
{"type": "gameState", "moves": "e2e4", "status": "mate"},
]
)
engine = MagicMock()
engine.max_time_sec = 5.0
engine.choose_move_with_explanation.return_value = (None, "no moves")
ctx = BotContext(api=api, engine=engine, bot_version=1)
with (
patch("python_pkg.lichess_bot.main.Path.cwd", return_value=tmp_path),
patch(
"python_pkg.lichess_bot.main._run_analysis_subprocess",
return_value=None,
),
):
_handle_game("game1", ctx, None)
def test_handle_game_request_error(self, tmp_path: Path) -> None:
"""Test handling a game with request error."""
api = MagicMock()
api.stream_game_events.side_effect = requests.RequestException("error")
ctx = BotContext(api=api, engine=MagicMock(), bot_version=1)
with (
patch("python_pkg.lichess_bot.main.Path.cwd", return_value=tmp_path),
patch(
"python_pkg.lichess_bot.main._run_analysis_subprocess",
return_value=None,
),
):
_handle_game("game1", ctx, None) # Should not raise
def test_handle_game_skips_chat_events(self, tmp_path: Path) -> None:
"""Test handling a game skips chat events."""
api = MagicMock()
api.stream_game_events.return_value = iter(
[
{"type": "chatLine", "text": "hello"},
{"type": "opponentGone", "gone": True},
]
)
ctx = BotContext(api=api, engine=MagicMock(), bot_version=1)
with (
patch("python_pkg.lichess_bot.main.Path.cwd", return_value=tmp_path),
patch(
"python_pkg.lichess_bot.main._run_analysis_subprocess",
return_value=None,
),
):
_handle_game("game1", ctx, None)
class TestProcessGameEventsLoop:
"""Tests for _process_game_events_loop."""
def test_empty_events_iterator(self) -> None:
"""Test processing empty events iterator."""
api = MagicMock()
ctx = BotContext(api=api, engine=MagicMock(), bot_version=1)
state = GameState(color="white")
meta = GameMeta(game_id="game1", bot_version=1)
empty_iter: list[Event] = []
# Should complete without error when iterator is empty
_process_game_events_loop(iter(empty_iter), ctx, state, meta)
def test_processes_all_events(self) -> None:
"""Test that all events are processed until break condition."""
api = MagicMock()
engine = MagicMock()
engine.max_time_sec = 5.0
engine.choose_move_with_explanation.return_value = (None, "no moves")
ctx = BotContext(api=api, engine=engine, bot_version=1)
state = GameState(color="white")
meta = GameMeta(game_id="game1", bot_version=1)
events: list[Event] = [
{"type": "chatLine", "text": "hello"}, # skipped
{"type": "gameState", "moves": "e2e4", "status": "resign"}, # game end
]
_process_game_events_loop(iter(events), ctx, state, meta)
def test_processes_multiple_game_events(self) -> None:
"""Test processing multiple game events that continue the game."""
api = MagicMock()
engine = MagicMock()
engine.max_time_sec = 5.0
engine.choose_move_with_explanation.return_value = (
chess.Move.from_uci("e2e4"),
"e4",
)
api.make_move.return_value = None
ctx = BotContext(api=api, engine=engine, bot_version=1)
state = GameState(color="white")
state.board = chess.Board()
meta = GameMeta(game_id="game1", bot_version=1)
events: list[Event] = [
# First event - game state, game continues
{"type": "gameState", "moves": "", "status": "started"},
# Second event - opponent moves, game continues
{"type": "gameState", "moves": "e2e4 e7e5", "status": "started"},
# Third event - game ends
{"type": "gameState", "moves": "e2e4 e7e5", "status": "mate"},
]
_process_game_events_loop(iter(events), ctx, state, meta)
class TestRunEventLoop:
"""Tests for _run_event_loop."""
def test_run_event_loop_zero_iterations(self) -> None:
"""Test running event loop with zero iterations."""
api = MagicMock()
ctx = BotContext(api=api, engine=MagicMock(), bot_version=1)
game_threads: GameThreads = {}
# Should complete immediately with 0 iterations
_run_event_loop(ctx, game_threads, 0, 0)
def test_run_event_loop_limited_iterations(self) -> None:
"""Test running event loop with limited iterations."""
api = MagicMock()
api.stream_bot_events.return_value = iter([])
ctx = BotContext(api=api, engine=MagicMock(), bot_version=1)
game_threads: GameThreads = {}
with patch(
"python_pkg.lichess_bot.main._safe_event_loop_iteration", return_value=0
) as mock_iter:
_run_event_loop(ctx, game_threads, 0, 3)
assert mock_iter.call_count == 3
def test_run_event_loop_none_iterations_needs_interrupt(self) -> None:
"""Test that None iterations runs until interrupted."""
api = MagicMock()
ctx = BotContext(api=api, engine=MagicMock(), bot_version=1)
game_threads: GameThreads = {}
call_count = 0
def stop_after_calls(*_args: object, **_kwargs: object) -> int:
nonlocal call_count
call_count += 1
if call_count >= 5:
raise KeyboardInterrupt
return 0
with (
patch(
"python_pkg.lichess_bot.main._safe_event_loop_iteration",
side_effect=stop_after_calls,
),
pytest.raises(KeyboardInterrupt),
):
_run_event_loop(ctx, game_threads, 0, None)
assert call_count == 5
class TestHandleChallenge:
"""Tests for _handle_challenge."""
def test_accept_standard_blitz(self) -> None:
"""Test accepting standard blitz challenge."""
api = MagicMock()
challenge: Event = {
"id": "ch1",
"variant": {"key": "standard"},
"speed": "blitz",
}
_handle_challenge(challenge, api, decline_correspondence=False)
api.accept_challenge.assert_called_once_with("ch1")
def test_decline_variant(self) -> None:
"""Test declining non-standard variant."""
api = MagicMock()
challenge: Event = {
"id": "ch1",
"variant": {"key": "chess960"},
"speed": "blitz",
}
_handle_challenge(challenge, api, decline_correspondence=False)
api.decline_challenge.assert_called_once()
def test_decline_correspondence(self) -> None:
"""Test declining correspondence when flag set."""
api = MagicMock()
challenge: Event = {
"id": "ch1",
"variant": {"key": "standard"},
"speed": "correspondence",
}
_handle_challenge(challenge, api, decline_correspondence=True)
api.decline_challenge.assert_called_once()
def test_accept_correspondence_when_allowed(self) -> None:
"""Test accepting correspondence when flag not set."""
api = MagicMock()
challenge: Event = {
"id": "ch1",
"variant": {"key": "standard"},
"speed": "correspondence",
}
_handle_challenge(challenge, api, decline_correspondence=False)
api.decline_challenge.assert_called_once() # Still declined due to perf_ok
def test_invalid_variant_data(self) -> None:
"""Test handling invalid variant data."""
api = MagicMock()
challenge: Event = {
"id": "ch1",
"variant": "invalid",
"speed": "blitz",
}
_handle_challenge(challenge, api, decline_correspondence=False)
api.accept_challenge.assert_called_once()
class TestProcessBotEvent:
"""Tests for _process_bot_event."""
def test_process_challenge_event(self) -> None:
"""Test processing challenge event."""
api = MagicMock()
ctx = BotContext(api=api, engine=MagicMock(), bot_version=1)
game_threads: GameThreads = {}
event: Event = {
"type": "challenge",
"challenge": {
"id": "ch1",
"variant": {"key": "standard"},
"speed": "blitz",
},
}
_process_bot_event(event, ctx, game_threads)
api.accept_challenge.assert_called_once()
def test_process_game_start_event(self) -> None:
"""Test processing gameStart event."""
api = MagicMock()
ctx = BotContext(api=api, engine=MagicMock(), bot_version=1)
game_threads: GameThreads = {}
event: Event = {"type": "gameStart", "game": {"id": "game1"}}
with patch("python_pkg.lichess_bot.main.threading.Thread") as mock_thread_class:
mock_thread = MagicMock()
mock_thread_class.return_value = mock_thread
_process_bot_event(event, ctx, game_threads)
assert "game1" in game_threads
mock_thread.start.assert_called_once()
def test_process_game_start_existing_thread(self) -> None:
"""Test processing gameStart with existing alive thread."""
api = MagicMock()
ctx = BotContext(api=api, engine=MagicMock(), bot_version=1)
mock_thread = MagicMock(spec=threading.Thread)
mock_thread.is_alive.return_value = True
game_threads: GameThreads = {"game1": mock_thread}
event: Event = {"type": "gameStart", "game": {"id": "game1"}}
_process_bot_event(event, ctx, game_threads)
# Should not create new thread
assert game_threads["game1"] is mock_thread
def test_process_game_finish_event(self) -> None:
"""Test processing gameFinish event."""
api = MagicMock()
ctx = BotContext(api=api, engine=MagicMock(), bot_version=1)
game_threads: GameThreads = {}
event: Event = {"type": "gameFinish", "game": {"id": "game1"}}
with patch("python_pkg.lichess_bot.main._logger") as mock_logger:
_process_bot_event(event, ctx, game_threads)
mock_logger.info.assert_called()
def test_process_game_finish_invalid_data(self) -> None:
"""Test processing gameFinish event with non-dict game data."""
api = MagicMock()
ctx = BotContext(api=api, engine=MagicMock(), bot_version=1)
game_threads: GameThreads = {}
event: Event = {"type": "gameFinish", "game": "not_a_dict"}
with patch("python_pkg.lichess_bot.main._logger") as mock_logger:
_process_bot_event(event, ctx, game_threads)
# Should not log info since game data is invalid
mock_logger.info.assert_not_called()
def test_process_unknown_event(self) -> None:
"""Test processing unknown event."""
api = MagicMock()
ctx = BotContext(api=api, engine=MagicMock(), bot_version=1)
game_threads: GameThreads = {}
event: Event = {"type": "unknown", "data": "test"}
with patch("python_pkg.lichess_bot.main._logger") as mock_logger:
_process_bot_event(event, ctx, game_threads)
mock_logger.debug.assert_called()
def test_process_challenge_invalid_data(self) -> None:
"""Test processing challenge with invalid data."""
api = MagicMock()
ctx = BotContext(api=api, engine=MagicMock(), bot_version=1)
game_threads: GameThreads = {}
event: Event = {"type": "challenge", "challenge": "invalid"}
_process_bot_event(event, ctx, game_threads)
api.accept_challenge.assert_not_called()
def test_process_game_start_invalid_data(self) -> None:
"""Test processing gameStart with invalid data."""
api = MagicMock()
ctx = BotContext(api=api, engine=MagicMock(), bot_version=1)
game_threads: GameThreads = {}
event: Event = {"type": "gameStart", "game": "invalid"}
_process_bot_event(event, ctx, game_threads)
assert len(game_threads) == 0
class TestStreamBotEvents:
"""Tests for _stream_bot_events."""
def test_stream_bot_events(self) -> None:
"""Test streaming bot events."""
api = MagicMock()
api.stream_events.return_value = iter([{"type": "test"}])
ctx = BotContext(api=api, engine=MagicMock(), bot_version=1)
events = list(_stream_bot_events(ctx))
assert len(events) == 1
class TestRunEventLoopIteration:
"""Tests for _run_event_loop_iteration."""
def test_run_event_loop_iteration(self) -> None:
"""Test running event loop iteration."""
api = MagicMock()
api.stream_events.return_value = iter([{"type": "unknown"}])
ctx = BotContext(api=api, engine=MagicMock(), bot_version=1)
game_threads: GameThreads = {}
result = _run_event_loop_iteration(ctx, game_threads)
assert result == 0
class TestSafeEventLoopIteration:
"""Tests for _safe_event_loop_iteration."""
def test_safe_event_loop_iteration_success(self) -> None:
"""Test safe event loop iteration success."""
api = MagicMock()
api.stream_events.return_value = iter([])
ctx = BotContext(api=api, engine=MagicMock(), bot_version=1)
game_threads: GameThreads = {}
result = _safe_event_loop_iteration(ctx, game_threads, 0)
assert result == 0
def test_safe_event_loop_iteration_error(self) -> None:
"""Test safe event loop iteration with error."""
api = MagicMock()
api.stream_events.side_effect = requests.RequestException("error")
ctx = BotContext(api=api, engine=MagicMock(), bot_version=1)
game_threads: GameThreads = {}
with patch("python_pkg.lichess_bot.main.backoff_sleep", return_value=5):
result = _safe_event_loop_iteration(ctx, game_threads, 2)
assert result == 5
class TestRunBot:
"""Tests for run_bot."""
def test_run_bot_no_token(self) -> None:
"""Test run_bot without token raises error."""
with (
patch.dict(os.environ, {}, clear=True),
pytest.raises(RuntimeError, match="LICHESS_TOKEN"),
):
run_bot()
def test_run_bot_with_token(self) -> None:
"""Test run_bot with token starts event loop."""
class _StopLoopError(Exception):
"""Custom exception to stop the loop."""
def stop_loop(*_args: object, **_kwargs: object) -> None:
raise _StopLoopError
with (
patch.dict(os.environ, {"LICHESS_TOKEN": "test_token"}),
patch(
"python_pkg.lichess_bot.main.get_and_increment_version",
return_value=1,
),
patch("python_pkg.lichess_bot.main.LichessAPI"),
patch("python_pkg.lichess_bot.main.RandomEngine"),
patch(
"python_pkg.lichess_bot.main._safe_event_loop_iteration",
side_effect=stop_loop,
),
pytest.raises(_StopLoopError),
):
run_bot("DEBUG", decline_correspondence=True)
class TestMain:
"""Tests for main function."""
def test_main_parses_args(self) -> None:
"""Test main parses command line arguments."""
class _StopExecutionError(Exception):
"""Custom exception to stop execution."""
with (
patch(
"sys.argv",
["main.py", "--log-level", "DEBUG", "--decline-correspondence"],
),
patch(
"python_pkg.lichess_bot.main.run_bot", side_effect=_StopExecutionError
) as mock_run_bot,
pytest.raises(_StopExecutionError),
):
main()
mock_run_bot.assert_called_once_with("DEBUG", decline_correspondence=True)

View File

@ -0,0 +1,403 @@
"""Tests for lichess_bot main module: game state helpers."""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from unittest.mock import MagicMock, patch
import chess
import requests
from python_pkg.lichess_bot.main import (
BotContext,
GameMeta,
GameState,
_apply_move_to_board,
_attempt_move,
_calculate_time_budget,
_extract_game_full_data,
_extract_game_state_data,
_extract_player_info,
_handle_move_if_needed,
_init_game_log,
_is_my_turn,
_log_move_to_file,
_rebuild_board_from_moves,
_update_clocks_from_state,
)
if TYPE_CHECKING:
from pathlib import Path
# Type alias to make mypy happy with test event dicts
Event = dict[str, Any]
class TestApplyMoveToBoard:
"""Tests for _apply_move_to_board."""
def test_apply_valid_move(self) -> None:
"""Test applying a valid move."""
board = chess.Board()
_apply_move_to_board(board, "e2e4", "game1")
assert board.fen() != chess.STARTING_FEN
def test_apply_invalid_move(self) -> None:
"""Test applying an invalid move logs debug."""
board = chess.Board()
with patch("python_pkg.lichess_bot.main._logger") as mock_logger:
_apply_move_to_board(board, "invalid", "game1")
mock_logger.debug.assert_called_once()
class TestInitGameLog:
"""Tests for _init_game_log."""
def test_init_game_log_success(self, tmp_path: Path) -> None:
"""Test successful log initialization."""
with patch("python_pkg.lichess_bot.main.Path.cwd", return_value=tmp_path):
result = _init_game_log("game123", 42)
assert result is not None
assert result.exists()
content = result.read_text()
assert "game game123 started" in content
assert "bot_version v42" in content
def test_init_game_log_oserror(self) -> None:
"""Test log initialization with OSError."""
with patch("python_pkg.lichess_bot.main.Path.cwd") as mock_cwd:
mock_path = MagicMock()
mock_path.__truediv__ = MagicMock(return_value=mock_path)
mock_path.open.side_effect = OSError("Permission denied")
mock_cwd.return_value = mock_path
result = _init_game_log("game123", 42)
assert result is None
class TestUpdateClocksFromState:
"""Tests for _update_clocks_from_state."""
def test_update_clocks_white(self) -> None:
"""Test clock update when playing as white."""
state = GameState(color="white")
state_data: Event = {"wtime": 60000, "btime": 55000, "winc": 1000}
_update_clocks_from_state(state_data, state)
assert state.my_ms == 60000
assert state.opp_ms == 55000
assert state.inc_ms == 1000
def test_update_clocks_black(self) -> None:
"""Test clock update when playing as black."""
state = GameState(color="black")
state_data: Event = {"wtime": 60000, "btime": 55000, "binc": 2000}
_update_clocks_from_state(state_data, state)
assert state.my_ms == 55000
assert state.opp_ms == 60000
assert state.inc_ms == 2000
def test_update_clocks_float_values(self) -> None:
"""Test clock update with float values."""
state = GameState(color="white")
state_data: Event = {"wtime": 60000.5, "btime": 55000.5}
_update_clocks_from_state(state_data, state)
assert state.my_ms == 60000
assert state.opp_ms == 55000
def test_update_clocks_none_values(self) -> None:
"""Test clock update with None values."""
state = GameState(color="white")
state_data: Event = {"wtime": None, "btime": None}
_update_clocks_from_state(state_data, state)
assert state.my_ms is None
assert state.opp_ms is None
class TestExtractPlayerInfo:
"""Tests for _extract_player_info."""
def test_extract_player_info_white(self) -> None:
"""Test extracting player info when bot is white."""
api = MagicMock()
api.get_my_user_id.return_value = "mybot"
state = GameState()
meta = GameMeta(game_id="game1", bot_version=1)
event: Event = {
"white": {"id": "mybot", "name": "MyBot"},
"black": {"id": "opp", "name": "Opponent"},
}
_extract_player_info(event, state, meta, api)
assert state.color == "white"
assert meta.white_name == "MyBot"
assert meta.black_name == "Opponent"
def test_extract_player_info_black(self) -> None:
"""Test extracting player info when bot is black."""
api = MagicMock()
api.get_my_user_id.return_value = "mybot"
state = GameState()
meta = GameMeta(game_id="game1", bot_version=1)
event: Event = {
"white": {"id": "opp", "name": "Opponent"},
"black": {"id": "mybot", "name": "MyBot"},
}
_extract_player_info(event, state, meta, api)
assert state.color == "black"
def test_extract_player_info_invalid_data(self) -> None:
"""Test extracting player info with invalid data."""
api = MagicMock()
state = GameState()
meta = GameMeta(game_id="game1", bot_version=1)
event: Event = {"white": "invalid", "black": "invalid"}
_extract_player_info(event, state, meta, api)
assert state.color is None
def test_extract_player_info_missing_name(self) -> None:
"""Test extracting player info with missing name uses id."""
api = MagicMock()
api.get_my_user_id.return_value = "mybot"
state = GameState()
meta = GameMeta(game_id="game1", bot_version=1)
event: Event = {
"white": {"id": "mybot"},
"black": {"id": "opponent"},
}
_extract_player_info(event, state, meta, api)
assert meta.white_name == "mybot"
assert meta.black_name == "opponent"
class TestExtractGameFullData:
"""Tests for _extract_game_full_data."""
def test_extract_game_full_data(self) -> None:
"""Test extracting gameFull data."""
api = MagicMock()
api.get_my_user_id.return_value = "mybot"
state = GameState()
meta = GameMeta(game_id="game1", bot_version=1)
event: Event = {
"state": {"moves": "e2e4 e7e5", "status": "started", "wtime": 60000},
"white": {"id": "mybot"},
"black": {"id": "opp"},
"createdAt": 1609459200000, # 2021-01-01
}
moves, status = _extract_game_full_data(event, state, meta, api)
assert moves == "e2e4 e7e5"
assert status == "started"
assert meta.site_url == "https://lichess.org/game1"
assert meta.date_iso == "2021.01.01"
def test_extract_game_full_data_invalid_state(self) -> None:
"""Test extracting gameFull data with invalid state."""
api = MagicMock()
state = GameState()
meta = GameMeta(game_id="game1", bot_version=1)
event: Event = {"state": "invalid"}
moves, status = _extract_game_full_data(event, state, meta, api)
assert moves == ""
assert status is None
class TestExtractGameStateData:
"""Tests for _extract_game_state_data."""
def test_extract_game_state_as_white(self) -> None:
"""Test extracting gameState data as white."""
state = GameState(color="white", my_ms=60000)
event: Event = {
"moves": "e2e4",
"status": "started",
"wtime": 59000,
"btime": 60000,
}
moves, status = _extract_game_state_data(event, state)
assert moves == "e2e4"
assert status == "started"
assert state.my_ms == 59000
assert state.opp_ms == 60000
def test_extract_game_state_as_black(self) -> None:
"""Test extracting gameState data as black."""
state = GameState(color="black")
event: Event = {
"moves": "e2e4 e7e5",
"wtime": 60000,
"btime": 59000,
"binc": 1000,
}
moves, __status = _extract_game_state_data(event, state)
assert moves == "e2e4 e7e5"
assert state.my_ms == 59000
assert state.opp_ms == 60000
assert state.inc_ms == 1000
class TestCalculateTimeBudget:
"""Tests for _calculate_time_budget."""
def test_calculate_time_budget_normal(self) -> None:
"""Test time budget calculation."""
state = GameState(my_ms=60000, inc_ms=1000)
board = chess.Board()
budget = _calculate_time_budget(state, board, 10.0)
assert 0.05 <= budget <= 10.0
def test_calculate_time_budget_low_time(self) -> None:
"""Test time budget with low time."""
state = GameState(my_ms=1000, inc_ms=0)
board = chess.Board()
budget = _calculate_time_budget(state, board, 10.0)
assert budget >= 0.05
class TestLogMoveToFile:
"""Tests for _log_move_to_file."""
def test_log_move_to_file(self, tmp_path: Path) -> None:
"""Test logging a move to file."""
log_path = tmp_path / "game.log"
log_path.write_text("header\n")
move = chess.Move.from_uci("e2e4")
_log_move_to_file(log_path, 1, move, "best move")
content = log_path.read_text()
assert "ply 1: e2e4" in content
assert "best move" in content
def test_log_move_to_file_none_path(self) -> None:
"""Test logging with None path does nothing."""
move = chess.Move.from_uci("e2e4")
_log_move_to_file(None, 1, move, "reason") # Should not raise
class TestAttemptMove:
"""Tests for _attempt_move."""
def test_attempt_move_success(self) -> None:
"""Test successful move attempt."""
api = MagicMock()
engine = MagicMock()
engine.max_time_sec = 5.0
engine.choose_move_with_explanation.return_value = (
chess.Move.from_uci("e2e4"),
"opening",
)
ctx = BotContext(api=api, engine=engine, bot_version=1)
state = GameState(my_ms=60000)
meta = GameMeta(game_id="game1", bot_version=1)
board = chess.Board()
result = _attempt_move(ctx, state, meta, board)
assert result is True
api.make_move.assert_called_once()
def test_attempt_move_no_moves(self) -> None:
"""Test move attempt with no legal moves."""
api = MagicMock()
engine = MagicMock()
engine.max_time_sec = 5.0
engine.choose_move_with_explanation.return_value = (None, "no moves")
ctx = BotContext(api=api, engine=engine, bot_version=1)
state = GameState()
meta = GameMeta(game_id="game1", bot_version=1)
board = chess.Board()
result = _attempt_move(ctx, state, meta, board)
assert result is False
def test_attempt_move_illegal(self) -> None:
"""Test move attempt with illegal move."""
api = MagicMock()
engine = MagicMock()
engine.max_time_sec = 5.0
# Return a move that's not legal (e.g., random square move)
engine.choose_move_with_explanation.return_value = (
chess.Move.from_uci("a1a8"),
"bad",
)
ctx = BotContext(api=api, engine=engine, bot_version=1)
state = GameState(my_ms=60000)
meta = GameMeta(game_id="game1", bot_version=1)
board = chess.Board()
result = _attempt_move(ctx, state, meta, board)
assert result is True
api.make_move.assert_not_called()
def test_attempt_move_request_error(self) -> None:
"""Test move attempt with request error."""
api = MagicMock()
api.make_move.side_effect = requests.RequestException("Network error")
engine = MagicMock()
engine.max_time_sec = 5.0
engine.choose_move_with_explanation.return_value = (
chess.Move.from_uci("e2e4"),
"opening",
)
ctx = BotContext(api=api, engine=engine, bot_version=1)
state = GameState(my_ms=60000)
meta = GameMeta(game_id="game1", bot_version=1)
board = chess.Board()
result = _attempt_move(ctx, state, meta, board)
assert result is True # Still returns True
class TestIsMyTurn:
"""Tests for _is_my_turn."""
def test_is_my_turn_white_to_move(self) -> None:
"""Test checking turn when white to move."""
board = chess.Board() # White to move
assert _is_my_turn(board, "white") is True
assert _is_my_turn(board, "black") is False
def test_is_my_turn_black_to_move(self) -> None:
"""Test checking turn when black to move."""
board = chess.Board()
board.push_uci("e2e4") # Black to move
assert _is_my_turn(board, "white") is False
assert _is_my_turn(board, "black") is True
class TestRebuildBoardFromMoves:
"""Tests for _rebuild_board_from_moves."""
def test_rebuild_board_from_moves(self) -> None:
"""Test rebuilding board from moves list."""
moves_list = ["e2e4", "e7e5", "g1f3"]
board = _rebuild_board_from_moves(moves_list, "game1")
assert len(board.move_stack) == 3
class TestHandleMoveIfNeeded:
"""Tests for _handle_move_if_needed."""
def test_handle_move_game_state_my_turn(self) -> None:
"""Test handling move on gameState when my turn."""
api = MagicMock()
engine = MagicMock()
engine.max_time_sec = 5.0
engine.choose_move_with_explanation.return_value = (
chess.Move.from_uci("e2e4"),
"opening",
)
ctx = BotContext(api=api, engine=engine, bot_version=1)
state = GameState(color="white", my_ms=60000, board=chess.Board())
meta = GameMeta(game_id="game1", bot_version=1)
result = _handle_move_if_needed(ctx, state, meta, "gameState", 0)
assert result is True
def test_handle_move_game_full_with_moves(self) -> None:
"""Test handling move on gameFull with existing moves (opponent's turn)."""
api = MagicMock()
engine = MagicMock()
ctx = BotContext(api=api, engine=engine, bot_version=1)
state = GameState(color="white", my_ms=60000, board=chess.Board())
meta = GameMeta(game_id="game1", bot_version=1)
# gameFull with moves - don't move
result = _handle_move_if_needed(ctx, state, meta, "gameFull", 1)
assert result is True
engine.choose_move_with_explanation.assert_not_called()

View File

@ -0,0 +1,445 @@
"""Classical segmentation methods: concept, thresholding, region growing, watershed."""
from __future__ import annotations
from moviepy import (
CompositeVideoClip,
VideoClip,
)
from moviepy.video.fx import FadeIn, FadeOut
import numpy as np
from python_pkg.praca_magisterska_video._q23_helpers import (
BG_COLOR,
FONT_B,
FONT_R,
FPS,
STEP_DUR,
H,
W,
_tc,
)
# ── Segmentation concept ─────────────────────────────────────────
def _segmentation_concept() -> list[CompositeVideoClip]:
"""Show what segmentation is: pixel-level labeling."""
slides = []
# Synthetic image: grid of colored pixels
def make_image_frame(_t: float) -> np.ndarray:
frame = np.zeros((H, W, 3), dtype=np.uint8)
frame[:] = BG_COLOR
# Draw a small "image" grid
grid_x, grid_y = 100, 150
cell = 40
# Sky (top rows)
colors_map = [
[(135, 206, 235)] * 8, # sky
[(135, 206, 235)] * 5 + [(34, 139, 34)] * 3, # sky + tree
[(34, 139, 34)] * 3
+ [(128, 128, 128)] * 3
+ [(34, 139, 34)] * 2, # tree+road+tree
[(128, 128, 128)] * 3
+ [(200, 50, 50)] * 2
+ [(128, 128, 128)] * 3, # road+car+road
]
labels_map = [
["niebo"] * 8,
["niebo"] * 5 + ["drzewo"] * 3,
["drzewo"] * 3 + ["droga"] * 3 + ["drzewo"] * 2,
["droga"] * 3 + ["samochód"] * 2 + ["droga"] * 3,
]
label_colors = {
"niebo": (100, 180, 255),
"drzewo": (50, 200, 50),
"droga": (180, 180, 180),
"samochód": (255, 80, 80),
}
for r, row in enumerate(colors_map):
for c, col in enumerate(row):
y = grid_y + r * cell
x = grid_x + c * cell
frame[y : y + cell - 2, x : x + cell - 2] = col
# Draw segmentation map on the right
seg_x = 600
for r, row in enumerate(labels_map):
for c, lab in enumerate(row):
y = grid_y + r * cell
x = seg_x + c * cell
frame[y : y + cell - 2, x : x + cell - 2] = label_colors[lab]
return frame
image_clip = VideoClip(make_image_frame, duration=STEP_DUR).with_fps(FPS)
labels_text = [
("Obraz wejściowy", 22, "white", FONT_B, (170, 100)),
("Mapa segmentacji", 22, "white", FONT_B, (660, 100)),
("", 50, "#FFE082", FONT_B, (450, 250)),
("Każdy piksel → etykieta klasy", 20, "#B0BEC5", FONT_R, (100, 420)),
("niebo | drzewo | droga | samochód", 18, "#90CAF9", FONT_R, (600, 420)),
("Segmentacja = klasyfikacja per-piksel", 24, "#FFE082", FONT_B, (100, 500)),
(
"Semantic: klasy bez instancji | Instance: "
"rozróżnia obiekty | Panoptic: oba",
16,
"#78909C",
FONT_R,
(100, 560),
),
]
clips: list[VideoClip] = [image_clip]
for text, fs, color, font, pos in labels_text:
tc = (
_tc(text=text, font_size=fs, color=color, font=font)
.with_duration(STEP_DUR)
.with_position(pos)
)
clips.append(tc)
slides.append(
CompositeVideoClip(clips, size=(W, H)).with_effects([FadeIn(0.3), FadeOut(0.3)])
)
return slides
# ── Thresholding / Otsu ───────────────────────────────────────────
def _thresholding_demo() -> list[CompositeVideoClip]:
"""Animate thresholding and Otsu concept."""
slides = []
# Show histogram & threshold
def make_threshold_frame(t: float) -> np.ndarray:
frame = np.zeros((H, W, 3), dtype=np.uint8)
frame[:] = BG_COLOR
# Draw bimodal histogram bars
bar_start_x = 80
bar_y = 500
bar_w = 4
for i in range(256):
# Bimodal: peaks at 60 and 190
h1 = 200 * np.exp(-((i - 60) ** 2) / (2 * 20**2))
h2 = 150 * np.exp(-((i - 190) ** 2) / (2 * 25**2))
bar_h = int(h1 + h2)
x = bar_start_x + i * bar_w
if x + bar_w < W:
frame[bar_y - bar_h : bar_y, x : x + bar_w - 1] = (150, 150, 170)
# Animated threshold line
threshold = int(60 + (190 - 60) * min(t / (STEP_DUR * 0.7), 1.0))
tx = bar_start_x + threshold * bar_w
if tx < W:
frame[bar_y - 250 : bar_y + 10, tx : tx + 3] = (255, 80, 80)
# Color the two sides
for i in range(threshold):
x = bar_start_x + i * bar_w
h1 = 200 * np.exp(-((i - 60) ** 2) / (2 * 20**2))
h2 = 150 * np.exp(-((i - 190) ** 2) / (2 * 25**2))
bar_h = int(h1 + h2)
if x + bar_w < W and bar_h > 0:
frame[bar_y - bar_h : bar_y, x : x + bar_w - 1] = (70, 130, 200)
for i in range(threshold, 256):
x = bar_start_x + i * bar_w
h1 = 200 * np.exp(-((i - 60) ** 2) / (2 * 20**2))
h2 = 150 * np.exp(-((i - 190) ** 2) / (2 * 25**2))
bar_h = int(h1 + h2)
if x + bar_w < W and bar_h > 0:
frame[bar_y - bar_h : bar_y, x : x + bar_w - 1] = (200, 100, 80)
return frame
hist_clip = VideoClip(make_threshold_frame, duration=STEP_DUR).with_fps(FPS)
text_clips: list[VideoClip] = [hist_clip]
labels = [
("Progowanie (Thresholding) z metodą Otsu", 28, "#FFE082", FONT_B, (80, 30)),
(
"Histogram jasności pikseli — dwumodalny (bimodal)",
20,
"#B0BEC5",
FONT_R,
(80, 80),
),
("Garb 1: piksele obiektu (ciemne ~60)", 16, "#64B5F6", FONT_R, (80, 120)),
("Garb 2: piksele tła (jasne ~190)", 16, "#EF9A9A", FONT_R, (80, 150)),
(
"Próg T (czerwona linia) dzieli piksele na 2 klasy",
18,
"white",
FONT_R,
(80, 540),
),
(
"Otsu: automatycznie testuje T=0..255, minimalizuje σ² wewnątrzklasową",
16,
"#A5D6A7",
FONT_R,
(80, 580),
),
(
"Piksel ≤ T → klasa 0 (tło) | Piksel > T → klasa 1 (obiekt)",
16,
"#78909C",
FONT_R,
(80, 620),
),
]
for text, fs, color, font, pos in labels:
tc = (
_tc(text=text, font_size=fs, color=color, font=font)
.with_duration(STEP_DUR)
.with_position(pos)
)
text_clips.append(tc)
slides.append(
CompositeVideoClip(text_clips, size=(W, H)).with_effects(
[FadeIn(0.3), FadeOut(0.3)]
)
)
return slides
# ── Region Growing ────────────────────────────────────────────────
def _region_growing_demo() -> list[CompositeVideoClip]:
"""Animate region growing BFS from a seed pixel."""
slides = []
grid_size = 10
cell_size = 40
rng = np.random.default_rng(42)
# Create a simple grid: dark region (30-80) and bright region (160-220)
grid = np.zeros((grid_size, grid_size), dtype=np.uint8)
grid[:] = 60 # dark background
grid[2:7, 3:8] = 180 # bright rectangle
# Add some noise
noise = rng.integers(-15, 15, (grid_size, grid_size))
grid = np.clip(grid.astype(int) + noise, 0, 255).astype(np.uint8)
# BFS steps from seed (4, 5)
seed = (4, 5)
threshold_val = 50
visited_order: list[tuple[int, int]] = []
queue = [seed]
visited_set = {seed}
while queue:
r, c = queue.pop(0)
visited_order.append((r, c))
for dr, dc in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
nr, nc = r + dr, c + dc
if (
0 <= nr < grid_size
and 0 <= nc < grid_size
and (nr, nc) not in visited_set
) and abs(int(grid[nr, nc]) - int(grid[seed])) < threshold_val:
visited_set.add((nr, nc))
queue.append((nr, nc))
def make_region_frame(t: float) -> np.ndarray:
frame = np.zeros((H, W, 3), dtype=np.uint8)
frame[:] = BG_COLOR
ox, oy = 100, 180
# How many cells to show as visited
progress = min(t / (STEP_DUR * 0.8), 1.0)
n_visited = int(progress * len(visited_order))
for r in range(grid_size):
for c in range(grid_size):
x = ox + c * cell_size
y = oy + r * cell_size
val = grid[r, c]
color = (val, val, val)
# Highlight visited
if (r, c) in visited_order[:n_visited]:
color = (80, 200, 120) # green for region
elif (r, c) == seed:
color = (255, 200, 50) # yellow seed
frame[y : y + cell_size - 2, x : x + cell_size - 2] = color
# Mark the seed with a bright border
sx = ox + seed[1] * cell_size
sy = ox + seed[0] * cell_size + 80
frame[sy : sy + cell_size, sx : sx + 2] = (255, 200, 50)
frame[sy : sy + cell_size, sx + cell_size - 2 : sx + cell_size] = (255, 200, 50)
frame[sy : sy + 2, sx : sx + cell_size] = (255, 200, 50)
frame[sy + cell_size - 2 : sy + cell_size, sx : sx + cell_size] = (255, 200, 50)
return frame
region_clip = VideoClip(make_region_frame, duration=STEP_DUR).with_fps(FPS)
text_clips: list[VideoClip] = [region_clip]
labels = [
("Region Growing — rozrastanie regionu", 28, "#FFE082", FONT_B, (100, 30)),
("Seed (ziarno) → BFS do podobnych sąsiadów", 20, "#B0BEC5", FONT_R, (100, 80)),
(
"Żółty = seed | Zielony = region | Szary = nieodwiedzone",
16,
"#78909C",
FONT_R,
(100, 120),
),
(
"Sąsiad PODOBNY (|jasność - jasność_regionu| < próg) → dodaj do regionu",
16,
"#A5D6A7",
FONT_R,
(100, 600),
),
(
"Algorytm zatrzymuje się gdy brak podobnych sąsiadów",
16,
"#90CAF9",
FONT_R,
(100, 640),
),
(
"Mnemonik: PLAMA atramentu — rozlewa się na podobne piksele",
18,
"#EF9A9A",
FONT_R,
(100, 670),
),
]
for text, fs, color, font, pos in labels:
tc = (
_tc(text=text, font_size=fs, color=color, font=font)
.with_duration(STEP_DUR)
.with_position(pos)
)
text_clips.append(tc)
slides.append(
CompositeVideoClip(text_clips, size=(W, H)).with_effects(
[FadeIn(0.3), FadeOut(0.3)]
)
)
return slides
# ── Watershed ─────────────────────────────────────────────────────
def _watershed_demo() -> list[CompositeVideoClip]:
"""Animate watershed flooding concept."""
slides = []
def make_watershed_frame(t: float) -> np.ndarray:
frame = np.zeros((H, W, 3), dtype=np.uint8)
frame[:] = BG_COLOR
# Draw terrain profile (1D cross-section)
ox, oy = 100, 450
terrain_w = 900
terrain_points = 100
xs = np.linspace(0, 1, terrain_points)
# Two valleys with a ridge
terrain = (
120 * np.exp(-((xs - 0.25) ** 2) / 0.005)
+ 80 * np.exp(-((xs - 0.75) ** 2) / 0.008)
+ 30
)
terrain = 250 - terrain # invert for visual (valleys at bottom)
# Water level rises over time
water_level = int(160 + 80 * min(t / (STEP_DUR * 0.7), 1.0))
for i in range(terrain_points - 1):
x1 = ox + int(xs[i] * terrain_w)
x2 = ox + int(xs[i + 1] * terrain_w)
y1 = oy - int(terrain[i])
y2 = oy - int(terrain[i + 1])
# Fill terrain
for x in range(x1, min(x2 + 1, W)):
top = min(y1, y2) - 5
frame[top:oy, x : x + 1] = (100, 80, 60)
# Fill water
water_y = oy - water_level
for x in range(x1, min(x2 + 1, W)):
t_y = oy - int(terrain[i])
if water_y < t_y:
# Water fills below terrain surface
fill_top = max(water_y, 0)
fill_bot = min(t_y, oy)
if fill_top < fill_bot:
frame[fill_top:fill_bot, x : x + 1] = (70, 130, 220)
# Dam marker at ridge
ridge_x = ox + int(0.5 * terrain_w)
dam_visible_threshold = 160
if water_level > dam_visible_threshold:
frame[oy - water_level : oy - 140, ridge_x - 2 : ridge_x + 2] = (
255,
80,
80,
)
return frame
ws_clip = VideoClip(make_watershed_frame, duration=STEP_DUR).with_fps(FPS)
text_clips: list[VideoClip] = [ws_clip]
labels = [
("Watershed — metoda zlewiska", 28, "#FFE082", FONT_B, (100, 20)),
(
"Obraz = mapa topograficzna (jasność = wysokość)",
20,
"#B0BEC5",
FONT_R,
(100, 65),
),
(
"Brązowy = teren (ciemne=doliny, jasne=szczyty)",
16,
"#8D6E63",
FONT_R,
(100, 100),
),
("Niebieski = woda zalewająca od minimów", 16, "#64B5F6", FONT_R, (100, 130)),
(
"Czerwony = TAMA (granica segmentu) — gdy woda z 2 dolin się spotka",
16,
"#EF9A9A",
FONT_R,
(100, 160),
),
(
"Problem: over-segmentation "
"(za dużo regionów). "
"Rozwiązanie: marker-controlled.",
16,
"#A5D6A7",
FONT_R,
(100, 560),
),
(
"Mnemonik: ZALEWANIE terenu — granie gór = granice segmentów",
18,
"#FFE082",
FONT_R,
(100, 600),
),
]
for text, fs, color, font, pos in labels:
tc = (
_tc(text=text, font_size=fs, color=color, font=font)
.with_duration(STEP_DUR)
.with_position(pos)
)
text_clips.append(tc)
slides.append(
CompositeVideoClip(text_clips, size=(W, H)).with_effects(
[FadeIn(0.3), FadeOut(0.3)]
)
)
return slides

View File

@ -0,0 +1,248 @@
"""DeepLab architecture animations for Q23 segmentation video."""
from __future__ import annotations
from moviepy import (
CompositeVideoClip,
VideoClip,
)
import numpy as np
from python_pkg.praca_magisterska_video._q23_helpers import (
BG_COLOR,
FONT_B,
FONT_R,
FPS,
STEP_DUR,
H,
W,
_compose_slide,
)
# ── DeepLab Architecture ─────────────────────────────────────────
def _make_dilated_frame(t: float) -> np.ndarray:
"""Render a dilated convolution comparison frame."""
frame = np.zeros((H, W, 3), dtype=np.uint8)
frame[:] = BG_COLOR
progress = min(t / (STEP_DUR * 0.7), 1.0)
cell = 36
grids = [
(
"rate=1",
60,
[
(0, 0),
(0, 1),
(0, 2),
(1, 0),
(1, 1),
(1, 2),
(2, 0),
(2, 1),
(2, 2),
],
),
(
"rate=2",
420,
[
(0, 0),
(0, 2),
(0, 4),
(2, 0),
(2, 2),
(2, 4),
(4, 0),
(4, 2),
(4, 4),
],
),
(
"rate=3",
820,
[
(0, 0),
(0, 3),
(0, 6),
(3, 0),
(3, 3),
(3, 6),
(6, 0),
(6, 3),
(6, 6),
],
),
]
for gi, (_label, gx, positions) in enumerate(grids):
if progress < gi * 0.3:
break
gy = 180
grid_size = 7
for r in range(grid_size):
for c in range(grid_size):
x = gx + c * cell
y = gy + r * cell
frame[y : y + cell - 2, x : x + cell - 2] = (35, 40, 55)
for r, c in positions:
x = gx + c * cell
y = gy + r * cell
frame[y : y + cell - 2, x : x + cell - 2] = (70, 130, 200)
frame[y : y + 2, x : x + cell - 2] = (120, 180, 255)
frame[y + cell - 4 : y + cell - 2, x : x + cell - 2] = (120, 180, 255)
return frame
def _make_aspp_frame(t: float) -> np.ndarray:
"""Render a single ASPP module animation frame."""
frame = np.zeros((H, W, 3), dtype=np.uint8)
frame[:] = BG_COLOR
progress = min(t / (STEP_DUR * 0.7), 1.0)
frame[250:330, 50:130] = (70, 130, 200)
frame[250:252, 50:130] = (120, 180, 255)
frame[328:330, 50:130] = (120, 180, 255)
branches = [
("1x1 conv", 250, (200, 170), (100, 40), (80, 200, 120)),
("rate=6", 310, (200, 250), (100, 40), (200, 160, 80)),
("rate=12", 370, (200, 330), (100, 40), (200, 120, 60)),
("rate=18", 430, (200, 410), (100, 40), (180, 100, 80)),
("GAP", 490, (200, 490), (100, 40), (160, 80, 160)),
]
n_branches = min(int(progress * 5) + 1, 5)
for i, (_lbl, _h, (bx, by), (bw, bh), color) in enumerate(branches):
if i < n_branches:
frame[by : by + bh, bx : bx + bw] = color
frame[by : by + 2, bx : bx + bw] = tuple(min(c + 50, 255) for c in color)
ay = by + bh // 2
frame[ay - 1 : ay + 2, 133:197] = (150, 150, 170)
concat_phase = 0.6
if progress > concat_phase:
frame[250:530, 380:420] = (50, 60, 80)
frame[250:252, 380:420] = (200, 200, 100)
frame[528:530, 380:420] = (200, 200, 100)
for i, (_lbl, _h, (bx, by), (bw, bh), _c) in enumerate(branches):
if i < n_branches:
ay = by + bh // 2
frame[ay - 1 : ay + 2, bx + bw + 3 : 378] = (150, 150, 170)
final_conv_phase = 0.8
if progress > final_conv_phase:
frame[350:420, 450:550] = (100, 200, 100)
frame[350:352, 450:550] = (150, 230, 150)
frame[418:420, 450:550] = (150, 230, 150)
frame[388:391, 423:448] = (150, 150, 170)
return frame
def _deeplab_demo() -> list[CompositeVideoClip]:
"""Animate DeepLab: dilated convolution + ASPP step by step."""
dur = STEP_DUR + 1
# Slide 1: Regular vs Dilated convolution
dil_clip = VideoClip(_make_dilated_frame, duration=dur).with_fps(FPS)
labels = [
("DeepLab: Atrous (Dilated) Convolution", 26, "#FFE082", FONT_B, (80, 20)),
(
"KROK 1: Zrozum dilated convolution — filtr z DZIURAMI",
18,
"#A5D6A7",
FONT_R,
(80, 60),
),
("rate=1 (zwykła)", 14, "#64B5F6", FONT_B, (60, 160)),
("RF = 3x3", 14, "#64B5F6", FONT_R, (60, 440)),
("9 wag, kontekst 3px", 12, "#78909C", FONT_R, (60, 470)),
("rate=2 (dilated)", 14, "#FFE082", FONT_B, (420, 160)),
("RF = 5x5", 14, "#FFE082", FONT_R, (420, 440)),
("9 wag, kontekst 5px!", 12, "#78909C", FONT_R, (420, 470)),
("rate=3 (dilated)", 14, "#A5D6A7", FONT_B, (820, 160)),
("RF = 7x7", 14, "#A5D6A7", FONT_R, (820, 440)),
("9 wag, kontekst 7px!", 12, "#78909C", FONT_R, (820, 470)),
(
"Niebieski = pozycja wag filtra 3x3 | Szary = pominięte (dziury)",
15,
"#B0BEC5",
FONT_R,
(80, 510),
),
(
"TE SAME 9 wag → WIĘKSZE pole widzenia "
"→ lepszy kontekst BEZ dodatkowych parametrów!",
16,
"white",
FONT_R,
(80, 550),
),
(
"Mnemonik: DZIURY w filtrze — à trous = z dziurami (fr.)",
16,
"#FFE082",
FONT_R,
(80, 600),
),
]
slides = [_compose_slide(dil_clip, labels, dur)]
# Slide 2: ASPP module step by step
aspp_clip = VideoClip(_make_aspp_frame, duration=dur).with_fps(FPS)
labels2 = [
(
"DeepLab: ASPP (Atrous Spatial Pyramid Pooling)",
24,
"#FFE082",
FONT_B,
(80, 20),
),
(
"KROK 2: Multi-scale — analizuj obraz na WIELU skalach naraz",
17,
"#A5D6A7",
FONT_R,
(80, 60),
),
("Wejście", 13, "#64B5F6", FONT_B, (55, 235)),
("Conv 1x1", 12, "white", FONT_R, (210, 178)),
("Dilated r=6", 12, "white", FONT_R, (205, 258)),
("Dilated r=12", 12, "white", FONT_R, (203, 338)),
("Dilated r=18", 12, "white", FONT_R, (203, 418)),
("GAP (global)", 12, "white", FONT_R, (205, 498)),
("Concat", 13, "#FFE082", FONT_B, (381, 537)),
("Conv", 13, "#A5D6A7", FONT_B, (470, 425)),
(
"5 gałęzi RÓWNOLEGŁYCH → różne skale kontekstu:",
16,
"#B0BEC5",
FONT_R,
(550, 170),
),
(" 1x1: kontekst punktowy (piksel)", 14, "#A5D6A7", FONT_R, (560, 210)),
(" r=6: kontekst lokalny (~13px)", 14, "#FFE082", FONT_R, (560, 245)),
(" r=12: kontekst średni (~25px)", 14, "#FFE082", FONT_R, (560, 280)),
(" r=18: kontekst szeroki (~37px)", 14, "#FFE082", FONT_R, (560, 315)),
(" GAP: kontekst GLOBALNY (cały obraz)", 14, "#CE93D8", FONT_R, (560, 350)),
("Concat → 1x1 conv → mapa segmentacji", 16, "#A5D6A7", FONT_R, (550, 400)),
(
"Efekt: sieć widzi OD piksela DO całego obrazu naraz!",
17,
"white",
FONT_R,
(80, 600),
),
(
"Mnemonik: ASPP = Piramida z DZIURAMI, patrzy na 5 skal jednocześnie",
15,
"#FFE082",
FONT_R,
(80, 645),
),
]
slides.append(_compose_slide(aspp_clip, labels2, dur))
return slides

View File

@ -0,0 +1,116 @@
"""Shared constants and helper functions for Q23 segmentation video."""
from __future__ import annotations
import logging
import os
from pathlib import Path
import numpy as np
os.environ["FFMPEG_BINARY"] = "/usr/bin/ffmpeg"
from moviepy import (
ColorClip,
CompositeVideoClip,
TextClip,
VideoClip,
)
from moviepy.video.fx import FadeIn, FadeOut
# ── Constants ─────────────────────────────────────────────────────
W, H = 1280, 720
FPS = 24
STEP_DUR = 7.0
HEADER_DUR = 4.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 / "q23_segmentation.mp4")
logging.basicConfig(level=logging.INFO)
_logger = logging.getLogger(__name__)
BG_COLOR = (15, 20, 35)
rng = np.random.default_rng(42)
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_COLOR).with_duration(duration)
t = (
_tc(
text=title,
font_size=48,
color="white",
font=FONT_B,
)
.with_duration(duration)
.with_position(("center", 260))
)
s = (
_tc(
text=subtitle,
font_size=24,
color="#90CAF9",
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 _text_slide(
lines: list[tuple[str, int, str, str, tuple[str | int, str | int]]],
duration: float = STEP_DUR,
) -> CompositeVideoClip:
"""Create a slide with multiple text elements."""
bg = ColorClip(size=(W, H), color=BG_COLOR).with_duration(duration)
clips: list[VideoClip] = [bg]
for text, font_size, color, font, pos in lines:
tc = (
_tc(
text=text,
font_size=font_size,
color=color,
font=font,
)
.with_duration(duration)
.with_position(pos)
)
clips.append(tc)
return CompositeVideoClip(clips, size=(W, H)).with_effects(
[FadeIn(0.3), FadeOut(0.3)]
)
def _compose_slide(
base_clip: VideoClip,
labels: list[tuple[str, int, str, str, tuple[int, int]]],
duration: float,
) -> CompositeVideoClip:
"""Overlay text labels on an animated base clip."""
text_clips: list[VideoClip] = [base_clip]
for text, fs, color, font, pos in labels:
tc = (
_tc(text=text, font_size=fs, color=color, font=font)
.with_duration(duration)
.with_position(pos)
)
text_clips.append(tc)
return CompositeVideoClip(text_clips, size=(W, H)).with_effects(
[FadeIn(0.3), FadeOut(0.3)]
)

View File

@ -0,0 +1,430 @@
"""Transformer segmentation and methods comparison for Q23 video."""
from __future__ import annotations
from moviepy import (
ColorClip,
CompositeVideoClip,
VideoClip,
)
from moviepy.video.fx import FadeIn, FadeOut
import numpy as np
from python_pkg.praca_magisterska_video._q23_helpers import (
BG_COLOR,
FONT_B,
FONT_R,
FPS,
STEP_DUR,
H,
W,
_compose_slide,
_tc,
_text_slide,
)
# ── Transformer Segmentation ────────────────────────────────────
def _draw_base_grid(
frame: np.ndarray,
gx: int,
gy: int,
grid_n: int,
cell: int,
) -> None:
"""Draw an empty grid of cells."""
for r in range(grid_n):
for c in range(grid_n):
x = gx + c * cell
y = gy + r * cell
frame[y : y + cell - 2, x : x + cell - 2] = (35, 40, 55)
def _draw_cnn_kernel(
frame: np.ndarray,
lx: int,
ly: int,
cell: int,
progress: float,
) -> None:
"""Highlight a 3x3 CNN kernel on the grid."""
cnn_phase = 0.2
if progress <= cnn_phase:
return
cx, cy = 2, 2
for dr in range(-1, 2):
for dc in range(-1, 2):
r, c = cy + dr, cx + dc
x = lx + c * cell
y = ly + r * cell
frame[y : y + cell - 2, x : x + cell - 2] = (70, 130, 200)
x = lx + cx * cell
y = ly + cy * cell
frame[y : y + cell - 2, x : x + cell - 2] = (120, 180, 255)
def _draw_conn_line(
frame: np.ndarray,
x0: int,
y0: int,
x1: int,
y1: int,
) -> None:
"""Draw a dashed connection line between two points."""
steps = max(abs(x1 - x0), abs(y1 - y0))
if steps <= 0:
return
for s in range(0, steps, 3):
px = x0 + int((x1 - x0) * s / steps)
py = y0 + int((y1 - y0) * s / steps)
if 0 <= px < W - 1 and 0 <= py < H - 1:
frame[py : py + 1, px : px + 1] = (200, 180, 50)
def _draw_attention_connections(
frame: np.ndarray,
origin: tuple[int, int],
grid_n: int,
cell: int,
progress: float,
) -> None:
"""Draw transformer self-attention connections on the grid."""
rx, ry = origin
transformer_phase = 0.4
if progress <= transformer_phase:
return
cx_t, cy_t = 2, 2
x0 = rx + cx_t * cell + cell // 2
y0 = ry + cy_t * cell + cell // 2
n_connections = int(progress * 36)
conn_idx = 0
for r in range(grid_n):
for c in range(grid_n):
conn_idx += 1
if conn_idx > n_connections:
break
x = rx + c * cell
y = ry + r * cell
dist = abs(r - cy_t) + abs(c - cx_t)
strength = max(30, 200 - dist * 30)
frame[y : y + cell - 2, x : x + cell - 2] = (
strength // 3,
strength // 2,
strength,
)
_draw_conn_line(frame, x0, y0, x + cell // 2, y + cell // 2)
else:
continue
break
x = rx + cx_t * cell
y = ry + cy_t * cell
frame[y : y + cell - 2, x : x + cell - 2] = (255, 200, 50)
def _make_attention_frame(t: float) -> np.ndarray:
"""Render a CNN-vs-Transformer attention comparison frame."""
frame = np.zeros((H, W, 3), dtype=np.uint8)
frame[:] = BG_COLOR
progress = min(t / (STEP_DUR * 0.7), 1.0)
cell = 40
grid_n = 6
lx, ly = 60, 200
_draw_base_grid(frame, lx, ly, grid_n, cell)
_draw_cnn_kernel(frame, lx, ly, cell, progress)
rx, ry = 680, 200
_draw_base_grid(frame, rx, ry, grid_n, cell)
_draw_attention_connections(frame, (rx, ry), grid_n, cell, progress)
return frame
def _transformer_seg_demo() -> list[CompositeVideoClip]:
"""Animate transformer-based segmentation: self-attention concept."""
dur = STEP_DUR + 1
# Slide 1: CNN local vs Transformer global
att_clip = VideoClip(_make_attention_frame, duration=dur).with_fps(FPS)
labels = [
("Transformer: Self-Attention w segmentacji", 26, "#FFE082", FONT_B, (80, 20)),
("CNN = LOKALNY kontekst", 18, "#64B5F6", FONT_B, (60, 160)),
("Transformer = GLOBALNY kontekst", 18, "#FFE082", FONT_B, (680, 160)),
("Filtr 3x3 widzi", 14, "#64B5F6", FONT_R, (60, 460)),
("TYLKO 9 sąsiadów", 14, "#64B5F6", FONT_R, (60, 485)),
("Self-attention: każdy", 14, "#FFE082", FONT_R, (680, 460)),
("piksel widzi WSZYSTKIE!", 14, "#FFE082", FONT_R, (680, 485)),
("vs", 28, "#B0BEC5", FONT_B, (450, 300)),
]
slides = [_compose_slide(att_clip, labels, dur)]
# Slide 2: Self-attention Q/K/V step by step
qkv_lines = [
("Self-Attention: Q / K / V krok po kroku", 26, "#FFE082", FONT_B, (80, 30)),
("Każdy piksel (token) tworzy 3 wektory:", 18, "#B0BEC5", FONT_R, (100, 100)),
(
" Q (Query) = 'czego szukam?' - pytanie piksela",
17,
"#64B5F6",
FONT_R,
(120, 145),
),
(
" K (Key) = 'co oferuj\u0119?' - odpowied\u017a piksela",
17,
"#A5D6A7",
FONT_R,
(120, 185),
),
(
" V (Value) = 'moja warto\u015b\u0107' - informacja do przekazania",
17,
"#FFE082",
FONT_R,
(120, 225),
),
("Algorytm attention:", 18, "#B0BEC5", FONT_R, (100, 285)),
(
" 1. Mnożenie Q x K\u1d40 → macierz NxN (kto ważny dla kogo)",
16,
"white",
FONT_R,
(120, 320),
),
(
" 2. Skalowanie: / \u221ad (stabilno\u015b\u0107 gradient\u00f3w)",
16,
"white",
FONT_R,
(120, 355),
),
(
" 3. Softmax \u2192 wagi attention (sumuj\u0105 si\u0119 do 1)",
16,
"white",
FONT_R,
(120, 390),
),
(
" 4. Mno\u017cenie wag x V \u2192 wa\u017cona suma warto\u015bci",
16,
"white",
FONT_R,
(120, 425),
),
(
"Attention(Q,K,V) = softmax(Q \u00b7 K\u1d40 / \u221ad) \u00b7 V",
20,
"#FFE082",
FONT_B,
(100, 480),
),
(
"Z\u0142o\u017cono\u015b\u0107: O(n\u00b2) pami\u0119ci \u2014 n = liczba pikseli/token\u00f3w",
16,
"#EF9A9A",
FONT_R,
(100, 535),
),
(
"Dlatego SegFormer u\u017cywa efficient attention (liniowa z\u0142o\u017cono\u015b\u0107)",
15,
"#78909C",
FONT_R,
(100, 570),
),
(
"SegFormer (2021): lightweight + hierarchiczny encoder",
16,
"#A5D6A7",
FONT_R,
(100, 610),
),
(
"Mask2Former (2022): masked attention + "
"unified (semantic+instance+panoptic)",
16,
"#CE93D8",
FONT_R,
(100, 645),
),
]
slides.append(_text_slide(qkv_lines, duration=STEP_DUR + 1))
# Slide 3: Encoder-Decoder in DL summary
summary_lines = [
(
"Podsumowanie: Encoder-Decoder w segmentacji DL",
24,
"#FFE082",
FONT_B,
(80, 30),
),
(
"Wsp\u00f3lna idea WSZYSTKICH sieci segmentacji:",
18,
"#B0BEC5",
FONT_R,
(80, 90),
),
(
"Encoder: obraz \u2192 cechy (zmniejsza rozdzielczo\u015b\u0107, wyci\u0105ga CO)",
16,
"#64B5F6",
FONT_R,
(100, 140),
),
(
"Decoder: cechy \u2192 mapa (zwi\u0119ksza rozdzielczo\u015b\u0107, odtwarza GDZIE)",
16,
"#A5D6A7",
FONT_R,
(100, 175),
),
(
"Skip: przenosi detale z encodera do decodera",
16,
"#FFE082",
FONT_R,
(100, 210),
),
("", 10, "white", FONT_R, (100, 240)),
(
"FCN (2015): Conv1x1 + skip \u2192 pierwsza end-to-end",
16,
"#64B5F6",
FONT_R,
(100, 275),
),
(
"U-Net (2015): U-shape + skip concat \u2192 segmentacja medyczna",
16,
"#A5D6A7",
FONT_R,
(100, 310),
),
(
"DeepLab (2018): dilated conv + ASPP \u2192 multi-scale kontekst",
16,
"#FFE082",
FONT_R,
(100, 345),
),
(
"SegFormer: transformer encoder (globalny kontekst)",
16,
"#CE93D8",
FONT_R,
(100, 380),
),
(
"Mask2Former: masked attention (unified, SOTA)",
16,
"#CE93D8",
FONT_R,
(100, 415),
),
("", 10, "white", FONT_R, (100, 440)),
(
"Ewolucja: wi\u0119cej kontekstu + lepsze skip connections:",
17,
"white",
FONT_R,
(80, 465),
),
(
" CNN lokal. \u2192 dilated (szersze RF) \u2192 transformer (global) \u2192 masked att.",
16,
"#B0BEC5",
FONT_R,
(80, 505),
),
(
" addition skip \u2192 concat skip \u2192 cross-attention skip",
16,
"#B0BEC5",
FONT_R,
(80, 540),
),
(
"Metryki: mIoU (standard), Dice (medycyna), Focal Loss (imbalance)",
16,
"#90CAF9",
FONT_R,
(80, 590),
),
(
"Loss: Cross-Entropy per piksel + opcjonalnie Dice/Focal",
15,
"#78909C",
FONT_R,
(80, 625),
),
]
slides.append(_text_slide(summary_lines, duration=STEP_DUR + 1))
return slides
# ── Methods comparison ────────────────────────────────────────────
def _methods_comparison() -> CompositeVideoClip:
"""Create a comparison table of all segmentation methods."""
bg = ColorClip(size=(W, H), color=BG_COLOR).with_duration(10.0)
title = (
_tc(
text="Por\u00f3wnanie metod segmentacji",
font_size=36,
color="white",
font=FONT_B,
)
.with_duration(10.0)
.with_position(("center", 20))
)
rows = [
("Metoda", "Typ", "Idea", "Mnemonik"),
(
"Thresholding",
"Klasyczna",
"piksel > T \u2192 klasa 1",
"PR\u00d3G na bramce",
),
("Otsu", "Klasyczna", "auto-pr\u00f3g, min \u03c3\u00b2", "AUTO-bramkarz"),
("Region Growing", "Klasyczna", "BFS od seeda", "PLAMA atramentu"),
("Watershed", "Klasyczna", "zalewanie minim\u00f3w", "ZALEWANIE terenu"),
(
"Mean Shift",
"Klasyczna",
"j\u0105dro \u2192 max g\u0119sto\u015bci",
"KULKI do do\u0142k\u00f3w",
),
("U-Net", "Deep Learning", "encoder-decoder + skip", "Litera U + mosty"),
("DeepLab", "Deep Learning", "dilated conv + ASPP", "DZIURY w filtrze"),
]
clips: list[VideoClip] = [bg, title]
mnemonic_col = 3
for i, row in enumerate(rows):
y_pos = 75 + i * 72
col_x = [40, 210, 340, 660]
for j, cell in enumerate(row):
fs = 16 if i > 0 else 18
color = (
"#64B5F6" if i == 0 else ("#E0E0E0" if j < mnemonic_col else "#FFE082")
)
tc = (
_tc(
text=cell,
font_size=fs,
color=color,
font=FONT_B if i == 0 else FONT_R,
)
.with_duration(10.0)
.with_position((col_x[j], y_pos))
)
clips.append(tc)
return CompositeVideoClip(clips, size=(W, H)).with_effects(
[FadeIn(0.5), FadeOut(0.5)]
)

View File

@ -0,0 +1,399 @@
"""U-Net and FCN architecture animations for Q23 segmentation video."""
from __future__ import annotations
from moviepy import (
CompositeVideoClip,
VideoClip,
)
import numpy as np
from python_pkg.praca_magisterska_video._q23_helpers import (
BG_COLOR,
FONT_B,
FONT_R,
FPS,
STEP_DUR,
H,
W,
_compose_slide,
_text_slide,
)
# ── U-Net Architecture ───────────────────────────────────────────
def _draw_unet_skips(
frame: np.ndarray,
enc_positions: list[tuple[int, int, int, int]],
n_blocks: int,
dec_x: int,
skip_threshold: int,
) -> None:
"""Draw horizontal dashed skip-connection lines."""
if n_blocks <= skip_threshold:
return
for i in range(min(n_blocks - 5, 4)):
ey = enc_positions[i][1] + enc_positions[i][3] // 2
ex_end = enc_positions[i][0] + enc_positions[i][2]
for dash_x in range(ex_end + 10, dec_x - 10, 15):
frame[ey : ey + 2, dash_x : dash_x + 8] = (255, 200, 50)
def _make_unet_frame(t: float) -> np.ndarray:
"""Render a single U-Net animation frame."""
frame = np.zeros((H, W, 3), dtype=np.uint8)
frame[:] = BG_COLOR
enc_sizes = [(80, 120), (60, 100), (45, 80), (30, 60)]
dec_sizes = list(reversed(enc_sizes))
enc_x = 150
dec_x = 850
progress = min(t / (STEP_DUR * 0.6), 1.0)
n_blocks = int(progress * 8) + 1
enc_positions: list[tuple[int, int, int, int]] = []
y_offset = 120
for i, (bw, bh) in enumerate(enc_sizes):
x = enc_x
y = y_offset + i * 130
enc_positions.append((x, y, bw, bh))
if i < n_blocks:
frame[y : y + bh, x : x + bw] = (70, 130, 200)
frame[y : y + 2, x : x + bw] = (100, 180, 255)
frame[y + bh - 2 : y + bh, x : x + bw] = (100, 180, 255)
frame[y : y + bh, x : x + 2] = (100, 180, 255)
frame[y : y + bh, x + bw - 2 : x + bw] = (100, 180, 255)
if i < len(enc_sizes) - 1:
ax = x + bw // 2
ay = y + bh + 10
frame[ay : ay + 20, ax - 1 : ax + 2] = (150, 150, 170)
bx, by = 500, y_offset + 3 * 130 + 30
encoder_count = 4
if n_blocks > encoder_count:
frame[by : by + 50, bx : bx + 25] = (200, 100, 80)
frame[by : by + 2, bx : bx + 25] = (255, 140, 100)
frame[by + 48 : by + 50, bx : bx + 25] = (255, 140, 100)
for i, (bw, bh) in enumerate(dec_sizes):
x = dec_x
y = y_offset + (3 - i) * 130
if n_blocks > 4 + i + 1:
frame[y : y + bh, x : x + bw] = (80, 200, 120)
frame[y : y + 2, x : x + bw] = (120, 230, 150)
frame[y + bh - 2 : y + bh, x : x + bw] = (120, 230, 150)
frame[y : y + bh, x : x + 2] = (120, 230, 150)
frame[y : y + bh, x + bw - 2 : x + bw] = (120, 230, 150)
if i < len(dec_sizes) - 1:
ax = x + bw // 2
ay = y - 30
frame[ay : ay + 20, ax - 1 : ax + 2] = (150, 150, 170)
skip_threshold = 5
_draw_unet_skips(frame, enc_positions, n_blocks, dec_x, skip_threshold)
return frame
def _unet_demo() -> list[CompositeVideoClip]:
"""Animate U-Net encoder-decoder architecture."""
dur = STEP_DUR + 1
unet_clip = VideoClip(_make_unet_frame, duration=dur).with_fps(FPS)
labels = [
("U-Net: Encoder-Decoder + Skip Connections", 28, "#FFE082", FONT_B, (80, 20)),
(
"Niebieski = Encoder (↓ zmniejsza rozdzielczość, wyciąga cechy)",
16,
"#64B5F6",
FONT_R,
(80, 65),
),
(
"Zielony = Decoder (↑ zwiększa rozdzielczość, odtwarza mapę)",
16,
"#A5D6A7",
FONT_R,
(80, 90),
),
(
"Żółte przerywane = Skip connections (przenoszą detale z encodera)",
16,
"#FFE082",
FONT_R,
(80, 115),
),
(
"Czerwony = Bottleneck (najgłębsza warstwa, max abstrakcja)",
16,
"#EF9A9A",
FONT_R,
(450, 570),
),
(
"Kształt U: encoder ↓ decoder ↑, mosty pośrodku",
18,
"white",
FONT_R,
(80, 640),
),
(
"Concatenation: skip łączy kanały (więcej informacji niż dodawanie)",
16,
"#78909C",
FONT_R,
(80, 670),
),
]
return [_compose_slide(unet_clip, labels, dur)]
# ── FCN Architecture ─────────────────────────────────────────────
def _draw_pipeline_blocks(
frame: np.ndarray,
blocks: list[tuple[tuple[int, int], tuple[int, int], tuple[int, int, int]]],
n_visible: int,
arrow_limit: int,
) -> None:
"""Draw coloured blocks with connecting arrows."""
for i, ((bx, by), (bw, bh), color) in enumerate(blocks):
if i < n_visible:
frame[by : by + bh, bx : bx + bw] = color
frame[by : by + 2, bx : bx + bw] = tuple(min(c + 50, 255) for c in color)
frame[by + bh - 2 : by + bh, bx : bx + bw] = tuple(
min(c + 50, 255) for c in color
)
if i < arrow_limit:
ax = bx + bw + 3
ay = by + bh // 2
frame[ay - 1 : ay + 2, ax : ax + 12] = (150, 150, 170)
def _draw_red_cross(
frame: np.ndarray,
x_start: int,
width: int,
top_y: int,
height: int,
) -> None:
"""Draw a red X across the given rectangle."""
for d in range(-2, 3):
for step in range(height):
x1 = x_start + int(step * width / height)
y1 = top_y + step + d
if 0 <= y1 < H and 0 <= x1 < W:
frame[y1, x1] = (255, 80, 80)
y2 = top_y + height - step + d
if 0 <= y2 < H and 0 <= x1 < W:
frame[y2, x1] = (255, 80, 80)
def _make_fcn_frame(t: float) -> np.ndarray:
"""Render a single FCN comparison frame."""
frame = np.zeros((H, W, 3), dtype=np.uint8)
frame[:] = BG_COLOR
progress = min(t / (STEP_DUR * 0.8), 1.0)
top_y = 140
blocks_classic = [
((80, top_y), (70, 50), (70, 130, 200)),
((170, top_y), (50, 40), (50, 100, 160)),
((240, top_y), (60, 50), (70, 130, 200)),
((320, top_y), (40, 35), (50, 100, 160)),
((385, top_y), (55, 50), (160, 80, 60)),
((465, top_y), (55, 50), (180, 60, 60)),
((545, top_y), (80, 50), (200, 80, 80)),
]
n_top = min(int(progress * 7) + 1, 7)
arrow_limit = 6
_draw_pipeline_blocks(frame, blocks_classic, n_top, arrow_limit)
cross_phase = 0.6
if progress > cross_phase:
_draw_red_cross(frame, 385, 135, top_y, 50)
bot_y = 380
blocks_fcn = [
((80, bot_y), (70, 50), (70, 130, 200)),
((170, bot_y), (50, 40), (50, 100, 160)),
((240, bot_y), (60, 50), (70, 130, 200)),
((320, bot_y), (40, 35), (50, 100, 160)),
((385, bot_y), (70, 50), (80, 200, 120)),
((480, bot_y), (75, 50), (200, 160, 80)),
((580, bot_y), (80, 50), (100, 200, 100)),
]
fcn_phase = 0.4
if progress > fcn_phase:
n_bot = min(int((progress - fcn_phase) / 0.6 * 7) + 1, 7)
_draw_pipeline_blocks(frame, blocks_fcn, n_bot, arrow_limit)
return frame
def _fcn_demo() -> list[CompositeVideoClip]:
"""Animate FCN step-by-step: FC → Conv 1x1 transformation."""
dur = STEP_DUR + 1
fcn_clip = VideoClip(_make_fcn_frame, duration=dur).with_fps(FPS)
labels = [
("FCN: Fully Convolutional Network (2015)", 26, "#FFE082", FONT_B, (80, 20)),
("KROK 1: Zamień FC → Conv 1x1", 18, "#A5D6A7", FONT_R, (80, 60)),
("Klasyczny CNN:", 16, "#EF9A9A", FONT_B, (80, 105)),
("Conv", 11, "white", FONT_R, (92, 148)),
("Pool", 11, "white", FONT_R, (178, 148)),
("Conv", 11, "white", FONT_R, (250, 148)),
("Pool", 11, "white", FONT_R, (325, 148)),
("Flatten", 11, "#EF9A9A", FONT_R, (390, 148)),
("FC", 11, "#EF9A9A", FONT_R, (480, 148)),
("1 label", 11, "#EF9A9A", FONT_R, (555, 148)),
("FCN:", 16, "#A5D6A7", FONT_B, (80, 350)),
("Conv", 11, "white", FONT_R, (92, 388)),
("Pool", 11, "white", FONT_R, (178, 388)),
("Conv", 11, "white", FONT_R, (250, 388)),
("Pool", 11, "white", FONT_R, (325, 388)),
("Conv1x1", 11, "#A5D6A7", FONT_R, (390, 388)),
("Upsample", 11, "#FFE082", FONT_R, (486, 388)),
("Mapa", 11, "#A5D6A7", FONT_R, (595, 388)),
(
"FC: spłaszcza 3D→1D, wymusza stały rozmiar → 1 etykieta",
16,
"#EF9A9A",
FONT_R,
(80, 250),
),
(
"Conv1x1: działa per piksel x kanały → DOWOLNY rozmiar → mapa klasy",
16,
"#A5D6A7",
FONT_R,
(80, 460),
),
(
"KROK 2: Skip connections — łączą wczesne detale z późną abstrakcją",
17,
"#64B5F6",
FONT_R,
(80, 510),
),
(
"Wczesne warstwy = krawędzie, tekstury | Późne = koncepty obiektów",
15,
"#78909C",
FONT_R,
(80, 545),
),
(
"FCN = PIERWSZA sieć end-to-end do segmentacji per-piksel!",
18,
"white",
FONT_R,
(80, 590),
),
(
"Mnemonik: FC → Conv 1x1 = otwieramy bramkę dla DOWOLNEGO rozmiaru",
16,
"#FFE082",
FONT_R,
(80, 640),
),
]
slides = [_compose_slide(fcn_clip, labels, dur)]
# Slide 2: FCN skip connections step by step
skip_lines = [
("FCN: Skip Connections — krok po kroku", 26, "#FFE082", FONT_B, (80, 30)),
(
"1. Encoder zmniejsza: 224→112→56→28→14 (pooling)",
18,
"#64B5F6",
FONT_R,
(100, 100),
),
(
" Każdy pooling traci detale przestrzenne (dokładne krawędzie)",
15,
"#78909C",
FONT_R,
(100, 135),
),
(
"2. Decoder powiększa: 14→28→56→112→224 (upsample/deconv)",
18,
"#A5D6A7",
FONT_R,
(100, 190),
),
(
" Upsample ODGADUJE piksele — rozmyty wynik!",
15,
"#78909C",
FONT_R,
(100, 225),
),
(
"3. Skip connections: dodaj cechy z encodera do decodera",
18,
"#FFE082",
FONT_R,
(100, 280),
),
(
" Wczesne cechy = GDZIE (precyzyjne krawędzie)",
15,
"#64B5F6",
FONT_R,
(100, 315),
),
(
" Późne cechy = CO (abstrakcyjne koncepty)",
15,
"#A5D6A7",
FONT_R,
(100, 345),
),
(
" Skip = daje decoderowi OBA → ostry wynik!",
15,
"#FFE082",
FONT_R,
(100, 375),
),
(
"Warianty: FCN-32s (brak skip, rozmyty) → FCN-16s → FCN-8s (najlepszy)",
16,
"#B0BEC5",
FONT_R,
(80, 440),
),
(
"FCN-32s: upsample 32x naraz → ROZMYTE granice",
15,
"#EF9A9A",
FONT_R,
(100, 485),
),
(
"FCN-16s: skip z pool4 + upsample 16x → lepiej",
15,
"#FFE082",
FONT_R,
(100, 520),
),
(
"FCN-8s: skip z pool3+pool4 + upsample 8x → OSTRE granice!",
15,
"#A5D6A7",
FONT_R,
(100, 555),
),
(
"Im więcej skip connections → tym więcej "
"detali z encodera → ostrzejszy wynik",
17,
"white",
FONT_R,
(80, 620),
),
]
slides.append(_text_slide(skip_lines, duration=STEP_DUR + 1))
return slides

View File

@ -0,0 +1,332 @@
"""Classical detection methods: detection concept, HOG+SVM, Viola-Jones."""
from __future__ import annotations
from _q24_common import (
BG_COLOR,
FONT_B,
FONT_R,
FPS,
STEP_DUR,
H,
W,
_tc,
)
from moviepy import CompositeVideoClip, VideoClip
from moviepy.video.fx import FadeIn, FadeOut
import numpy as np
# ── Detection concept ────────────────────────────────────────────
def _detection_concept() -> list[CompositeVideoClip]:
"""Show what detection is: bounding box + class + confidence."""
slides = []
def make_det_frame(_t: float) -> np.ndarray:
frame = np.zeros((H, W, 3), dtype=np.uint8)
frame[:] = BG_COLOR
# Draw a "scene" with colored rectangles representing objects
# Sky background area
frame[140:500, 100:700] = (40, 50, 70)
# "Car" object
frame[350:430, 150:320] = (180, 60, 60)
# "Person" object
frame[280:440, 450:520] = (60, 120, 180)
# "Tree" object
frame[200:400, 580:650] = (40, 130, 50)
# Bounding boxes (with labels drawn as colored borders)
# Car bbox
for thickness in range(3):
t = thickness
frame[348 - t : 432 + t, 148 - t : 148 - t + 2] = (255, 80, 80)
frame[348 - t : 432 + t, 322 + t - 2 : 322 + t] = (255, 80, 80)
frame[348 - t : 348 - t + 2, 148 - t : 322 + t] = (255, 80, 80)
frame[432 + t - 2 : 432 + t, 148 - t : 322 + t] = (255, 80, 80)
# Person bbox
for thickness in range(3):
t = thickness
frame[278 - t : 442 + t, 448 - t : 448 - t + 2] = (80, 180, 255)
frame[278 - t : 442 + t, 522 + t - 2 : 522 + t] = (80, 180, 255)
frame[278 - t : 278 - t + 2, 448 - t : 522 + t] = (80, 180, 255)
frame[442 + t - 2 : 442 + t, 448 - t : 522 + t] = (80, 180, 255)
# Tree bbox
for thickness in range(3):
t = thickness
frame[198 - t : 402 + t, 578 - t : 578 - t + 2] = (80, 220, 100)
frame[198 - t : 402 + t, 652 + t - 2 : 652 + t] = (80, 220, 100)
frame[198 - t : 198 - t + 2, 578 - t : 652 + t] = (80, 220, 100)
frame[402 + t - 2 : 402 + t, 578 - t : 652 + t] = (80, 220, 100)
# Comparison boxes on right side
# Classification
frame[180:260, 800:1150] = (35, 45, 65)
# Detection
frame[290:370, 800:1150] = (35, 45, 65)
# Segmentation
frame[400:480, 800:1150] = (35, 45, 65)
return frame
det_clip = VideoClip(make_det_frame, duration=STEP_DUR).with_fps(FPS)
text_clips: list[VideoClip] = [det_clip]
labels = [
("Detekcja obiektów — co to jest?", 28, "#FFE082", FONT_B, (100, 20)),
("Wynik: (klasa, bounding box, pewność)", 20, "#B0BEC5", FONT_R, (100, 65)),
("samochód 95%", 14, "#EF9A9A", FONT_B, (150, 340)),
("osoba 88%", 14, "#64B5F6", FONT_B, (450, 268)),
("drzewo 72%", 14, "#A5D6A7", FONT_B, (580, 188)),
("Klasyfikacja: cały obraz → 1 etykieta", 15, "#78909C", FONT_R, (810, 210)),
("Detekcja: bbox + klasa + pewność", 15, "#FFE082", FONT_R, (810, 320)),
("Segmentacja: maska per piksel", 15, "#78909C", FONT_R, (810, 430)),
("← granulacja rośnie →", 14, "#90CAF9", FONT_R, (810, 520)),
]
for text, fs, color, font, pos in labels:
tc = (
_tc(text=text, font_size=fs, color=color, font=font)
.with_duration(STEP_DUR)
.with_position(pos)
)
text_clips.append(tc)
slides.append(
CompositeVideoClip(text_clips, size=(W, H)).with_effects(
[FadeIn(0.3), FadeOut(0.3)]
)
)
return slides
# ── HOG + SVM pipeline ───────────────────────────────────────────
def _hog_svm_demo() -> list[CompositeVideoClip]:
"""Animate HOG feature computation and SVM classification."""
slides = []
def make_hog_frame(t: float) -> np.ndarray:
frame = np.zeros((H, W, 3), dtype=np.uint8)
frame[:] = BG_COLOR
progress = min(t / (STEP_DUR * 0.8), 1.0)
# Pipeline stages as boxes with arrows
stages = [
("Gradient", (80, 250), (130, 80), (100, 160, 220)),
("Orientacja", (260, 250), (130, 80), (80, 180, 140)),
("Komórki 8x8", (440, 250), (130, 80), (200, 160, 80)),
("Bloki 2x2", (620, 250), (130, 80), (200, 120, 60)),
("Normalizacja", (800, 250), (130, 80), (180, 100, 80)),
("SVM", (980, 250), (130, 80), (220, 80, 80)),
]
n_active = int(progress * len(stages)) + 1
for i, (_label, (sx, sy), (sw, sh), color) in enumerate(stages):
if i < n_active:
frame[sy : sy + sh, sx : sx + sw] = color
# Border
frame[sy : sy + 2, sx : sx + sw] = tuple(
min(c + 60, 255) for c in color
)
frame[sy + sh - 2 : sy + sh, sx : sx + sw] = tuple(
min(c + 60, 255) for c in color
)
# Arrow to next
if i < len(stages) - 1:
ax = sx + sw + 5
ay = sy + sh // 2
frame[ay - 1 : ay + 2, ax : ax + 20] = (150, 150, 170)
# Show gradient computation example at bottom
gradient_phase = 0.2
if progress > gradient_phase:
# Mini pixel grid showing gradient computation
gx, gy = 100, 430
pixels = [50, 50, 200]
for idx, val in enumerate(pixels):
x = gx + idx * 50
frame[gy : gy + 40, x : x + 40] = (val, val, val)
return frame
hog_clip = VideoClip(make_hog_frame, duration=STEP_DUR).with_fps(FPS)
text_clips: list[VideoClip] = [hog_clip]
labels = [
("HOG + SVM — pipeline detekcji pieszych", 28, "#FFE082", FONT_B, (80, 20)),
(
"Mnemonik: GOKBN = Gradienty→Orientacja→Komórki→Bloki→Normalizacja",
16,
"#A5D6A7",
FONT_R,
(80, 65),
),
("Gradient: siła i kierunek zmiany jasności", 14, "#64B5F6", FONT_R, (80, 95)),
(
"Histogram: 9 binów (0°-180°, co 20°) per komórka 8x8",
14,
"#78909C",
FONT_R,
(80, 120),
),
(
"[50][50][200] → Gx = 200-50 = 150 = silna krawędź!",
16,
"#EF9A9A",
FONT_R,
(80, 490),
),
(
"Wektor HOG (3780 cech) → SVM: pieszy (+1) / tło (-1)",
16,
"white",
FONT_R,
(80, 540),
),
(
"Sliding window 64x128 przesuwa się po obrazie → NMS → wynik",
16,
"#90CAF9",
FONT_R,
(80, 580),
),
(
"SVM = LINIA MAKSYMALNEGO ODDECHU (max margines, support vectors)",
16,
"#FFE082",
FONT_R,
(80, 620),
),
]
for text, fs, color, font, pos in labels:
tc = (
_tc(text=text, font_size=fs, color=color, font=font)
.with_duration(STEP_DUR)
.with_position(pos)
)
text_clips.append(tc)
slides.append(
CompositeVideoClip(text_clips, size=(W, H)).with_effects(
[FadeIn(0.3), FadeOut(0.3)]
)
)
return slides
# ── Viola-Jones ───────────────────────────────────────────────────
def _viola_jones_demo() -> list[CompositeVideoClip]:
"""Animate Viola-Jones cascade concept."""
slides = []
def make_cascade_frame(t: float) -> np.ndarray:
frame = np.zeros((H, W, 3), dtype=np.uint8)
frame[:] = BG_COLOR
progress = min(t / (STEP_DUR * 0.8), 1.0)
# Draw cascade "funnel" — stages filtering out non-faces
stages = 5
start_width = 1000
start_count = 10000
x_center = W // 2
for i in range(stages):
stage_progress = min(progress * stages - i, 1.0)
if stage_progress <= 0:
break
width = int(start_width * (1 - i * 0.18))
int(start_count * (0.3**i))
y = 150 + i * 100
h_box = 60
# Stage box
x1 = x_center - width // 2
frame[y : y + h_box, x1 : x1 + width] = (
50 + i * 10,
60 + i * 10,
80 + i * 10,
)
# Border
frame[y : y + 2, x1 : x1 + width] = (100 + i * 20, 130 + i * 15, 200)
frame[y + h_box - 2 : y + h_box, x1 : x1 + width] = (
100 + i * 20,
130 + i * 15,
200,
)
# Arrow down to next
if i < stages - 1:
frame[y + h_box + 5 : y + h_box + 25, x_center - 1 : x_center + 2] = (
150,
150,
170,
)
# Red "rejected" arrows on sides
if i > 0:
# Left reject arrow
rx = x1 - 30
ry = y + h_box // 2
frame[ry - 1 : ry + 2, rx : rx + 25] = (200, 80, 80)
return frame
cascade_clip = VideoClip(make_cascade_frame, duration=STEP_DUR).with_fps(FPS)
text_clips: list[VideoClip] = [cascade_clip]
labels = [
(
"Viola-Jones — kaskada klasyfikatorów (2001)",
28,
"#FFE082",
FONT_B,
(80, 20),
),
(
"3 innowacje: HIC = Haar + Integral Image + Cascade",
20,
"#B0BEC5",
FONT_R,
(80, 65),
),
("Etap 1: 2 cechy Haar", 14, "#64B5F6", FONT_R, (170, 170)),
("Etap 2: 10 cech", 14, "#64B5F6", FONT_R, (210, 270)),
("Etap 3: 25 cech", 14, "#64B5F6", FONT_R, (240, 370)),
("Etap 4: 50 cech", 14, "#64B5F6", FONT_R, (260, 470)),
("→ TWARZ!", 16, "#A5D6A7", FONT_B, (590, 560)),
(
"SITO: 99% okien odpada w pierwszych 3 etapach → REAL-TIME!",
16,
"#EF9A9A",
FONT_R,
(80, 620),
),
(
"Haar: kontrast jasna/ciemna | Integral Image: "
"suma prostokąta O(1) = 4 odczyty",
14,
"#78909C",
FONT_R,
(80, 655),
),
("odrzucone →", 12, "#EF9A9A", FONT_R, (60, 275)),
("odrzucone →", 12, "#EF9A9A", FONT_R, (60, 375)),
]
for text, fs, color, font, pos in labels:
tc = (
_tc(text=text, font_size=fs, color=color, font=font)
.with_duration(STEP_DUR)
.with_position(pos)
)
text_clips.append(tc)
slides.append(
CompositeVideoClip(text_clips, size=(W, H)).with_effects(
[FadeIn(0.3), FadeOut(0.3)]
)
)
return slides

View File

@ -0,0 +1,115 @@
"""Shared constants and helpers for Q24 object detection visualization."""
from __future__ import annotations
import logging
import os
from pathlib import Path
import numpy as np
os.environ["FFMPEG_BINARY"] = "/usr/bin/ffmpeg"
from moviepy import (
ColorClip,
CompositeVideoClip,
TextClip,
VideoClip,
)
from moviepy.video.fx import FadeIn, FadeOut
# ── Constants ─────────────────────────────────────────────────────
W, H = 1280, 720
FPS = 24
STEP_DUR = 7.0
HEADER_DUR = 4.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 / "q24_object_detection.mp4")
BG_COLOR = (15, 20, 35)
_logger = logging.getLogger(__name__)
# Re-export numpy for sub-modules that need it alongside constants.
__all__ = [
"BG_COLOR",
"FONT_B",
"FONT_R",
"FPS",
"HEADER_DUR",
"OUTPUT",
"OUTPUT_DIR",
"STEP_DUR",
"H",
"W",
"_logger",
"_make_header",
"_tc",
"_text_slide",
"np",
]
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:
"""Create a title/subtitle header slide."""
bg = ColorClip(size=(W, H), color=BG_COLOR).with_duration(duration)
t = (
_tc(
text=title,
font_size=48,
color="white",
font=FONT_B,
)
.with_duration(duration)
.with_position(("center", 260))
)
s = (
_tc(
text=subtitle,
font_size=24,
color="#90CAF9",
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 _text_slide(
lines: list[tuple[str, int, str, str, tuple[str | int, str | int]]],
duration: float = STEP_DUR,
) -> CompositeVideoClip:
"""Create a text-only slide from a list of (text, size, color, font, pos)."""
bg = ColorClip(size=(W, H), color=BG_COLOR).with_duration(duration)
clips: list[VideoClip] = [bg]
for text, font_size, color, font, pos in lines:
tc = (
_tc(
text=text,
font_size=font_size,
color=color,
font=font,
)
.with_duration(duration)
.with_position(pos)
)
clips.append(tc)
return CompositeVideoClip(clips, size=(W, H)).with_effects(
[FadeIn(0.3), FadeOut(0.3)]
)

View File

@ -0,0 +1,239 @@
"""NMS/IoU, detector-from-classifier, and methods comparison."""
from __future__ import annotations
from _q24_common import (
BG_COLOR,
FONT_B,
FONT_R,
FPS,
STEP_DUR,
H,
W,
_tc,
_text_slide,
)
from moviepy import ColorClip, CompositeVideoClip, VideoClip
from moviepy.video.fx import FadeIn, FadeOut
import numpy as np
# ── NMS + IoU ─────────────────────────────────────────────────────
def _nms_iou_demo() -> list[CompositeVideoClip]:
"""Animate NMS and IoU concepts."""
slides = []
def make_nms_frame(t: float) -> np.ndarray:
frame = np.zeros((H, W, 3), dtype=np.uint8)
frame[:] = BG_COLOR
progress = min(t / (STEP_DUR * 0.7), 1.0)
# Draw overlapping bounding boxes
ox, oy = 100, 200
obj_w, obj_h = 150, 120
# Multiple overlapping detections for same object
boxes = [
(ox, oy, obj_w, obj_h, 0.95, (255, 80, 80)), # best
(ox + 15, oy - 10, obj_w + 10, obj_h + 5, 0.90, (200, 60, 60)),
(ox - 10, oy + 5, obj_w - 5, obj_h + 10, 0.85, (160, 50, 50)),
]
# Different object far away
boxes.append((ox + 350, oy + 50, 100, 100, 0.40, (80, 180, 255)))
for i, (bx, by, bw, bh, _conf, color) in enumerate(boxes):
dc = color
nms_phase = 0.4
nms_limit = 3
if progress > nms_phase and i > 0 and i < nms_limit:
# After NMS, these get removed (shown as faded/crossed)
dc = (60, 40, 40)
for tt in range(2):
frame[by - tt : by + bh + tt, bx - tt : bx - tt + 2] = dc
frame[by - tt : by + bh + tt, bx + bw + tt - 2 : bx + bw + tt] = dc
frame[by - tt : by - tt + 2, bx - tt : bx + bw + tt] = dc
frame[by + bh + tt - 2 : by + bh + tt, bx - tt : bx + bw + tt] = dc
# IoU visualization on right side
iou_x, iou_y = 700, 200
# Box A
frame[iou_y : iou_y + 100, iou_x : iou_x + 100] = (80, 80, 200)
# Box B (overlapping)
frame[iou_y + 40 : iou_y + 140, iou_x + 40 : iou_x + 140] = (200, 80, 80)
# Intersection highlighted
frame[iou_y + 40 : iou_y + 100, iou_x + 40 : iou_x + 100] = (200, 150, 200)
return frame
nms_clip = VideoClip(make_nms_frame, duration=STEP_DUR).with_fps(FPS)
text_clips: list[VideoClip] = [nms_clip]
labels = [
("NMS (Non-Maximum Suppression) + IoU", 28, "#FFE082", FONT_B, (80, 20)),
(
"NMS = Najlepszy Ma Się dobrze — zachowaj najlepszą, usuń duplikaty",
18,
"#B0BEC5",
FONT_R,
(80, 65),
),
("conf=0.95 ✓", 14, "#A5D6A7", FONT_B, (100, 340)),
("0.90 ✗ IoU>0.5", 13, "#EF9A9A", FONT_R, (100, 365)),
("0.85 ✗ IoU>0.5", 13, "#EF9A9A", FONT_R, (100, 390)),
("0.40 ✓ INNY obiekt", 13, "#64B5F6", FONT_R, (100, 420)),
("IoU = Intersection over Union", 18, "#FFE082", FONT_B, (700, 160)),
("IoU = pole(∩) / pole(AUB)", 16, "white", FONT_R, (700, 380)),
("Fioletowy = intersection", 14, "#CE93D8", FONT_R, (700, 410)),
("IoU > 0.5 → TEN SAM obiekt → usuń", 14, "#EF9A9A", FONT_R, (700, 440)),
("IoU < 0.5 → INNY obiekt → zachowaj", 14, "#A5D6A7", FONT_R, (700, 470)),
(
"DETR: jedyny detektor BEZ NMS (Hungarian matching zamiast tego)",
14,
"#78909C",
FONT_R,
(80, 620),
),
]
for text, fs, color, font, pos in labels:
tc = (
_tc(text=text, font_size=fs, color=color, font=font)
.with_duration(STEP_DUR)
.with_position(pos)
)
text_clips.append(tc)
slides.append(
CompositeVideoClip(text_clips, size=(W, H)).with_effects(
[FadeIn(0.3), FadeOut(0.3)]
)
)
return slides
# ── Detector from Classifier ─────────────────────────────────────
def _detector_from_classifier() -> list[CompositeVideoClip]:
"""Show 3 approaches to building a detector from a classifier."""
slides = []
approaches = [
(
"Podejście 1: Sliding Window (NAJWOLNIEJSZE)",
[
("Okno przesuwa się po obrazie w wielu skalach", "#B0BEC5"),
("Każde okno → klasyfikator (np. ResNet) → klasa + pewność", "#B0BEC5"),
("~18 000 okien x 10ms = ~3 minuty na obraz!", "#EF9A9A"),
("Mnemonik: WYCINAJ i PYTAJ — jak wycinanie ciasteczek", "#FFE082"),
],
"SRF",
),
(
"Podejście 2: Region Proposals (= R-CNN)",
[
("Selective Search → ~2000 inteligentnych regionów", "#B0BEC5"),
("Każdy region → CNN → wektor cech → SVM klasyfikuje", "#B0BEC5"),
("~2000 x 10ms = ~20 sec — 9x szybciej!", "#64B5F6"),
(
"Mnemonik: INTELIGENTNE CIĘCIE — wytnij tylko tam gdzie wiśnie",
"#FFE082",
),
],
"SRF",
),
(
"Podejście 3: Fine-tune backbone (NAJLEPSZE)",
[
(
"Pretrained backbone (ResNet) → odetnij FC → dodaj detection head",
"#B0BEC5",
),
(
"Detection head = głowica klasyfikacji + głowica regresji bbox",
"#B0BEC5",
),
("~0.2 sec/obraz, najlepsza jakość (mAP ~42%)", "#A5D6A7"),
("Mnemonik: PRZESZCZEP GŁOWY — ten sam silnik, nowa głowa", "#FFE082"),
],
"SRF",
),
]
for title, points, _mnem in approaches:
lines = [
(title, 24, "#FFE082", FONT_B, (80, 140)),
]
for i, (text, color) in enumerate(points):
lines.append((f"{text}", 18, color, FONT_R, (100, 220 + i * 50)))
lines.append(
(
"Detektor z klasyfikatora: SRF = Sliding → Region → Fine-tune",
16,
"#78909C",
FONT_R,
(80, 520),
)
)
lines.append(
(
"= Szukaj Ręcznie, Finalnie optymalizuj!",
16,
"#90CAF9",
FONT_R,
(80, 550),
)
)
slides.append(_text_slide(lines, duration=STEP_DUR))
return slides
# ── Methods comparison ────────────────────────────────────────────
def _methods_comparison() -> CompositeVideoClip:
"""Create a comparison table of all detection methods."""
bg = ColorClip(size=(W, H), color=BG_COLOR).with_duration(10.0)
title = (
_tc(
text="Porównanie detektorów",
font_size=36,
color="white",
font=FONT_B,
)
.with_duration(10.0)
.with_position(("center", 20))
)
rows = [
("Model", "Rok", "Typ", "Szybkość", "Kluczowe"),
("HOG+SVM", "2005", "Klasyczny", "~1 fps", "Gradient histogramy"),
("Viola-Jones", "2001", "Klasyczny", "30+ fps", "Haar+Cascade"),
("R-CNN", "2014", "Two-stage", "50 sec!", "CNN per region"),
("Fast R-CNN", "2015", "Two-stage", "2 sec", "ROI Pooling"),
("Faster R-CNN", "2015", "Two-stage", "5 fps", "RPN w sieci"),
("YOLO", "2016", "One-stage", "45+ fps", "Siatka SxS"),
("DETR", "2020", "Transformer", "~40 fps", "Bez NMS!"),
]
clips: list[VideoClip] = [bg, title]
for i, row in enumerate(rows):
y_pos = 75 + i * 72
col_x = [40, 200, 280, 400, 530]
for j, cell in enumerate(row):
fs = 16 if i > 0 else 18
color = "#64B5F6" if i == 0 else "#E0E0E0"
tc = (
_tc(
text=cell,
font_size=fs,
color=color,
font=FONT_B if i == 0 else FONT_R,
)
.with_duration(10.0)
.with_position((col_x[j], y_pos))
)
clips.append(tc)
return CompositeVideoClip(clips, size=(W, H)).with_effects(
[FadeIn(0.5), FadeOut(0.5)]
)

View File

@ -0,0 +1,405 @@
"""R-CNN family: evolution, detailed pipeline, ROI pooling."""
from __future__ import annotations
from _q24_common import (
BG_COLOR,
FONT_B,
FONT_R,
FPS,
STEP_DUR,
H,
W,
_tc,
)
from moviepy import CompositeVideoClip, VideoClip
from moviepy.video.fx import FadeIn, FadeOut
import numpy as np
# ── R-CNN Evolution ───────────────────────────────────────────────
def _rcnn_evolution() -> list[CompositeVideoClip]:
"""Animate R-CNN → Fast R-CNN → Faster R-CNN evolution."""
slides = []
def make_evolution_frame(t: float) -> np.ndarray:
frame = np.zeros((H, W, 3), dtype=np.uint8)
frame[:] = BG_COLOR
progress = min(t / (STEP_DUR * 0.8), 1.0)
# Three rows: R-CNN, Fast R-CNN, Faster R-CNN
models = [
(
"R-CNN (2014)",
50,
[
("Selective\nSearch", (200, 150), (100, 50), (120, 100, 60)),
("2000x\nCNN", (350, 150), (80, 50), (180, 60, 60)),
("2000x\nSVM", (480, 150), (80, 50), (180, 60, 60)),
("NMS", (610, 150), (60, 50), (100, 140, 100)),
],
"50 sec/obraz!",
),
(
"Fast R-CNN (2015)",
300,
[
("Selective\nSearch", (200, 150), (100, 50), (120, 100, 60)),
("1x CNN\n(cały obraz)", (350, 150), (100, 50), (80, 140, 200)),
("ROI Pool\n(2000)", (500, 150), (90, 50), (200, 160, 80)),
("FC", (640, 150), (50, 50), (100, 140, 100)),
],
"2 sec/obraz",
),
(
"Faster R-CNN (2015)",
300,
[
("CNN\nbackbone", (200, 150), (90, 50), (80, 140, 200)),
("RPN\n(~300)", (340, 150), (80, 50), (200, 120, 60)),
("ROI Pool", (470, 150), (80, 50), (200, 160, 80)),
("FC", (600, 150), (50, 50), (100, 140, 100)),
],
"0.2 sec → 5 fps!",
),
]
n_models = int(progress * 3) + 1
for mi, (_name, base_y, stages, _speed) in enumerate(models):
if mi >= n_models:
break
for _label, (bx, by_off), (bw, bh), color in stages:
by = base_y + by_off - 150
frame[by : by + bh, bx : bx + bw] = color
frame[by : by + 2, bx : bx + bw] = tuple(
min(c + 50, 255) for c in color
)
frame[by + bh - 2 : by + bh, bx : bx + bw] = tuple(
min(c + 50, 255) for c in color
)
# Arrows between stages
for si in range(len(stages) - 1):
sx = stages[si][1][0] + stages[si][2][0]
ex = stages[si + 1][1][0]
ay = base_y + 25
frame[ay - 1 : ay + 2, sx + 3 : ex - 3] = (150, 150, 170)
return frame
evo_clip = VideoClip(make_evolution_frame, duration=STEP_DUR + 1).with_fps(FPS)
text_clips: list[VideoClip] = [evo_clip]
labels = [
("Ewolucja R-CNN — CORAZ MNIEJ MARNOWANIA", 28, "#FFE082", FONT_B, (80, 20)),
("R-CNN (2014)", 20, "#EF9A9A", FONT_B, (50, 80)),
("50 sec/obraz (2000x forward pass!)", 14, "#EF9A9A", FONT_R, (720, 100)),
("Fast R-CNN (2015)", 20, "#64B5F6", FONT_B, (50, 330)),
("2 sec/obraz (CNN raz + ROI Pool)", 14, "#64B5F6", FONT_R, (720, 350)),
("Faster R-CNN (2015)", 20, "#A5D6A7", FONT_B, (50, 580)),
("0.2 sec → 5 fps (RPN w sieci!)", 14, "#A5D6A7", FONT_R, (720, 600)),
(
"Kluczowe innowacje: ROI Pooling → stały rozmiar "
"| RPN → propozycje w sieci",
14,
"#78909C",
FONT_R,
(80, 660),
),
]
for text, fs, color, font, pos in labels:
tc = (
_tc(text=text, font_size=fs, color=color, font=font)
.with_duration(STEP_DUR + 1)
.with_position(pos)
)
text_clips.append(tc)
slides.append(
CompositeVideoClip(text_clips, size=(W, H)).with_effects(
[FadeIn(0.3), FadeOut(0.3)]
)
)
return slides
# ── R-CNN Detailed Pipeline ──────────────────────────────────────
def _rcnn_detailed() -> list[CompositeVideoClip]:
"""Animate R-CNN step-by-step pipeline in detail."""
slides = []
# Slide 1: R-CNN pipeline step by step
def make_rcnn_pipeline(t: float) -> np.ndarray:
frame = np.zeros((H, W, 3), dtype=np.uint8)
frame[:] = BG_COLOR
progress = min(t / (STEP_DUR * 0.8), 1.0)
# Step boxes arranged vertically with arrows
steps = [
((80, 130), (200, 55), (120, 100, 60), "1. Selective Search"),
((80, 230), (200, 55), (180, 60, 60), "2. Wytnij 2000 regionów"),
((80, 330), (200, 55), (70, 130, 200), "3. CNN per region"),
((80, 430), (200, 55), (200, 100, 80), "4. SVM klasyfikuje"),
((80, 530), (200, 55), (100, 180, 100), "5. Bbox regresja + NMS"),
]
n_steps = min(int(progress * 5) + 1, 5)
for i, ((bx, by), (bw, bh), color, _lbl) in enumerate(steps):
if i < n_steps:
frame[by : by + bh, bx : bx + bw] = color
frame[by : by + 2, bx : bx + bw] = tuple(
min(c + 50, 255) for c in color
)
frame[by + bh - 2 : by + bh, bx : bx + bw] = tuple(
min(c + 50, 255) for c in color
)
# Arrow down
arrow_limit = 4
if i < arrow_limit:
ax = bx + bw // 2
ay = by + bh + 5
frame[ay : ay + 20, ax - 1 : ax + 2] = (150, 150, 170)
# Illustration: many overlapping regions from Selective Search
overlay_phase = 0.2
if progress > overlay_phase:
rng_local = np.random.default_rng(42)
n_boxes = min(int((progress - 0.2) * 15), 8)
for i in range(n_boxes):
rx = 500 + rng_local.integers(-30, 100)
ry = 200 + rng_local.integers(-20, 120)
rw = 60 + rng_local.integers(0, 80)
rh = 50 + rng_local.integers(0, 70)
c = (80 + i * 15, 100 + i * 10, 60 + i * 20)
for tt in range(2):
frame[ry - tt : ry + rh + tt, rx - tt : rx - tt + 2] = c
frame[ry - tt : ry + rh + tt, rx + rw + tt - 2 : rx + rw + tt] = c
frame[ry - tt : ry - tt + 2, rx - tt : rx + rw + tt] = c
frame[ry + rh + tt - 2 : ry + rh + tt, rx - tt : rx + rw + tt] = c
return frame
rcnn_clip = VideoClip(make_rcnn_pipeline, duration=STEP_DUR + 1).with_fps(FPS)
dur = STEP_DUR + 1
labels = [
("R-CNN: krok po kroku (2014, Girshick)", 26, "#FFE082", FONT_B, (80, 20)),
("Pipeline detekcji two-stage", 16, "#B0BEC5", FONT_R, (80, 60)),
("Selective Search", 11, "white", FONT_R, (105, 145)),
("2000 regionów", 11, "white", FONT_R, (105, 245)),
("CNN per region", 11, "white", FONT_R, (105, 345)),
("SVM klasyfikuje", 11, "white", FONT_R, (105, 445)),
("Regresja + NMS", 11, "white", FONT_R, (105, 545)),
("~2000 propozycji regionów", 14, "#78909C", FONT_R, (500, 155)),
("(inteligentne łączenie", 13, "#78909C", FONT_R, (500, 180)),
("podobnych fragmentów)", 13, "#78909C", FONT_R, (500, 200)),
("Problem: 2000 x CNN forward pass", 16, "#EF9A9A", FONT_R, (400, 400)),
("= 50 SEKUND na obraz!", 18, "#EF9A9A", FONT_B, (400, 430)),
("CNN liczy cechy per region OSOBNO", 14, "#EF9A9A", FONT_R, (400, 470)),
(
"→ regiony się nakładają → obliczenia się powtarzają!",
14,
"#EF9A9A",
FONT_R,
(400, 495),
),
(
"Rozwiązanie: CNN raz na cały obraz → Fast R-CNN →",
16,
"#A5D6A7",
FONT_R,
(80, 620),
),
]
text_clips: list[VideoClip] = [rcnn_clip]
for text, fs, color, font, pos in labels:
tc = (
_tc(text=text, font_size=fs, color=color, font=font)
.with_duration(dur)
.with_position(pos)
)
text_clips.append(tc)
slides.append(
CompositeVideoClip(text_clips, size=(W, H)).with_effects(
[FadeIn(0.3), FadeOut(0.3)]
)
)
return slides
# ── ROI Pooling ──────────────────────────────────────────────────
def _draw_roi_pool_grid(frame: np.ndarray) -> None:
"""Draw the 3x3 ROI pool grid with max-pooled feature values."""
out_x, out_y = 400, 220
out_cell = 50
out_n = 3
roi_r1, roi_c1 = 2, 1
roi_r2, roi_c2 = 6, 5
roi_h = roi_r2 - roi_r1
roi_w = roi_c2 - roi_c1
for r in range(out_n):
for c in range(out_n):
x = out_x + c * out_cell
y = out_y + r * out_cell
# Compute the max from corresponding region
src_r1 = roi_r1 + r * roi_h // out_n
src_r2 = roi_r1 + (r + 1) * roi_h // out_n
src_c1 = roi_c1 + c * roi_w // out_n
src_c2 = roi_c1 + (c + 1) * roi_w // out_n
max_val = 0
for sr in range(src_r1, src_r2):
for sc in range(src_c1, src_c2):
v = 30 + ((sr * 7 + sc * 13 + 42) % 40)
max_val = max(max_val, v)
frame[y : y + out_cell - 2, x : x + out_cell - 2] = (
max_val,
max_val + 20,
max_val + 40,
)
frame[y : y + 2, x : x + out_cell - 2] = (80, 200, 120)
frame[y + out_cell - 4 : y + out_cell - 2, x : x + out_cell - 2] = (
80,
200,
120,
)
def _make_roi_frame(t: float) -> np.ndarray:
"""Render a single frame for the ROI pooling animation."""
frame = np.zeros((H, W, 3), dtype=np.uint8)
frame[:] = BG_COLOR
progress = min(t / (STEP_DUR * 0.7), 1.0)
# Left: feature map with ROI highlighted
fm_x, fm_y = 60, 180
fm_cell = 30
fm_grid = 8
for r in range(fm_grid):
for c in range(fm_grid):
x = fm_x + c * fm_cell
y = fm_y + r * fm_cell
# Random-looking feature values
val = 30 + ((r * 7 + c * 13 + 42) % 40)
frame[y : y + fm_cell - 1, x : x + fm_cell - 1] = (
val,
val + 10,
val + 20,
)
# ROI region highlighted
roi_r1, roi_c1 = 2, 1
roi_r2, roi_c2 = 6, 5
for tt in range(3):
ry1 = fm_y + roi_r1 * fm_cell - tt
ry2 = fm_y + roi_r2 * fm_cell + tt
rx1 = fm_x + roi_c1 * fm_cell - tt
rx2 = fm_x + roi_c2 * fm_cell + tt
frame[ry1:ry2, rx1 : rx1 + 2] = (255, 200, 50)
frame[ry1:ry2, rx2 - 2 : rx2] = (255, 200, 50)
frame[ry1 : ry1 + 2, rx1:rx2] = (255, 200, 50)
frame[ry2 - 2 : ry2, rx1:rx2] = (255, 200, 50)
# Arrow
arrow_phase = 0.3
if progress > arrow_phase:
frame[300:303, 310:380] = (150, 150, 170)
# Middle: ROI divided into 3x3 grid (output_size)
grid_phase = 0.3
if progress > grid_phase:
_draw_roi_pool_grid(frame)
# Arrow to FC
fc_phase = 0.6
if progress > fc_phase:
frame[300:303, 560:630] = (150, 150, 170)
# FC box
frame[270:340, 650:730] = (200, 100, 80)
frame[270:272, 650:730] = (240, 140, 120)
frame[338:340, 650:730] = (240, 140, 120)
return frame
def _roi_pooling_demo() -> list[CompositeVideoClip]:
"""Animate ROI Pooling: key Fast R-CNN innovation."""
slides = []
roi_clip = VideoClip(_make_roi_frame, duration=STEP_DUR + 1).with_fps(FPS)
dur = STEP_DUR + 1
labels = [
("ROI Pooling: kluczowa innowacja Fast R-CNN", 26, "#FFE082", FONT_B, (80, 20)),
(
"KROK 1: CNN raz na CAŁY obraz → feature mapa",
17,
"#64B5F6",
FONT_R,
(80, 60),
),
(
"KROK 2: Wytnij ROI z feature mapy (nie z obrazu!)",
17,
"#FFE082",
FONT_R,
(80, 90),
),
(
"KROK 3: Siatkuj ROI na 3x3 → max pool per komórka → stały rozmiar",
17,
"#A5D6A7",
FONT_R,
(80, 120),
),
("Feature mapa", 14, "#64B5F6", FONT_B, (60, 160)),
("ROI (żółta ramka)", 13, "#FFE082", FONT_R, (60, 440)),
("ROI Pool 3x3", 14, "#A5D6A7", FONT_B, (400, 195)),
("(max z komórki)", 13, "#78909C", FONT_R, (400, 380)),
("FC", 14, "white", FONT_B, (670, 280)),
(
"Problem: ROI mają RÓŻNE rozmiary, FC wymaga STAŁEGO",
15,
"#B0BEC5",
FONT_R,
(80, 500),
),
(
"ROI Pooling: dzieli ROI na siatkę, max pool → STAŁY rozmiar!",
16,
"white",
FONT_R,
(80, 535),
),
(
"Fast R-CNN: CNN raz → 1 feature mapa → "
"ROI Pool 2000 regionów → 25x szybciej!",
16,
"#A5D6A7",
FONT_R,
(80, 580),
),
(
"(R-CNN: 2000x CNN = 50s | Fast R-CNN: 1xCNN + ROI Pool = 2s)",
15,
"#EF9A9A",
FONT_R,
(80, 620),
),
]
text_clips: list[VideoClip] = [roi_clip]
for text, fs, color, font, pos in labels:
tc = (
_tc(text=text, font_size=fs, color=color, font=font)
.with_duration(dur)
.with_position(pos)
)
text_clips.append(tc)
slides.append(
CompositeVideoClip(text_clips, size=(W, H)).with_effects(
[FadeIn(0.3), FadeOut(0.3)]
)
)
return slides

View File

@ -0,0 +1,383 @@
"""RPN anchor boxes and YOLO grid detection."""
from __future__ import annotations
from _q24_common import (
BG_COLOR,
FONT_B,
FONT_R,
FPS,
STEP_DUR,
H,
W,
_tc,
_text_slide,
)
from moviepy import CompositeVideoClip, VideoClip
from moviepy.video.fx import FadeIn, FadeOut
import numpy as np
# ── RPN + Anchor Boxes ───────────────────────────────────────────
def _rpn_anchors_demo() -> list[CompositeVideoClip]:
"""Animate RPN and anchor boxes: Faster R-CNN innovation."""
slides = []
# Slide 1: Anchor boxes concept
def make_anchors_frame(t: float) -> np.ndarray:
frame = np.zeros((H, W, 3), dtype=np.uint8)
frame[:] = BG_COLOR
progress = min(t / (STEP_DUR * 0.7), 1.0)
# Draw feature map grid point with multiple anchors
cx, cy = 350, 360 # center point on feature map
# Draw a "feature map" grid background
cell = 60
for r in range(-3, 4):
for c in range(-3, 4):
x = cx + c * cell - cell // 2
y = cy + r * cell - cell // 2
frame[y : y + cell - 1, x : x + cell - 1] = (30, 35, 48)
# Center point highlighted
frame[cy - 5 : cy + 5, cx - 5 : cx + 5] = (255, 200, 50)
# Draw anchors around center: 3 sizes x 3 ratios = 9
anchor_specs = [
(30, 30, (200, 80, 80)), # small 1:1
(20, 40, (200, 60, 60)), # small 1:2
(40, 20, (180, 60, 60)), # small 2:1
(60, 60, (80, 200, 80)), # medium 1:1
(40, 80, (60, 180, 60)), # medium 1:2
(80, 40, (60, 160, 60)), # medium 2:1
(90, 90, (80, 80, 200)), # large 1:1
(60, 120, (60, 60, 180)), # large 1:2
(120, 60, (60, 60, 160)), # large 2:1
]
n_anchors = min(int(progress * 9) + 1, 9)
for i in range(n_anchors):
hw, hh, color = anchor_specs[i]
x1 = max(0, cx - hw)
y1 = max(0, cy - hh)
x2 = min(W - 1, cx + hw)
y2 = min(H - 1, cy + hh)
for tt in range(2):
frame[y1 - tt : y2 + tt, x1 - tt : x1 - tt + 2] = color
frame[y1 - tt : y2 + tt, x2 + tt - 2 : x2 + tt] = color
frame[y1 - tt : y1 - tt + 2, x1 - tt : x2 + tt] = color
frame[y2 + tt - 2 : y2 + tt, x1 - tt : x2 + tt] = color
return frame
anch_clip = VideoClip(make_anchors_frame, duration=STEP_DUR + 1).with_fps(FPS)
dur = STEP_DUR + 1
labels = [
("Anchor Boxes + RPN (Faster R-CNN)", 26, "#FFE082", FONT_B, (80, 20)),
(
"KROK 1: Anchory = predefiniowane kształty w każdej pozycji",
17,
"#A5D6A7",
FONT_R,
(80, 60),
),
(
"3 rozmiary x 3 proporcje = 9 anchorów per punkt",
16,
"#B0BEC5",
FONT_R,
(80, 90),
),
("Małe (1:1, 1:2, 2:1)", 14, "#EF9A9A", FONT_R, (750, 170)),
("Średnie (1:1, 1:2, 2:1)", 14, "#A5D6A7", FONT_R, (750, 210)),
("Duże (1:1, 1:2, 2:1)", 14, "#64B5F6", FONT_R, (750, 250)),
("Żółty punkt = pozycja", 14, "#FFE082", FONT_R, (750, 310)),
("na feature mapie", 14, "#FFE082", FONT_R, (750, 335)),
("Sieć NIE predykuje bbox od zera!", 16, "white", FONT_R, (80, 530)),
(
"Predykuje OFFSET od najbliższego anchora: (Δx, Δy, Δw, Δh)",
16,
"#FFE082",
FONT_R,
(80, 565),
),
(
"+ P(obiekt) = 'czy w tym anchorze jest coś?'",
16,
"#A5D6A7",
FONT_R,
(80, 600),
),
(
"Mnemonik: Anchor = KOTWICA — sieć dopasowuje bbox do kotwicy",
15,
"#78909C",
FONT_R,
(80, 645),
),
]
text_clips: list[VideoClip] = [anch_clip]
for text, fs, color, font, pos in labels:
tc = (
_tc(text=text, font_size=fs, color=color, font=font)
.with_duration(dur)
.with_position(pos)
)
text_clips.append(tc)
slides.append(
CompositeVideoClip(text_clips, size=(W, H)).with_effects(
[FadeIn(0.3), FadeOut(0.3)]
)
)
# Slide 2: RPN step by step
rpn_lines = [
(
"RPN: Region Proposal Network — krok po kroku",
24,
"#FFE082",
FONT_B,
(80, 30),
),
(
"Zastępuje Selective Search SIECIĄ NEURONOWĄ (end-to-end!)",
17,
"#B0BEC5",
FONT_R,
(80, 85),
),
("", 10, "white", FONT_R, (80, 110)),
(
"1. Backbone (ResNet) przetwarza obraz → feature mapa [40x60x256]",
16,
"#64B5F6",
FONT_R,
(100, 140),
),
(
"2. Filtr 3x3 przesuwa się po feature mapie",
16,
"#A5D6A7",
FONT_R,
(100, 180),
),
(
"3. W KAŻDEJ pozycji (x,y) rozważ k=9 anchorów:",
16,
"#FFE082",
FONT_R,
(100, 220),
),
(" → P(obiekt) — 'czy tu jest coś?'", 15, "white", FONT_R, (120, 255)),
(" → (Δx, Δy, Δw, Δh) — poprawka pozycji", 15, "white", FONT_R, (120, 285)),
(
"4. 40x60 pozycji x 9 anchorów = 21 600 kandydatów!",
16,
"#EF9A9A",
FONT_R,
(100, 325),
),
(
"5. Weź ~300 z najwyższym P(obiekt) → ROI Pool → FC",
16,
"#A5D6A7",
FONT_R,
(100, 365),
),
("", 10, "white", FONT_R, (100, 395)),
("Porównanie generowania propozycji:", 17, "white", FONT_B, (80, 420)),
(
" Selective Search: ~2000 regionów, osobny algorytm, ~2 sec",
15,
"#EF9A9A",
FONT_R,
(100, 460),
),
(
" RPN: ~300 regionów, W SIECI, ~10 ms → 200x szybciej!",
15,
"#A5D6A7",
FONT_R,
(100, 495),
),
("", 10, "white", FONT_R, (100, 520)),
(
"Faster R-CNN = Backbone + RPN + ROI Pool + FC — WSZYSTKO end-to-end",
17,
"#FFE082",
FONT_R,
(80, 545),
),
(
"→ 5 fps (0.2 sec/obraz) vs R-CNN 50 sec = 250x szybciej!",
17,
"#A5D6A7",
FONT_R,
(80, 585),
),
(
"Wciąż two-stage: (1) RPN generuje propozycje, (2) FC klasyfikuje",
15,
"#78909C",
FONT_R,
(80, 630),
),
]
slides.append(_text_slide(rpn_lines, duration=STEP_DUR + 1))
return slides
# ── YOLO ──────────────────────────────────────────────────────────
def _yolo_demo() -> list[CompositeVideoClip]:
"""Animate YOLO grid detection concept."""
slides = []
def make_yolo_frame(t: float) -> np.ndarray:
frame = np.zeros((H, W, 3), dtype=np.uint8)
frame[:] = BG_COLOR
progress = min(t / (STEP_DUR * 0.7), 1.0)
# Draw image with grid overlay
img_x, img_y = 100, 140
img_size = 420
grid_n = 7
# Background "image"
frame[img_y : img_y + img_size, img_x : img_x + img_size] = (50, 55, 70)
# Objects in the image
frame[img_y + 80 : img_y + 200, img_x + 50 : img_x + 180] = (
180,
60,
60,
) # "car"
frame[img_y + 150 : img_y + 350, img_x + 250 : img_x + 330] = (
60,
120,
180,
) # "person"
# Grid lines
cell = img_size // grid_n
for i in range(grid_n + 1):
# Vertical
x = img_x + i * cell
frame[img_y : img_y + img_size, x : x + 1] = (100, 100, 120)
# Horizontal
y = img_y + i * cell
frame[y : y + 1, img_x : img_x + img_size] = (100, 100, 120)
# Highlight cells containing object centers
car_phase = 0.3
if progress > car_phase:
# Car center ~ cell (1, 1)
cx, cy = 1, 2
hx = img_x + cx * cell
hy = img_y + cy * cell
frame[hy : hy + cell, hx : hx + cell] = np.clip(
frame[hy : hy + cell, hx : hx + cell].astype(int) + 40, 0, 255
).astype(np.uint8)
person_phase = 0.5
if progress > person_phase:
# Person center ~ cell (4, 4)
cx, cy = 4, 4
hx = img_x + cx * cell
hy = img_y + cy * cell
frame[hy : hy + cell, hx : hx + cell] = np.clip(
frame[hy : hy + cell, hx : hx + cell].astype(int) + 40, 0, 255
).astype(np.uint8)
# Bounding boxes predictions from cells
bbox_phase = 0.6
if progress > bbox_phase:
# Car bbox
for tt in range(2):
frame[
img_y + 78 - tt : img_y + 202 + tt,
img_x + 48 - tt : img_x + 48 - tt + 2,
] = (255, 80, 80)
frame[
img_y + 78 - tt : img_y + 202 + tt,
img_x + 182 + tt - 2 : img_x + 182 + tt,
] = (255, 80, 80)
frame[
img_y + 78 - tt : img_y + 78 - tt + 2,
img_x + 48 - tt : img_x + 182 + tt,
] = (255, 80, 80)
frame[
img_y + 202 + tt - 2 : img_y + 202 + tt,
img_x + 48 - tt : img_x + 182 + tt,
] = (255, 80, 80)
# Person bbox
for tt in range(2):
frame[
img_y + 148 - tt : img_y + 352 + tt,
img_x + 248 - tt : img_x + 248 - tt + 2,
] = (80, 180, 255)
frame[
img_y + 148 - tt : img_y + 352 + tt,
img_x + 332 + tt - 2 : img_x + 332 + tt,
] = (80, 180, 255)
frame[
img_y + 148 - tt : img_y + 148 - tt + 2,
img_x + 248 - tt : img_x + 332 + tt,
] = (80, 180, 255)
frame[
img_y + 352 + tt - 2 : img_y + 352 + tt,
img_x + 248 - tt : img_x + 332 + tt,
] = (80, 180, 255)
return frame
yolo_clip = VideoClip(make_yolo_frame, duration=STEP_DUR).with_fps(FPS)
text_clips: list[VideoClip] = [yolo_clip]
labels = [
("YOLO — You Only Look Once", 28, "#FFE082", FONT_B, (80, 20)),
(
"Jednoetapowy detektor: siatka SxS → wszystkie detekcje naraz!",
18,
"#B0BEC5",
FONT_R,
(80, 65),
),
("Siatka 7x7 = 49 komórek", 16, "#64B5F6", FONT_R, (600, 180)),
("Każda komórka predykuje:", 16, "white", FONT_R, (600, 220)),
(" • B bbox (x, y, w, h, conf)", 14, "#B0BEC5", FONT_R, (600, 255)),
(" • C klas (prawdopodobieństwa)", 14, "#B0BEC5", FONT_R, (600, 285)),
("Komórka odpowiada za obiekt", 14, "#A5D6A7", FONT_R, (600, 325)),
("którego ŚRODEK w niej wpada", 14, "#A5D6A7", FONT_R, (600, 350)),
("45-155 fps! (vs 5 fps Faster R-CNN)", 18, "#EF9A9A", FONT_B, (600, 400)),
(
"Jedno przejście przez sieć → WSZYSTKIE detekcje naraz → NMS → wynik",
14,
"#78909C",
FONT_R,
(80, 620),
),
(
"Two-stage (R-CNN): propozycje+klasyfikacja "
"| One-stage (YOLO): bez propozycji!",
14,
"#90CAF9",
FONT_R,
(80, 655),
),
]
for text, fs, color, font, pos in labels:
tc = (
_tc(text=text, font_size=fs, color=color, font=font)
.with_duration(STEP_DUR)
.with_position(pos)
)
text_clips.append(tc)
slides.append(
CompositeVideoClip(text_clips, size=(W, H)).with_effects(
[FadeIn(0.3), FadeOut(0.3)]
)
)
return slides

View File

@ -0,0 +1,459 @@
"""YOLO architecture detail and DETR transformer detection."""
from __future__ import annotations
from _q24_common import (
BG_COLOR,
FONT_B,
FONT_R,
FPS,
STEP_DUR,
H,
W,
_tc,
_text_slide,
)
from moviepy import CompositeVideoClip, VideoClip
from moviepy.video.fx import FadeIn, FadeOut
import numpy as np
# ── YOLO Architecture Detail ──────────────────────────────────────
def _yolo_architecture() -> list[CompositeVideoClip]:
"""Show YOLO architecture: backbone → head, output tensor."""
slides = []
# Slide 1: YOLO architecture breakdown
def make_yolo_arch(t: float) -> np.ndarray:
frame = np.zeros((H, W, 3), dtype=np.uint8)
frame[:] = BG_COLOR
progress = min(t / (STEP_DUR * 0.7), 1.0)
# Pipeline: Image → Backbone → Neck → Head → SxSx(B*5+C) tensor
blocks = [
((60, 280), (100, 80), (50, 70, 90), "Obraz"),
((200, 280), (100, 80), (70, 130, 200), "Backbone"),
((340, 280), (100, 80), (200, 160, 80), "Neck"),
((480, 280), (100, 80), (200, 100, 60), "Head"),
((620, 280), (160, 80), (80, 200, 120), "SxSx(B*5+C)"),
]
n_blocks = min(int(progress * 5) + 1, 5)
for i, ((bx, by), (bw, bh), color, _lbl) in enumerate(blocks):
if i < n_blocks:
frame[by : by + bh, bx : bx + bw] = color
frame[by : by + 2, bx : bx + bw] = tuple(
min(c + 50, 255) for c in color
)
frame[by + bh - 2 : by + bh, bx : bx + bw] = tuple(
min(c + 50, 255) for c in color
)
arrow_limit = 4
if i < arrow_limit:
ax = bx + bw + 5
ay = by + bh // 2
frame[ay - 1 : ay + 2, ax : ax + 25] = (150, 150, 170)
# Output tensor breakdown (right side)
tensor_phase = 0.6
if progress > tensor_phase:
# Show SxS grid
gx, gy = 850, 180
gs = 120
gn = 4 # simplified from 7
gc = gs // gn
for r in range(gn):
for c in range(gn):
x = gx + c * gc
y = gy + r * gc
frame[y : y + gc - 1, x : x + gc - 1] = (40, 50, 65)
# Highlight one cell
frame[gy + gc : gy + 2 * gc - 1, gx + gc : gx + 2 * gc - 1] = (
80,
200,
120,
)
return frame
arch_clip = VideoClip(make_yolo_arch, duration=STEP_DUR + 1).with_fps(FPS)
dur = STEP_DUR + 1
labels = [
("YOLO: Architektura — krok po kroku", 26, "#FFE082", FONT_B, (80, 20)),
(
"One-stage: JEDEN forward pass → WSZYSTKIE detekcje naraz",
17,
"#B0BEC5",
FONT_R,
(80, 60),
),
("Obraz", 13, "white", FONT_R, (85, 295)),
("Backbone", 13, "white", FONT_R, (215, 295)),
("(ResNet/", 11, "#78909C", FONT_R, (210, 370)),
("Darknet)", 11, "#78909C", FONT_R, (210, 390)),
("Neck", 13, "white", FONT_R, (365, 295)),
("(FPN/", 11, "#78909C", FONT_R, (360, 370)),
("PANet)", 11, "#78909C", FONT_R, (360, 390)),
("Head", 13, "white", FONT_R, (505, 295)),
("(conv)", 11, "#78909C", FONT_R, (500, 370)),
("Tensor wyjścia", 13, "#A5D6A7", FONT_R, (640, 295)),
("Każda komórka SxS predykuje:", 15, "#FFE082", FONT_R, (830, 320)),
(" B bbox x (x,y,w,h,conf)", 13, "#B0BEC5", FONT_R, (830, 350)),
(" + C klas (prob.)", 13, "#B0BEC5", FONT_R, (830, 375)),
("= SxSx(Bx5+C) tensor", 13, "#A5D6A7", FONT_R, (830, 400)),
("Np. 7x7x(2x5+20) = 7x7x30", 13, "#78909C", FONT_R, (830, 430)),
(
"Two-stage (R-CNN): (1) propozycje → (2) klasyfikacja = 2 przejścia",
15,
"#EF9A9A",
FONT_R,
(80, 470),
),
(
"One-stage (YOLO): siatka → predykcja all-in-one = 1 przejście!",
15,
"#A5D6A7",
FONT_R,
(80, 505),
),
(
"Ewolucja YOLO: v1(2016)→v3→v5→v8(2023, anchor-free, SOTA)",
16,
"#FFE082",
FONT_R,
(80, 555),
),
(
"SSD (2016): multi-scale feature maps → lepsza detekcja małych obiektów",
15,
"#64B5F6",
FONT_R,
(80, 595),
),
(
"FPN: łączy wczesne warstwy (małe obiekty) + późne (duże obiekty)",
15,
"#78909C",
FONT_R,
(80, 630),
),
]
text_clips: list[VideoClip] = [arch_clip]
for text, fs, color, font, pos in labels:
tc = (
_tc(text=text, font_size=fs, color=color, font=font)
.with_duration(dur)
.with_position(pos)
)
text_clips.append(tc)
slides.append(
CompositeVideoClip(text_clips, size=(W, H)).with_effects(
[FadeIn(0.3), FadeOut(0.3)]
)
)
return slides
# ── DETR ──────────────────────────────────────────────────────────
def _detr_demo() -> list[CompositeVideoClip]:
"""Animate DETR: transformer detection, object queries, no NMS."""
slides = []
# Slide 1: DETR pipeline
def make_detr_frame(t: float) -> np.ndarray:
frame = np.zeros((H, W, 3), dtype=np.uint8)
frame[:] = BG_COLOR
progress = min(t / (STEP_DUR * 0.7), 1.0)
# DETR pipeline: Image → Backbone → Encoder → Decoder → N predictions
blocks = [
((50, 260), (80, 60), (50, 70, 90)),
((170, 260), (90, 60), (70, 130, 200)),
((300, 260), (110, 60), (200, 120, 60)),
((450, 260), (110, 60), (200, 80, 160)),
((600, 260), (120, 60), (80, 200, 120)),
]
n_blocks = min(int(progress * 5) + 1, 5)
for i, ((bx, by), (bw, bh), color) in enumerate(blocks):
if i < n_blocks:
frame[by : by + bh, bx : bx + bw] = color
frame[by : by + 2, bx : bx + bw] = tuple(
min(c + 50, 255) for c in color
)
frame[by + bh - 2 : by + bh, bx : bx + bw] = tuple(
min(c + 50, 255) for c in color
)
arrow_limit = 4
if i < arrow_limit:
ax = bx + bw + 5
ay = by + bh // 2
frame[ay - 1 : ay + 2, ax : ax + 25] = (150, 150, 170)
# Object queries illustration (right side)
query_phase = 0.5
if progress > query_phase:
qx, qy = 800, 140
for i in range(6):
y = qy + i * 50
w = 130
active_limit = 3
active = i < active_limit
color = (80, 180, 120) if active else (60, 50, 50)
frame[y : y + 35, qx : qx + w] = color
frame[y : y + 1, qx : qx + w] = tuple(min(c + 40, 255) for c in color)
# Arrow from decoder to queries
frame[285:288, 723:798] = (150, 150, 170)
return frame
detr_clip = VideoClip(make_detr_frame, duration=STEP_DUR + 1).with_fps(FPS)
dur = STEP_DUR + 1
labels = [
("DETR: DEtection TRansformer (2020)", 26, "#FFE082", FONT_B, (80, 20)),
(
"Radykalnie prostszy pipeline: BEZ anchorów, BEZ NMS!",
17,
"#B0BEC5",
FONT_R,
(80, 60),
),
("Obraz", 12, "white", FONT_R, (65, 275)),
("Backbone", 12, "white", FONT_R, (185, 275)),
("Transformer", 12, "white", FONT_R, (310, 275)),
("Encoder", 12, "white", FONT_R, (325, 295)),
("Transformer", 12, "white", FONT_R, (460, 275)),
("Decoder", 12, "white", FONT_R, (478, 295)),
("N predykcji", 12, "white", FONT_R, (615, 275)),
("Object Queries:", 14, "#FFE082", FONT_B, (800, 115)),
("samochód 95%", 11, "white", FONT_R, (810, 148)),
("pies 88%", 11, "white", FONT_R, (810, 198)),
("rower 72%", 11, "white", FONT_R, (810, 248)),
("brak", 11, "#78909C", FONT_R, (810, 298)),
("brak", 11, "#78909C", FONT_R, (810, 348)),
("brak", 11, "#78909C", FONT_R, (810, 398)),
("100 wyuczonych queries", 13, "#FFE082", FONT_R, (800, 440)),
("→ każdy 'szuka' obiektu", 13, "#FFE082", FONT_R, (800, 465)),
]
text_clips: list[VideoClip] = [detr_clip]
for text, fs, color, font, pos in labels:
tc = (
_tc(text=text, font_size=fs, color=color, font=font)
.with_duration(dur)
.with_position(pos)
)
text_clips.append(tc)
slides.append(
CompositeVideoClip(text_clips, size=(W, H)).with_effects(
[FadeIn(0.3), FadeOut(0.3)]
)
)
# Slide 2: Why no NMS + Hungarian matching
detr_details = [
("DETR: Dlaczego bez NMS? — krok po kroku", 24, "#FFE082", FONT_B, (80, 30)),
(
"Problem NMS: duplikaty detekcji → ręcznie usuwaj post-hoc",
16,
"#EF9A9A",
FONT_R,
(80, 90),
),
(
"DETR rozwiązanie: Hungarian matching (dopasowanie węgierskie)",
17,
"#A5D6A7",
FONT_R,
(80, 130),
),
("", 10, "white", FONT_R, (80, 155)),
("Jak to działa podczas TRENINGU:", 17, "white", FONT_B, (80, 180)),
(
" 1. Sieć daje N=100 predykcji (queries)",
15,
"#64B5F6",
FONT_R,
(100, 220),
),
(
" 2. Na obrazie jest np. 5 obiektów (ground truth)",
15,
"#64B5F6",
FONT_R,
(100, 255),
),
(
" 3. Hungarian matching: optymalne dopasowanie 1:1",
15,
"#FFE082",
FONT_R,
(100, 290),
),
(
" → query_1 ↔ gt_samochód (najlepsze dopasowanie)",
14,
"#A5D6A7",
FONT_R,
(120, 325),
),
(" → query_7 ↔ gt_pies", 14, "#A5D6A7", FONT_R, (120, 355)),
(" → query_3 ↔ gt_rower", 14, "#A5D6A7", FONT_R, (120, 385)),
(
" → pozostałe 97 queries ↔ klasa 'brak obiektu'",
14,
"#78909C",
FONT_R,
(120, 415),
),
(
" 4. Każdy obiekt ma DOKŁADNIE 1 predykcję → BRAK duplikatów!",
15,
"#A5D6A7",
FONT_R,
(100, 455),
),
("", 10, "white", FONT_R, (100, 475)),
(
"Self-attention w encoderze: cechy obrazu 'rozmawiają' ze sobą",
15,
"#64B5F6",
FONT_R,
(80, 500),
),
(
"Cross-attention w decoderze: queries 'pytają' cechy obrazu",
15,
"#CE93D8",
FONT_R,
(80, 535),
),
(
"→ query 'rozumie' który fragment obrazu to 'jego' obiekt",
15,
"#FFE082",
FONT_R,
(80, 570),
),
(
"DETR = Detekcja Eliminująca Trikowe Redundancje (NMS, anchory)",
16,
"#FFE082",
FONT_R,
(80, 620),
),
(
"Wada: wolniejszy trening (O(n²) attention) | Zaleta: prostszy pipeline!",
15,
"#78909C",
FONT_R,
(80, 660),
),
]
slides.append(_text_slide(detr_details, duration=STEP_DUR + 1))
# Slide 3: Two-stage vs One-stage vs Transformer summary
summary_lines = [
(
"Podsumowanie: Two-stage vs One-stage vs Transformer",
22,
"#FFE082",
FONT_B,
(80, 30),
),
("", 10, "white", FONT_R, (80, 55)),
("TWO-STAGE (R-CNN family):", 18, "#EF9A9A", FONT_B, (80, 90)),
(
" (1) Generuj propozycje → (2) Klasyfikuj per region",
15,
"white",
FONT_R,
(100, 125),
),
(
" + Wysoka precyzja | - Wolniejsze (2 przejścia)",
15,
"#78909C",
FONT_R,
(100, 155),
),
(
" R-CNN → Fast R-CNN → Faster R-CNN (0.2s)",
15,
"#B0BEC5",
FONT_R,
(100, 185),
),
("", 10, "white", FONT_R, (80, 210)),
("ONE-STAGE (YOLO, SSD):", 18, "#A5D6A7", FONT_B, (80, 240)),
(
" Siatka → predykcja all-in-one (1 przejście)",
15,
"white",
FONT_R,
(100, 275),
),
(
" + Bardzo szybkie (45-155 fps) | - Historycznie mniej precyzyjne",
15,
"#78909C",
FONT_R,
(100, 305),
),
(
" YOLOv8 (2023): anchor-free, dorównuje two-stage!",
15,
"#B0BEC5",
FONT_R,
(100, 335),
),
("", 10, "white", FONT_R, (80, 360)),
("TRANSFORMER (DETR):", 18, "#CE93D8", FONT_B, (80, 390)),
(
" Object queries + self-attention (globalny kontekst)",
15,
"white",
FONT_R,
(100, 425),
),
(
" + Brak NMS/anchorów | - Wolniejszy trening (O(n²))",
15,
"#78909C",
FONT_R,
(100, 455),
),
(
" Hungarian matching → 1:1 obiekt↔predykcja → brak duplikatów",
15,
"#B0BEC5",
FONT_R,
(100, 485),
),
("", 10, "white", FONT_R, (80, 510)),
(
"Trend: coraz prostsze pipeline, mniej ręcznych komponentów",
17,
"white",
FONT_R,
(80, 540),
),
(
" R-CNN (SS+CNN+SVM+NMS) → YOLO "
"(backbone+head+NMS) → DETR (backbone+transformer)",
14,
"#90CAF9",
FONT_R,
(80, 580),
),
(
"Metryki: mAP@0.5 (standard), mAP@0.5:0.95 (surowsza), "
"IoU do dopasowania",
15,
"#78909C",
FONT_R,
(80, 630),
),
]
slides.append(_text_slide(summary_lines, duration=STEP_DUR + 1))
return slides

View File

@ -0,0 +1,235 @@
"""Common drawing primitives and constants for Pub/Sub diagrams."""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from typing import TYPE_CHECKING
import matplotlib as mpl
mpl.use("Agg")
import matplotlib.patches as mpatches
from matplotlib.patches import FancyBboxPatch
import matplotlib.pyplot as plt
if TYPE_CHECKING:
from matplotlib.axes import Axes
DPI = 300
BG = "white"
LN = "black"
FS = 9
FS_TITLE = 13
FIG_W = 8.27 # A4 width in inches
OUTPUT_DIR = str(Path(__file__).resolve().parent / "img")
Path(OUTPUT_DIR).mkdir(parents=True, exist_ok=True)
GRAY1 = "#E8E8E8"
GRAY2 = "#D0D0D0"
GRAY3 = "#B8B8B8"
GRAY4 = "#F5F5F5"
GRAY5 = "#C0C0C0"
@dataclass(frozen=True)
class BoxStyle:
"""Optional styling for boxes."""
fill: str = "white"
lw: float = 1.2
fontsize: float = FS
fontweight: str = "normal"
ha: str = "center"
va: str = "center"
rounded: bool = True
@dataclass(frozen=True)
class ArrowCfg:
"""Config for arrows."""
lw: float = 1.2
style: str = "->"
color: str = LN
label: str = ""
label_offset: float = 0.15
label_fs: float = 8
@dataclass(frozen=True)
class DashedCfg:
"""Config for dashed arrows."""
lw: float = 1.0
color: str = LN
label: str = ""
label_offset: float = 0.15
label_fs: float = 8
def draw_box(
ax: Axes,
pos: tuple[float, float],
size: tuple[float, float],
text: str,
style: BoxStyle | None = None,
) -> None:
"""Draw box."""
s = style or BoxStyle()
x, y = pos
w, h = size
if s.rounded:
rect = FancyBboxPatch(
(x, y),
w,
h,
boxstyle="round,pad=0.05",
lw=s.lw,
edgecolor=LN,
facecolor=s.fill,
)
else:
rect = mpatches.Rectangle(
(x, y),
w,
h,
lw=s.lw,
edgecolor=LN,
facecolor=s.fill,
)
ax.add_patch(rect)
ax.text(
x + w / 2,
y + h / 2,
text,
ha=s.ha,
va=s.va,
fontsize=s.fontsize,
fontweight=s.fontweight,
wrap=True,
)
def draw_arrow(
ax: Axes,
start: tuple[float, float],
end: tuple[float, float],
cfg: ArrowCfg | None = None,
) -> None:
"""Draw arrow."""
c = cfg or ArrowCfg()
ax.annotate(
"",
xy=end,
xytext=start,
arrowprops={
"arrowstyle": c.style,
"color": c.color,
"lw": c.lw,
},
)
if c.label:
mx = (start[0] + end[0]) / 2
my = (start[1] + end[1]) / 2 + c.label_offset
ax.text(
mx,
my,
c.label,
ha="center",
va="bottom",
fontsize=c.label_fs,
color=c.color,
)
def draw_dashed_arrow(
ax: Axes,
start: tuple[float, float],
end: tuple[float, float],
cfg: DashedCfg | None = None,
) -> None:
"""Draw dashed arrow."""
c = cfg or DashedCfg()
ax.annotate(
"",
xy=end,
xytext=start,
arrowprops={
"arrowstyle": "->",
"color": c.color,
"lw": c.lw,
"linestyle": "dashed",
},
)
if c.label:
mx = (start[0] + end[0]) / 2
my = (start[1] + end[1]) / 2 + c.label_offset
ax.text(
mx,
my,
c.label,
ha="center",
va="bottom",
fontsize=c.label_fs,
color=c.color,
)
def draw_cross(
ax: Axes,
pos: tuple[float, float],
size: float = 0.15,
lw: float = 2.5,
color: str = "black",
) -> None:
"""Draw cross."""
x, y = pos
ax.plot(
[x - size, x + size],
[y - size, y + size],
color=color,
lw=lw,
)
ax.plot(
[x - size, x + size],
[y + size, y - size],
color=color,
lw=lw,
)
def draw_check(
ax: Axes,
pos: tuple[float, float],
size: float = 0.15,
lw: float = 2.5,
color: str = "black",
) -> None:
"""Draw check."""
x, y = pos
ax.plot(
[x - size, x - size * 0.2],
[y, y - size * 0.7],
color=color,
lw=lw,
)
ax.plot(
[x - size * 0.2, x + size],
[y - size * 0.7, y + size * 0.5],
color=color,
lw=lw,
)
def save(fig: plt.Figure, name: str) -> None:
"""Save."""
plt.tight_layout()
fig.savefig(
str(Path(OUTPUT_DIR) / name),
dpi=DPI,
bbox_inches="tight",
facecolor=BG,
)
plt.close(fig)

View File

@ -0,0 +1,430 @@
"""QoS delivery guarantee diagrams: at-most-once, at-least-once, exactly-once."""
from __future__ import annotations
from _pubsub_common import (
FIG_W,
FS_TITLE,
GRAY1,
GRAY2,
GRAY3,
GRAY4,
LN,
ArrowCfg,
BoxStyle,
draw_arrow,
draw_box,
draw_check,
draw_cross,
draw_dashed_arrow,
save,
)
import matplotlib.pyplot as plt
# ============================================================
# 5. At-most-once (QoS 0)
# ============================================================
def draw_qos_at_most_once() -> None:
"""Draw qos at most once."""
fig, ax = plt.subplots(1, 1, figsize=(FIG_W, 4.5))
ax.set_xlim(0, 12)
ax.set_ylim(0, 6)
ax.set_aspect("equal")
ax.axis("off")
ax.set_title(
"QoS: At-most-once"
" \u2014 \u201ewy\u015blij i zapomnij\u201d"
" (0 lub 1 dostarczenie)",
fontsize=FS_TITLE,
fontweight="bold",
pad=12,
)
px, bx, sx = 1.0, 4.8, 8.5
pw, bw, sw = 2.0, 2.2, 2.0
bh = 0.8
bold10_g1 = BoxStyle(fill=GRAY1, fontsize=10, fontweight="bold")
bold10_g2 = BoxStyle(fill=GRAY2, fontsize=10, fontweight="bold")
draw_box(ax, (px, 5.0), (pw, bh), "Publisher", bold10_g1)
draw_box(ax, (bx, 5.0), (bw, bh), "Broker", bold10_g2)
draw_box(
ax,
(sx, 5.0),
(sw, bh),
"Subscriber",
bold10_g1,
)
for xc in [px + pw / 2, bx + bw / 2, sx + sw / 2]:
ax.plot(
[xc, xc],
[5.0, 1.2],
color=GRAY3,
lw=1,
linestyle=":",
)
# Scenario A: success
y = 4.3
ax.text(
0.2,
y + 0.15,
"Scenariusz A:",
fontsize=8.5,
fontweight="bold",
)
msg9 = ArrowCfg(label="MSG", label_fs=9)
draw_arrow(ax, (px + pw / 2, y), (bx + bw / 2, y), msg9)
draw_arrow(
ax,
(bx + bw / 2, y - 0.6),
(sx + sw / 2, y - 0.6),
msg9,
)
draw_check(ax, (sx + sw / 2 + 0.4, y - 0.6), size=0.18)
ax.text(
sx + sw / 2 + 0.7,
y - 0.6,
"OK",
fontsize=9,
fontweight="bold",
)
# Scenario B: lost
y = 2.6
ax.text(
0.2,
y + 0.15,
"Scenariusz B:",
fontsize=8.5,
fontweight="bold",
)
draw_arrow(ax, (px + pw / 2, y), (bx + bw / 2, y), msg9)
draw_dashed_arrow(ax, (bx + bw / 2, y - 0.6), (7.5, y - 0.6))
draw_cross(ax, (7.8, y - 0.6), size=0.2)
ax.text(
8.2,
y - 0.55,
"UTRACONA",
fontsize=9,
fontweight="bold",
)
ax.text(
8.2,
y - 1.0,
"(brak retransmisji)",
fontsize=8,
style="italic",
)
ax.text(
6.0,
0.5,
"Brak ACK, brak retransmisji."
" Najszybszy. Use case:"
" logi, metryki, telemetria.",
ha="center",
va="center",
fontsize=9,
bbox={
"boxstyle": "round,pad=0.4",
"facecolor": GRAY4,
"edgecolor": GRAY3,
},
)
save(fig, "pubsub_qos_at_most_once.png")
# ============================================================
# 6. At-least-once (QoS 1)
# ============================================================
def draw_qos_at_least_once() -> None:
"""Draw qos at least once."""
fig, ax = plt.subplots(1, 1, figsize=(FIG_W, 5.0))
ax.set_xlim(0, 12)
ax.set_ylim(0, 6.5)
ax.set_aspect("equal")
ax.axis("off")
ax.set_title(
"QoS: At-least-once"
" \u2014 \u201epowtarzaj a\u017c potwierdz\u0105\u201d"
" (\u22651 dostarczenie)",
fontsize=FS_TITLE,
fontweight="bold",
pad=12,
)
bx, bw = 3.5, 2.2
sx, sw = 8.0, 2.2
bh = 0.8
draw_box(
ax,
(bx, 5.5),
(bw, bh),
"Broker",
BoxStyle(fill=GRAY2, fontsize=10, fontweight="bold"),
)
draw_box(
ax,
(sx, 5.5),
(sw, bh),
"Subscriber",
BoxStyle(fill=GRAY1, fontsize=10, fontweight="bold"),
)
for xc in [bx + bw / 2, sx + sw / 2]:
ax.plot(
[xc, xc],
[5.5, 0.8],
color=GRAY3,
lw=1,
linestyle=":",
)
# Step 1: send MSG
y1 = 4.8
draw_arrow(
ax,
(bx + bw / 2, y1),
(sx + sw / 2, y1),
ArrowCfg(label="MSG #1", label_fs=9),
)
draw_check(ax, (sx + sw + 0.2, y1), size=0.15)
ax.text(sx + sw + 0.5, y1, "odebrano", fontsize=8)
# Step 2: ACK lost
y2 = 3.9
draw_dashed_arrow(
ax,
(sx + sw / 2, y2),
(bx + bw + 1.2, y2),
)
ax.text(
(bx + bw / 2 + sx + sw / 2) / 2,
y2 + 0.18,
"ACK",
fontsize=9,
)
draw_cross(ax, (bx + bw + 0.8, y2), size=0.18)
ax.text(
bx + 0.3,
y2 - 0.35,
"ACK utracony!",
fontsize=8.5,
style="italic",
)
# Step 3: timeout -> retry
y3 = 2.9
ax.text(
bx + bw / 2,
y3 + 0.45,
"timeout...",
fontsize=8.5,
style="italic",
ha="center",
)
draw_arrow(
ax,
(bx + bw / 2, y3),
(sx + sw / 2, y3),
ArrowCfg(label="MSG #1 (retry)", label_fs=9),
)
draw_check(ax, (sx + sw + 0.2, y3), size=0.15)
ax.text(
sx + sw + 0.5,
y3,
"odebrano\n(ponownie!)",
fontsize=8,
)
# Step 4: ACK ok
y4 = 2.0
draw_arrow(
ax,
(sx + sw / 2, y4),
(bx + bw / 2, y4),
ArrowCfg(label="ACK", label_fs=9),
)
draw_check(ax, (bx + bw / 2 - 0.5, y4), size=0.18)
# Duplicate bracket
ax.annotate(
"",
xy=(sx + sw + 1.3, y1),
xytext=(sx + sw + 1.3, y3),
arrowprops={
"arrowstyle": "<->",
"color": "black",
"lw": 1.2,
},
)
ax.text(
sx + sw + 1.6,
(y1 + y3) / 2,
"DUPLIKAT!\nSubscriber\notrzyma\u0142 2x",
fontsize=9,
ha="left",
va="center",
fontweight="bold",
bbox={
"boxstyle": "round,pad=0.25",
"facecolor": GRAY4,
"edgecolor": GRAY3,
},
)
ax.text(
6.0,
0.5,
"Broker czeka na ACK, retransmituje"
" po timeout. Mog\u0105 by\u0107 duplikaty!\n"
"Use case: zam\u00f3wienia, p\u0142atno\u015bci"
" (subscriber musi by\u0107 idempotentny).",
ha="center",
va="center",
fontsize=9,
bbox={
"boxstyle": "round,pad=0.4",
"facecolor": GRAY4,
"edgecolor": GRAY3,
},
)
save(fig, "pubsub_qos_at_least_once.png")
# ============================================================
# 7. Exactly-once (QoS 2)
# ============================================================
def draw_qos_exactly_once() -> None:
"""Draw qos exactly once."""
fig, ax = plt.subplots(1, 1, figsize=(FIG_W, 5.5))
ax.set_xlim(0, 12)
ax.set_ylim(0, 7)
ax.set_aspect("equal")
ax.axis("off")
ax.set_title(
"QoS: Exactly-once \u2014 4-krokowy"
" handshake (dok\u0142adnie 1 dostarczenie)",
fontsize=FS_TITLE,
fontweight="bold",
pad=12,
)
bx, bw = 2.5, 2.2
sx, sw = 7.5, 2.2
bh = 0.8
draw_box(
ax,
(bx, 6.0),
(bw, bh),
"Broker",
BoxStyle(fill=GRAY2, fontsize=10, fontweight="bold"),
)
draw_box(
ax,
(sx, 6.0),
(sw, bh),
"Subscriber",
BoxStyle(fill=GRAY1, fontsize=10, fontweight="bold"),
)
for xc in [bx + bw / 2, sx + sw / 2]:
ax.plot(
[xc, xc],
[6.0, 1.0],
color=GRAY3,
lw=1,
linestyle=":",
)
steps = [
(
5.2,
"right",
"PUBLISH (msg_id=42)",
"Broker wysy\u0142a wiadomo\u015b\u0107",
),
(
4.2,
"left",
"PUBREC (otrzyma\u0142em id=42)",
"Sub potwierdza odbi\u00f3r," " zapisuje id",
),
(
3.2,
"right",
"PUBREL (mo\u017cesz przetworzy\u0107)",
"Broker zwalnia wiadomo\u015b\u0107",
),
(
2.2,
"left",
"PUBCOMP (zako\u0144czone)",
"Sub potwierdza przetworzenie",
),
]
for i, (y, direction, label, desc) in enumerate(steps):
ax.text(
bx + bw / 2 - 0.7,
y,
f"{i + 1}",
fontsize=9,
fontweight="bold",
ha="center",
va="center",
bbox={
"boxstyle": "circle,pad=0.18",
"facecolor": GRAY3,
"edgecolor": LN,
},
)
if direction == "right":
draw_arrow(
ax,
(bx + bw / 2, y),
(sx + sw / 2, y),
ArrowCfg(label=label, label_fs=9),
)
else:
draw_arrow(
ax,
(sx + sw / 2, y),
(bx + bw / 2, y),
ArrowCfg(label=label, label_fs=9),
)
ax.text(
sx + sw + 0.3,
y,
desc,
fontsize=8,
ha="left",
va="center",
style="italic",
)
ax.text(
6.0,
0.6,
"Deduplikacja po msg_id."
" Sub nie przetwarza przed PUBREL.\n"
"Najkosztowniejszy (4 pakiety)."
" Use case: transakcje finansowe,"
" krytyczne zdarzenia.",
ha="center",
va="center",
fontsize=9,
bbox={
"boxstyle": "round,pad=0.4",
"facecolor": GRAY4,
"edgecolor": GRAY3,
},
)
save(fig, "pubsub_qos_exactly_once.png")

View File

@ -0,0 +1,239 @@
"""Subscription-type diagrams: topic-based and content-based."""
from __future__ import annotations
from _pubsub_common import (
FIG_W,
FS_TITLE,
GRAY1,
GRAY2,
GRAY3,
GRAY4,
ArrowCfg,
BoxStyle,
DashedCfg,
draw_arrow,
draw_box,
draw_dashed_arrow,
save,
)
import matplotlib.pyplot as plt
# ============================================================
# 1. Topic-based subscription
# ============================================================
def draw_sub_topic() -> None:
"""Draw sub topic."""
fig, ax = plt.subplots(1, 1, figsize=(FIG_W, 4.0))
ax.set_xlim(0, 12)
ax.set_ylim(0, 5.5)
ax.set_aspect("equal")
ax.axis("off")
ax.set_title(
"Subskrypcja topic-based" " \u2014 routing po nazwie tematu",
fontsize=FS_TITLE,
fontweight="bold",
pad=12,
)
bold10 = BoxStyle(fill=GRAY1, fontsize=10, fontweight="bold")
fs85 = BoxStyle(fill=GRAY1, fontsize=8.5)
draw_box(ax, (0.2, 3.2), (2.4, 1.1), "Publisher", bold10)
draw_box(
ax,
(0.3, 1.8),
(2.2, 0.8),
'topic: "orders"',
BoxStyle(fill=GRAY4, fontsize=8),
)
draw_box(
ax,
(0.3, 0.7),
(2.2, 0.8),
'topic: "payments"',
BoxStyle(fill=GRAY4, fontsize=8),
)
draw_box(
ax,
(4.2, 1.5),
(2.8, 2.2),
"BROKER\n\ntopic routing",
BoxStyle(fill=GRAY2, fontsize=10, fontweight="bold"),
)
draw_box(
ax,
(8.5, 3.8),
(3.0, 1.0),
'Subscriber A\nsubskrybuje: "orders"',
fs85,
)
draw_box(
ax,
(8.5, 2.2),
(3.0, 1.0),
'Subscriber B\nsubskrybuje: "payments"',
fs85,
)
draw_box(
ax,
(8.5, 0.6),
(3.0, 1.0),
'Subscriber C\nsubskrybuje: "orders"',
fs85,
)
fs8 = ArrowCfg(label_fs=8)
draw_arrow(ax, (2.6, 2.2), (4.2, 2.8), fs8)
draw_arrow(ax, (2.6, 1.1), (4.2, 2.2), fs8)
draw_arrow(
ax,
(7.0, 3.4),
(8.5, 4.2),
ArrowCfg(label='"orders"', label_fs=8),
)
draw_arrow(
ax,
(7.0, 2.6),
(8.5, 2.7),
ArrowCfg(label='"payments"', label_fs=8),
)
draw_arrow(
ax,
(7.0, 2.2),
(8.5, 1.2),
ArrowCfg(label='"orders"', label_fs=8),
)
ax.text(
6.0,
0.1,
"Subscriber deklaruje nazw\u0119 tematu."
" Broker kieruje wiadomo\u015bci\n"
"do WSZYSTKICH subscriber\u00f3w"
" danego tematu. Najprostszy model.",
ha="center",
va="bottom",
fontsize=8.5,
style="italic",
bbox={
"boxstyle": "round,pad=0.3",
"facecolor": GRAY4,
"edgecolor": GRAY3,
},
)
save(fig, "pubsub_sub_topic.png")
# ============================================================
# 2. Content-based subscription
# ============================================================
def draw_sub_content() -> None:
"""Draw sub content."""
fig, ax = plt.subplots(1, 1, figsize=(FIG_W, 4.5))
ax.set_xlim(0, 12)
ax.set_ylim(0, 6)
ax.set_aspect("equal")
ax.axis("off")
ax.set_title(
"Subskrypcja content-based"
" \u2014 filtrowanie po tre\u015bci wiadomo\u015bci",
fontsize=FS_TITLE,
fontweight="bold",
pad=12,
)
bold10 = BoxStyle(fill=GRAY1, fontsize=10, fontweight="bold")
draw_box(ax, (0.2, 3.5), (2.4, 1.1), "Publisher", bold10)
draw_box(
ax,
(0.2, 1.8),
(2.4, 1.2),
'price: 150\ntype: "book"\ncategory: "IT"',
BoxStyle(fill=GRAY4, fontsize=8.5),
)
draw_box(
ax,
(4.0, 2.0),
(3.0, 2.5),
"BROKER\n\newaluuje filtry\n" "ka\u017cdego subscribera",
BoxStyle(fill=GRAY2, fontsize=9, fontweight="bold"),
)
fs9 = BoxStyle(fill=GRAY1, fontsize=9)
draw_box(
ax,
(8.5, 4.2),
(3.2, 1.0),
"Sub A\nfiltr: price > 100",
fs9,
)
draw_box(
ax,
(8.5, 2.6),
(3.2, 1.0),
'Sub B\nfiltr: type = "food"',
fs9,
)
draw_box(
ax,
(8.5, 1.0),
(3.2, 1.0),
"Sub C\nfiltr: price < 50",
fs9,
)
draw_arrow(ax, (2.6, 2.4), (4.0, 3.0))
draw_arrow(
ax,
(7.0, 4.0),
(8.5, 4.6),
ArrowCfg(
label="150 > 100 \u2713 dostarczono",
label_fs=8,
),
)
draw_dashed_arrow(
ax,
(7.0, 3.2),
(8.5, 3.1),
DashedCfg(
label='"book" \u2260 "food"' " \u2717 odrzucono",
label_fs=8,
),
)
draw_dashed_arrow(
ax,
(7.0, 2.5),
(8.5, 1.6),
DashedCfg(
label="150 < 50 \u2717 odrzucono",
label_fs=8,
),
)
ax.text(
6.0,
0.2,
"Broker analizuje TRE\u015a\u0106 wiadomo\u015bci"
" i ewaluuje predykaty.\n"
"Bardziej elastyczny ni\u017c topic-based,"
" ale wolniejszy (koszt ewaluacji).",
ha="center",
va="bottom",
fontsize=8.5,
style="italic",
bbox={
"boxstyle": "round,pad=0.3",
"facecolor": GRAY4,
"edgecolor": GRAY3,
},
)
save(fig, "pubsub_sub_content.png")

View File

@ -0,0 +1,279 @@
"""Subscription-type diagrams: type-based and hierarchical."""
from __future__ import annotations
from _pubsub_common import (
FIG_W,
FS_TITLE,
GRAY1,
GRAY2,
GRAY3,
GRAY4,
ArrowCfg,
BoxStyle,
draw_arrow,
draw_box,
save,
)
import matplotlib.pyplot as plt
# ============================================================
# 3. Type-based subscription
# ============================================================
def draw_sub_type() -> None:
"""Draw sub type."""
fig, ax = plt.subplots(1, 1, figsize=(FIG_W, 5.0))
ax.set_xlim(0, 12)
ax.set_ylim(0, 6.5)
ax.set_aspect("equal")
ax.axis("off")
ax.set_title(
"Subskrypcja type-based" " \u2014 routing po typie (klasie) obiektu",
fontsize=FS_TITLE,
fontweight="bold",
pad=12,
)
bold10 = BoxStyle(fill=GRAY1, fontsize=10, fontweight="bold")
draw_box(ax, (0.2, 4.2), (2.4, 1.1), "Publisher", bold10)
fs9_g4 = BoxStyle(fill=GRAY4, fontsize=9)
draw_box(
ax,
(0.1, 2.8),
(2.6, 0.9),
"new OrderEvent()",
fs9_g4,
)
draw_box(
ax,
(0.1, 1.5),
(2.6, 0.9),
"new PaymentEvent()",
fs9_g4,
)
draw_box(
ax,
(4.0, 2.3),
(3.0, 2.4),
"BROKER\n\nrouting po\ntypie klasy",
BoxStyle(fill=GRAY2, fontsize=10, fontweight="bold"),
)
fs9 = BoxStyle(fill=GRAY1, fontsize=9)
draw_box(
ax,
(8.5, 4.8),
(3.2, 1.0),
"Sub A\n\u2192 OrderEvent",
fs9,
)
draw_box(
ax,
(8.5, 3.2),
(3.2, 1.0),
"Sub B\n\u2192 PaymentEvent",
fs9,
)
draw_box(
ax,
(8.5, 1.6),
(3.2, 1.0),
"Sub C\n\u2192 Event (base)",
fs9,
)
draw_arrow(ax, (2.7, 3.2), (4.0, 3.8))
draw_arrow(ax, (2.7, 2.0), (4.0, 3.0))
draw_arrow(
ax,
(7.0, 4.3),
(8.5, 5.2),
ArrowCfg(label="OrderEvent", label_fs=8),
)
draw_arrow(
ax,
(7.0, 3.5),
(8.5, 3.7),
ArrowCfg(label="PaymentEvent", label_fs=8),
)
draw_arrow(
ax,
(7.0, 3.0),
(8.5, 2.2),
ArrowCfg(label="oba (dziedziczenie!)", label_fs=8),
)
hx, hy = 0.5, 0.0
draw_box(
ax,
(hx + 2.0, hy + 0.2),
(1.8, 0.6),
"Event",
BoxStyle(fill=GRAY3, fontsize=8, fontweight="bold"),
)
draw_box(
ax,
(hx, hy + 0.2),
(1.8, 0.6),
"OrderEvent",
BoxStyle(fill=GRAY4, fontsize=7.5),
)
draw_box(
ax,
(hx + 4.0, hy + 0.2),
(2.0, 0.6),
"PaymentEvent",
BoxStyle(fill=GRAY4, fontsize=7.5),
)
draw_arrow(
ax,
(hx + 2.9, hy + 0.2),
(hx + 0.9, hy + 0.2),
ArrowCfg(
lw=1.0,
label="extends",
label_offset=-0.3,
label_fs=7,
),
)
draw_arrow(
ax,
(hx + 2.9, hy + 0.2),
(hx + 5.0, hy + 0.2),
ArrowCfg(
lw=1.0,
label="extends",
label_offset=-0.3,
label_fs=7,
),
)
ax.text(
9.5,
0.5,
"Sub C subskrybuje bazowy Event\n" "\u2192 otrzymuje WSZYSTKIE podtypy",
ha="center",
va="center",
fontsize=8.5,
style="italic",
bbox={
"boxstyle": "round,pad=0.3",
"facecolor": GRAY4,
"edgecolor": GRAY3,
},
)
save(fig, "pubsub_sub_type.png")
# ============================================================
# 4. Hierarchical / Wildcards subscription
# ============================================================
def draw_sub_hierarchical() -> None:
"""Draw sub hierarchical."""
fig, ax = plt.subplots(1, 1, figsize=(FIG_W, 5.5))
ax.set_xlim(0, 12)
ax.set_ylim(0, 7)
ax.set_aspect("equal")
ax.axis("off")
ax.set_title(
"Subskrypcja hierarchiczna (wildcards)" " \u2014 wzorce temat\u00f3w",
fontsize=FS_TITLE,
fontweight="bold",
pad=12,
)
bold10 = BoxStyle(fill=GRAY2, fontsize=10, fontweight="bold")
draw_box(ax, (4.5, 5.8), (2.4, 0.8), "sensors/", bold10)
fs9_g3 = BoxStyle(fill=GRAY3, fontsize=9)
draw_box(
ax,
(1.5, 4.2),
(2.4, 0.8),
"temperature/",
fs9_g3,
)
draw_box(
ax,
(7.5, 4.2),
(2.4, 0.8),
"humidity/",
fs9_g3,
)
fs85_g4 = BoxStyle(fill=GRAY4, fontsize=8.5)
draw_box(ax, (0.2, 2.8), (1.8, 0.7), "room1", fs85_g4)
draw_box(ax, (2.4, 2.8), (1.8, 0.7), "room2", fs85_g4)
draw_box(ax, (6.8, 2.8), (1.8, 0.7), "room1", fs85_g4)
draw_box(ax, (9.0, 2.8), (1.8, 0.7), "room2", fs85_g4)
thin = ArrowCfg(lw=1.0)
draw_arrow(ax, (5.7, 5.8), (2.7, 5.0), thin)
draw_arrow(ax, (5.7, 5.8), (8.7, 5.0), thin)
draw_arrow(ax, (2.2, 4.2), (1.1, 3.5), thin)
draw_arrow(ax, (3.2, 4.2), (3.3, 3.5), thin)
draw_arrow(ax, (8.2, 4.2), (7.7, 3.5), thin)
draw_arrow(ax, (9.2, 4.2), (9.9, 3.5), thin)
ax.text(
1.1,
2.4,
"sensors/temperature/room1",
fontsize=7,
ha="center",
fontfamily="monospace",
style="italic",
)
ax.text(
3.3,
2.4,
"sensors/temperature/room2",
fontsize=7,
ha="center",
fontfamily="monospace",
style="italic",
)
ax.text(
0.3,
1.5,
"Wzorce subskrypcji (MQTT-style):",
fontsize=10,
fontweight="bold",
)
patterns = [
(
'"sensors/temperature/room1"',
"\u2192 TYLKO room1",
"(dok\u0142adne dopasowanie)",
),
(
'"sensors/temperature/*"',
"\u2192 room1, room2",
"( * = jeden poziom)",
),
(
'"sensors/#"',
"\u2192 WSZYSTKO",
"( # = dowolna g\u0142\u0119boko\u015b\u0107)",
),
]
for i, (pat, result, note) in enumerate(patterns):
yy = 0.9 - i * 0.55
ax.text(
0.5,
yy,
pat,
fontsize=9,
fontweight="bold",
fontfamily="monospace",
)
ax.text(7.0, yy, result, fontsize=9, fontweight="bold")
ax.text(9.5, yy, note, fontsize=8, style="italic")
save(fig, "pubsub_sub_hierarchical.png")

View File

@ -0,0 +1,421 @@
"""Spark Streaming, Lambda/Kappa architecture, and exactly-once diagrams for Q20."""
from __future__ import annotations
from _q20_common import (
FS,
FS_LABEL,
FS_SMALL,
FS_TITLE,
GRAY1,
GRAY2,
GRAY3,
GRAY4,
GRAY5,
LN,
draw_arrow,
draw_box,
draw_table,
plt,
save_fig,
)
# ============================================================
# 12. Spark Streaming architecture
# ============================================================
def gen_spark_streaming_arch() -> None:
"""Gen spark streaming arch."""
fig, ax = plt.subplots(figsize=(9, 5))
ax.set_xlim(0, 12)
ax.set_ylim(0, 7)
ax.set_aspect("auto")
ax.axis("off")
ax.set_title(
"Spark Streaming — architektura (micro-batch)",
fontsize=FS_TITLE,
fontweight="bold",
pad=10,
)
# Cluster border
draw_box(ax, 0.3, 0.5, 11.4, 5.8, "", fill=GRAY4, rounded=True, lw=2.5)
ax.text(
6.0, 6.0, "SPARK CLUSTER", fontsize=FS_LABEL, ha="center", fontweight="bold"
)
# Driver
draw_box(
ax,
1.0,
4.5,
3.0,
1.2,
"Driver\n(planuje mini-batche)",
fill=GRAY2,
fontsize=FS,
fontweight="bold",
)
draw_arrow(ax, 2.5, 4.5, 6.0, 4.0, lw=1.5)
# Batches
batches = ["batch 1\n(e1,e2,e3)", "batch 2\n(e4,e5,e6)", "batch 3\n(e7,e8,e9)"]
for i, b in enumerate(batches):
y = 2.8 - i * 1.0
draw_box(
ax, 4.5, y, 2.5, 0.8, b, fill=GRAY1, fontsize=FS_SMALL, fontweight="bold"
)
# map → reduce
draw_arrow(ax, 7.0, y + 0.4, 7.5, y + 0.4, lw=1)
draw_box(ax, 7.5, y, 1.3, 0.8, "map→\nreduce", fill=GRAY3, fontsize=5.5)
draw_arrow(ax, 8.8, y + 0.4, 9.3, y + 0.4, lw=1)
draw_box(
ax, 9.3, y, 1.5, 0.8, f"result {i + 1}", fill="white", fontsize=FS_SMALL
)
# Spark ecosystem
draw_box(
ax,
1.0,
1.0,
3.0,
1.0,
"Spark SQL / MLlib\n(ten sam ekosystem!)",
fill=GRAY5,
fontsize=FS,
fontweight="bold",
)
ax.text(
6.0,
0.3,
"ZALETA: batch API | WADA: latencja ≥ batch interval (~100ms)",
fontsize=FS,
ha="center",
fontweight="bold",
bbox={"boxstyle": "round,pad=0.2", "facecolor": "white", "edgecolor": LN},
)
save_fig(fig, "q20_spark_streaming_arch.png")
# ============================================================
# 13. Lambda vs Kappa architecture
# ============================================================
def gen_lambda_vs_kappa() -> None:
"""Gen lambda vs kappa."""
fig, axes = plt.subplots(2, 1, figsize=(10, 7))
fig.suptitle("Architektura Lambda vs Kappa", fontsize=FS_TITLE, fontweight="bold")
# --- Lambda ---
ax = axes[0]
ax.set_xlim(0, 12)
ax.set_ylim(0, 5)
ax.set_aspect("auto")
ax.axis("off")
ax.set_title(
"LAMBDA — 2 ścieżki (batch + speed)", fontsize=FS_LABEL, fontweight="bold"
)
# Source
draw_box(
ax,
0.3,
1.8,
2.0,
1.5,
"Źródło\ndanych",
fill=GRAY2,
fontsize=FS,
fontweight="bold",
)
# Batch layer (top)
draw_box(
ax,
3.5,
3.3,
3.0,
1.2,
"Batch Layer\n(Spark)\nprzelicza co godzinę",
fill=GRAY1,
fontsize=FS_SMALL,
fontweight="bold",
)
draw_arrow(ax, 2.3, 3.0, 3.5, 3.9, lw=1.5)
# Speed layer (bottom)
draw_box(
ax,
3.5,
0.8,
3.0,
1.2,
"Speed Layer\n(Flink)\nreal-time",
fill=GRAY3,
fontsize=FS_SMALL,
fontweight="bold",
)
draw_arrow(ax, 2.3, 2.2, 3.5, 1.4, lw=1.5)
# Results
draw_box(
ax,
7.5,
3.3,
2.0,
1.2,
"Dokładne\nwyniki\n(wolne)",
fill=GRAY4,
fontsize=FS_SMALL,
)
draw_arrow(ax, 6.5, 3.9, 7.5, 3.9, lw=1.5)
draw_box(
ax,
7.5,
0.8,
2.0,
1.2,
"Przybliżone\nwyniki\n(szybkie)",
fill=GRAY4,
fontsize=FS_SMALL,
)
draw_arrow(ax, 6.5, 1.4, 7.5, 1.4, lw=1.5)
# Merge
draw_box(
ax,
10.0,
2.0,
1.5,
1.5,
"MERGE\n→ UI",
fill=GRAY5,
fontsize=FS,
fontweight="bold",
)
draw_arrow(ax, 9.5, 3.5, 10.0, 3.0, lw=1.5)
draw_arrow(ax, 9.5, 1.8, 10.0, 2.5, lw=1.5)
ax.text(
6.0,
0.1,
"2 systemy, 2 kody — złożone ale pewne",
fontsize=FS,
ha="center",
style="italic",
bbox={"boxstyle": "round,pad=0.2", "facecolor": GRAY4, "edgecolor": GRAY5},
)
# --- Kappa ---
ax = axes[1]
ax.set_xlim(0, 12)
ax.set_ylim(0, 4)
ax.set_aspect("auto")
ax.axis("off")
ax.set_title(
"KAPPA — 1 ścieżka (streaming only)", fontsize=FS_LABEL, fontweight="bold"
)
# Source
draw_box(
ax,
0.3,
1.3,
2.0,
1.5,
"Źródło\ndanych",
fill=GRAY2,
fontsize=FS,
fontweight="bold",
)
# Single streaming layer
draw_box(
ax,
3.5,
1.3,
3.5,
1.5,
"Streaming Layer\n(Flink)\n+ replay z Kafka log",
fill=GRAY1,
fontsize=FS,
fontweight="bold",
)
draw_arrow(ax, 2.3, 2.05, 3.5, 2.05, lw=2)
# Output
draw_box(
ax,
8.0,
1.3,
2.5,
1.5,
"Wyniki\n→ UI",
fill=GRAY4,
fontsize=FS,
fontweight="bold",
)
draw_arrow(ax, 7.0, 2.05, 8.0, 2.05, lw=2)
# Replay arrow
ax.annotate(
"",
xy=(3.5, 1.0),
xytext=(7.0, 1.0),
arrowprops={
"arrowstyle": "<-",
"lw": 1.5,
"color": LN,
"connectionstyle": "arc3,rad=0.3",
"linestyle": "--",
},
)
ax.text(
5.25,
0.3,
"Replay z Kafka\n(przetwórz historię od nowa)",
fontsize=FS_SMALL,
ha="center",
style="italic",
)
ax.text(
6.0,
3.3,
"1 system, 1 kod — prostsze, ale replay = dużo I/O",
fontsize=FS,
ha="center",
style="italic",
bbox={"boxstyle": "round,pad=0.2", "facecolor": GRAY4, "edgecolor": GRAY5},
)
fig.tight_layout(rect=[0, 0, 1, 0.92])
save_fig(fig, "q20_lambda_vs_kappa.png")
# ============================================================
# 14. Lambda vs Kappa comparison table
# ============================================================
def gen_lambda_kappa_table() -> None:
"""Gen lambda kappa table."""
fig, ax = plt.subplots(figsize=(8, 3.5))
ax.set_xlim(0, 10)
ax.set_ylim(-4.5, 1)
ax.set_aspect("auto")
ax.axis("off")
ax.set_title(
"Lambda vs Kappa — porównanie", fontsize=FS_TITLE, fontweight="bold", pad=10
)
headers = ["Cecha", "Lambda", "Kappa"]
col_w = [2.5, 3.5, 3.5]
rows = [
["Ścieżki", "2 (batch + speed)", "1 (streaming)"],
["Kod", "2 implementacje", "1 implementacja"],
["Złożoność", "wysoka", "niska"],
["Replay", "batch przelicza", "Kafka replay"],
["Spójność", "merge wymagany", "natywna"],
["Przykład", "Netflix, LinkedIn", "Uber, Confluent"],
]
draw_table(
ax, headers, rows, x0=0.25, y0=0.5, col_widths=col_w, row_h=0.55, fontsize=7.5
)
save_fig(fig, "q20_lambda_kappa_table.png")
# ============================================================
# 15. Exactly-once comparison
# ============================================================
def gen_exactly_once() -> None:
"""Gen exactly once."""
fig, ax = plt.subplots(figsize=(9, 5))
ax.set_xlim(0, 12)
ax.set_ylim(0, 7)
ax.set_aspect("auto")
ax.axis("off")
ax.set_title(
"Exactly-Once — mechanizmy na 3 platformach",
fontsize=FS_TITLE,
fontweight="bold",
pad=10,
)
# Flink
draw_box(ax, 0.3, 4.3, 11.0, 2.0, "", fill=GRAY4, rounded=True, lw=1.5)
ax.text(
1.0,
5.9,
"Flink — Distributed Snapshots (Chandy-Lamport)",
fontsize=FS,
fontweight="bold",
)
flink_steps = ["source", "|B|", "map()", "|B|", "sink()"]
bx = 1.0
for s in flink_steps:
if s == "|B|":
ax.text(
bx + 0.25,
4.85,
s,
fontsize=FS,
ha="center",
fontweight="bold",
bbox={"boxstyle": "round,pad=0.1", "facecolor": GRAY5, "edgecolor": LN},
)
draw_arrow(ax, bx - 0.1, 4.85, bx + 0.05, 4.85, lw=1)
bx += 0.7
else:
draw_box(
ax,
bx,
4.6,
1.5,
0.55,
s,
fill=GRAY1,
fontsize=FS_SMALL,
fontweight="bold",
)
bx += 1.8
ax.text(
8.5,
5.0,
"barrier → save state\n→ checkpoint (HDFS/S3)",
fontsize=FS_SMALL,
style="italic",
)
# Kafka Streams
draw_box(ax, 0.3, 2.3, 11.0, 1.5, "", fill=GRAY1, rounded=True, lw=1.5)
ax.text(
1.0, 3.5, "Kafka Streams — Transakcje Kafka", fontsize=FS, fontweight="bold"
)
ax.text(
1.5,
2.85,
"idempotent producer + begin TX → produce → commit TX → consumer offsets w TX",
fontsize=FS_SMALL,
)
# Spark
draw_box(ax, 0.3, 0.5, 11.0, 1.5, "", fill=GRAY3, rounded=True, lw=1.5)
ax.text(
1.0,
1.7,
"Spark Streaming — Write-Ahead Log (WAL)",
fontsize=FS,
fontweight="bold",
)
ax.text(
1.5,
1.05,
"WAL + checkpointing micro-batchów + idempotent sinks (np. upsert do DB)",
fontsize=FS_SMALL,
)
save_fig(fig, "q20_exactly_once.png")

View File

@ -0,0 +1,449 @@
"""Batch vs streaming concept and window type diagrams for Q20."""
from __future__ import annotations
from typing import TYPE_CHECKING
from _q20_common import (
FS,
FS_LABEL,
FS_SMALL,
FS_TITLE,
GRAY1,
GRAY2,
GRAY3,
GRAY4,
GRAY5,
LN,
draw_arrow,
draw_box,
plt,
save_fig,
)
import matplotlib.patches as mpatches
if TYPE_CHECKING:
from matplotlib.axes import Axes
# ============================================================
# 1. Batch vs Streaming concept
# ============================================================
def gen_batch_vs_streaming() -> None:
"""Gen batch vs streaming."""
fig, axes = plt.subplots(2, 1, figsize=(9, 5))
fig.suptitle(
"Batch vs Streaming — dwa modele przetwarzania",
fontsize=FS_TITLE,
fontweight="bold",
)
# Batch
ax = axes[0]
ax.set_xlim(0, 12)
ax.set_ylim(0, 3)
ax.set_aspect("auto")
ax.axis("off")
ax.set_title("BATCH (wsadowe)", fontsize=FS_LABEL, fontweight="bold")
# Data collected
draw_box(
ax,
0.5,
0.8,
3.0,
1.4,
"Zbierz WSZYSTKIE\ndane\n(godziny / dni)",
fill=GRAY1,
fontsize=FS,
fontweight="bold",
)
draw_arrow(ax, 3.5, 1.5, 4.5, 1.5, lw=2)
draw_box(
ax,
4.5,
0.8,
2.5,
1.4,
"Analiza\n(batch job)",
fill=GRAY2,
fontsize=FS,
fontweight="bold",
)
draw_arrow(ax, 7.0, 1.5, 8.0, 1.5, lw=2)
draw_box(
ax,
8.0,
0.8,
2.5,
1.4,
"Wynik\n(jednorazowy)",
fill=GRAY3,
fontsize=FS,
fontweight="bold",
)
ax.text(11.0, 1.5, "min-h", fontsize=FS, va="center", fontweight="bold")
# Streaming
ax = axes[1]
ax.set_xlim(0, 12)
ax.set_ylim(0, 3)
ax.set_aspect("auto")
ax.axis("off")
ax.set_title("STREAMING (strumieniowe)", fontsize=FS_LABEL, fontweight="bold")
# Events flowing
events_x = [0.5, 1.5, 2.5, 3.5]
for i, ex in enumerate(events_x):
draw_box(
ax,
ex,
1.0,
0.8,
0.8,
f"e{i + 1}",
fill=GRAY4,
fontsize=FS,
fontweight="bold",
rounded=False,
)
if i < len(events_x) - 1:
draw_arrow(ax, ex + 0.8, 1.4, ex + 1.0, 1.4, lw=1)
ax.text(4.8, 1.4, "...", fontsize=FS_LABEL, va="center")
draw_arrow(ax, 5.2, 1.4, 5.8, 1.4, lw=2)
draw_box(
ax,
5.8,
0.8,
2.8,
1.4,
"Analiza\nCIĄGŁA\n(event-by-event)",
fill=GRAY2,
fontsize=FS,
fontweight="bold",
)
draw_arrow(ax, 8.6, 1.5, 9.3, 1.5, lw=2)
draw_box(
ax,
9.3,
0.8,
2.0,
1.4,
"Wyniki\nciągłe",
fill=GRAY3,
fontsize=FS,
fontweight="bold",
)
ax.text(11.5, 0.5, "ms-s", fontsize=FS, va="center", fontweight="bold")
# Arrow marking infinity
ax.annotate(
"",
xy=(0.2, 1.4),
xytext=(-0.3, 1.4),
arrowprops={"arrowstyle": "->", "lw": 1.5, "color": LN},
)
ax.text(0.0, 2.3, "∞ zdarzeń", fontsize=FS_SMALL, ha="center", style="italic")
fig.tight_layout(rect=[0, 0, 1, 0.92])
save_fig(fig, "q20_batch_vs_streaming.png")
# ============================================================
# 2. All 4 window types (TSSG)
# ============================================================
def _draw_tumbling_window(ax: Axes, events: list[int]) -> None:
"""Draw tumbling window section."""
ax.set_xlim(0, 14)
ax.set_ylim(0, 4)
ax.set_aspect("auto")
ax.axis("off")
ax.set_title(
"Tumbling Window (okno przerzutne) — rozłączne, stały rozmiar",
fontsize=FS_LABEL,
fontweight="bold",
)
# Time axis
ax.annotate(
"",
xy=(13.5, 1.0),
xytext=(0.3, 1.0),
arrowprops={"arrowstyle": "->", "lw": 1.5, "color": LN},
)
ax.text(13.5, 0.6, "czas", fontsize=FS_SMALL, ha="center")
# Events
for i, e in enumerate(events):
x = 1.0 + i * 1.0
ax.plot(x, 1.0, "ko", markersize=5)
ax.text(x, 0.5, f"e{e}", fontsize=FS_SMALL, ha="center")
# Windows
colors_w = [GRAY1, GRAY3, GRAY1, GRAY3]
for w in range(4):
x_start = 1.0 + w * 3.0 - 0.3
rect = mpatches.FancyBboxPatch(
(x_start, 1.5),
3.0,
1.2,
boxstyle="round,pad=0.1",
facecolor=colors_w[w],
edgecolor=LN,
lw=1.5,
)
ax.add_patch(rect)
ax.text(
x_start + 1.5,
2.1,
f"Okno {w + 1}",
fontsize=FS,
ha="center",
fontweight="bold",
)
# Braces down to events
for j in range(3):
ex = 1.0 + w * 3.0 + j * 1.0
ax.plot([ex, ex], [1.0, 1.5], color=LN, lw=0.8, linestyle="--")
ax.text(
7.0,
3.2,
"Każde zdarzenie → DOKŁADNIE 1 okno. Zero nakładania.",
fontsize=FS,
ha="center",
style="italic",
bbox={"boxstyle": "round,pad=0.2", "facecolor": GRAY4, "edgecolor": GRAY5},
)
def _draw_sliding_window(ax: Axes, events: list[int]) -> None:
"""Draw sliding window section."""
ax.set_xlim(0, 14)
ax.set_ylim(0, 5)
ax.set_aspect("auto")
ax.axis("off")
ax.set_title(
"Sliding Window (okno przesuwne) — nakładające, stały rozmiar + krok",
fontsize=FS_LABEL,
fontweight="bold",
)
ax.annotate(
"",
xy=(13.5, 1.0),
xytext=(0.3, 1.0),
arrowprops={"arrowstyle": "->", "lw": 1.5, "color": LN},
)
ax.text(13.5, 0.6, "czas", fontsize=FS_SMALL, ha="center")
for i, e in enumerate(events[:8]):
x = 1.0 + i * 1.0
ax.plot(x, 1.0, "ko", markersize=5)
ax.text(x, 0.5, f"e{e}", fontsize=FS_SMALL, ha="center")
# Sliding windows: size=4, slide=2
slide_colors = [GRAY1, GRAY2, GRAY3]
for w in range(3):
x_start = 0.7 + w * 2.0
y_base = 1.5 + w * 0.9
rect = mpatches.FancyBboxPatch(
(x_start, y_base),
4.0,
0.7,
boxstyle="round,pad=0.08",
facecolor=slide_colors[w],
edgecolor=LN,
lw=1.5,
alpha=0.7,
)
ax.add_patch(rect)
ax.text(
x_start + 2.0,
y_base + 0.35,
f"Okno {w + 1} (size=4)",
fontsize=FS_SMALL,
ha="center",
fontweight="bold",
)
ax.text(
10.5,
3.5,
"krok=2\nNakładanie!\ne3,e4 → w oknie 1 i 2",
fontsize=FS,
ha="center",
style="italic",
bbox={"boxstyle": "round,pad=0.2", "facecolor": GRAY4, "edgecolor": GRAY5},
)
def _draw_session_window(ax: Axes) -> None:
"""Draw session window section."""
ax.set_xlim(0, 14)
ax.set_ylim(0, 4)
ax.set_aspect("auto")
ax.axis("off")
ax.set_title(
"Session Window (okno sesji) — dynamiczny rozmiar, gap = przerwa",
fontsize=FS_LABEL,
fontweight="bold",
)
ax.annotate(
"",
xy=(13.5, 1.0),
xytext=(0.3, 1.0),
arrowprops={"arrowstyle": "->", "lw": 1.5, "color": LN},
)
ax.text(13.5, 0.6, "czas", fontsize=FS_SMALL, ha="center")
# Cluster 1: events close together
cluster1 = [1.0, 1.8, 2.3, 3.0]
for x in cluster1:
ax.plot(x, 1.0, "ko", markersize=5)
# Gap
ax.annotate(
"",
xy=(7.0, 0.7),
xytext=(4.0, 0.7),
arrowprops={"arrowstyle": "<->", "lw": 1, "color": LN},
)
ax.text(
5.5,
0.3,
"GAP > timeout",
fontsize=FS,
ha="center",
fontweight="bold",
style="italic",
)
# Cluster 2
cluster2 = [8.0, 8.8, 9.5]
for x in cluster2:
ax.plot(x, 1.0, "ko", markersize=5)
# Session boxes
rect1 = mpatches.FancyBboxPatch(
(0.7, 1.4),
2.6,
1.0,
boxstyle="round,pad=0.1",
facecolor=GRAY1,
edgecolor=LN,
lw=1.5,
)
ax.add_patch(rect1)
ax.text(
2.0, 1.9, "Sesja 1\n(4 zdarzenia)", fontsize=FS, ha="center", fontweight="bold"
)
rect2 = mpatches.FancyBboxPatch(
(7.7, 1.4),
2.1,
1.0,
boxstyle="round,pad=0.1",
facecolor=GRAY3,
edgecolor=LN,
lw=1.5,
)
ax.add_patch(rect2)
ax.text(
8.75, 1.9, "Sesja 2\n(3 zdarzenia)", fontsize=FS, ha="center", fontweight="bold"
)
ax.text(
5.5,
3.0,
"Nowa sesja po przerwie > gap",
fontsize=FS,
ha="center",
style="italic",
)
def _draw_global_window(ax: Axes) -> None:
"""Draw global window section."""
ax.set_xlim(0, 14)
ax.set_ylim(0, 4)
ax.set_aspect("auto")
ax.axis("off")
ax.set_title(
"Global Window — jedno okno na cały strumień + trigger",
fontsize=FS_LABEL,
fontweight="bold",
)
ax.annotate(
"",
xy=(13.5, 1.0),
xytext=(0.3, 1.0),
arrowprops={"arrowstyle": "->", "lw": 1.5, "color": LN},
)
ax.text(13.5, 0.6, "czas", fontsize=FS_SMALL, ha="center")
for i in range(12):
x = 1.0 + i * 1.0
ax.plot(x, 1.0, "ko", markersize=5)
# One big window
rect = mpatches.FancyBboxPatch(
(0.5, 1.4),
12.5,
1.0,
boxstyle="round,pad=0.1",
facecolor=GRAY1,
edgecolor=LN,
lw=2,
)
ax.add_patch(rect)
ax.text(
6.75,
1.9,
"GLOBAL WINDOW (cały strumień)",
fontsize=FS,
ha="center",
fontweight="bold",
)
# Trigger markers
for tx in [4.0, 8.0, 12.0]:
ax.plot([tx, tx], [1.4, 2.4], color=LN, lw=2, linestyle="--")
ax.text(
tx,
2.7,
"EMIT",
fontsize=FS_SMALL,
ha="center",
fontweight="bold",
bbox={"boxstyle": "round,pad=0.1", "facecolor": GRAY3, "edgecolor": LN},
)
ax.text(
6.75,
3.3,
"Trigger decyduje kiedy emitować (np. co N zdarzeń)",
fontsize=FS,
ha="center",
style="italic",
)
def gen_window_types() -> None:
"""Gen window types."""
fig, axes = plt.subplots(4, 1, figsize=(9, 10))
fig.suptitle("4 typy okien — TSSG", fontsize=FS_TITLE, fontweight="bold")
events = list(range(1, 13))
_draw_tumbling_window(axes[0], events)
_draw_sliding_window(axes[1], events)
_draw_session_window(axes[2])
_draw_global_window(axes[3])
fig.tight_layout(rect=[0, 0, 1, 0.94])
save_fig(fig, "q20_window_types.png")

View File

@ -0,0 +1,180 @@
"""Common utilities and constants for Q20 diagram generation.
Monochrome, A4-printable PNGs (300 DPI).
"""
from __future__ import annotations
import logging
from pathlib import Path
from typing import TYPE_CHECKING
import matplotlib as mpl
mpl.use("Agg")
import matplotlib.patches as mpatches
from matplotlib.patches import FancyBboxPatch
import matplotlib.pyplot as plt
import numpy as np
if TYPE_CHECKING:
from matplotlib.axes import Axes
from matplotlib.figure import Figure
_logger = logging.getLogger(__name__)
rng = np.random.default_rng(42)
DPI = 300
BG = "white"
LN = "black"
FS = 8
FS_TITLE = 11
FS_SMALL = 6.5
FS_LABEL = 9
OUTPUT_DIR = str(Path(__file__).resolve().parent / "img")
Path(OUTPUT_DIR).mkdir(parents=True, exist_ok=True)
GRAY1 = "#E8E8E8"
GRAY2 = "#D0D0D0"
GRAY3 = "#B8B8B8"
GRAY4 = "#F5F5F5"
GRAY5 = "#C0C0C0"
def draw_box(
ax: Axes,
x: float,
y: float,
w: float,
h: float,
text: str,
*,
fill: str = "white",
lw: float = 1.2,
fontsize: float = FS,
fontweight: str = "normal",
ha: str = "center",
va: str = "center",
rounded: bool = True,
edgecolor: str = LN,
linestyle: str = "-",
) -> None:
"""Draw box."""
if rounded:
rect = FancyBboxPatch(
(x, y),
w,
h,
boxstyle="round,pad=0.05",
lw=lw,
edgecolor=edgecolor,
facecolor=fill,
linestyle=linestyle,
)
else:
rect = mpatches.Rectangle(
(x, y),
w,
h,
lw=lw,
edgecolor=edgecolor,
facecolor=fill,
linestyle=linestyle,
)
ax.add_patch(rect)
ax.text(
x + w / 2,
y + h / 2,
text,
ha=ha,
va=va,
fontsize=fontsize,
fontweight=fontweight,
wrap=True,
)
def draw_arrow(
ax: Axes,
x1: float,
y1: float,
x2: float,
y2: float,
lw: float = 1.2,
style: str = "->",
color: str = LN,
) -> None:
"""Draw arrow."""
ax.annotate(
"",
xy=(x2, y2),
xytext=(x1, y1),
arrowprops={"arrowstyle": style, "color": color, "lw": lw},
)
def save_fig(fig: Figure, name: str) -> None:
"""Save fig."""
path = str(Path(OUTPUT_DIR) / name)
fig.savefig(path, dpi=DPI, bbox_inches="tight", facecolor=BG, pad_inches=0.15)
plt.close(fig)
_logger.info(" Saved: %s", path)
def draw_table(
ax: Axes,
headers: list[str],
rows: list[list[str]],
x0: float,
y0: float,
col_widths: list[float],
row_h: float = 0.4,
header_fill: str = GRAY2,
row_fills: list[str] | None = None,
fontsize: float = FS,
header_fontsize: float | None = None,
) -> None:
"""Draw table."""
if header_fontsize is None:
header_fontsize = fontsize
len(headers)
# Header
cx = x0
for j, hdr in enumerate(headers):
draw_box(
ax,
cx,
y0,
col_widths[j],
row_h,
hdr,
fill=header_fill,
fontsize=header_fontsize,
fontweight="bold",
rounded=False,
)
cx += col_widths[j]
# Rows
for i, row in enumerate(rows):
cy = y0 - (i + 1) * row_h
cx = x0
fill = GRAY4 if (i % 2 == 0) else "white"
if row_fills and i < len(row_fills):
fill = row_fills[i]
for j, cell in enumerate(row):
fw = "bold" if j == 0 else "normal"
draw_box(
ax,
cx,
cy,
col_widths[j],
row_h,
cell,
fill=fill,
fontsize=fontsize,
fontweight=fw,
rounded=False,
)
cx += col_widths[j]

View File

@ -0,0 +1,240 @@
"""Late data strategies and decision tree diagrams for Q20."""
from __future__ import annotations
from _q20_common import (
FS,
FS_LABEL,
FS_SMALL,
FS_TITLE,
GRAY1,
GRAY2,
GRAY3,
GRAY4,
GRAY5,
draw_arrow,
draw_box,
plt,
save_fig,
)
# ============================================================
# 16. Late data strategies (DRAS)
# ============================================================
def gen_late_data_strategies() -> None:
"""Gen late data strategies."""
fig, ax = plt.subplots(figsize=(9, 5.5))
ax.set_xlim(0, 12)
ax.set_ylim(0, 7)
ax.set_aspect("auto")
ax.axis("off")
ax.set_title(
"Late Data — 4 strategie (mnemonik DRAS)",
fontsize=FS_TITLE,
fontweight="bold",
pad=10,
)
# Setup: window closed, late event arrives
draw_box(
ax,
0.5,
5.5,
4.5,
1.0,
"Okno [14:00-14:05]\nZAMKNIĘTE o 14:05",
fill=GRAY2,
fontsize=FS,
fontweight="bold",
)
draw_box(
ax,
6.0,
5.5,
4.5,
1.0,
"Spóźnione zdarzenie\nevent_time=14:00:03\narrives=14:05:30",
fill="#F8D7DA",
fontsize=FS_SMALL,
fontweight="bold",
)
draw_arrow(ax, 10.5, 6.0, 5.0, 6.0, lw=2, color="#C62828", style="->")
ax.text(
7.5,
5.2,
"LATE!",
fontsize=FS_LABEL,
ha="center",
fontweight="bold",
color="#C62828",
)
# 4 strategies
strategies = [
("D — Drop", "Odrzuć spóźnione", "/dev/null", GRAY4),
("R — Recompute", "Przelicz okno ponownie", "poprawne ale kosztowne", GRAY1),
(
"A — Allowed lateness",
"Czekaj dodatkowy czas\n(np. +2 min)",
"kompromis pamięci",
GRAY2,
),
(
"S — Side output",
"Przekieruj do osobnej\nkolejki",
"elastyczne, ręczna analiza",
GRAY3,
),
]
for i, (name, desc, tradeoff, color) in enumerate(strategies):
y = 3.8 - i * 1.1
draw_box(ax, 0.5, y, 2.5, 0.9, name, fill=color, fontsize=FS, fontweight="bold")
ax.text(3.3, y + 0.45, desc, fontsize=FS_SMALL, va="center")
ax.text(
8.5,
y + 0.45,
tradeoff,
fontsize=FS_SMALL,
va="center",
style="italic",
color="#555",
)
save_fig(fig, "q20_late_data_strategies.png")
# ============================================================
# 17. Decision tree — which platform
# ============================================================
def gen_decision_tree() -> None:
"""Gen decision tree."""
fig, ax = plt.subplots(figsize=(10, 5.5))
ax.set_xlim(0, 12)
ax.set_ylim(0, 7)
ax.set_aspect("auto")
ax.axis("off")
ax.set_title(
"Drzewo decyzyjne — wybór platformy",
fontsize=FS_TITLE,
fontweight="bold",
pad=10,
)
# Root question
draw_box(
ax,
3.5,
5.5,
4.5,
1.0,
"Latencja < 10ms\nwymagana?",
fill=GRAY2,
fontsize=FS,
fontweight="bold",
)
# TAK branch
draw_arrow(ax, 3.5, 5.7, 2.0, 5.0, lw=1.5)
ax.text(2.3, 5.3, "TAK", fontsize=FS, fontweight="bold")
draw_box(
ax,
0.3,
3.5,
3.5,
1.0,
"Dane już w Kafce?\nProste transformacje?",
fill=GRAY1,
fontsize=FS,
fontweight="bold",
)
# TAK → Kafka Streams
draw_arrow(ax, 0.3, 3.7, -0.1, 3.0, lw=1.5)
ax.text(0.0, 3.3, "TAK", fontsize=FS_SMALL, fontweight="bold")
draw_box(
ax,
-0.3,
1.8,
2.5,
1.0,
"Kafka\nStreams",
fill=GRAY5,
fontsize=FS_LABEL,
fontweight="bold",
)
# NIE → Flink
draw_arrow(ax, 3.8, 3.7, 4.5, 3.0, lw=1.5)
ax.text(4.0, 3.3, "NIE\n(złożona logika)", fontsize=FS_SMALL)
draw_box(
ax,
3.0,
1.8,
2.5,
1.0,
"Apache\nFlink",
fill=GRAY5,
fontsize=FS_LABEL,
fontweight="bold",
)
# NIE branch
draw_arrow(ax, 8.0, 5.7, 9.5, 5.0, lw=1.5)
ax.text(8.7, 5.3, "NIE", fontsize=FS, fontweight="bold")
draw_box(
ax,
7.5,
3.5,
4.2,
1.0,
"~100ms-1s OK?\nPotrzeba ML / SQL?",
fill=GRAY1,
fontsize=FS,
fontweight="bold",
)
# TAK + ML → Spark
draw_arrow(ax, 9.5, 3.5, 9.5, 3.0, lw=1.5)
ax.text(10.0, 3.3, "TAK + ML/SQL", fontsize=FS_SMALL)
draw_box(
ax,
8.0,
1.8,
2.5,
1.0,
"Spark\nStreaming",
fill=GRAY5,
fontsize=FS_LABEL,
fontweight="bold",
)
# TAK + proste → Kafka Streams too
draw_arrow(ax, 7.5, 3.7, 6.5, 3.0, lw=1.5)
ax.text(6.3, 3.3, "proste + TAK", fontsize=FS_SMALL)
draw_box(
ax,
5.8,
1.8,
2.0,
1.0,
"Kafka\nStreams",
fill=GRAY5,
fontsize=FS,
fontweight="bold",
)
# Legend
ax.text(
6.0,
0.7,
"Reguła: Kafka Streams = najprostsze (library) | "
"Flink = najpotężniejszy (true streaming) | Spark = ekosystem ML",
fontsize=FS,
ha="center",
bbox={"boxstyle": "round,pad=0.2", "facecolor": GRAY4, "edgecolor": GRAY5},
)
save_fig(fig, "q20_decision_tree.png")

View File

@ -0,0 +1,471 @@
"""Streaming ecosystem, micro-batch, platform comparison, and engine diagrams for Q20."""
from __future__ import annotations
from _q20_common import (
FS,
FS_LABEL,
FS_SMALL,
FS_TITLE,
GRAY1,
GRAY2,
GRAY3,
GRAY4,
GRAY5,
LN,
draw_arrow,
draw_box,
draw_table,
plt,
save_fig,
)
# ============================================================
# 7. Streaming ecosystem overview
# ============================================================
def gen_streaming_ecosystem() -> None:
"""Gen streaming ecosystem."""
fig, ax = plt.subplots(figsize=(10, 5.5))
ax.set_xlim(0, 12)
ax.set_ylim(0, 7)
ax.set_aspect("auto")
ax.axis("off")
ax.set_title(
"Ekosystem przetwarzania strumieniowego",
fontsize=FS_TITLE,
fontweight="bold",
pad=10,
)
# Source
draw_box(
ax,
0.3,
2.5,
2.0,
3.0,
"Kafka\nTopics\n(źródło)",
fill=GRAY2,
fontsize=FS,
fontweight="bold",
)
# Engines
engines = [
("Kafka Streams\n(library w JVM)", GRAY1, 4.7),
("Apache Flink\n(klaster)", GRAY3, 3.2),
("Spark Streaming\n(klaster)", GRAY5, 1.7),
]
for label, color, y in engines:
draw_box(
ax, 4.0, y, 3.0, 1.2, label, fill=color, fontsize=FS, fontweight="bold"
)
draw_arrow(ax, 2.3, 4.0, 4.0, y + 0.6, lw=1.5)
# Sinks
sinks = [
("Kafka topic\n/ baza danych", GRAY4, 4.7),
("DB / Kafka\n/ S3", GRAY4, 3.2),
("HDFS / DB\n/ dashboard", GRAY4, 1.7),
]
for label, color, y in sinks:
draw_box(ax, 8.5, y, 2.5, 1.2, label, fill=color, fontsize=FS)
draw_arrow(ax, 7.0, y + 0.6, 8.5, y + 0.6, lw=1.5)
# Labels
ax.text(1.3, 6.0, "ŹRÓDŁO", fontsize=FS_LABEL, ha="center", fontweight="bold")
ax.text(5.5, 6.2, "SILNIK", fontsize=FS_LABEL, ha="center", fontweight="bold")
ax.text(9.75, 6.2, "WYNIK", fontsize=FS_LABEL, ha="center", fontweight="bold")
# Latency annotations
ax.text(5.5, 5.95, "~1-10 ms", fontsize=FS_SMALL, ha="center", style="italic")
ax.text(5.5, 4.5, "<10 ms", fontsize=FS_SMALL, ha="center", style="italic")
ax.text(5.5, 3.0, "~100 ms", fontsize=FS_SMALL, ha="center", style="italic")
save_fig(fig, "q20_streaming_ecosystem.png")
# ============================================================
# 8. True streaming vs Micro-batch
# ============================================================
def gen_true_vs_microbatch() -> None:
"""Gen true vs microbatch."""
fig, axes = plt.subplots(2, 1, figsize=(10, 5.5))
fig.suptitle("True Streaming vs Micro-Batch", fontsize=FS_TITLE, fontweight="bold")
# True streaming
ax = axes[0]
ax.set_xlim(0, 12)
ax.set_ylim(0, 3.5)
ax.set_aspect("auto")
ax.axis("off")
ax.set_title(
"TRUE STREAMING (Flink, Kafka Streams) — event-by-event",
fontsize=FS_LABEL,
fontweight="bold",
)
for i in range(6):
x = 1.0 + i * 1.8
# Event
draw_box(
ax,
x,
2.0,
0.8,
0.7,
f"e{i + 1}",
fill=GRAY1,
fontsize=FS,
fontweight="bold",
rounded=False,
)
# Arrow down
draw_arrow(ax, x + 0.4, 2.0, x + 0.4, 1.4, lw=1)
# Result
draw_box(
ax,
x,
0.5,
0.8,
0.7,
f"r{i + 1}",
fill=GRAY3,
fontsize=FS,
fontweight="bold",
rounded=False,
)
# Latency label
ax.text(x + 0.4, 1.6, "~ms", fontsize=5, ha="center", color="#555")
ax.text(
11.5,
1.3,
"Latencja:\n< 10 ms",
fontsize=FS,
ha="center",
fontweight="bold",
bbox={"boxstyle": "round,pad=0.2", "facecolor": GRAY4, "edgecolor": GRAY5},
)
# Micro-batch
ax = axes[1]
ax.set_xlim(0, 12)
ax.set_ylim(0, 3.5)
ax.set_aspect("auto")
ax.axis("off")
ax.set_title(
"MICRO-BATCH (Spark Streaming) — grupami co ~100ms",
fontsize=FS_LABEL,
fontweight="bold",
)
batch_colors = [GRAY1, GRAY2, GRAY3]
for b in range(3):
bx = 0.8 + b * 3.5
# Batch boundary
draw_box(ax, bx, 1.8, 3.0, 1.0, "", fill=batch_colors[b], rounded=True, lw=1.5)
ax.text(
bx + 1.5, 2.6, f"Batch {b + 1}", fontsize=FS, ha="center", fontweight="bold"
)
for j in range(3):
ex = bx + 0.3 + j * 0.9
draw_box(
ax,
ex,
2.0,
0.7,
0.5,
f"e{b * 3 + j + 1}",
fill="white",
fontsize=FS_SMALL,
rounded=False,
)
# Arrow down
draw_arrow(ax, bx + 1.5, 1.8, bx + 1.5, 1.2, lw=1.5)
# Result
draw_box(
ax,
bx + 0.5,
0.4,
2.0,
0.7,
f"result {b + 1}",
fill=GRAY4,
fontsize=FS,
fontweight="bold",
)
ax.text(
11.5,
1.3,
"Latencja:\n~100ms-s",
fontsize=FS,
ha="center",
fontweight="bold",
bbox={"boxstyle": "round,pad=0.2", "facecolor": GRAY4, "edgecolor": GRAY5},
)
fig.tight_layout(rect=[0, 0, 1, 0.92])
save_fig(fig, "q20_true_vs_microbatch.png")
# ============================================================
# 9. Platform comparison table
# ============================================================
def gen_platform_comparison() -> None:
"""Gen platform comparison."""
fig, ax = plt.subplots(figsize=(9, 5))
ax.set_xlim(0, 11.5)
ax.set_ylim(-6, 1)
ax.set_aspect("auto")
ax.axis("off")
ax.set_title(
"Porównanie platform strumieniowych",
fontsize=FS_TITLE,
fontweight="bold",
pad=10,
)
headers = ["Cecha", "Kafka Streams", "Apache Flink", "Spark Streaming"]
col_w = [2.5, 2.8, 2.8, 2.8]
rows = [
["Model", "event-by-event", "event-by-event", "micro-batch (~100ms)"],
["Deployment", "library (w JVM)", "klaster", "klaster"],
["Latencja", "~1-10 ms", "< 10 ms", "100 ms - sekundy"],
["Exactly-once", "Kafka TXN", "checkpointing", "WAL"],
["State", "RocksDB local", "RocksDB + ckpt", "in-memory / ext"],
["Okna", "T, S, Session", "wszystkie + custom", "T, S"],
["Use case", "Kafka → Kafka", "złożona analityka", "ETL + ML / SQL"],
]
draw_table(
ax,
headers,
rows,
x0=0.25,
y0=0.5,
col_widths=col_w,
row_h=0.6,
fontsize=7,
header_fontsize=8,
)
save_fig(fig, "q20_platform_comparison.png")
# ============================================================
# 10. Kafka Streams architecture
# ============================================================
def gen_kafka_streams_arch() -> None:
"""Gen kafka streams arch."""
fig, ax = plt.subplots(figsize=(9, 5))
ax.set_xlim(0, 12)
ax.set_ylim(0, 7)
ax.set_aspect("auto")
ax.axis("off")
ax.set_title(
"Kafka Streams — architektura (library w JVM)",
fontsize=FS_TITLE,
fontweight="bold",
pad=10,
)
# Outer box: Your Java application
draw_box(ax, 0.5, 0.5, 11.0, 5.5, "", fill=GRAY4, rounded=True, lw=2.5)
ax.text(
6.0,
5.7,
"Twoja aplikacja Java (JVM)",
fontsize=FS_LABEL,
ha="center",
fontweight="bold",
)
# Kafka Consumer
draw_box(
ax,
1.0,
3.0,
2.5,
1.5,
"Kafka\nConsumer\n(input topic)",
fill=GRAY1,
fontsize=FS,
fontweight="bold",
)
# Processing
draw_box(
ax,
4.5,
3.0,
2.5,
1.5,
"Kafka Streams\n(logika\nbiznesowa)",
fill=GRAY2,
fontsize=FS,
fontweight="bold",
)
# Kafka Producer
draw_box(
ax,
8.0,
3.0,
2.5,
1.5,
"Kafka\nProducer\n(output topic)",
fill=GRAY1,
fontsize=FS,
fontweight="bold",
)
# Arrows
draw_arrow(ax, 3.5, 3.75, 4.5, 3.75, lw=2)
draw_arrow(ax, 7.0, 3.75, 8.0, 3.75, lw=2)
# RocksDB state store
draw_box(
ax,
4.5,
1.0,
2.5,
1.3,
"RocksDB\n(stan lokalny)",
fill=GRAY3,
fontsize=FS,
fontweight="bold",
)
ax.plot([5.75, 5.75], [3.0, 2.3], color=LN, lw=1.5)
ax.text(
7.3,
1.6,
"okna, joiny,\nagregacje",
fontsize=FS_SMALL,
style="italic",
va="center",
)
# Key message
ax.text(
6.0,
0.2,
"NIE potrzebujesz osobnego klastra! Skalujesz = więcej instancji JVM.",
fontsize=FS,
ha="center",
fontweight="bold",
bbox={"boxstyle": "round,pad=0.2", "facecolor": "white", "edgecolor": LN},
)
save_fig(fig, "q20_kafka_streams_arch.png")
# ============================================================
# 11. Flink architecture + checkpointing
# ============================================================
def gen_flink_arch() -> None:
"""Gen flink arch."""
fig, ax = plt.subplots(figsize=(9, 6))
ax.set_xlim(0, 12)
ax.set_ylim(0, 8)
ax.set_aspect("auto")
ax.axis("off")
ax.set_title(
"Apache Flink — architektura klastra + checkpointing",
fontsize=FS_TITLE,
fontweight="bold",
pad=10,
)
# Cluster border
draw_box(ax, 0.3, 1.0, 11.4, 6.2, "", fill=GRAY4, rounded=True, lw=2.5)
ax.text(
6.0, 6.95, "FLINK CLUSTER", fontsize=FS_LABEL, ha="center", fontweight="bold"
)
# Job Manager
draw_box(
ax,
1.0,
5.5,
3.0,
1.2,
"Job Manager\n(koordynacja,\ncheckpointy)",
fill=GRAY2,
fontsize=FS,
fontweight="bold",
)
# Task Managers
draw_box(ax, 1.0, 3.0, 10.0, 2.0, "", fill="white", rounded=True, lw=1.5)
ax.text(
6.0, 4.7, "Task Managers (workery)", fontsize=FS, ha="center", fontweight="bold"
)
slots = ["source\n& map()", "map()", "window()\n& reduce", "sink()"]
for i, s in enumerate(slots):
x = 1.5 + i * 2.4
draw_box(
ax,
x,
3.3,
2.0,
1.2,
f"Slot {i + 1}\n{s}",
fill=GRAY1,
fontsize=FS_SMALL,
fontweight="bold",
)
draw_arrow(ax, 2.5, 5.5, 6.0, 5.0, lw=1.5, style="->")
ax.text(5.0, 5.5, "przydziela\npodzadania", fontsize=FS_SMALL, style="italic")
# Checkpoint storage
draw_box(
ax,
5.5,
1.2,
3.5,
1.2,
"Checkpoint Storage\n(HDFS / S3)",
fill=GRAY3,
fontsize=FS,
fontweight="bold",
)
ax.plot([7.25, 7.25], [2.4, 3.3], color=LN, lw=1.5, linestyle="--")
ax.text(8.0, 2.7, "snapshoty\nstanu", fontsize=FS_SMALL, style="italic")
# Barrier concept at bottom
ax.text(3.0, 1.6, "Barrier:", fontsize=FS, fontweight="bold")
barrier_boxes = ["source", "|B|", "map", "|B|", "sink"]
bx = 0.8
for _i, b in enumerate(barrier_boxes):
if b == "|B|":
ax.text(
bx + 0.3,
1.5,
b,
fontsize=FS,
ha="center",
fontweight="bold",
bbox={"boxstyle": "round,pad=0.1", "facecolor": GRAY5, "edgecolor": LN},
)
draw_arrow(ax, bx, 1.5, bx + 0.1, 1.5, lw=1)
bx += 0.7
else:
draw_box(
ax,
bx,
1.3,
1.0,
0.45,
b,
fill=GRAY1,
fontsize=FS_SMALL,
fontweight="bold",
)
bx += 1.2
save_fig(fig, "q20_flink_arch.png")

View File

@ -0,0 +1,464 @@
"""Event time, fraud detection, SLA monitoring, and session diagrams for Q20."""
from __future__ import annotations
from _q20_common import (
FS,
FS_LABEL,
FS_SMALL,
FS_TITLE,
GRAY1,
GRAY3,
GRAY4,
GRAY5,
LN,
draw_box,
np,
plt,
rng,
save_fig,
)
import matplotlib.patches as mpatches
# ============================================================
# 3. Event Time vs Processing Time scatter + watermark
# ============================================================
def gen_event_vs_processing_time() -> None:
"""Gen event vs processing time."""
fig, axes = plt.subplots(1, 2, figsize=(11, 5))
fig.suptitle(
"Event Time vs Processing Time + Watermark",
fontsize=FS_TITLE,
fontweight="bold",
)
# --- Panel 1: Ideal vs Real ---
ax = axes[0]
ax.set_xlim(0, 10)
ax.set_ylim(0, 10)
ax.set_aspect("equal")
ax.set_xlabel("Event Time", fontsize=FS_LABEL)
ax.set_ylabel("Processing Time", fontsize=FS_LABEL)
ax.set_title("Idealny vs Realny świat", fontsize=FS_LABEL, fontweight="bold")
ax.set_xticks([])
ax.set_yticks([])
# Ideal line
ax.plot([0, 9], [0, 9], "k--", lw=1.5, label="ideał (brak opóźnień)")
# Real scattered points (processing >= event, some out of order)
event_times = np.sort(rng.uniform(1, 8, 15))
proc_times = event_times + rng.exponential(0.5, 15)
# Make some out of order
idx = [3, 7, 11]
for i in idx:
proc_times[i] += 1.5
ax.scatter(
event_times, proc_times, c="black", s=30, zorder=5, label="zdarzenia (realne)"
)
# Highlight out-of-order
for i in idx:
ax.annotate(
"out-of-order",
xy=(event_times[i], proc_times[i]),
xytext=(event_times[i] + 0.8, proc_times[i] + 0.5),
fontsize=FS_SMALL,
ha="left",
arrowprops={"arrowstyle": "->", "lw": 0.8, "color": "#555"},
)
ax.legend(fontsize=FS_SMALL, loc="upper left")
ax.text(
7,
2,
"Opóźnienie\nsieciowe ↑",
fontsize=FS,
ha="center",
style="italic",
color="#555",
)
# --- Panel 2: Watermark concept ---
ax = axes[1]
ax.set_xlim(0, 10)
ax.set_ylim(0, 10)
ax.set_aspect("equal")
ax.set_xlabel("Event Time", fontsize=FS_LABEL)
ax.set_ylabel("Processing Time", fontsize=FS_LABEL)
ax.set_title("Watermark — granica postępu", fontsize=FS_LABEL, fontweight="bold")
ax.set_xticks([])
ax.set_yticks([])
# Events
ax.scatter(event_times, proc_times, c="black", s=30, zorder=5)
# Watermark line (below most points, tracks progress)
wm_x = np.linspace(0, 9, 50)
wm_y = wm_x + 0.3 # watermark slightly above ideal
ax.plot(wm_x, wm_y, "k-", lw=2.5, label="Watermark")
ax.fill_between(wm_x, 0, wm_y, alpha=0.15, color="gray")
ax.text(
2.0,
1.0,
'PONIŻEJ watermark:\n„na pewno dotarło"',
fontsize=FS,
ha="center",
fontweight="bold",
bbox={"boxstyle": "round,pad=0.2", "facecolor": GRAY4, "edgecolor": GRAY5},
)
# Late event
late_x, late_y = event_times[7], proc_times[7]
ax.scatter(
[late_x], [late_y], c="white", s=80, zorder=6, edgecolors="black", linewidths=2
)
ax.annotate(
"LATE DATA!\n(po watermarku)",
xy=(late_x, late_y),
xytext=(late_x + 1.2, late_y + 0.8),
fontsize=FS_SMALL,
ha="left",
fontweight="bold",
arrowprops={"arrowstyle": "->", "lw": 1, "color": LN},
)
ax.legend(fontsize=FS_SMALL, loc="upper left")
fig.tight_layout(rect=[0, 0, 1, 0.92])
save_fig(fig, "q20_event_vs_processing_time.png")
# ============================================================
# 4. Tumbling window example — fraud detection
# ============================================================
def gen_tumbling_fraud() -> None:
"""Gen tumbling fraud."""
fig, ax = plt.subplots(figsize=(9, 4))
ax.set_xlim(0, 12)
ax.set_ylim(0, 5.5)
ax.set_aspect("auto")
ax.axis("off")
ax.set_title(
"Tumbling Window — fraud detection (okno = 1 min)",
fontsize=FS_TITLE,
fontweight="bold",
pad=10,
)
# Time axis
ax.annotate(
"",
xy=(11.5, 1.0),
xytext=(0.5, 1.0),
arrowprops={"arrowstyle": "->", "lw": 1.5, "color": LN},
)
ax.text(6.0, 0.4, "czas", fontsize=FS, ha="center")
# Window 1: normal
draw_box(ax, 1.0, 1.5, 4.5, 3.0, "", fill=GRAY4, rounded=True, lw=2)
ax.text(
3.25, 4.2, "[14:00 — 14:01]", fontsize=FS_LABEL, ha="center", fontweight="bold"
)
# Transactions
txns1 = ["Sklep A: 50 zł", "Sklep B: 30 zł", "Stacja: 80 zł"]
for i, t in enumerate(txns1):
draw_box(
ax,
1.3,
3.3 - i * 0.55,
4.0,
0.45,
t,
fill=GRAY1,
fontsize=FS_SMALL,
rounded=False,
)
ax.text(
3.25,
1.7,
"count = 3 → OK",
fontsize=FS,
ha="center",
fontweight="bold",
color="#2E7D32",
bbox={
"boxstyle": "round,pad=0.15",
"facecolor": "#E8F5E9",
"edgecolor": "#2E7D32",
},
)
# Window 2: fraud!
draw_box(ax, 6.0, 1.5, 4.5, 3.0, "", fill=GRAY1, rounded=True, lw=2)
ax.text(
8.25, 4.2, "[14:01 — 14:02]", fontsize=FS_LABEL, ha="center", fontweight="bold"
)
txns2 = ["ATM Warszawa: 500 zł", "ATM Kraków: 500 zł", "... +45 transakcji"]
for i, t in enumerate(txns2):
draw_box(
ax,
6.3,
3.3 - i * 0.55,
4.0,
0.45,
t,
fill=GRAY3,
fontsize=FS_SMALL,
rounded=False,
)
ax.text(
8.25,
1.7,
"count = 47 → ALERT!",
fontsize=FS,
ha="center",
fontweight="bold",
color="#C62828",
bbox={
"boxstyle": "round,pad=0.15",
"facecolor": "#F8D7DA",
"edgecolor": "#C62828",
},
)
save_fig(fig, "q20_tumbling_fraud.png")
# ============================================================
# 5. Sliding window — SLA monitoring
# ============================================================
def gen_sliding_sla() -> None:
"""Gen sliding sla."""
fig, ax = plt.subplots(figsize=(9, 4.5))
ax.set_xlim(0, 12)
ax.set_ylim(0, 6)
ax.set_aspect("auto")
ax.axis("off")
ax.set_title(
"Sliding Window — monitoring SLA (okno=5min, krok=1min)",
fontsize=FS_TITLE,
fontweight="bold",
pad=10,
)
# Time axis
ax.annotate(
"",
xy=(11.5, 0.5),
xytext=(0.5, 0.5),
arrowprops={"arrowstyle": "->", "lw": 1.5, "color": LN},
)
times = ["14:05", "14:06", "14:07", "14:08", "14:09"]
latencies = [120, 180, 340, 290, 150]
sla = 200
for i, (t, lat) in enumerate(zip(times, latencies, strict=False)):
x = 1.5 + i * 2.0
ax.text(x, 0.1, t, fontsize=FS, ha="center")
# Bar proportional to latency
bar_h = lat / 100.0
is_breach = lat > sla
fill = "#F8D7DA" if is_breach else GRAY1
edge = "#C62828" if is_breach else LN
draw_box(
ax,
x - 0.5,
1.0,
1.0,
bar_h,
"",
fill=fill,
rounded=False,
edgecolor=edge,
lw=1.5,
)
ax.text(
x,
1.0 + bar_h + 0.15,
f"{lat}ms",
fontsize=FS,
ha="center",
fontweight="bold",
color="#C62828" if is_breach else LN,
)
# Status
status = "ALERT!" if is_breach else "OK"
ax.text(
x,
1.0 + bar_h + 0.55,
status,
fontsize=FS_SMALL,
ha="center",
fontweight="bold",
color="#C62828" if is_breach else "#2E7D32",
)
# SLA line
sla_y = 1.0 + sla / 100.0
ax.plot([0.8, 11.2], [sla_y, sla_y], "k--", lw=1.5)
ax.text(11.3, sla_y, f"SLA={sla}ms", fontsize=FS, va="center", fontweight="bold")
# Sliding window bracket
ax.annotate(
"",
xy=(1.0, 5.3),
xytext=(5.0, 5.3),
arrowprops={"arrowstyle": "<->", "lw": 1.5, "color": LN},
)
ax.text(3.0, 5.6, "okno = 5 min", fontsize=FS, ha="center", fontweight="bold")
ax.annotate(
"",
xy=(3.0, 4.8),
xytext=(5.0, 4.8),
arrowprops={"arrowstyle": "<->", "lw": 1, "color": "#555"},
)
ax.text(
4.0,
4.4,
"krok = 1 min\n(nakładanie!)",
fontsize=FS_SMALL,
ha="center",
style="italic",
)
save_fig(fig, "q20_sliding_sla.png")
# ============================================================
# 6. Session window — user sessions
# ============================================================
def gen_session_users() -> None:
"""Gen session users."""
fig, axes = plt.subplots(2, 1, figsize=(10, 5))
fig.suptitle(
"Session Window — sesje użytkowników (gap = 30 min)",
fontsize=FS_TITLE,
fontweight="bold",
)
# Anna: 2 sessions
ax = axes[0]
ax.set_xlim(0, 14)
ax.set_ylim(0, 3.5)
ax.set_aspect("auto")
ax.axis("off")
ax.set_title("Użytkownik Anna", fontsize=FS_LABEL, fontweight="bold")
ax.annotate(
"",
xy=(13.5, 1.0),
xytext=(0.3, 1.0),
arrowprops={"arrowstyle": "->", "lw": 1.5, "color": LN},
)
# Clicks cluster 1
for x in [1.0, 1.8, 2.5, 3.2]:
ax.plot(x, 1.0, "ko", markersize=6)
# Clicks cluster 2
for x in [9.0, 9.8, 10.5]:
ax.plot(x, 1.0, "ko", markersize=6)
# Sessions
rect1 = mpatches.FancyBboxPatch(
(0.7, 1.5),
2.8,
1.2,
boxstyle="round,pad=0.1",
facecolor=GRAY1,
edgecolor=LN,
lw=1.5,
)
ax.add_patch(rect1)
ax.text(
2.1,
2.1,
"Sesja 1\n4 kliknięcia, 12 min",
fontsize=FS,
ha="center",
fontweight="bold",
)
rect2 = mpatches.FancyBboxPatch(
(8.7, 1.5),
2.1,
1.2,
boxstyle="round,pad=0.1",
facecolor=GRAY3,
edgecolor=LN,
lw=1.5,
)
ax.add_patch(rect2)
ax.text(
9.75,
2.1,
"Sesja 2\n3 kliknięcia, 8 min",
fontsize=FS,
ha="center",
fontweight="bold",
)
# Gap
ax.annotate(
"",
xy=(8.5, 0.5),
xytext=(3.8, 0.5),
arrowprops={"arrowstyle": "<->", "lw": 1.5, "color": LN},
)
ax.text(
6.15,
0.1,
"cisza 45 min > gap(30)",
fontsize=FS,
ha="center",
fontweight="bold",
style="italic",
)
# Bob: 1 session
ax = axes[1]
ax.set_xlim(0, 14)
ax.set_ylim(0, 3.5)
ax.set_aspect("auto")
ax.axis("off")
ax.set_title("Użytkownik Bob", fontsize=FS_LABEL, fontweight="bold")
ax.annotate(
"",
xy=(13.5, 1.0),
xytext=(0.3, 1.0),
arrowprops={"arrowstyle": "->", "lw": 1.5, "color": LN},
)
# Clicks spread evenly
bobs = [1.0, 2.5, 4.0, 5.5, 7.0, 8.5, 10.0]
for x in bobs:
ax.plot(x, 1.0, "ko", markersize=6)
rect = mpatches.FancyBboxPatch(
(0.7, 1.5),
9.6,
1.2,
boxstyle="round,pad=0.1",
facecolor=GRAY1,
edgecolor=LN,
lw=2,
)
ax.add_patch(rect)
ax.text(
5.5,
2.1,
"Sesja 1 (ciągła) — 7 kliknięć, każde < 30 min od poprzedniego",
fontsize=FS,
ha="center",
fontweight="bold",
)
fig.tight_layout(rect=[0, 0, 1, 0.92])
save_fig(fig, "q20_session_users.png")

View File

@ -0,0 +1,467 @@
"""FCN and U-Net architecture diagram generators."""
from __future__ import annotations
from typing import TYPE_CHECKING
from _q23_common import (
ACCENT,
ACCENT_LIGHT,
BLACK,
FS,
FS_SMALL,
FS_TINY,
FS_TITLE,
GRAY1,
GRAY2,
GRAY5,
GREEN_ACCENT,
RED_ACCENT,
_save_figure,
plt,
)
from matplotlib.patches import FancyBboxPatch
if TYPE_CHECKING:
from matplotlib.axes import Axes
def generate_fcn() -> None:
"""Generate fcn."""
_fig, axes = plt.subplots(2, 1, figsize=(10, 7))
# --- Panel 1: FC vs Conv 1x1 ---
ax = axes[0]
ax.set_xlim(0, 20)
ax.set_ylim(0, 6)
ax.axis("off")
ax.set_title(
"FC (Fully Connected) vs Conv 1x1", fontsize=FS_TITLE, fontweight="bold"
)
# Classic CNN with FC
layer_info_fc = [
(1.5, "Obraz\n224x224x3", 2.2, GRAY2),
(4.5, "Conv+Pool\n112x112x64", 1.8, GRAY2),
(7.5, "Conv+Pool\n7x7x512", 1.0, GRAY2),
(10, "Flatten\n25088", 0.5, ACCENT_LIGHT),
(12, "FC\n4096", 0.5, ACCENT_LIGHT),
(14, "FC\n1000", 0.3, ACCENT_LIGHT),
(16, '"Kot"', 0.3, "#FFCDD2"),
]
y_fc = 4.5
for i, (x, label, w, color) in enumerate(layer_info_fc):
rect = FancyBboxPatch(
(x - w / 2, y_fc - 0.6),
w,
1.2,
boxstyle="round,pad=0.05",
facecolor=color,
edgecolor=BLACK,
linewidth=0.8,
)
ax.add_patch(rect)
ax.text(x, y_fc, label, ha="center", va="center", fontsize=FS_TINY)
if i < len(layer_info_fc) - 1:
next_x = layer_info_fc[i + 1][0]
ax.annotate(
"",
xy=(next_x - layer_info_fc[i + 1][2] / 2, y_fc),
xytext=(x + w / 2, y_fc),
arrowprops={"arrowstyle": "->", "color": GRAY5, "lw": 1},
)
ax.text(
0.3, y_fc, "CNN:", fontsize=FS, fontweight="bold", color=RED_ACCENT, va="center"
)
ax.text(
12,
y_fc + 1,
"PROBLEM: FC wymaga\nSTAŁEGO rozmiaru\n(np. 224x224)",
ha="center",
fontsize=FS_SMALL,
color=RED_ACCENT,
fontweight="bold",
bbox={
"boxstyle": "round",
"facecolor": "#FFCDD2",
"edgecolor": RED_ACCENT,
"alpha": 0.3,
},
)
# FCN with Conv 1x1
layer_info_fcn = [
(1.5, "Obraz\nHxWx3", 2.2, GRAY2),
(4.5, "Conv+Pool\nH/2 x W/2\nx64", 1.8, GRAY2),
(7.5, "Conv+Pool\nH/32 x W/32\nx512", 1.0, GRAY2),
(10.5, "Conv 1x1\nH/32 x W/32\nxC", 0.8, "#C8E6C9"),
(13.5, "Upsample\nHxWxC", 1.8, "#C8E6C9"),
(16.5, "Mapa\nsegmentacji", 1.5, "#C8E6C9"),
]
y_fcn = 1.5
for i, (x, label, w, color) in enumerate(layer_info_fcn):
rect = FancyBboxPatch(
(x - w / 2, y_fcn - 0.7),
w,
1.4,
boxstyle="round,pad=0.05",
facecolor=color,
edgecolor=BLACK,
linewidth=0.8,
)
ax.add_patch(rect)
ax.text(x, y_fcn, label, ha="center", va="center", fontsize=FS_TINY)
if i < len(layer_info_fcn) - 1:
next_x = layer_info_fcn[i + 1][0]
ax.annotate(
"",
xy=(next_x - layer_info_fcn[i + 1][2] / 2, y_fcn),
xytext=(x + w / 2, y_fcn),
arrowprops={"arrowstyle": "->", "color": GRAY5, "lw": 1},
)
ax.text(
0.3,
y_fcn,
"FCN:",
fontsize=FS,
fontweight="bold",
color=GREEN_ACCENT,
va="center",
)
ax.text(
10.5,
y_fcn + 1.2,
"Conv 1x1:\nkażdy piksel\nosobno x wagi\n(jak FC ale\nzachowuje HxW)",
ha="center",
fontsize=FS_TINY,
color=GREEN_ACCENT,
bbox={
"boxstyle": "round",
"facecolor": "#C8E6C9",
"edgecolor": GREEN_ACCENT,
"alpha": 0.3,
},
)
# --- Panel 2: What FC and Conv do ---
ax = axes[1]
ax.set_xlim(0, 20)
ax.set_ylim(0, 6)
ax.axis("off")
ax.set_title(
"Co robi warstwa FC? Co robi konwolucja?", fontsize=FS_TITLE, fontweight="bold"
)
# FC explanation
rect = FancyBboxPatch(
(0.3, 3.2),
9,
2.5,
boxstyle="round,pad=0.15",
facecolor=ACCENT_LIGHT,
edgecolor=ACCENT,
linewidth=1,
)
ax.add_patch(rect)
ax.text(
4.8, 5.2, "Fully Connected (FC)", fontsize=FS, fontweight="bold", ha="center"
)
ax.text(
4.8,
4.5,
"KAŻDY neuron połączony z KAŻDYM wejściem\n"
"25 088 wejść x 4 096 neuronów = ~103 MLN wag!\n"
"Traci informację GDZIE (przestrzenną)\n"
"Wymaga STAŁEGO rozmiaru wejścia",
fontsize=FS_TINY,
ha="center",
va="top",
)
# Conv explanation
rect = FancyBboxPatch(
(10.3, 3.2),
9,
2.5,
boxstyle="round,pad=0.15",
facecolor="#C8E6C9",
edgecolor=GREEN_ACCENT,
linewidth=1,
)
ax.add_patch(rect)
ax.text(14.8, 5.2, "Konwolucja (Conv)", fontsize=FS, fontweight="bold", ha="center")
ax.text(
14.8,
4.5,
'Filtr (np. 3x3) „jedzie" po obrazie\n'
"Te same wagi dla KAŻDEJ pozycji\n"
"Zachowuje informację GDZIE\n"
"Akceptuje DOWOLNY rozmiar wejścia",
fontsize=FS_TINY,
ha="center",
va="top",
)
# Conv 1x1 explanation
rect = FancyBboxPatch(
(3, 0.3),
14,
2.2,
boxstyle="round,pad=0.15",
facecolor=GRAY1,
edgecolor=BLACK,
linewidth=1,
)
ax.add_patch(rect)
ax.text(
10,
2.1,
'Conv 1x1 = „FC per piksel"',
fontsize=FS,
fontweight="bold",
ha="center",
)
ax.text(
10,
1.5,
"Filtr 1x1: patrzy na JEDEN piksel, ale WSZYSTKIE kanały (512→C klas)\n"
"Działa jak FC ale zachowuje mapę HxW → każdy piksel osobno klasyfikowany\n"
"FCN: zamień FC na Conv1x1 → koniec z wymogiem stałego rozmiaru!",
fontsize=FS_TINY,
ha="center",
va="top",
)
_save_figure("q23_fc_vs_conv1x1.png")
def generate_unet() -> None:
"""Generate unet."""
_fig, ax = plt.subplots(1, 1, figsize=(10, 6))
ax.set_xlim(-1, 21)
ax.set_ylim(-1, 12)
ax.axis("off")
ax.set_title(
"U-Net: architektura w kształcie litery U",
fontsize=FS_TITLE + 1,
fontweight="bold",
)
# Encoder layers (going DOWN-LEFT)
encoder_layers = [
(2, 10, 2.5, 1.5, "572x572x1\n(wejście)", 64),
(2, 7.5, 2.2, 1.3, "284x284\nx64", 64),
(2, 5, 1.8, 1.1, "140x140\nx128", 128),
(2, 2.5, 1.5, 1.0, "68x68\nx256", 256),
]
# Bottleneck
bottleneck = (8, 0.5, 2.5, 1.2, "32x32x512\n(bottleneck)", 512)
# Decoder layers (going UP-RIGHT)
decoder_layers = [
(14, 2.5, 1.5, 1.0, "68x68\nx256", 256),
(14, 5, 1.8, 1.1, "140x140\nx128", 128),
(14, 7.5, 2.2, 1.3, "284x284\nx64", 64),
(14, 10, 2.5, 1.5, "572x572xC\n(mapa seg.)", "C"),
]
def draw_block(
ax: Axes,
x: float,
y: float,
w: float,
h: float,
label: str,
color: str,
) -> None:
"""Draw block."""
rect = FancyBboxPatch(
(x - w / 2, y - h / 2),
w,
h,
boxstyle="round,pad=0.05",
facecolor=color,
edgecolor=BLACK,
linewidth=1.2,
)
ax.add_patch(rect)
ax.text(x, y, label, ha="center", va="center", fontsize=FS_TINY)
# Draw encoder
for x, y, w, h, label, _channels in encoder_layers:
draw_block(ax, x, y, w, h, label, ACCENT_LIGHT)
# Draw arrows down (encoder)
for i in range(len(encoder_layers) - 1):
x1, y1 = encoder_layers[i][0], encoder_layers[i][1] - encoder_layers[i][3] / 2
x2, y2 = (
encoder_layers[i + 1][0],
encoder_layers[i + 1][1] + encoder_layers[i + 1][3] / 2,
)
ax.annotate(
"",
xy=(x2, y2),
xytext=(x1, y1),
arrowprops={"arrowstyle": "->", "color": ACCENT, "lw": 2},
)
ax.text(
x1 - 1.7,
(y1 + y2) / 2,
"MaxPool\n2x2\n↓ zmniejsz",
fontsize=FS_TINY,
ha="center",
color=ACCENT,
fontweight="bold",
)
# Encoder to bottleneck
x1, y1 = encoder_layers[-1][0], encoder_layers[-1][1] - encoder_layers[-1][3] / 2
draw_block(
ax,
bottleneck[0],
bottleneck[1],
bottleneck[2],
bottleneck[3],
bottleneck[4],
GRAY2,
)
ax.annotate(
"",
xy=(bottleneck[0] - bottleneck[2] / 2, bottleneck[1] + bottleneck[3] / 2),
xytext=(x1, y1),
arrowprops={"arrowstyle": "->", "color": ACCENT, "lw": 2},
)
# Bottleneck to decoder
ax.annotate(
"",
xy=(
decoder_layers[0][0] - decoder_layers[0][2] / 2,
decoder_layers[0][1] - decoder_layers[0][3] / 2,
),
xytext=(bottleneck[0] + bottleneck[2] / 2, bottleneck[1] + bottleneck[3] / 2),
arrowprops={"arrowstyle": "->", "color": RED_ACCENT, "lw": 2},
)
# Draw decoder
for x, y, w, h, label, channels in decoder_layers:
color = "#C8E6C9" if channels != "C" else "#A5D6A7"
draw_block(ax, x, y, w, h, label, color)
# Draw arrows up (decoder)
for i in range(len(decoder_layers) - 1):
x1, y1 = decoder_layers[i][0], decoder_layers[i][1] + decoder_layers[i][3] / 2
x2, y2 = (
decoder_layers[i + 1][0],
decoder_layers[i + 1][1] - decoder_layers[i + 1][3] / 2,
)
ax.annotate(
"",
xy=(x2, y2),
xytext=(x1, y1),
arrowprops={"arrowstyle": "->", "color": GREEN_ACCENT, "lw": 2},
)
ax.text(
x1 + 2,
(y1 + y2) / 2,
"UpConv\n2x2\n↑ zwiększ",
fontsize=FS_TINY,
ha="center",
color=GREEN_ACCENT,
fontweight="bold",
)
# Skip connections (horizontal arrows)
for i in range(len(encoder_layers)):
enc = encoder_layers[i]
dec = decoder_layers[len(decoder_layers) - 1 - i]
ax.annotate(
"",
xy=(dec[0] - dec[2] / 2, dec[1]),
xytext=(enc[0] + enc[2] / 2, enc[1]),
arrowprops={
"arrowstyle": "->",
"color": GRAY5,
"lw": 1.5,
"linestyle": "dashed",
},
)
mid_x = (enc[0] + enc[2] / 2 + dec[0] - dec[2] / 2) / 2
ax.text(
mid_x,
enc[1] + 0.6,
"skip\n(concat)",
fontsize=FS_TINY,
ha="center",
color=GRAY5,
fontweight="bold",
)
# Labels
ax.text(
0,
11.5,
"ENCODER\n(↓ zmniejsza)",
fontsize=FS,
fontweight="bold",
color=ACCENT,
ha="center",
)
ax.text(
17,
11.5,
"DECODER\n(↑ zwiększa)",
fontsize=FS,
fontweight="bold",
color=GREEN_ACCENT,
ha="center",
)
ax.text(
8,
-0.8,
'Kształt litery „U": encoder schodzi ↓ → bottleneck na dnie → decoder wraca ↑',
fontsize=FS_SMALL,
ha="center",
color=GRAY5,
fontweight="bold",
)
# Concatenation explanation
rect = FancyBboxPatch(
(17.5, 3),
3,
5,
boxstyle="round,pad=0.15",
facecolor=GRAY1,
edgecolor=GRAY5,
linewidth=1,
linestyle="--",
)
ax.add_patch(rect)
ax.text(
19, 7.5, "Concatenation:", fontsize=FS_SMALL, ha="center", fontweight="bold"
)
ax.text(
19,
6.5,
"Encoder: 64 kanały\nDecoder: 64 kanały\n→ concat → 128 kanałów\n\n"
"Jak sklejenie\ndwóch stosów\nkart:",
fontsize=FS_TINY,
ha="center",
)
ax.text(
19,
3.7,
"[enc₁|enc₂|...|dec₁|dec₂|...]",
fontsize=FS_TINY - 1,
ha="center",
fontweight="bold",
color=ACCENT,
)
_save_figure("q23_unet_arch.png")

View File

@ -0,0 +1,96 @@
"""Common utilities and constants for Q23 diagram generation.
A4-compatible, monochrome-friendly (grays + one accent), 300 DPI.
"""
from __future__ import annotations
import logging
from pathlib import Path
from typing import TYPE_CHECKING
import matplotlib as mpl
mpl.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
if TYPE_CHECKING:
from matplotlib.axes import Axes
_logger = logging.getLogger(__name__)
rng = np.random.default_rng(42)
DPI = 300
OUTPUT_DIR = str(Path(__file__).resolve().parent / "img")
Path(OUTPUT_DIR).mkdir(parents=True, exist_ok=True)
# Color palette — monochrome-friendly
BLACK = "#000000"
WHITE = "#FFFFFF"
GRAY1 = "#F5F5F5"
GRAY2 = "#E0E0E0"
GRAY3 = "#BDBDBD"
GRAY4 = "#9E9E9E"
GRAY5 = "#757575"
GRAY6 = "#424242"
ACCENT = "#4A90D9" # single blue accent for highlights
ACCENT_LIGHT = "#B3D4FC"
RED_ACCENT = "#D32F2F"
GREEN_ACCENT = "#388E3C"
FS = 9
FS_TITLE = 11
FS_SMALL = 7
FS_TINY = 6
_RIDGE_X = 5
_VALLEY2_END = 9
_DARK_PIXEL_THRESHOLD = 100
_GRID_LAST_IDX = 3
_HIGHLIGHT_START = 3
_HIGHLIGHT_END = 5
_BRIGHT_THRESHOLD = 170
_OTSU_THRESHOLD = 128
def _save_figure(name: str) -> None:
"""Save current figure and log."""
plt.tight_layout()
plt.savefig(
str(Path(OUTPUT_DIR) / name),
dpi=DPI,
bbox_inches="tight",
facecolor="white",
)
plt.close()
_logger.info("%s", name)
def _render_text_lines(
ax: Axes,
lines: list[tuple[str, int, str, str]],
*,
x_pos: float = 0.5,
start_y: float,
y_step: float = 0.5,
y_empty_step: float = 0.2,
) -> None:
"""Render a list of styled text lines on an axis."""
y = start_y
for txt, size, color, weight in lines:
if txt == "":
y -= y_empty_step
continue
ax.text(
x_pos,
y,
txt,
fontsize=size,
color=color,
fontweight=weight,
va="top",
)
y -= y_step

View File

@ -0,0 +1,251 @@
"""DIY U-Net step-by-step diagram generator."""
from __future__ import annotations
from typing import TYPE_CHECKING
from _q23_common import (
ACCENT,
ACCENT_LIGHT,
BLACK,
FS,
FS_SMALL,
FS_TINY,
GRAY1,
GRAY3,
GRAY5,
GREEN_ACCENT,
WHITE,
_save_figure,
np,
plt,
rng,
)
from matplotlib.patches import FancyBboxPatch
if TYPE_CHECKING:
from matplotlib.axes import Axes
def _draw_unet_layer_stack(
ax: Axes,
layer_sizes: list[tuple[int, int]],
*,
face_color: str,
edge_color: str,
arrow_color: str,
arrow_label: str,
add_skip: bool = False,
) -> None:
"""Draw encoder or decoder layer stack for DIY U-Net."""
ax.set_xlim(0, 10)
ax.set_ylim(0, 10)
ax.axis("off")
y_pos = 8.5
for i, (s, c) in enumerate(layer_sizes):
w = s / 64 * 4
h = 0.8
rect = FancyBboxPatch(
(5 - w / 2, y_pos),
w,
h,
boxstyle="round,pad=0.05",
facecolor=face_color,
edgecolor=edge_color,
linewidth=1,
)
ax.add_patch(rect)
label = f"{s}x{s}x{c}"
if add_skip and i < len(layer_sizes) - 1:
label += " + skip!"
ax.text(
5,
y_pos + h / 2,
label,
ha="center",
va="center",
fontsize=FS_SMALL,
fontweight="bold",
)
if i < len(layer_sizes) - 1:
ax.annotate(
"",
xy=(5, y_pos - 0.3),
xytext=(5, y_pos),
arrowprops={
"arrowstyle": "->",
"color": arrow_color,
"lw": 1.5,
},
)
ax.text(
7,
y_pos - 0.15,
arrow_label,
fontsize=FS_TINY,
color=arrow_color,
)
y_pos -= 2.2
def _draw_unet_pseudocode(ax: Axes) -> None:
"""Draw panel 6: U-Net pseudocode."""
ax.set_xlim(0, 10)
ax.set_ylim(0, 10)
ax.axis("off")
ax.set_title("Pseudokod U-Net", fontsize=FS, fontweight="bold")
code_lines = [
"# ENCODER",
"e1 = conv_block(input, 64) # 64x64",
"e2 = conv_block(pool(e1), 128) # 32x32",
"e3 = conv_block(pool(e2), 256) # 16x16",
"",
"# BOTTLENECK",
"b = conv_block(pool(e3), 512) # 8x8",
"",
"# DECODER + SKIP",
"d3 = conv_block(concat(",
" upconv(b), e3), 256) # 16x16",
"d2 = conv_block(concat(",
" upconv(d3), e2), 128) # 32x32",
"d1 = conv_block(concat(",
" upconv(d2), e1), 64) # 64x64",
"",
"output = conv_1x1(d1, n_classes)",
]
for i, line in enumerate(code_lines):
txt_color = (
ACCENT
if "concat" in line
else (GREEN_ACCENT if "output" in line else BLACK)
)
ax.text(
0.3,
9.5 - i * 0.55,
line,
fontsize=FS_TINY,
fontfamily="monospace",
color=txt_color,
)
def generate_diy_unet() -> None:
"""Generate diy unet."""
fig, axes = plt.subplots(2, 3, figsize=(11, 7))
size = 64
# Create synthetic image with two regions
img = np.ones((size, size, 3), dtype=np.uint8) * 200 # bright bg
# Dark region (object 1)
yy, xx = np.mgrid[:size, :size]
mask1 = ((xx - 20) ** 2 + (yy - 30) ** 2) < 12**2
img[mask1] = [60, 60, 60]
# Medium region (object 2)
mask2 = ((xx - 45) ** 2 + (yy - 25) ** 2) < 8**2
img[mask2] = [120, 120, 120]
gt = np.zeros((size, size), dtype=np.uint8)
gt[mask1] = 1 # class 1
gt[mask2] = 2 # class 2
# --- Panel 1: Input image ---
ax = axes[0, 0]
ax.imshow(img)
ax.set_title("Krok 1: obraz RGB\n64x64x3", fontsize=FS, fontweight="bold")
ax.axis("off")
# --- Panel 2: Encoder shrinks ---
ax = axes[0, 1]
ax.set_title("Krok 2: Encoder ZMNIEJSZA", fontsize=FS, fontweight="bold")
_draw_unet_layer_stack(
ax,
[(64, 3), (32, 64), (16, 128), (8, 256)],
face_color=ACCENT_LIGHT,
edge_color=ACCENT,
arrow_color=ACCENT,
arrow_label="Conv+Pool",
)
ax.text(
5,
0.3,
"Wyciąga cechy:\nkrawędzie → tekstury → obiekty",
ha="center",
fontsize=FS_TINY,
color=GRAY5,
)
# --- Panel 3: Bottleneck ---
ax = axes[0, 2]
# Show feature maps at bottleneck (abstract)
ax.set_xlim(0, 10)
ax.set_ylim(0, 10)
ax.axis("off")
ax.set_title(
"Krok 3: Bottleneck\n(najbardziej abstrakcyjne cechy)",
fontsize=FS,
fontweight="bold",
)
# Show small abstract feature maps
for k in range(4):
small = rng.random((4, 4))
ax_inset = fig.add_axes(
[0.68 + (k % 2) * 0.08, 0.72 - (k // 2) * 0.1, 0.06, 0.06]
)
ax_inset.imshow(small, cmap="gray")
ax_inset.axis("off")
ax.text(
5,
5,
'8x8x256\n\nMałe mapy, ale DUŻO kanałów\nKażdy kanał = jedna „cecha"\n'
'(np. kanał 42 = „wykrył koło"\n kanał 78 = „wykrył krawędź")\n\n'
"Wie CO jest na obrazie\nale nie wie GDZIE dokładnie",
ha="center",
va="center",
fontsize=FS_SMALL,
bbox={"boxstyle": "round", "facecolor": GRAY1, "edgecolor": GRAY3},
)
# --- Panel 4: Decoder enlarges ---
ax = axes[1, 0]
ax.set_title(
"Krok 4: Decoder ZWIĘKSZA\n(+ skip connections!)",
fontsize=FS,
fontweight="bold",
)
_draw_unet_layer_stack(
ax,
[(8, 256), (16, 128), (32, 64), (64, 3)],
face_color="#C8E6C9",
edge_color=GREEN_ACCENT,
arrow_color=GREEN_ACCENT,
arrow_label="UpConv+Concat",
add_skip=True,
)
ax.text(
5,
0.3,
"Odtwarza rozdzielczość:\nskip → przywraca krawędzie",
ha="center",
fontsize=FS_TINY,
color=GRAY5,
)
# --- Panel 5: Output segmentation map ---
ax = axes[1, 1]
cmap = plt.cm.colors.ListedColormap([WHITE, ACCENT_LIGHT, "#FFCDD2"])
ax.imshow(gt, cmap=cmap, interpolation="nearest")
ax.set_title(
"Krok 5: mapa segmentacji\n64x64 (3 klasy)", fontsize=FS, fontweight="bold"
)
ax.axis("off")
ax.text(20, -3, "Tło=0, obiekt A=1, obiekt B=2", fontsize=FS_TINY, ha="center")
# --- Panel 6: Summary pseudocode ---
_draw_unet_pseudocode(axes[1, 2])
_save_figure("q23_diy_unet.png")

View File

@ -0,0 +1,380 @@
"""Mean shift and normalized cuts diagram generators."""
from __future__ import annotations
from typing import TYPE_CHECKING
from _q23_common import (
_DARK_PIXEL_THRESHOLD,
_GRID_LAST_IDX,
ACCENT,
ACCENT_LIGHT,
BLACK,
FS,
FS_SMALL,
FS_TINY,
FS_TITLE,
GRAY1,
GRAY3,
GRAY4,
GRAY5,
GRAY6,
GREEN_ACCENT,
RED_ACCENT,
_render_text_lines,
_save_figure,
np,
plt,
rng,
)
from matplotlib import patches
if TYPE_CHECKING:
from matplotlib.axes import Axes
def generate_mean_shift() -> None:
"""Generate mean shift."""
_fig, axes = plt.subplots(1, 3, figsize=(11, 4))
# --- Panel 1: Feature space concept ---
ax = axes[0]
# Three clusters in 2D feature space (brightness, x-position)
c1x = rng.normal(2, 0.5, 40)
c1y = rng.normal(2, 0.5, 40)
c2x = rng.normal(6, 0.6, 35)
c2y = rng.normal(7, 0.5, 35)
c3x = rng.normal(8, 0.4, 25)
c3y = rng.normal(3, 0.6, 25)
ax.scatter(c1x, c1y, c=GRAY4, s=15, alpha=0.7, zorder=3)
ax.scatter(c2x, c2y, c=GRAY4, s=15, alpha=0.7, zorder=3)
ax.scatter(c3x, c3y, c=GRAY4, s=15, alpha=0.7, zorder=3)
# Label peaks
ax.scatter([2], [2], c=RED_ACCENT, s=80, marker="*", zorder=5, label="Max gęstości")
ax.scatter([6], [7], c=RED_ACCENT, s=80, marker="*", zorder=5)
ax.scatter([8], [3], c=RED_ACCENT, s=80, marker="*", zorder=5)
ax.set_xlabel("Cecha 1: jasność", fontsize=FS)
ax.set_ylabel("Cecha 2: pozycja x", fontsize=FS)
ax.set_title("Przestrzeń cech", fontsize=FS_TITLE, fontweight="bold")
for lx, ly, ltxt in [
(2, 0.3, "Klaster 1\n(ciemne, lewo)"),
(6, 5.3, "Klaster 2\n(jasne, prawo)"),
(8, 1.3, "Klaster 3\n(jasne, dół)"),
]:
ax.text(lx, ly, ltxt, ha="center", fontsize=FS_TINY, color=GRAY6)
ax.legend(fontsize=FS_SMALL, loc="upper left")
# --- Panel 2: Kernel/window moving ---
ax = axes[1]
ax.scatter(c1x, c1y, c=ACCENT_LIGHT, s=15, alpha=0.7, zorder=3)
ax.scatter(c2x, c2y, c=GRAY3, s=15, alpha=0.7, zorder=3)
ax.scatter(c3x, c3y, c=GRAY3, s=15, alpha=0.7, zorder=3)
# Show kernel movement
path_x = [4.5, 3.8, 3.0, 2.3, 2.05]
path_y = [4.0, 3.3, 2.7, 2.2, 2.03]
for i, (px, py) in enumerate(zip(path_x, path_y, strict=False)):
alpha = 0.3 + 0.15 * i
circle = plt.Circle(
(px, py),
1.2,
fill=False,
edgecolor=ACCENT,
linewidth=1.5,
linestyle="--" if i < len(path_x) - 1 else "-",
alpha=alpha,
)
ax.add_patch(circle)
if i < len(path_x) - 1:
ax.annotate(
"",
xy=(path_x[i + 1], path_y[i + 1]),
xytext=(px, py),
arrowprops={"arrowstyle": "->", "color": RED_ACCENT, "lw": 1.5},
)
ax.scatter([path_x[0]], [path_y[0]], c=ACCENT, s=50, marker="o", zorder=5)
ax.scatter([path_x[-1]], [path_y[-1]], c=RED_ACCENT, s=80, marker="*", zorder=5)
ax.text(
4.5, 5.2, "Start: losowy\npiksel", fontsize=FS_SMALL, ha="center", color=ACCENT
)
ax.text(
2.05,
0.5,
"Koniec: max\ngęstości",
fontsize=FS_SMALL,
ha="center",
color=RED_ACCENT,
fontweight="bold",
)
ax.text(
7,
8,
"Okno (jądro)\nprzesuwa się\ndo skupiska",
fontsize=FS_SMALL,
ha="center",
color=GRAY6,
bbox={"boxstyle": "round", "facecolor": GRAY1, "edgecolor": GRAY3},
)
ax.set_xlabel("Cecha 1", fontsize=FS)
ax.set_ylabel("Cecha 2", fontsize=FS)
ax.set_title("Jądro → max gęstości", fontsize=FS_TITLE, fontweight="bold")
ax.set_xlim(0, 10)
ax.set_ylim(0, 9)
# --- Panel 3: Why no K parameter ---
ax = axes[2]
ax.set_xlim(0, 10)
ax.set_ylim(0, 10)
ax.axis("off")
ax.set_title("Dlaczego bez K?", fontsize=FS_TITLE, fontweight="bold")
lines = [
("K-means wymaga:", FS, RED_ACCENT, "bold"),
(' „Podaj K=3 klastry"', FS_SMALL, "black", "normal"),
(" Problem: skąd wiesz ile klastrów?", FS_SMALL, GRAY5, "normal"),
("", 0, "", ""),
("Mean Shift NIE wymaga K:", FS, GREEN_ACCENT, "bold"),
(" Każdy piksel startuje → toczy się", FS_SMALL, "black", "normal"),
(" → trafia do najbliższego szczytu", FS_SMALL, "black", "normal"),
(" → ile szczytów = tyle segmentów", FS_SMALL, "black", "normal"),
(" → automatycznie!", FS_SMALL, GREEN_ACCENT, "bold"),
("", 0, "", ""),
("Parametr: bandwidth (szerokość okna)", FS, "black", "bold"),
(" Duże okno → mało segmentów", FS_SMALL, "black", "normal"),
(" Małe okno → dużo segmentów", FS_SMALL, "black", "normal"),
("", 0, "", ""),
("Okno = jądro (kernel):", FS, "black", "bold"),
(" Koło o promieniu h wokół punktu.", FS_SMALL, "black", "normal"),
(" Oblicz średnią pikseli W oknie.", FS_SMALL, "black", "normal"),
(" Przesuń okno na tę średnią.", FS_SMALL, "black", "normal"),
(" Powtórz aż się zatrzyma.", FS_SMALL, "black", "normal"),
]
_render_text_lines(ax, lines, start_y=9.0)
_save_figure("q23_mean_shift.png")
def _draw_ncuts_pixel_grid(
ax: Axes,
pixel_vals: np.ndarray,
) -> None:
"""Draw 4x4 pixel grid with value labels and edge weights."""
for i in range(4):
for j in range(4):
v = pixel_vals[i, j]
gray_val = v / 255.0
str(gray_val)
rect = patches.Rectangle(
(j - 0.4, 3 - i - 0.4),
0.8,
0.8,
facecolor=(gray_val, gray_val, gray_val),
edgecolor=BLACK,
linewidth=0.8,
)
ax.add_patch(rect)
text_color = "white" if v < _DARK_PIXEL_THRESHOLD else "black"
ax.text(
j,
3 - i,
str(v),
ha="center",
va="center",
fontsize=FS_SMALL,
color=text_color,
fontweight="bold",
)
def _draw_ncuts_edges(
ax: Axes,
pixel_vals: np.ndarray,
) -> None:
"""Draw weighted edges between adjacent pixels."""
for i in range(4):
for j in range(4):
if j < _GRID_LAST_IDX:
similarity = max(
0,
1 - abs(pixel_vals[i, j] - pixel_vals[i, j + 1]) / 255,
)
lw = similarity * 2.5 + 0.3
alpha = similarity * 0.8 + 0.2
ax.plot(
[j + 0.4, j + 0.6],
[3 - i, 3 - i],
color=GRAY5,
linewidth=lw,
alpha=alpha,
)
if i < _GRID_LAST_IDX:
similarity = max(
0,
1 - abs(pixel_vals[i, j] - pixel_vals[i + 1, j]) / 255,
)
lw = similarity * 2.5 + 0.3
alpha = similarity * 0.8 + 0.2
ax.plot(
[j, j],
[3 - i - 0.4, 3 - i - 0.6],
color=GRAY5,
linewidth=lw,
alpha=alpha,
)
def generate_normalized_cuts() -> None:
"""Generate normalized cuts."""
_fig, axes = plt.subplots(1, 3, figsize=(11, 4))
# --- Panel 1: Image as graph ---
ax = axes[0]
ax.set_xlim(-0.5, 4.5)
ax.set_ylim(-0.5, 4.5)
ax.set_aspect("equal")
ax.set_title("Obraz → graf", fontsize=FS_TITLE, fontweight="bold")
pixel_vals = np.array(
[
[30, 35, 180, 190],
[40, 30, 185, 200],
[170, 180, 40, 35],
[190, 175, 30, 45],
]
)
_draw_ncuts_pixel_grid(ax, pixel_vals)
_draw_ncuts_edges(ax, pixel_vals)
ax.text(
2,
-0.8,
"Grube linie = duże podobieństwo\n(silna krawędź grafu)",
ha="center",
fontsize=FS_TINY,
color=GRAY5,
)
ax.axis("off")
# --- Panel 2: Cut concept ---
ax = axes[1]
ax.set_xlim(0, 10)
ax.set_ylim(0, 10)
ax.axis("off")
ax.set_title("Cięcie grafu (graph cut)", fontsize=FS_TITLE, fontweight="bold")
# Draw two groups of nodes
# Group A (dark pixels)
positions_a = [(2, 7), (3, 8), (2, 5), (3, 6)]
positions_b = [(7, 7), (8, 8), (7, 5), (8, 6)]
# Intra-group edges (thick = similar)
for i, (x1, y1) in enumerate(positions_a):
for x2, y2 in positions_a[i + 1 :]:
ax.plot([x1, x2], [y1, y2], color=ACCENT, linewidth=2, alpha=0.5)
for i, (x1, y1) in enumerate(positions_b):
for x2, y2 in positions_b[i + 1 :]:
ax.plot([x1, x2], [y1, y2], color=RED_ACCENT, linewidth=2, alpha=0.5)
# Inter-group edges (thin = dissimilar) — these get cut
cut_edges = [((3, 8), (7, 7)), ((3, 6), (7, 5)), ((2, 5), (7, 5))]
for (x1, y1), (x2, y2) in cut_edges:
ax.plot([x1, x2], [y1, y2], color=GRAY4, linewidth=0.8, linestyle="--")
# Draw nodes
for x, y in positions_a:
ax.scatter(x, y, c=ACCENT, s=120, zorder=5, edgecolors=BLACK, linewidth=0.8)
for x, y in positions_b:
ax.scatter(x, y, c="#FFCDD2", s=120, zorder=5, edgecolors=BLACK, linewidth=0.8)
# Cut line
ax.plot(
[5, 5], [3.5, 9.5], color=RED_ACCENT, linewidth=2.5, linestyle="-", zorder=4
)
ax.text(
5, 9.8, "CIĘCIE", ha="center", fontsize=FS, fontweight="bold", color=RED_ACCENT
)
ax.text(
2.5,
3.8,
"Segment A\n(ciemne piksele)",
ha="center",
fontsize=FS_SMALL,
color=ACCENT,
)
ax.text(
7.5,
3.8,
"Segment B\n(jasne piksele)",
ha="center",
fontsize=FS_SMALL,
color=RED_ACCENT,
)
# Formula
ax.text(
5,
1.8,
"Ncut(A,B) = cut(A,B)/assoc(A,V)\n + cut(A,B)/assoc(B,V)",
ha="center",
fontsize=FS_SMALL,
fontweight="bold",
bbox={"boxstyle": "round", "facecolor": GRAY1, "edgecolor": GRAY3},
)
ax.text(
5,
0.5,
"Minimalizuj Ncut → tnij SŁABE krawędzie\nzachowuj SILNE (wewnątrz grupy)",
ha="center",
fontsize=FS_TINY,
color=GRAY5,
)
# --- Panel 3: Algorithm summary ---
ax = axes[2]
ax.set_xlim(0, 10)
ax.set_ylim(0, 10)
ax.axis("off")
ax.set_title("Algorytm Normalized Cuts", fontsize=FS_TITLE, fontweight="bold")
steps = [
(
"1. Zbuduj graf",
"Piksele = węzły\nKrawędzie = podobieństwo"
" sąsiadów\n(kolor, jasność, odległość)",
),
(
"2. Macierz podobieństwa W",
"W[i,j] = exp(-|kolori - kolorj|² / σ²)"
"\n→ im podobniejsze, tym wyższa waga",
),
("3. Macierz stopni D", "D[i,i] = Σ W[i,j]\n(suma wszystkich wag z węzła i)"),
("4. Rozwiąż problem własny", "(D-W)·y = λ·D·y\n→ drugi najm. wektor własny y"),
("5. Podziel wg y", "y[i] > 0 → segment A\ny[i] ≤ 0 → segment B"),
]
y = 9.5
for title, desc in steps:
ax.text(0.5, y, title, fontsize=FS, fontweight="bold", va="top")
y -= 0.4
ax.text(0.8, y, desc, fontsize=FS_TINY, va="top", color=GRAY6)
y -= 1.2
ax.text(
5,
0.3,
"Złożoność: O(n³) — wymaga eigen decomposition!",
ha="center",
fontsize=FS_SMALL,
fontweight="bold",
color=RED_ACCENT,
)
_save_figure("q23_normalized_cuts.png")

View File

@ -0,0 +1,327 @@
"""Mnemonic summary diagram generator."""
from __future__ import annotations
from typing import TYPE_CHECKING
from _q23_common import (
ACCENT,
ACCENT_LIGHT,
BLACK,
FS,
FS_SMALL,
FS_TINY,
FS_TITLE,
GRAY1,
GRAY5,
GRAY6,
GREEN_ACCENT,
RED_ACCENT,
_save_figure,
plt,
)
from matplotlib.patches import FancyBboxPatch
if TYPE_CHECKING:
from matplotlib.axes import Axes
def generate_mnemonics() -> None:
"""Generate mnemonics."""
_fig, ax = plt.subplots(1, 1, figsize=(10, 8))
ax.set_xlim(0, 20)
ax.set_ylim(0, 16)
ax.axis("off")
ax.set_title(
"Mnemoniki — segmentacja obrazu", fontsize=FS_TITLE + 2, fontweight="bold"
)
def draw_card(
ax: Axes,
x: float,
y: float,
w: float,
h: float,
title: str,
mnemonic: str,
color: str,
detail: str = "",
) -> None:
"""Draw card."""
rect = FancyBboxPatch(
(x, y),
w,
h,
boxstyle="round,pad=0.15",
facecolor=color,
edgecolor=BLACK,
linewidth=1,
)
ax.add_patch(rect)
ax.text(
x + w / 2,
y + h - 0.3,
title,
ha="center",
va="top",
fontsize=FS,
fontweight="bold",
)
ax.text(
x + w / 2,
y + h / 2 - 0.1,
mnemonic,
ha="center",
va="center",
fontsize=FS_SMALL,
fontstyle="italic",
color=GRAY6,
)
if detail:
ax.text(
x + w / 2,
y + 0.4,
detail,
ha="center",
va="bottom",
fontsize=FS_TINY,
color=GRAY5,
)
# Title: STRATEGIE KLASYCZNE
ax.text(
5,
15.5,
"STRATEGIE KLASYCZNE",
fontsize=FS_TITLE,
fontweight="bold",
color=ACCENT,
ha="center",
)
cards_classic = [
(
0.2,
12.5,
4.5,
2.5,
"Thresholding",
'„PRÓG na bramce"\nPrzepuszcza > T,\nblokuje ≤ T',
ACCENT_LIGHT,
"jasne=1, ciemne=0",
),
(
5,
12.5,
4.5,
2.5,
"Otsu",
'„AUTO-bramkarz"\nSam dobiera próg\nmin σ² wewnątrz',
ACCENT_LIGHT,
"histogram bimodalny",
),
(
0.2,
9.5,
4.5,
2.5,
"Region Growing",
'„PLAMA rozlana"\nSeed → BFS po\npodobnych sąsiadach',
ACCENT_LIGHT,
"jak atrament na papierze",
),
(
5,
9.5,
4.5,
2.5,
"Watershed",
'„ZALEWANIE terenu"\nDoliny=obiekty\nGranie=granice',
ACCENT_LIGHT,
"woda + geography",
),
(
0.2,
6.5,
4.5,
2.5,
"Mean Shift",
'„KULKI toczą się"\nKażda → max gęstości\nBez K!',
ACCENT_LIGHT,
"bandwidth = okno",
),
(
5,
6.5,
4.5,
2.5,
"Normalized Cuts",
'„CIĘCIE sznurków"\nGraf: tnij słabe\nkrawędzie (O(n³)!)',
ACCENT_LIGHT,
"eigenvector problem",
),
]
for args in cards_classic:
draw_card(ax, *args)
# Title: SIECI NEURONOWE
ax.text(
15,
15.5,
"SIECI NEURONOWE",
fontsize=FS_TITLE,
fontweight="bold",
color=GREEN_ACCENT,
ha="center",
)
cards_nn = [
(
10.5,
12.5,
4.5,
2.5,
"FCN (2015)",
'„FC → Conv 1x1"\nPierwsza end-to-end\nDowolny rozmiar',
"#C8E6C9",
"skip connections",
),
(
15.3,
12.5,
4.5,
2.5,
"U-Net (2015)",
'„Litera U"\nEncoder↓ Decoder↑\nSkip = concat',
"#C8E6C9",
"medycyna, małe dane",
),
(
10.5,
9.5,
4.5,
2.5,
"DeepLab v3+",
'„DZIURY w filtrze"\nAtrous conv (rate)\nASPP multi-scale',
"#C8E6C9",
"à trous = z dziurami",
),
(
15.3,
9.5,
4.5,
2.5,
"Transformer",
'„WSZYSCY ze\nWSZYSTKIMI"\nSelf-attention O(n²)',
"#C8E6C9",
"SegFormer, Mask2Former",
),
]
for args in cards_nn:
draw_card(ax, *args)
# Metryki
ax.text(
10,
8.3,
"METRYKI I LOSS",
fontsize=FS_TITLE,
fontweight="bold",
color=RED_ACCENT,
ha="center",
)
cards_metrics = [
(
10.5,
6.5,
4.5,
1.6,
"mIoU",
'„Nakładka / Suma"\nIoU = A∩B / A\u222aB',
"#FFCDD2",
"",
),
(
15.3,
6.5,
4.5,
1.6,
"Dice / Focal",
'„Dice=2·nakładka"\nFocal=trudne px',
"#FFCDD2",
"",
),
]
for args in cards_metrics:
draw_card(ax, *args)
# Master mnemonic at bottom
rect = FancyBboxPatch(
(1, 0.3),
18,
5.5,
boxstyle="round,pad=0.2",
facecolor=GRAY1,
edgecolor=BLACK,
linewidth=1.5,
)
ax.add_patch(rect)
ax.text(
10,
5.3,
"SUPER-MNEMONIK: kolejność algorytmów segmentacji",
ha="center",
fontsize=FS,
fontweight="bold",
)
ax.text(
10,
4.5,
'„TORW-MN FUD-T"',
ha="center",
fontsize=FS_TITLE + 2,
fontweight="bold",
color=RED_ACCENT,
)
ax.text(
10,
3.5,
"Klasyczne: Thresholding → Otsu → Region"
" growing → Watershed → Mean shift → Norm. cuts",
ha="center",
fontsize=FS_SMALL,
)
ax.text(
10,
2.8,
"Neuronowe: FCN → U-Net → DeepLab → Transformer",
ha="center",
fontsize=FS_SMALL,
)
ax.text(
10,
1.8,
"„Turyści Oglądają Rzekę, Wodospad,"
" Morze, Nurt — Fotografują Uroczy"
' Dwór Tajemnic"',
ha="center",
fontsize=FS_SMALL,
fontstyle="italic",
color=ACCENT,
)
ax.text(
10,
1.0,
"Klasyczne: proste→auto→BFS→flood→"
"gęstość→graf | Neuronowe:"
" FC→U-skip→dilated→attention",
ha="center",
fontsize=FS_TINY,
color=GRAY5,
)
_save_figure("q23_mnemonics.png")

View File

@ -0,0 +1,293 @@
"""ReLU and dot product diagram generators."""
from __future__ import annotations
from _q23_common import (
ACCENT,
ACCENT_LIGHT,
BLACK,
FS,
FS_SMALL,
FS_TINY,
FS_TITLE,
GRAY1,
GRAY2,
GRAY3,
GRAY5,
GREEN_ACCENT,
RED_ACCENT,
_save_figure,
np,
plt,
)
from matplotlib import patches
def generate_relu() -> None:
"""Generate relu."""
_fig, axes = plt.subplots(1, 2, figsize=(8, 3.5))
# --- Panel 1: ReLU plot ---
ax = axes[0]
x = np.linspace(-5, 5, 200)
relu = np.maximum(0, x)
ax.plot(x, relu, color=ACCENT, linewidth=2.5, label="ReLU(x) = max(0, x)")
ax.axhline(y=0, color=GRAY3, linewidth=0.5)
ax.axvline(x=0, color=GRAY3, linewidth=0.5)
ax.fill_between(x[x < 0], 0, 0, color=RED_ACCENT, alpha=0.1)
ax.fill_between(x[x >= 0], 0, relu[x >= 0], color=ACCENT, alpha=0.1)
# Annotations
ax.annotate(
'x < 0 → output = 0\n(neuron „wyłączony")',
xy=(-3, 0),
fontsize=FS_SMALL,
ha="center",
va="bottom",
color=RED_ACCENT,
arrowprops={"arrowstyle": "->", "color": RED_ACCENT},
xytext=(-3, 2),
)
ax.annotate(
'x ≥ 0 → output = x\n(neuron „włączony")',
xy=(3, 3),
fontsize=FS_SMALL,
ha="center",
va="bottom",
color=ACCENT,
arrowprops={"arrowstyle": "->", "color": ACCENT},
xytext=(3, 4.5),
)
ax.scatter([0], [0], c=BLACK, s=40, zorder=5)
ax.text(0.3, -0.5, "(0,0)", fontsize=FS_SMALL, color=GRAY5)
ax.set_xlabel("x (wejście neuronu)", fontsize=FS)
ax.set_ylabel("ReLU(x)", fontsize=FS)
ax.set_title("ReLU — Rectified Linear Unit", fontsize=FS_TITLE, fontweight="bold")
ax.legend(fontsize=FS_SMALL, loc="upper left")
ax.set_ylim(-1, 6)
ax.grid(visible=True, alpha=0.2)
# --- Panel 2: Why ReLU ---
ax = axes[1]
ax.set_xlim(0, 10)
ax.set_ylim(0, 10)
ax.axis("off")
ax.set_title("Dlaczego ReLU?", fontsize=FS_TITLE, fontweight="bold")
y = 9.0
lines = [
("Neuron oblicza:", FS, BLACK, "bold"),
(" z = w₁·x₁ + w₂·x₂ + ... + bias", FS_SMALL, BLACK, "normal"),
(" output = ReLU(z) = max(0, z)", FS_SMALL, ACCENT, "bold"),
("", 0, "", ""),
("Przykład:", FS, BLACK, "bold"),
(" wagi: w₁=0.5, w₂=-0.3, bias=0.1", FS_SMALL, BLACK, "normal"),
(" wejścia: x₁=2.0, x₂=4.0", FS_SMALL, BLACK, "normal"),
(" z = 0.5·2 + (-0.3)·4 + 0.1 = -0.1", FS_SMALL, BLACK, "normal"),
(" ReLU(-0.1) = max(0, -0.1) = 0", FS_SMALL, RED_ACCENT, "bold"),
(" → neuron milczy (wejście nieistotne)", FS_SMALL, GRAY5, "normal"),
("", 0, "", ""),
("Gdyby z = 2.3:", FS, BLACK, "bold"),
(" ReLU(2.3) = max(0, 2.3) = 2.3", FS_SMALL, GREEN_ACCENT, "bold"),
(" → neuron aktywny! Przekazuje sygnał", FS_SMALL, GRAY5, "normal"),
("", 0, "", ""),
("Szybsza niż sigmoid/tanh", FS_SMALL, GRAY5, "normal"),
("(brak exp() → szybkie obliczenia)", FS_SMALL, GRAY5, "normal"),
]
for txt, size, color, weight in lines:
if txt == "":
y -= 0.2
continue
ax.text(0.5, y, txt, fontsize=size, color=color, fontweight=weight, va="top")
y -= 0.5
_save_figure("q23_relu.png")
def generate_dot_product() -> None:
"""Generate dot product."""
_fig, axes = plt.subplots(1, 3, figsize=(11, 3.5))
# --- Panel 1: Concept ---
ax = axes[0]
ax.set_xlim(0, 10)
ax.set_ylim(0, 10)
ax.axis("off")
ax.set_title(
"Iloczyn skalarny\n(dot product)", fontsize=FS_TITLE, fontweight="bold"
)
y = 8.5
lines = [
("Dwa wektory (listy liczb) → JEDNA liczba", FS, BLACK, "bold"),
("", 0, "", ""),
("a = [a₁, a₂, a₃] b = [b₁, b₂, b₃]", FS, ACCENT, "normal"),
("", 0, "", ""),
("a · b = a₁·b₁ + a₂·b₂ + a₃·b₃", FS, BLACK, "bold"),
("", 0, "", ""),
("Przykład:", FS, BLACK, "bold"),
("a = [1, 3, -2] b = [4, -1, 5]", FS_SMALL, BLACK, "normal"),
("a·b = 1·4 + 3·(-1) + (-2)·5", FS_SMALL, BLACK, "normal"),
(" = 4 + (-3) + (-10) = -9", FS_SMALL, RED_ACCENT, "bold"),
("", 0, "", ""),
(
'Duży wynik → wektory „podobne" (w tym samym kierunku)',
FS_SMALL,
GREEN_ACCENT,
"normal",
),
('Mały/ujemny → wektory „różne"', FS_SMALL, RED_ACCENT, "normal"),
]
for txt, size, color, weight in lines:
if txt == "":
y -= 0.25
continue
ax.text(0.5, y, txt, fontsize=size, color=color, fontweight=weight, va="top")
y -= 0.55
# --- Panel 2: Convolution as dot product ---
ax = axes[1]
ax.set_xlim(-0.5, 5.5)
ax.set_ylim(-0.5, 5.5)
ax.set_aspect("equal")
ax.set_title(
"Konwolucja = iloczyn skalarny\nfiltra x fragment obrazu",
fontsize=FS_TITLE,
fontweight="bold",
)
# Filter 3x3
filter_vals = [[-1, 0, 1], [-1, 0, 1], [-1, 0, 1]]
for i in range(3):
for j in range(3):
rect = patches.Rectangle(
(j - 0.4, 4 - i - 0.4),
0.8,
0.8,
facecolor=ACCENT_LIGHT,
edgecolor=BLACK,
linewidth=0.8,
)
ax.add_patch(rect)
ax.text(
j,
4 - i,
str(filter_vals[i][j]),
ha="center",
va="center",
fontsize=FS,
fontweight="bold",
)
ax.text(1, 1.5, "Filtr", ha="center", fontsize=FS, fontweight="bold", color=ACCENT)
# Image patch
img_vals = [[50, 50, 200], [50, 50, 200], [50, 50, 200]]
for i in range(3):
for j in range(3):
rect = patches.Rectangle(
(j + 2.6, 4 - i - 0.4),
0.8,
0.8,
facecolor=GRAY2,
edgecolor=BLACK,
linewidth=0.8,
)
ax.add_patch(rect)
ax.text(
j + 3,
4 - i,
str(img_vals[i][j]),
ha="center",
va="center",
fontsize=FS,
fontweight="bold",
)
ax.text(
4,
1.5,
"Fragment\nobrazu",
ha="center",
fontsize=FS,
fontweight="bold",
color=GRAY5,
)
ax.text(
2.5,
0.5,
"(-1)·50 + 0·50 + 1·200 +\n"
"(-1)·50 + 0·50 + 1·200 +\n"
"(-1)·50 + 0·50 + 1·200\n= 450 (krawędź!)",
ha="center",
fontsize=FS_TINY,
fontweight="bold",
bbox={"boxstyle": "round", "facecolor": GRAY1, "edgecolor": GREEN_ACCENT},
)
ax.axis("off")
# --- Panel 3: Vector visualization ---
ax = axes[2]
# Draw two vectors
ax.quiver(
0,
0,
3,
4,
angles="xy",
scale_units="xy",
scale=1,
color=ACCENT,
width=0.025,
label="a = [3, 4]",
)
ax.quiver(
0,
0,
4,
1,
angles="xy",
scale_units="xy",
scale=1,
color=RED_ACCENT,
width=0.025,
label="b = [4, 1]",
)
# Show angle
theta = np.linspace(np.arctan2(1, 4), np.arctan2(4, 3), 30)
r = 1.5
ax.plot(r * np.cos(theta), r * np.sin(theta), color=GREEN_ACCENT, linewidth=1.5)
ax.text(1.8, 1.3, "θ", fontsize=FS, color=GREEN_ACCENT, fontweight="bold")
ax.text(3.2, 4.2, "a", fontsize=FS, color=ACCENT, fontweight="bold")
ax.text(4.2, 1.2, "b", fontsize=FS, color=RED_ACCENT, fontweight="bold")
ax.text(
2.5,
-1.0,
"a · b = |a|·|b|·cos(θ)\n= 3·4 + 4·1 = 16",
ha="center",
fontsize=FS_SMALL,
fontweight="bold",
bbox={"boxstyle": "round", "facecolor": GRAY1, "edgecolor": GRAY3},
)
ax.text(
2.5,
-2.0,
'Mały kąt θ → duży dot product\n= wektory „zgadają się"',
ha="center",
fontsize=FS_TINY,
color=GRAY5,
)
ax.set_xlim(-0.5, 5.5)
ax.set_ylim(-2.5, 5.5)
ax.set_aspect("equal")
ax.grid(visible=True, alpha=0.2)
ax.legend(fontsize=FS_SMALL, loc="upper left")
ax.set_title("Geometrycznie: kąt", fontsize=FS_TITLE, fontweight="bold")
_save_figure("q23_dot_product.png")

View File

@ -0,0 +1,408 @@
"""Otsu thresholding and watershed diagram generators."""
from __future__ import annotations
from typing import TYPE_CHECKING
from _q23_common import (
_RIDGE_X,
_VALLEY2_END,
ACCENT,
ACCENT_LIGHT,
BLACK,
FS,
FS_SMALL,
FS_TITLE,
GRAY1,
GRAY2,
GRAY3,
GRAY4,
GRAY5,
GREEN_ACCENT,
RED_ACCENT,
_render_text_lines,
_save_figure,
np,
plt,
rng,
)
from matplotlib.patches import FancyBboxPatch
if TYPE_CHECKING:
from matplotlib.axes import Axes
def _draw_otsu_variance_panel(ax: Axes) -> None:
"""Draw panel 2: within-class variance explanation."""
ax.set_xlim(0, 10)
ax.set_ylim(0, 10)
ax.axis("off")
ax.set_title("Wariancja wewnątrzklasowa", fontsize=FS_TITLE, fontweight="bold")
texts = [
(
"Wariancja = jak bardzo wartości\nróżnią się od średniej",
FS,
"black",
"normal",
),
("", 0, "black", "normal"),
("Klasa 0 (piksele ≤ T):", FS, ACCENT, "bold"),
(" wartości: 30, 50, 45, 60, 55", FS_SMALL, "black", "normal"),
(" średnia μ₀ = 48", FS_SMALL, "black", "normal"),
(" σ₀² = ((30-48)²+(50-48)²+...)/5 = 108", FS_SMALL, "black", "normal"),
("", 0, "black", "normal"),
("Klasa 1 (piksele > T):", FS, RED_ACCENT, "bold"),
(" wartości: 180, 200, 190, 210, 195", FS_SMALL, "black", "normal"),
(" średnia μ₁ = 195", FS_SMALL, "black", "normal"),
(" σ₁² = ((180-195)²+...)/5 = 100", FS_SMALL, "black", "normal"),
("", 0, "black", "normal"),
("σ²_wewnątrz = w₀·σ₀² + w₁·σ₁²", FS, BLACK, "bold"),
("= 0.6·108 + 0.4·100 = 104.8", FS_SMALL, "black", "normal"),
("", 0, "black", "normal"),
("Otsu próbuje KAŻDE T: 0,1,...,255", FS_SMALL, GREEN_ACCENT, "bold"),
("Wybiera T dające MINIMUM σ²_wewnątrz", FS_SMALL, GREEN_ACCENT, "bold"),
]
_render_text_lines(
ax,
texts,
x_pos=0.3,
start_y=9.2,
y_step=0.55,
y_empty_step=0.25,
)
def generate_otsu_bimodal() -> None:
"""Generate otsu bimodal."""
_fig, axes = plt.subplots(1, 3, figsize=(11, 3.5))
# --- Panel 1: Bimodal histogram ---
ax = axes[0]
dark = rng.normal(60, 20, 3000).clip(0, 255)
bright = rng.normal(190, 25, 2000).clip(0, 255)
all_pixels = np.concatenate([dark, bright])
counts, _bins, _bars = ax.hist(
all_pixels, bins=64, color=GRAY3, edgecolor=GRAY5, linewidth=0.5
)
ax.axvline(
x=128, color=RED_ACCENT, linewidth=2, linestyle="--", label="Próg Otsu T=128"
)
ax.fill_betweenx([0, max(counts) * 1.1], 0, 128, alpha=0.12, color=ACCENT)
ax.fill_betweenx([0, max(counts) * 1.1], 128, 255, alpha=0.12, color=RED_ACCENT)
ax.text(
45,
max(counts) * 0.85,
"Klasa 0\n(tło)",
ha="center",
fontsize=FS,
fontweight="bold",
color=ACCENT,
)
ax.text(
195,
max(counts) * 0.85,
"Klasa 1\n(obiekt)",
ha="center",
fontsize=FS,
fontweight="bold",
color=RED_ACCENT,
)
ax.annotate(
"Garb 1",
xy=(60, max(counts) * 0.6),
fontsize=FS_SMALL,
ha="center",
arrowprops={"arrowstyle": "->", "color": GRAY5},
xytext=(30, max(counts) * 0.45),
)
ax.annotate(
"Garb 2",
xy=(190, max(counts) * 0.5),
fontsize=FS_SMALL,
ha="center",
arrowprops={"arrowstyle": "->", "color": GRAY5},
xytext=(220, max(counts) * 0.35),
)
ax.set_xlabel("Jasność piksela (0-255)", fontsize=FS)
ax.set_ylabel("Liczba pikseli", fontsize=FS)
ax.set_title("Histogram bimodalny", fontsize=FS_TITLE, fontweight="bold")
ax.legend(fontsize=FS_SMALL, loc="upper right")
ax.set_xlim(0, 255)
# --- Panel 2: Within-class variance explanation ---
_draw_otsu_variance_panel(axes[1])
# --- Panel 3: Jednorodność explanation ---
ax = axes[2]
ax.set_xlim(0, 10)
ax.set_ylim(0, 10)
ax.axis("off")
ax.set_title('"Jednorodne" = małe σ²', fontsize=FS_TITLE, fontweight="bold")
# Draw two clusters
# Good separation
c0 = rng.normal(2, 0.4, 15)
c1 = rng.normal(7, 0.4, 15)
y_pos_0 = rng.uniform(6, 8, 15)
y_pos_1 = rng.uniform(6, 8, 15)
ax.scatter(c0, y_pos_0, c=ACCENT, s=30, zorder=5, label="Klasa 0")
ax.scatter(c1, y_pos_1, c=RED_ACCENT, s=30, zorder=5, label="Klasa 1")
ax.axvline(x=4.5, color=GREEN_ACCENT, linewidth=2, linestyle="--")
ax.text(
4.5,
8.8,
"T optymalny",
ha="center",
fontsize=FS_SMALL,
color=GREEN_ACCENT,
fontweight="bold",
)
ax.text(
2, 5.3, "σ₀² mała\n(skupione)", ha="center", fontsize=FS_SMALL, color=ACCENT
)
ax.text(
7, 5.3, "σ₁² mała\n(skupione)", ha="center", fontsize=FS_SMALL, color=RED_ACCENT
)
ax.text(
5,
4,
"σ²_wewnątrz MINIMALNA\n→ klasy JEDNORODNE\n→ dobra segmentacja!",
ha="center",
fontsize=FS,
fontweight="bold",
color=GREEN_ACCENT,
)
# Bad separation
c0b = rng.normal(3.5, 1.5, 15)
c1b = rng.normal(6, 1.5, 15)
y_pos_0b = rng.uniform(1, 3, 15)
y_pos_1b = rng.uniform(1, 3, 15)
ax.scatter(c0b, y_pos_0b, c=ACCENT, s=30, marker="x", zorder=5)
ax.scatter(c1b, y_pos_1b, c=RED_ACCENT, s=30, marker="x", zorder=5)
ax.axvline(x=4.5, color=GRAY4, linewidth=1, linestyle=":", ymin=0, ymax=0.35)
ax.text(
5,
0.3,
"σ²_wewnątrz DUŻA → klasy mieszają się → zły próg",
ha="center",
fontsize=FS_SMALL,
color=GRAY5,
)
ax.legend(fontsize=FS_SMALL, loc="upper left")
_save_figure("q23_otsu_bimodal.png")
def _draw_watershed_result_panel(ax: Axes) -> None:
"""Draw panel 3: watershed result with over-segmentation problem."""
ax.set_xlim(0, 10)
ax.set_ylim(0, 10)
ax.axis("off")
ax.set_title("Krok 3: wynik", fontsize=FS_TITLE, fontweight="bold")
rect1 = FancyBboxPatch(
(0.5, 6),
3.5,
3.2,
boxstyle="round,pad=0.1",
facecolor=ACCENT_LIGHT,
edgecolor=BLACK,
linewidth=1,
)
ax.add_patch(rect1)
ax.text(2.25, 8.8, "Ideał: 2 segmenty", fontsize=FS, ha="center", fontweight="bold")
ax.text(2.25, 7.5, "Segment A Segment B", fontsize=FS_SMALL, ha="center")
ax.text(
2.25,
6.7,
"(po marker-controlled)",
fontsize=FS_SMALL,
ha="center",
color=GREEN_ACCENT,
)
rect2 = FancyBboxPatch(
(5.5, 6),
4,
3.2,
boxstyle="round,pad=0.1",
facecolor="#FFCDD2",
edgecolor=BLACK,
linewidth=1,
)
ax.add_patch(rect2)
ax.text(
7.5,
8.8,
"Problem: over-segmentation",
fontsize=FS,
ha="center",
fontweight="bold",
color=RED_ACCENT,
)
ax.text(
7.5,
7.8,
"47 regionów zamiast 2!",
fontsize=FS_SMALL,
ha="center",
color=RED_ACCENT,
)
ax.text(7.5, 7.1, "Każde mini-minimum", fontsize=FS_SMALL, ha="center")
ax.text(7.5, 6.5, '→ osobna „dolina"', fontsize=FS_SMALL, ha="center")
# Apply marker-controlled solution
rect3 = FancyBboxPatch(
(1, 0.5),
8,
4.5,
boxstyle="round,pad=0.15",
facecolor=GRAY1,
edgecolor=GREEN_ACCENT,
linewidth=1.5,
)
ax.add_patch(rect3)
ax.text(
5,
4.3,
"Rozwiązanie: Marker-controlled watershed",
fontsize=FS,
ha="center",
fontweight="bold",
color=GREEN_ACCENT,
)
ax.text(
5,
3.4,
'1. Zaznacz ręcznie „seeds" (markery) w każdym obiekcie',
fontsize=FS_SMALL,
ha="center",
)
ax.text(
5,
2.7,
"2. Zalewaj TYLKO od tych markerów (nie od wszystkich minimów)",
fontsize=FS_SMALL,
ha="center",
)
ax.text(
5,
2.0,
"3. Eliminuje fałszywe doliny z szumu",
fontsize=FS_SMALL,
ha="center",
)
ax.text(
5,
1.2,
"Wynik: tyle segmentów, ile podano markerów",
fontsize=FS_SMALL,
ha="center",
fontweight="bold",
)
def generate_watershed() -> None:
"""Generate watershed."""
_fig, axes = plt.subplots(1, 3, figsize=(11, 3.8))
# --- Panel 1: Image as topographic surface ---
ax = axes[0]
x = np.linspace(0, 10, 200)
# Create a surface with two valleys and a ridge
surface = (
3 * np.exp(-((x - 3) ** 2) / 1.5)
+ 4 * np.exp(-((x - 7) ** 2) / 1.2)
+ 0.5 * np.sin(x * 2)
+ 1
)
# Invert: valleys at objects (dark), peaks at boundaries (bright)
surface_inv = 6 - surface + 1
ax.fill_between(x, 0, surface_inv, color=GRAY2, alpha=0.7)
ax.plot(x, surface_inv, color=BLACK, linewidth=1.5)
# Mark valleys
ax.annotate(
"Dolina 1\n(obiekt A)",
xy=(3, surface_inv[60]),
fontsize=FS_SMALL,
ha="center",
va="bottom",
arrowprops={"arrowstyle": "->", "color": ACCENT},
xytext=(1.5, 5.5),
)
ax.annotate(
"Dolina 2\n(obiekt B)",
xy=(7, surface_inv[140]),
fontsize=FS_SMALL,
ha="center",
va="bottom",
arrowprops={"arrowstyle": "->", "color": RED_ACCENT},
xytext=(8.5, 5.5),
)
# Mark ridge
ax.annotate(
"Grań\n(granica)",
xy=(5, surface_inv[100]),
fontsize=FS_SMALL,
ha="center",
va="bottom",
arrowprops={"arrowstyle": "->", "color": GREEN_ACCENT},
xytext=(5, 6.5),
)
ax.set_xlabel("Pozycja piksela", fontsize=FS)
ax.set_ylabel("Jasność (= wysokość)", fontsize=FS)
ax.set_title("Krok 1: obraz → teren", fontsize=FS_TITLE, fontweight="bold")
ax.set_ylim(0, 7)
# --- Panel 2: Flooding ---
ax = axes[1]
ax.fill_between(x, 0, surface_inv, color=GRAY2, alpha=0.7)
ax.plot(x, surface_inv, color=BLACK, linewidth=1.5)
# Water level
water_level = 3.2
# Fill water in valley 1
x_v1 = x[(x > 1) & (x < _RIDGE_X)]
s_v1 = surface_inv[(x > 1) & (x < _RIDGE_X)]
ax.fill_between(
x_v1, s_v1, water_level, where=s_v1 < water_level, color=ACCENT_LIGHT, alpha=0.6
)
# Fill water in valley 2
x_v2 = x[(x > _RIDGE_X) & (x < _VALLEY2_END)]
s_v2 = surface_inv[(x > _RIDGE_X) & (x < _VALLEY2_END)]
ax.fill_between(
x_v2, s_v2, water_level, where=s_v2 < water_level, color="#FFCDD2", alpha=0.6
)
ax.axhline(y=water_level, color=ACCENT, linewidth=1, linestyle="--", alpha=0.5)
ax.text(3, 2.5, "Woda A", fontsize=FS, ha="center", color=ACCENT, fontweight="bold")
ax.text(
7, 2.2, "Woda B", fontsize=FS, ha="center", color=RED_ACCENT, fontweight="bold"
)
ax.annotate(
"Tu się spotkają!\n→ GRANICA",
xy=(5, surface_inv[100]),
fontsize=FS_SMALL,
ha="center",
color=GREEN_ACCENT,
fontweight="bold",
arrowprops={"arrowstyle": "->", "color": GREEN_ACCENT},
xytext=(5, 6.2),
)
ax.set_xlabel("Pozycja piksela", fontsize=FS)
ax.set_title("Krok 2: zalewanie", fontsize=FS_TITLE, fontweight="bold")
ax.set_ylim(0, 7)
# --- Panel 3: Result with problem ---
_draw_watershed_result_panel(axes[2])
_save_figure("q23_watershed.png")

View File

@ -0,0 +1,286 @@
"""Receptive field and transformer diagram generators."""
from __future__ import annotations
from _q23_common import (
_HIGHLIGHT_END,
_HIGHLIGHT_START,
ACCENT,
ACCENT_LIGHT,
BLACK,
FS,
FS_SMALL,
FS_TITLE,
GRAY3,
GRAY5,
GREEN_ACCENT,
RED_ACCENT,
WHITE,
_save_figure,
plt,
)
from matplotlib import patches
def generate_receptive_field() -> None:
"""Generate receptive field."""
_fig, axes = plt.subplots(1, 3, figsize=(11, 4))
def draw_grid(
ax: patches.Axes,
size: int,
highlight_cells: list[tuple[int, int]],
highlight_color: str,
title: str,
grid_offset: tuple[int, int] = (0, 0),
) -> None:
"""Draw grid."""
ox, oy = grid_offset
for i in range(size):
for j in range(size):
color = WHITE
if (i, j) in highlight_cells:
color = highlight_color
rect = patches.Rectangle(
(ox + j, oy + size - 1 - i),
1,
1,
facecolor=color,
edgecolor=GRAY3, # Use GRAY3 instead of GRAY4 since unused
linewidth=0.5,
)
ax.add_patch(rect)
ax.set_title(title, fontsize=FS_TITLE, fontweight="bold")
# --- Panel 1: Standard 3x3 conv receptive field ---
ax = axes[0]
ax.set_xlim(-0.5, 7.5)
ax.set_ylim(-1, 8)
ax.set_aspect("equal")
ax.axis("off")
# 7x7 input grid
highlight_3x3 = [
(2, 2),
(2, 3),
(2, 4),
(3, 2),
(3, 3),
(3, 4),
(4, 2),
(4, 3),
(4, 4),
]
draw_grid(ax, 7, highlight_3x3, ACCENT_LIGHT, "Zwykła conv 3x3")
ax.text(
3.5,
-0.5,
"RF = 3x3 pikseli",
fontsize=FS,
ha="center",
fontweight="bold",
color=ACCENT,
)
# --- Panel 2: Dilated conv (rate=2) ---
ax = axes[1]
ax.set_xlim(-0.5, 7.5)
ax.set_ylim(-1, 8)
ax.set_aspect("equal")
ax.axis("off")
# 7x7 input grid with dilated highlights
highlight_dilated = [
(1, 1),
(1, 3),
(1, 5),
(3, 1),
(3, 3),
(3, 5),
(5, 1),
(5, 3),
(5, 5),
]
draw_grid(ax, 7, highlight_dilated, "#FFCDD2", "Dilated conv 3x3\n(rate=2)")
ax.text(
3.5,
-0.5,
"RF = 5x5, ale 9 parametrów!",
fontsize=FS,
ha="center",
fontweight="bold",
color=RED_ACCENT,
)
# Connect dots to show pattern
dots_x = [1.5, 3.5, 5.5, 1.5, 3.5, 5.5, 1.5, 3.5, 5.5]
dots_y = [5.5, 5.5, 5.5, 3.5, 3.5, 3.5, 1.5, 1.5, 1.5]
ax.scatter(dots_x, dots_y, c=RED_ACCENT, s=30, zorder=5)
# --- Panel 3: Comparison ---
ax = axes[2]
ax.set_xlim(0, 10)
ax.set_ylim(0, 10)
ax.axis("off")
ax.set_title(
"Receptive Field\n(pole widzenia neuronu)", fontsize=FS_TITLE, fontweight="bold"
)
y = 8.5
lines = [
("RF = ile pikseli WEJŚCIOWYCH", FS, BLACK, "bold"),
("wpływa na JEDEN piksel wyjścia", FS, BLACK, "bold"),
("", 0, "", ""),
("Rate (współczynnik dylatacji):", FS, BLACK, "bold"),
(' rate=1: filtr „dotyka" sąsiadów', FS_SMALL, BLACK, "normal"),
(" rate=2: co drugi piksel → RF = 5x5", FS_SMALL, BLACK, "normal"),
(" rate=3: co trzeci → RF = 7x7", FS_SMALL, BLACK, "normal"),
(" WIĘCEJ kontekstu, TE SAME wagi!", FS_SMALL, GREEN_ACCENT, "bold"),
("", 0, "", ""),
("Dlaczego ważne w segmentacji?", FS, BLACK, "bold"),
(" Piksel sam nie wie czym jest.", FS_SMALL, BLACK, "normal"),
(" Potrzebuje KONTEKSTU (otoczenia).", FS_SMALL, BLACK, "normal"),
(" Większe RF → widzi obok budynki", FS_SMALL, BLACK, "normal"),
(' → wie, że TEN piksel to „droga"', FS_SMALL, GREEN_ACCENT, "bold"),
("", 0, "", ""),
("Global Average Pooling:", FS, BLACK, "bold"),
(" Mapa HxWxC → 1x1xC", FS_SMALL, BLACK, "normal"),
(" Średnia z CAŁEGO feature map", FS_SMALL, BLACK, "normal"),
(" RF = nieskończone (cały obraz)", FS_SMALL, GREEN_ACCENT, "bold"),
]
for txt, size, color, weight in lines:
if txt == "":
y -= 0.2
continue
ax.text(0.5, y, txt, fontsize=size, color=color, fontweight=weight, va="top")
y -= 0.45
_save_figure("q23_receptive_field.png")
def generate_transformer() -> None:
"""Generate transformer."""
_fig, axes = plt.subplots(1, 3, figsize=(11, 4))
# --- Panel 1: CNN local vs Transformer global ---
ax = axes[0]
ax.set_xlim(-0.5, 8.5)
ax.set_ylim(-1.5, 8.5)
ax.set_aspect("equal")
ax.axis("off")
ax.set_title("CNN: widzi LOKALNIE", fontsize=FS_TITLE, fontweight="bold")
# Draw 8x8 grid
for i in range(8):
for j in range(8):
color = WHITE
if (
_HIGHLIGHT_START <= i <= _HIGHLIGHT_END
and _HIGHLIGHT_START <= j <= _HIGHLIGHT_END
):
color = ACCENT_LIGHT
rect = patches.Rectangle(
(j, 7 - i), 1, 1, facecolor=color, edgecolor=GRAY3, linewidth=0.3
)
ax.add_patch(rect)
# Highlight center
rect = patches.Rectangle(
(4, 4), 1, 1, facecolor=RED_ACCENT, edgecolor=BLACK, linewidth=1.5, alpha=0.7
)
ax.add_patch(rect)
ax.text(
4.5,
4.5,
"?",
ha="center",
va="center",
fontsize=FS,
fontweight="bold",
color=WHITE,
)
ax.text(
4.5,
-0.8,
"Filtr 3x3 widzi tylko\n9 sąsiednich pikseli",
fontsize=FS_SMALL,
ha="center",
color=ACCENT,
)
# --- Panel 2: Transformer global ---
ax = axes[1]
ax.set_xlim(-0.5, 8.5)
ax.set_ylim(-1.5, 8.5)
ax.set_aspect("equal")
ax.axis("off")
ax.set_title("Transformer: widzi GLOBALNIE", fontsize=FS_TITLE, fontweight="bold")
# Draw 8x8 grid all highlighted
for i in range(8):
for j in range(8):
color = "#FFCDD2"
rect = patches.Rectangle(
(j, 7 - i), 1, 1, facecolor=color, edgecolor=GRAY3, linewidth=0.3
)
ax.add_patch(rect)
rect = patches.Rectangle(
(4, 4), 1, 1, facecolor=RED_ACCENT, edgecolor=BLACK, linewidth=1.5, alpha=0.9
)
ax.add_patch(rect)
ax.text(
4.5,
4.5,
"?",
ha="center",
va="center",
fontsize=FS,
fontweight="bold",
color=WHITE,
)
ax.text(
4.5,
-0.8,
'Self-attention „pyta"\nALL 64 piksele naraz',
fontsize=FS_SMALL,
ha="center",
color=RED_ACCENT,
)
# --- Panel 3: SOTA + Transformer explanation ---
ax = axes[2]
ax.set_xlim(0, 10)
ax.set_ylim(0, 10)
ax.axis("off")
ax.set_title("Transformer & SOTA", fontsize=FS_TITLE, fontweight="bold")
y = 9.2
lines = [
("Transformer:", FS, BLACK, "bold"),
(" Architektura z 2017 (Vaswani et al.)", FS_SMALL, BLACK, "normal"),
(" Oryginalnie do NLP (tłumaczenie)", FS_SMALL, BLACK, "normal"),
(" Kluczowy mechanizm: SELF-ATTENTION", FS_SMALL, ACCENT, "bold"),
("", 0, "", ""),
("Self-attention w skrócie:", FS, BLACK, "bold"),
(" Każdy piksel tworzy trzy wektory:", FS_SMALL, BLACK, "normal"),
(' Q (Query — „czego szukam?")', FS_SMALL, ACCENT, "normal"),
(' K (Key — „co oferuję innych")', FS_SMALL, RED_ACCENT, "normal"),
(' V (Value — „moja wartość")', FS_SMALL, GREEN_ACCENT, "normal"),
(" Attention = softmax(Q·Kᵀ/√d)·V", FS_SMALL, BLACK, "bold"),
(" Koszt: O(n²) — n=liczba pikseli", FS_SMALL, RED_ACCENT, "normal"),
("", 0, "", ""),
("SOTA = State Of The Art:", FS, BLACK, "bold"),
(" Najlepszy znany wynik na benchmarku", FS_SMALL, BLACK, "normal"),
(' Np. „mIoU 85.1% na ADE20K = SOTA"', FS_SMALL, BLACK, "normal"),
(" Ciągle się zmienia (nowy paper", FS_SMALL, GRAY5, "normal"),
(" → nowy SOTA)", FS_SMALL, GRAY5, "normal"),
]
for txt, size, color, weight in lines:
if txt == "":
y -= 0.15
continue
ax.text(0.3, y, txt, fontsize=size, color=color, fontweight=weight, va="top")
y -= 0.45
_save_figure("q23_transformer_attention.png")

View File

@ -0,0 +1,408 @@
"""Region growing and DIY thresholding diagram generators."""
from __future__ import annotations
from typing import TYPE_CHECKING
from _q23_common import (
_BRIGHT_THRESHOLD,
_OTSU_THRESHOLD,
ACCENT,
ACCENT_LIGHT,
BLACK,
FS,
FS_SMALL,
FS_TINY,
FS_TITLE,
GRAY3,
GRAY4,
GRAY5,
GREEN_ACCENT,
RED_ACCENT,
WHITE,
_save_figure,
np,
plt,
rng,
)
from matplotlib import patches
if TYPE_CHECKING:
from matplotlib.axes import Axes
def _draw_region_growing_grid(ax: Axes) -> None:
"""Draw panel 2: region growing step-by-step grid."""
ax.set_xlim(-0.5, 6.5)
ax.set_ylim(-1.5, 7.5)
ax.set_aspect("equal")
ax.axis("off")
ax.set_title(
"Region Growing: krok po kroku",
fontsize=FS_TITLE,
fontweight="bold",
)
pixel_grid = np.array(
[
[150, 153, 148, 200, 210, 205],
[147, 155, 152, 195, 208, 200],
[145, 148, 160, 190, 195, 210],
[200, 195, 190, 155, 148, 150],
[210, 205, 200, 150, 152, 145],
[215, 208, 195, 148, 147, 155],
]
)
region_mask = np.array(
[
[1, 1, 1, 0, 0, 0],
[1, 1, 1, 0, 0, 0],
[1, 1, 1, 0, 0, 0],
[0, 0, 0, 1, 1, 1],
[0, 0, 0, 1, 1, 1],
[0, 0, 0, 1, 1, 1],
]
)
for i in range(6):
for j in range(6):
v = pixel_grid[i, j]
if region_mask[i, j] == 1 and v < _BRIGHT_THRESHOLD:
cell_color = ACCENT_LIGHT
elif region_mask[i, j] == 1:
cell_color = "#E0E0E0"
else:
cell_color = WHITE
if i == 1 and j == 1:
cell_color = "#FFD54F"
rect = patches.Rectangle(
(j, 5 - i),
1,
1,
facecolor=cell_color,
edgecolor=GRAY4,
linewidth=0.5,
)
ax.add_patch(rect)
ax.text(
j + 0.5,
5 - i + 0.5,
str(v),
ha="center",
va="center",
fontsize=FS_TINY,
fontweight="bold",
)
ax.annotate(
"SEED\n(155)",
xy=(1.5, 4.5),
fontsize=FS_SMALL,
ha="center",
color=RED_ACCENT,
fontweight="bold",
arrowprops={"arrowstyle": "->", "color": RED_ACCENT},
xytext=(-0.5, 7),
)
ax.text(
3,
-0.8,
"Próg = 20\nNiebieski = region (|val - seed| < 20)",
fontsize=FS_TINY,
ha="center",
color=ACCENT,
)
def _draw_bfs_expansion(ax: Axes) -> None:
"""Draw panel 3: BFS expansion visualization."""
ax.set_xlim(-0.5, 6.5)
ax.set_ylim(-1.5, 7.5)
ax.set_aspect("equal")
ax.axis("off")
ax.set_title(
"Rosnący region (BFS)",
fontsize=FS_TITLE,
fontweight="bold",
)
wave_colors = ["#FFD54F", "#FFF176", "#FFF9C4", ACCENT_LIGHT, "#B3D4FC"]
wave_labels = ["Seed", "Fala 1", "Fala 2", "Fala 3", "Fala 4"]
waves = [
[(1, 1)],
[(0, 1), (1, 0), (1, 2), (2, 1)],
[(0, 0), (0, 2), (2, 0), (2, 2)],
]
for i in range(6):
for j in range(6):
cell_color = WHITE
for w_idx, wave in enumerate(waves):
if (i, j) in wave:
cell_color = wave_colors[w_idx]
rect = patches.Rectangle(
(j, 5 - i),
1,
1,
facecolor=cell_color,
edgecolor=GRAY4,
linewidth=0.5,
)
ax.add_patch(rect)
seed_x, seed_y = 1.5, 4.5
for dx, dy, _label in [
(0, 1, ""),
(0, -1, ""),
(1, 0, ""),
(-1, 0, ""),
]:
ax.annotate(
"",
xy=(seed_x + dx * 0.7, seed_y + dy * 0.7),
xytext=(seed_x, seed_y),
arrowprops={
"arrowstyle": "->",
"color": RED_ACCENT,
"lw": 1.2,
},
)
ax.text(
3,
-0.5,
"BFS: sprawdzaj sąsiadów,\ndodawaj podobne do kolejki",
fontsize=FS_TINY,
ha="center",
color=GRAY5,
)
for w_idx, (wave_color, label) in enumerate(
zip(wave_colors[:3], wave_labels[:3], strict=False)
):
rect = patches.Rectangle(
(4, 6.5 - w_idx * 0.7),
0.5,
0.5,
facecolor=wave_color,
edgecolor=GRAY4,
linewidth=0.5,
)
ax.add_patch(rect)
ax.text(
4.8,
6.75 - w_idx * 0.7,
label,
fontsize=FS_TINY,
va="center",
)
def generate_region_growing() -> None:
"""Generate region growing."""
_fig, axes = plt.subplots(1, 3, figsize=(11, 4.2))
# --- Panel 1: Manual vs automatic seed ---
ax = axes[0]
ax.set_xlim(0, 10)
ax.set_ylim(0, 10)
ax.axis("off")
ax.set_title("Seed: ręcznie vs automatycznie", fontsize=FS_TITLE, fontweight="bold")
y = 9.2
lines = [
("Ręczny seed:", FS, ACCENT, "bold"),
(" Użytkownik klika na obraz", FS_SMALL, BLACK, "normal"),
(' → „tu jest obiekt, od tego zacznij"', FS_SMALL, BLACK, "normal"),
(" Użycie: segmentacja interaktywna", FS_SMALL, GRAY5, "normal"),
(" (np. Photoshop — magic wand tool)", FS_SMALL, GRAY5, "normal"),
("", 0, "", ""),
("Automatyczny seed:", FS, RED_ACCENT, "bold"),
(" 1. Histogram → lokalne maxima", FS_SMALL, BLACK, "normal"),
(" (najczęstsza jasność → seed)", FS_SMALL, GRAY5, "normal"),
(" 2. Grid: siatka co N pikseli", FS_SMALL, BLACK, "normal"),
(" (np. seed co 50 px → 100 seedów)", FS_SMALL, GRAY5, "normal"),
(" 3. Losowe próbkowanie", FS_SMALL, BLACK, "normal"),
(" 4. Ekstrema lokalne gradientu", FS_SMALL, BLACK, "normal"),
("", 0, "", ""),
("Dlaczego OR?", FS, GREEN_ACCENT, "bold"),
(" Ręczny → precyzyjny, ale wolny", FS_SMALL, BLACK, "normal"),
(" Auto → szybki, ale over-segmentation", FS_SMALL, BLACK, "normal"),
]
for txt, size, color, weight in lines:
if txt == "":
y -= 0.15
continue
ax.text(0.3, y, txt, fontsize=size, color=color, fontweight=weight, va="top")
y -= 0.45
# --- Panel 2: Region growing step by step ---
_draw_region_growing_grid(axes[1])
# --- Panel 3: BFS expansion ---
_draw_bfs_expansion(axes[2])
_save_figure("q23_region_growing.png")
def _draw_otsu_variance_and_pseudocode(
ax_var: Axes,
ax_code: Axes,
img: np.ndarray,
) -> int:
"""Draw panels 4 and 5: Otsu variance plot and pseudocode."""
thresholds = range(10, 245)
variances = []
for t in thresholds:
c0 = img[img <= t].ravel()
c1 = img[img > t].ravel()
if len(c0) == 0 or len(c1) == 0:
variances.append(np.nan)
continue
w0 = len(c0) / len(img.ravel())
w1 = len(c1) / len(img.ravel())
var = w0 * np.var(c0) + w1 * np.var(c1)
variances.append(var)
ax_var.plot(list(thresholds), variances, color=ACCENT, linewidth=1.5)
best_t = list(thresholds)[np.nanargmin(variances)]
ax_var.axvline(
x=best_t,
color=RED_ACCENT,
linewidth=1.5,
linestyle="--",
label=f"Otsu T={best_t}",
)
ax_var.scatter(
[best_t],
[np.nanmin(variances)],
c=RED_ACCENT,
s=60,
zorder=5,
)
ax_var.set_xlabel("Próg T", fontsize=FS_SMALL)
ax_var.set_ylabel("σ² wewnątrzklasowa", fontsize=FS_SMALL)
ax_var.set_title(
"Krok 4: Otsu szuka min σ²",
fontsize=FS,
fontweight="bold",
)
ax_var.legend(fontsize=FS_TINY)
ax_code.set_xlim(0, 10)
ax_code.set_ylim(0, 10)
ax_code.axis("off")
ax_code.set_title("Pseudokod Otsu", fontsize=FS, fontweight="bold")
code_lines = [
"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)",
" w1 = len(c1) / len(all)",
" var = w0·var(c0) + w1·var(c1)",
" if var < min_var:",
" min_var = var",
" best_T = T",
"",
"return best_T # optymalny próg",
]
for i, line in enumerate(code_lines):
txt_color = ACCENT if "best_T = T" in line or "return" in line else BLACK
ax_code.text(
0.5,
9.5 - i * 0.65,
line,
fontsize=FS_TINY,
fontfamily="monospace",
color=txt_color,
fontweight="bold" if txt_color == ACCENT else "normal",
)
return int(best_t)
def generate_diy_thresholding() -> None:
"""Generate diy thresholding."""
_fig, axes = plt.subplots(2, 3, figsize=(11, 7))
# Create a simple synthetic image: dark circle on bright background
size = 64
img = np.ones((size, size)) * 200 # bright background
yy, xx = np.mgrid[:size, :size]
mask = ((xx - 32) ** 2 + (yy - 32) ** 2) < 15**2
img[mask] = 60 # dark circle
# Add some noise
img += rng.normal(0, 10, img.shape)
img = np.clip(img, 0, 255)
# --- Panel 1: Original image ---
ax = axes[0, 0]
ax.imshow(img, cmap="gray", vmin=0, vmax=255)
ax.set_title("Krok 1: obraz wejściowy", fontsize=FS, fontweight="bold")
ax.axis("off")
ax.text(32, -3, "64x64 pikseli, szare", fontsize=FS_TINY, ha="center")
# --- Panel 2: Histogram ---
ax = axes[0, 1]
counts, _bins, _ = ax.hist(
img.ravel(), bins=50, color=GRAY3, edgecolor=GRAY5, linewidth=0.5
)
ax.axvline(
x=128, color=RED_ACCENT, linewidth=2, linestyle="--", label="T=128 (Otsu)"
)
ax.set_xlabel("Jasność", fontsize=FS_SMALL)
ax.set_ylabel("Piksele", fontsize=FS_SMALL)
ax.set_title("Krok 2: histogram\n(bimodalny!)", fontsize=FS, fontweight="bold")
ax.legend(fontsize=FS_TINY)
ax.annotate(
"Garb 1\n(obiekt)",
xy=(60, max(counts) * 0.5),
fontsize=FS_TINY,
ha="center",
color=ACCENT,
fontweight="bold",
)
ax.annotate(
"Garb 2\n(tło)",
xy=(200, max(counts) * 0.5),
fontsize=FS_TINY,
ha="center",
color=RED_ACCENT,
fontweight="bold",
)
# --- Panel 3: Thresholding result ---
ax = axes[0, 2]
binary = (img > _OTSU_THRESHOLD).astype(float)
ax.imshow(binary, cmap="gray", vmin=0, vmax=1)
ax.set_title("Krok 3: progowanie T=128", fontsize=FS, fontweight="bold")
ax.axis("off")
ax.text(32, -3, "Biały = tło, Czarny = obiekt", fontsize=FS_TINY, ha="center")
# --- Panels 4+5: Otsu variance plot + pseudocode ---
best_t = _draw_otsu_variance_and_pseudocode(
axes[1, 0],
axes[1, 1],
img,
)
# --- Panel 6: Final result with Otsu ---
ax = axes[1, 2]
binary_otsu = (img > best_t).astype(float)
ax.imshow(binary_otsu, cmap="gray", vmin=0, vmax=1)
ax.set_title(f"Krok 5: wynik Otsu (T={best_t})", fontsize=FS, fontweight="bold")
ax.axis("off")
ax.text(
32,
-3,
"Automatyczny próg!",
fontsize=FS_TINY,
ha="center",
color=GREEN_ACCENT,
fontweight="bold",
)
_save_figure("q23_diy_thresholding.png")

View File

@ -0,0 +1,186 @@
"""Common utilities and constants for Q24 diagram generation.
Monochrome, A4-printable PNGs (300 DPI).
"""
from __future__ import annotations
import logging
from pathlib import Path
from typing import TYPE_CHECKING
import matplotlib as mpl
mpl.use("Agg")
import matplotlib.patches as mpatches
from matplotlib.patches import FancyBboxPatch
import matplotlib.pyplot as plt
import numpy as np
if TYPE_CHECKING:
from matplotlib.axes import Axes
from matplotlib.figure import Figure
_logger = logging.getLogger(__name__)
rng = np.random.default_rng(42)
DPI = 300
BG = "white"
LN = "black"
FS = 8
FS_TITLE = 11
FS_SMALL = 6.5
FS_LABEL = 9
OUTPUT_DIR = str(Path(__file__).resolve().parent / "img")
Path(OUTPUT_DIR).mkdir(parents=True, exist_ok=True)
GRAY1 = "#E8E8E8"
GRAY2 = "#D0D0D0"
GRAY3 = "#B8B8B8"
GRAY4 = "#F5F5F5"
GRAY5 = "#C0C0C0"
_PIXEL_BRIGHT_THRESH = 127
_GRADIENT_BRIGHT_THRESH = 100
_DATA_BRIGHT_THRESH = 5
_II_BRIGHT_THRESH = 25
_DOTS_STAGE_IDX = 2
def draw_box(
ax: Axes,
x: float,
y: float,
w: float,
h: float,
text: str,
*,
fill: str = "white",
lw: float = 1.2,
fontsize: float = FS,
fontweight: str = "normal",
ha: str = "center",
va: str = "center",
rounded: bool = True,
edgecolor: str = LN,
linestyle: str = "-",
) -> None:
"""Draw box."""
if rounded:
rect = FancyBboxPatch(
(x, y),
w,
h,
boxstyle="round,pad=0.05",
lw=lw,
edgecolor=edgecolor,
facecolor=fill,
linestyle=linestyle,
)
else:
rect = mpatches.Rectangle(
(x, y),
w,
h,
lw=lw,
edgecolor=edgecolor,
facecolor=fill,
linestyle=linestyle,
)
ax.add_patch(rect)
ax.text(
x + w / 2,
y + h / 2,
text,
ha=ha,
va=va,
fontsize=fontsize,
fontweight=fontweight,
wrap=True,
)
def draw_arrow(
ax: Axes,
x1: float,
y1: float,
x2: float,
y2: float,
*,
lw: float = 1.2,
style: str = "->",
color: str = LN,
) -> None:
"""Draw arrow."""
ax.annotate(
"",
xy=(x2, y2),
xytext=(x1, y1),
arrowprops={"arrowstyle": style, "color": color, "lw": lw},
)
def save_fig(fig: Figure, name: str) -> None:
"""Save fig."""
path = str(Path(OUTPUT_DIR) / name)
fig.savefig(path, dpi=DPI, bbox_inches="tight", facecolor=BG, pad_inches=0.15)
plt.close(fig)
_logger.info(" Saved: %s", path)
def draw_table(
ax: Axes,
headers: list[str],
rows: list[list[str]],
x0: float,
y0: float,
col_widths: list[float],
*,
row_h: float = 0.4,
header_fill: str = GRAY2,
row_fills: list[str] | None = None,
fontsize: float = FS,
header_fontsize: float | None = None,
) -> None:
"""Draw table."""
if header_fontsize is None:
header_fontsize = fontsize
len(headers)
cx = x0
for j, hdr in enumerate(headers):
draw_box(
ax,
cx,
y0,
col_widths[j],
row_h,
hdr,
fill=header_fill,
fontsize=header_fontsize,
fontweight="bold",
rounded=False,
)
cx += col_widths[j]
for i, row in enumerate(rows):
cy = y0 - (i + 1) * row_h
cx = x0
fill = GRAY4 if (i % 2 == 0) else "white"
if row_fills and i < len(row_fills):
fill = row_fills[i]
for j, cell in enumerate(row):
fw = "bold" if j == 0 else "normal"
draw_box(
ax,
cx,
cy,
col_widths[j],
row_h,
cell,
fill=fill,
fontsize=fontsize,
fontweight=fw,
rounded=False,
)
cx += col_widths[j]

View File

@ -0,0 +1,412 @@
"""FPN, anchor boxes, detection tasks, and CNN architecture diagrams."""
from __future__ import annotations
from _q24_common import (
FS,
FS_SMALL,
FS_TITLE,
GRAY1,
GRAY2,
GRAY3,
GRAY4,
LN,
draw_arrow,
draw_box,
np,
plt,
save_fig,
)
import matplotlib.patches as mpatches
# ============================================================
# 16. FPN (Feature Pyramid Network)
# ============================================================
def draw_fpn() -> None:
"""Draw fpn."""
fig, ax = plt.subplots(figsize=(9, 5))
ax.set_xlim(-0.5, 9.5)
ax.set_ylim(-0.5, 5.5)
ax.set_aspect("equal")
ax.axis("off")
ax.set_title(
"FPN (Feature Pyramid Network) — detekcja obiektów wszystkich rozmiarów",
fontsize=FS_TITLE,
fontweight="bold",
pad=12,
)
levels = [
(0, 0, 2.0, 2.0, "C2\n56x56", "duże\ndetale"),
(0, 2.2, 1.5, 1.5, "C3\n28x28", ""),
(0, 3.9, 1.0, 1.0, "C4\n14x14", ""),
(0, 5.1, 0.6, 0.6, "C5\n7x7", "kontekst"),
]
for x, y, w, h, label, note in levels:
ax.add_patch(
mpatches.Rectangle((x, y - h), w, h, facecolor=GRAY4, edgecolor=LN, lw=1.5)
)
ax.text(
x + w / 2,
y - h / 2,
label,
ha="center",
va="center",
fontsize=FS_SMALL,
fontweight="bold",
)
if note:
ax.text(
x + w + 0.15,
y - h / 2,
note,
ha="left",
va="center",
fontsize=5,
style="italic",
)
ax.text(
1.0, -0.3, "Bottom-up\n(backbone)", ha="center", fontsize=FS, fontweight="bold"
)
# Top-down + lateral
td_levels = [
(4.5, 5.1, 0.6, 0.6, "P5"),
(4.5, 3.9, 1.0, 1.0, "P4"),
(4.5, 2.2, 1.5, 1.5, "P3"),
(4.5, 0, 2.0, 2.0, "P2"),
]
for x, y, w, h, label in td_levels:
ax.add_patch(
mpatches.Rectangle(
(x, y - h + h), w, h, facecolor=GRAY2, edgecolor=LN, lw=1.5
)
)
ax.text(
x + w / 2,
y - h / 2 + h,
label,
ha="center",
va="center",
fontsize=FS_SMALL,
fontweight="bold",
)
# Lateral connections
for (_, y1, w1, h1, _, _), (x2, y2, _w2, h2, _) in zip(
levels, td_levels, strict=False
):
draw_arrow(ax, w1 + 0.2, y1 - h1 / 2, x2 - 0.1, y2 + h2 / 2, lw=1, style="->")
# Top-down arrows
for i in range(len(td_levels) - 1):
x2, y2, w2, h2, _ = td_levels[i]
x3, y3, w3, h3, _ = td_levels[i + 1]
draw_arrow(
ax,
x2 + w2 / 2,
y2,
x3 + w3 / 2,
y3 + h3 + 0.1,
lw=1.2,
style="->",
color=GRAY3,
)
ax.text(
5.5,
-0.3,
"Top-down + lateral\n(FPN)",
ha="center",
fontsize=FS,
fontweight="bold",
)
# Detection outputs
det_labels = ["małe obj.", "średnie", "duże", "b. duże"]
for i, (x, y, w, h, _label) in enumerate(td_levels):
draw_arrow(ax, x + w + 0.1, y + h / 2, 7.5, y + h / 2, lw=0.8)
ax.text(
7.7,
y + h / 2,
f"detekcja:\n{det_labels[3 - i]}",
fontsize=FS_SMALL,
va="center",
)
save_fig(fig, "q24_fpn.png")
# ============================================================
# 17. Anchor boxes
# ============================================================
def draw_anchor_boxes() -> None:
"""Draw anchor boxes."""
fig, ax = plt.subplots(figsize=(7, 5))
ax.set_title(
"Anchor boxes — predefiniowane kształty",
fontsize=FS_TITLE,
fontweight="bold",
pad=12,
)
ax.add_patch(mpatches.Rectangle((0, 0), 6, 5, facecolor=GRAY4, edgecolor=LN, lw=1))
# Center point
cx, cy = 3, 2.5
ax.plot(cx, cy, "ko", markersize=8, zorder=5)
ax.text(cx + 0.15, cy + 0.15, "(x, y)", fontsize=FS, fontweight="bold")
# 9 anchors: 3 sizes x 3 ratios
anchors = [
(0.8, 0.8, "-", "1:1 small"),
(1.6, 1.6, "-", "1:1 medium"),
(2.4, 2.4, "-", "1:1 large"),
(0.6, 1.2, "--", "1:2 small"),
(1.2, 2.4, "--", "1:2 medium"),
(1.8, 3.6, "--", "1:2 large"),
(1.2, 0.6, ":", "2:1 small"),
(2.4, 1.2, ":", "2:1 medium"),
(3.6, 1.8, ":", "2:1 large"),
]
for w, h, ls, _label in anchors:
rect = mpatches.Rectangle(
(cx - w / 2, cy - h / 2),
w,
h,
facecolor="none",
edgecolor=LN,
lw=1.2,
linestyle=ls,
)
ax.add_patch(rect)
# Legend-style labels
ax.text(
3,
-0.5,
"9 anchorów = 3 rozmiary x 3 proporcje (1:1, 1:2, 2:1)\n"
"Sieć predykuje PRZESUNIĘCIE od najbliższego anchora",
ha="center",
fontsize=FS,
style="italic",
bbox={"boxstyle": "round,pad=0.3", "facecolor": GRAY4, "edgecolor": GRAY3},
)
ax.set_xlim(-0.5, 6.5)
ax.set_ylim(-1.2, 5.5)
ax.set_aspect("equal")
ax.axis("off")
save_fig(fig, "q24_anchor_boxes.png")
# ============================================================
# 18. Detection task comparison
# ============================================================
def draw_detection_tasks() -> None:
"""Draw detection tasks."""
fig, axes = plt.subplots(1, 3, figsize=(12, 4))
fig.suptitle(
"Klasyfikacja vs Detekcja vs Segmentacja",
fontsize=FS_TITLE,
fontweight="bold",
y=1.02,
)
# Classification
ax = axes[0]
ax.add_patch(
mpatches.Rectangle((0, 0), 4, 4, facecolor=GRAY4, edgecolor=LN, lw=1.5)
)
# Simple cat silhouette
ax.add_patch(mpatches.Ellipse((2, 2), 2, 1.5, facecolor=GRAY3, edgecolor=LN, lw=1))
ax.add_patch(mpatches.Ellipse((2, 3), 1, 0.8, facecolor=GRAY3, edgecolor=LN, lw=1))
# Ears
ax.plot([1.6, 1.5, 1.8], [3.3, 3.8, 3.4], color=LN, lw=1.5)
ax.plot([2.2, 2.5, 2.4], [3.3, 3.8, 3.4], color=LN, lw=1.5)
ax.text(
2, -0.4, '"KOT" (jedna etykieta)', ha="center", fontsize=FS, fontweight="bold"
)
ax.set_xlim(-0.5, 4.5)
ax.set_ylim(-0.8, 4.5)
ax.set_aspect("equal")
ax.axis("off")
ax.set_title("Klasyfikacja\n(co?)", fontsize=FS, fontweight="bold")
# Detection
ax = axes[1]
ax.add_patch(
mpatches.Rectangle((0, 0), 4, 4, facecolor=GRAY4, edgecolor=LN, lw=1.5)
)
# Cat
ax.add_patch(
mpatches.Ellipse((1.2, 2), 1.2, 1, facecolor=GRAY3, edgecolor=LN, lw=1)
)
ax.add_patch(
mpatches.Ellipse((1.2, 2.8), 0.7, 0.5, facecolor=GRAY3, edgecolor=LN, lw=1)
)
# Dog
ax.add_patch(
mpatches.Ellipse((3, 1.5), 1.2, 1, facecolor=GRAY2, edgecolor=LN, lw=1)
)
ax.add_patch(
mpatches.Ellipse((3, 2.3), 0.7, 0.5, facecolor=GRAY2, edgecolor=LN, lw=1)
)
# Bounding boxes
ax.add_patch(
mpatches.Rectangle((0.3, 1.2), 1.8, 2.2, facecolor="none", edgecolor=LN, lw=2.5)
)
ax.text(1.2, 3.5, "KOT", ha="center", fontsize=FS_SMALL, fontweight="bold")
ax.add_patch(
mpatches.Rectangle((2.1, 0.8), 1.7, 2.0, facecolor="none", edgecolor=LN, lw=2.5)
)
ax.text(3.0, 2.9, "PIES", ha="center", fontsize=FS_SMALL, fontweight="bold")
ax.text(
2,
-0.4,
"→ bbox + klasa (N obiektów)",
ha="center",
fontsize=FS,
fontweight="bold",
)
ax.set_xlim(-0.5, 4.5)
ax.set_ylim(-0.8, 4.5)
ax.set_aspect("equal")
ax.axis("off")
ax.set_title("Detekcja\n(co? + gdzie?)", fontsize=FS, fontweight="bold")
# Segmentation
ax = axes[2]
ax.add_patch(
mpatches.Rectangle((0, 0), 4, 4, facecolor=GRAY4, edgecolor=LN, lw=1.5)
)
# Cat mask (detailed)
theta = np.linspace(0, 2 * np.pi, 30)
cat_x = 1.2 + 0.6 * np.cos(theta) + 0.1 * np.sin(3 * theta)
cat_y = 2 + 0.5 * np.sin(theta) + 0.1 * np.cos(2 * theta)
ax.fill(cat_x, cat_y, facecolor=GRAY3, edgecolor=LN, lw=1.5)
# Dog mask
dog_x = 3.0 + 0.6 * np.cos(theta) + 0.05 * np.sin(4 * theta)
dog_y = 1.5 + 0.5 * np.sin(theta) + 0.08 * np.cos(3 * theta)
ax.fill(dog_x, dog_y, facecolor=GRAY2, edgecolor=LN, lw=1.5)
ax.text(1.2, 2, "KOT", ha="center", fontsize=FS_SMALL, fontweight="bold")
ax.text(3.0, 1.5, "PIES", ha="center", fontsize=FS_SMALL, fontweight="bold")
ax.text(
2,
-0.4,
"→ maska pikseli (per piksel)",
ha="center",
fontsize=FS,
fontweight="bold",
)
ax.set_xlim(-0.5, 4.5)
ax.set_ylim(-0.8, 4.5)
ax.set_aspect("equal")
ax.axis("off")
ax.set_title("Segmentacja\n(dokładna maska)", fontsize=FS, fontweight="bold")
fig.tight_layout()
save_fig(fig, "q24_detection_tasks.png")
# ============================================================
# 19. CNN Architecture overview
# ============================================================
def draw_cnn_architecture() -> None:
"""Draw cnn architecture."""
fig, ax = plt.subplots(figsize=(12, 4))
ax.set_xlim(-0.5, 12.5)
ax.set_ylim(-1, 4.5)
ax.set_aspect("equal")
ax.axis("off")
ax.set_title(
"CNN — od obrazu do predykcji (architektura)",
fontsize=FS_TITLE,
fontweight="bold",
pad=12,
)
# Input image
draw_box(ax, 0, 0.5, 1.5, 3, "Obraz\n224x224x3", fill=GRAY1, fontsize=FS)
# Conv1
draw_arrow(ax, 1.6, 2.0, 2.1, 2.0, lw=1.2)
draw_box(
ax, 2.2, 0.8, 1.2, 2.4, "Conv1\n+ReLU\n55x55x96", fill=GRAY4, fontsize=FS_SMALL
)
# Pool1
draw_arrow(ax, 3.5, 2.0, 3.9, 2.0, lw=1.2)
draw_box(ax, 4.0, 1.0, 1.0, 2.0, "Pool\n27x27\nx96", fill=GRAY2, fontsize=FS_SMALL)
# Conv2
draw_arrow(ax, 5.1, 2.0, 5.5, 2.0, lw=1.2)
draw_box(
ax,
5.6,
0.8,
1.2,
2.4,
"Conv2\n+ReLU\n27x27\nx256",
fill=GRAY4,
fontsize=FS_SMALL,
)
# Pool2
draw_arrow(ax, 6.9, 2.0, 7.3, 2.0, lw=1.2)
draw_box(ax, 7.4, 1.2, 0.8, 1.6, "Pool\n13x13\nx256", fill=GRAY2, fontsize=FS_SMALL)
# More conv...
draw_arrow(ax, 8.3, 2.0, 8.7, 2.0, lw=1.2)
ax.text(9.0, 2.0, "...", fontsize=14, ha="center", va="center")
draw_arrow(ax, 9.3, 2.0, 9.7, 2.0, lw=1.2)
# FC
draw_box(ax, 9.8, 1.2, 1.0, 1.6, "FC\n4096", fill=GRAY3, fontsize=FS)
draw_arrow(ax, 10.9, 2.0, 11.3, 2.0, lw=1.2)
# Output
draw_box(
ax, 11.4, 1.5, 1.0, 1.0, "Softmax\n1000 klas", fill=GRAY1, fontsize=FS_SMALL
)
# Annotations below
ax.text(
3.0,
0.0,
"rozmiar maleje\n224→55→27→13→6",
ha="center",
fontsize=FS_SMALL,
style="italic",
)
ax.text(
6.0,
0.0,
"kanały rosną\n3→96→256→384",
ha="center",
fontsize=FS_SMALL,
style="italic",
)
ax.text(
10.0, 0.0, "decyzja\nkońcowa", ha="center", fontsize=FS_SMALL, style="italic"
)
# hierarchy
ax.text(
6.0,
4.0,
"Hierarchia: krawędzie → rogi → fragmenty → obiekty (K-R-F-O)",
ha="center",
fontsize=FS,
fontweight="bold",
bbox={"boxstyle": "round,pad=0.3", "facecolor": GRAY4, "edgecolor": GRAY3},
)
save_fig(fig, "q24_cnn_architecture.png")

View File

@ -0,0 +1,342 @@
"""Haar features, integral image, and SVM hyperplane diagrams."""
from __future__ import annotations
from typing import TYPE_CHECKING
from _q24_common import (
_DATA_BRIGHT_THRESH,
_II_BRIGHT_THRESH,
FS,
FS_LABEL,
FS_SMALL,
FS_TITLE,
GRAY3,
GRAY4,
LN,
np,
plt,
rng,
save_fig,
)
import matplotlib.patches as mpatches
if TYPE_CHECKING:
from matplotlib.axes import Axes
# ============================================================
# 4. Haar Features
# ============================================================
def draw_haar_features() -> None:
"""Draw haar features."""
fig, axes = plt.subplots(1, 4, figsize=(11, 3))
fig.suptitle(
"Cechy Haar — typy i zastosowanie na twarzy",
fontsize=FS_TITLE,
fontweight="bold",
y=1.02,
)
# Feature 1: Vertical edge
ax = axes[0]
ax.add_patch(
mpatches.Rectangle((0, 0), 1, 2, facecolor=GRAY4, edgecolor=LN, lw=1.5)
)
ax.add_patch(
mpatches.Rectangle((1, 0), 1, 2, facecolor=GRAY3, edgecolor=LN, lw=1.5)
)
ax.text(
0.5, 1, "+Σ₁", ha="center", va="center", fontsize=FS_LABEL, fontweight="bold"
)
ax.text(
1.5, 1, "-Σ₂", ha="center", va="center", fontsize=FS_LABEL, fontweight="bold"
)
ax.set_xlim(-0.2, 2.2)
ax.set_ylim(-0.5, 2.5)
ax.set_aspect("equal")
ax.axis("off")
ax.set_title("Krawędź pionowa\nwartość = Σ₁ - Σ₂", fontsize=FS)
# Feature 2: Horizontal edge
ax = axes[1]
ax.add_patch(
mpatches.Rectangle((0, 1), 2, 1, facecolor=GRAY4, edgecolor=LN, lw=1.5)
)
ax.add_patch(
mpatches.Rectangle((0, 0), 2, 1, facecolor=GRAY3, edgecolor=LN, lw=1.5)
)
ax.text(
1, 1.5, "+Σ₁", ha="center", va="center", fontsize=FS_LABEL, fontweight="bold"
)
ax.text(
1, 0.5, "-Σ₂", ha="center", va="center", fontsize=FS_LABEL, fontweight="bold"
)
ax.set_xlim(-0.2, 2.2)
ax.set_ylim(-0.5, 2.5)
ax.set_aspect("equal")
ax.axis("off")
ax.set_title("Krawędź pozioma\n(oczy vs czoło)", fontsize=FS)
# Feature 3: Three-rectangle (line)
ax = axes[2]
ax.add_patch(
mpatches.Rectangle((0, 0), 0.7, 2, facecolor=GRAY3, edgecolor=LN, lw=1.5)
)
ax.add_patch(
mpatches.Rectangle((0.7, 0), 0.7, 2, facecolor=GRAY4, edgecolor=LN, lw=1.5)
)
ax.add_patch(
mpatches.Rectangle((1.4, 0), 0.7, 2, facecolor=GRAY3, edgecolor=LN, lw=1.5)
)
ax.text(
0.35, 1, "-Σ₁", ha="center", va="center", fontsize=FS_SMALL, fontweight="bold"
)
ax.text(
1.05, 1, "+Σ₂", ha="center", va="center", fontsize=FS_SMALL, fontweight="bold"
)
ax.text(
1.75, 1, "-Σ₃", ha="center", va="center", fontsize=FS_SMALL, fontweight="bold"
)
ax.set_xlim(-0.2, 2.3)
ax.set_ylim(-0.5, 2.5)
ax.set_aspect("equal")
ax.axis("off")
ax.set_title("Linia (3 prostokąty)\n(nos vs policzki)", fontsize=FS)
_draw_haar_face_panel(axes[3])
fig.tight_layout()
save_fig(fig, "q24_haar_features.png")
def _draw_haar_face_panel(ax: Axes) -> None:
"""Draw Haar feature application on face schematic."""
face = mpatches.Ellipse(
(1.2, 1.2),
2.0,
2.4,
facecolor=GRAY4,
edgecolor=LN,
lw=1.5,
)
ax.add_patch(face)
ax.add_patch(
mpatches.Ellipse((0.7, 1.6), 0.4, 0.2, facecolor=GRAY3, edgecolor=LN, lw=1)
)
ax.add_patch(
mpatches.Ellipse((1.7, 1.6), 0.4, 0.2, facecolor=GRAY3, edgecolor=LN, lw=1)
)
ax.plot([1.2, 1.1, 1.3], [1.3, 0.9, 0.9], color=LN, lw=1)
ax.plot([0.8, 1.0, 1.2, 1.4, 1.6], [0.55, 0.5, 0.55, 0.5, 0.55], color=LN, lw=1)
ax.add_patch(
mpatches.Rectangle(
(0.3, 1.4),
1.8,
0.4,
facecolor="none",
edgecolor=LN,
lw=2,
linestyle="--",
)
)
ax.annotate(
"cechy Haar\n(oczy ciemne\nvs czoło jasne)",
xy=(1.2, 1.85),
xytext=(2.2, 2.3),
fontsize=FS_SMALL,
ha="center",
arrowprops={"arrowstyle": "->", "color": LN, "lw": 1},
)
ax.set_xlim(-0.2, 3.0)
ax.set_ylim(-0.2, 2.8)
ax.set_aspect("equal")
ax.axis("off")
ax.set_title("Zastosowanie na twarzy", fontsize=FS)
# ============================================================
# 5. Integral Image
# ============================================================
def draw_integral_image() -> None:
"""Draw integral image."""
fig, axes = plt.subplots(1, 3, figsize=(11, 3.5))
fig.suptitle(
"Integral Image — suma prostokąta w O(1)",
fontsize=FS_TITLE,
fontweight="bold",
y=1.02,
)
# Original image
ax = axes[0]
data = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
ax.imshow(data, cmap="gray", vmin=0, vmax=10)
for i in range(3):
for j in range(3):
ax.text(
j,
i,
str(data[i, j]),
ha="center",
va="center",
fontsize=12,
fontweight="bold",
color="white" if data[i, j] > _DATA_BRIGHT_THRESH else "black",
)
ax.set_title("① Obraz oryginalny", fontsize=FS, fontweight="bold")
ax.set_xticks([])
ax.set_yticks([])
# Integral image
ax = axes[1]
ii = np.array([[1, 3, 6], [5, 12, 21], [12, 27, 45]])
ax.imshow(ii, cmap="gray", vmin=0, vmax=50)
for i in range(3):
for j in range(3):
ax.text(
j,
i,
str(ii[i, j]),
ha="center",
va="center",
fontsize=12,
fontweight="bold",
color="white" if ii[i, j] > _II_BRIGHT_THRESH else "black",
)
ax.set_title("② Integral Image\n(sumy kumulatywne)", fontsize=FS, fontweight="bold")
ax.set_xticks([])
ax.set_yticks([])
# Formula illustration
ax = axes[2]
ax.axis("off")
ax.set_xlim(0, 4)
ax.set_ylim(0, 4)
# Draw rectangle
ax.add_patch(
mpatches.Rectangle((0.5, 0.5), 3, 3, facecolor="white", edgecolor=LN, lw=1)
)
ax.add_patch(
mpatches.Rectangle((1.5, 0.5), 2, 2, facecolor=GRAY3, edgecolor=LN, lw=2)
)
# Labels
ax.text(0.3, 3.7, "A", fontsize=12, fontweight="bold")
ax.text(3.6, 3.7, "B", fontsize=12, fontweight="bold")
ax.text(0.3, 0.3, "C", fontsize=12, fontweight="bold")
ax.text(3.6, 0.3, "D", fontsize=12, fontweight="bold")
ax.text(
2.5,
1.5,
"SZUKANA\nSUMA",
ha="center",
va="center",
fontsize=FS,
fontweight="bold",
)
ax.text(
2.0,
-0.3,
"Suma = D - B - C + A\n= 4 odczyty → O(1) ZAWSZE!",
ha="center",
fontsize=FS,
fontweight="bold",
bbox={"boxstyle": "round,pad=0.3", "facecolor": GRAY4, "edgecolor": GRAY3},
)
ax.set_title(
"③ Formuła: 4 odczyty\n= O(1) niezależnie od rozmiaru",
fontsize=FS,
fontweight="bold",
)
fig.tight_layout()
save_fig(fig, "q24_integral_image.png")
# ============================================================
# 11. SVM Hyperplane
# ============================================================
def draw_svm_hyperplane() -> None:
"""Draw svm hyperplane."""
fig, ax = plt.subplots(figsize=(6, 5))
ax.set_title(
"SVM — hiperpłaszczyzna i margines",
fontsize=FS_TITLE,
fontweight="bold",
pad=12,
)
x_pos = rng.standard_normal(15) * 0.5 + 3
y_pos = rng.standard_normal(15) * 0.5 + 3
ax.scatter(
x_pos,
y_pos,
marker="o",
s=50,
facecolors="white",
edgecolors=LN,
linewidths=1.5,
label="klasa +1 (pieszy)",
zorder=3,
)
x_neg = rng.standard_normal(15) * 0.5 + 1
y_neg = rng.standard_normal(15) * 0.5 + 1
ax.scatter(
x_neg,
y_neg,
marker="x",
s=50,
c=LN,
linewidths=1.5,
label="klasa -1 (tło)",
zorder=3,
)
# Hyperplane (decision boundary)
x_line = np.linspace(-0.5, 5, 100)
y_line = -x_line + 4.0
ax.plot(x_line, y_line, "k-", lw=2, label="hiperpłaszczyzna")
# Margin lines
ax.plot(x_line, y_line + 0.7, "k--", lw=1, alpha=0.5)
ax.plot(x_line, y_line - 0.7, "k--", lw=1, alpha=0.5)
# Margin annotation
ax.annotate(
"",
xy=(2.5, 1.5 + 0.7),
xytext=(2.5, 1.5 - 0.7),
arrowprops={"arrowstyle": "<->", "color": LN, "lw": 1.5},
)
ax.text(2.8, 1.5, "margines\n(MAX!)", fontsize=FS, fontweight="bold")
# Support vectors (highlight closest points)
ax.scatter(
[2.5],
[2.2],
marker="o",
s=120,
facecolors="none",
edgecolors=LN,
linewidths=2.5,
zorder=4,
)
ax.scatter([1.5], [1.8], marker="x", s=120, c=LN, linewidths=2.5, zorder=4)
ax.annotate(
"support\nvectors",
xy=(1.5, 1.8),
xytext=(0.2, 3.0),
fontsize=FS,
fontweight="bold",
arrowprops={"arrowstyle": "->", "color": LN, "lw": 1},
)
ax.set_xlim(-0.5, 5)
ax.set_ylim(-0.5, 5)
ax.set_xlabel("cecha 1 (np. gradient pionowy)", fontsize=FS)
ax.set_ylabel("cecha 2 (np. gradient poziomy)", fontsize=FS)
ax.legend(fontsize=FS_SMALL, loc="lower right")
ax.set_aspect("equal")
save_fig(fig, "q24_svm_hyperplane.png")

View File

@ -0,0 +1,380 @@
"""HOG + SVM pipeline, HOG gradient steps, Viola-Jones cascade."""
from __future__ import annotations
from _q24_common import (
_DOTS_STAGE_IDX,
_GRADIENT_BRIGHT_THRESH,
_PIXEL_BRIGHT_THRESH,
FS,
FS_LABEL,
FS_SMALL,
FS_TITLE,
GRAY1,
GRAY2,
GRAY3,
GRAY4,
GRAY5,
LN,
draw_arrow,
draw_box,
np,
plt,
save_fig,
)
import matplotlib.patches as mpatches
# ============================================================
# 1. HOG + SVM Pipeline
# ============================================================
def draw_hog_svm_pipeline() -> None:
"""Draw hog svm pipeline."""
fig, ax = plt.subplots(figsize=(10, 4.5))
ax.set_xlim(-0.5, 10.5)
ax.set_ylim(-1, 4.5)
ax.set_aspect("equal")
ax.axis("off")
ax.set_title(
"HOG + SVM — pipeline detekcji pieszych",
fontsize=FS_TITLE,
fontweight="bold",
pad=12,
)
# Step 1: Image with sliding window
ax.add_patch(
mpatches.Rectangle((0, 1.5), 2, 2, lw=1.5, edgecolor=LN, facecolor=GRAY1)
)
ax.text(1, 2.5, "Obraz\nwejściowy", ha="center", va="center", fontsize=FS)
# sliding window overlay
ax.add_patch(
mpatches.Rectangle(
(0.3, 1.8),
0.8,
1.2,
lw=1.5,
edgecolor="black",
facecolor="none",
linestyle="--",
)
)
ax.text(
0.7,
1.35,
"okno 64x128",
ha="center",
va="center",
fontsize=FS_SMALL,
style="italic",
)
draw_arrow(ax, 2.1, 2.5, 2.8, 2.5, lw=1.5)
ax.text(2.45, 2.75, "", ha="center", fontsize=FS_LABEL, fontweight="bold")
# Step 2: Gradient computation
draw_box(
ax, 2.9, 1.8, 1.6, 1.4, "Oblicz\ngradienty\nGx, Gy", fill=GRAY4, fontsize=FS
)
ax.text(
3.7, 1.55, "kierunek + siła", ha="center", fontsize=FS_SMALL, style="italic"
)
draw_arrow(ax, 4.6, 2.5, 5.2, 2.5, lw=1.5)
ax.text(4.9, 2.75, "", ha="center", fontsize=FS_LABEL, fontweight="bold")
# Step 3: HOG histogram
draw_box(
ax,
5.3,
1.8,
1.6,
1.4,
"Histogramy\nkierunkowe\n9 binów/cel",
fill=GRAY4,
fontsize=FS,
)
ax.text(6.1, 1.55, "komórki 8x8 px", ha="center", fontsize=FS_SMALL, style="italic")
draw_arrow(ax, 7.0, 2.5, 7.6, 2.5, lw=1.5)
ax.text(7.3, 2.75, "", ha="center", fontsize=FS_LABEL, fontweight="bold")
# Step 4: SVM
draw_box(
ax,
7.7,
1.8,
1.4,
1.4,
"SVM\nklasyfikator\npieszy/tło",
fill=GRAY3,
fontsize=FS,
fontweight="bold",
)
draw_arrow(ax, 9.2, 2.5, 9.7, 2.5, lw=1.5)
ax.text(9.45, 2.75, "", ha="center", fontsize=FS_LABEL, fontweight="bold")
# Step 5: NMS + output
draw_box(ax, 9.3, 2.0, 1.0, 1.0, "NMS\n→ wynik", fill=GRAY1, fontsize=FS)
# Bottom: HOG feature vector illustration
ax.text(
5.0,
0.7,
"Wektor HOG: 3780 cech = 105 bloków x 4 komórki x 9 binów",
ha="center",
fontsize=FS,
style="italic",
bbox={"boxstyle": "round,pad=0.3", "facecolor": GRAY4, "edgecolor": GRAY3},
)
# Show small histogram bars
bar_x = 3.2
bar_y = 0.0
angles = [0, 20, 40, 60, 80, 100, 120, 140, 160]
values = [0.3, 0.1, 0.5, 0.8, 0.2, 0.6, 0.15, 0.4, 0.25]
for i, (_a, v) in enumerate(zip(angles, values, strict=False)):
ax.add_patch(
mpatches.Rectangle(
(bar_x + i * 0.18, bar_y),
0.15,
v * 0.6,
facecolor=GRAY3,
edgecolor=LN,
lw=0.5,
)
)
ax.text(bar_x + 0.8, -0.2, "9 binów (0°-160°)", ha="center", fontsize=FS_SMALL)
save_fig(fig, "q24_hog_svm_pipeline.png")
# ============================================================
# 2. HOG Gradient Step-by-Step
# ============================================================
def draw_hog_gradient_steps() -> None:
"""Draw hog gradient steps."""
fig, axes = plt.subplots(1, 4, figsize=(12, 3.5))
fig.suptitle(
"HOG — kroki obliczania cech", fontsize=FS_TITLE, fontweight="bold", y=1.02
)
# Step 1: Original patch
ax = axes[0]
patch = np.array([[50, 50, 200], [50, 50, 200], [50, 50, 200]])
ax.imshow(patch, cmap="gray", vmin=0, vmax=255)
for i in range(3):
for j in range(3):
ax.text(
j,
i,
str(patch[i, j]),
ha="center",
va="center",
fontsize=FS_LABEL,
fontweight="bold",
color="white" if patch[i, j] > _PIXEL_BRIGHT_THRESH else "black",
)
ax.set_title("① Fragment obrazu\n(jasność pikseli)", fontsize=FS, fontweight="bold")
ax.set_xticks([])
ax.set_yticks([])
# Step 2: Gradient magnitude
ax = axes[1]
gx = np.array([[0, 150, 0], [0, 150, 0], [0, 150, 0]])
ax.imshow(gx, cmap="gray", vmin=0, vmax=255)
for i in range(3):
for j in range(3):
ax.text(
j,
i,
str(gx[i, j]),
ha="center",
va="center",
fontsize=FS_LABEL,
fontweight="bold",
color="white" if gx[i, j] > _GRADIENT_BRIGHT_THRESH else "black",
)
ax.set_title("② Gradient Gx\n(krawędź pionowa!)", fontsize=FS, fontweight="bold")
ax.set_xticks([])
ax.set_yticks([])
# Step 3: Cell histogram
ax = axes[2]
angles = ["", "20°", "40°", "60°", "80°", "100°", "120°", "140°", "160°"]
values = [150, 0, 0, 0, 0, 0, 0, 0, 0]
bars = ax.bar(range(9), values, color=GRAY3, edgecolor=LN, linewidth=0.5)
bars[0].set_facecolor(GRAY5)
ax.set_xticks(range(9))
ax.set_xticklabels(angles, fontsize=5, rotation=45)
ax.set_title(
"③ Histogram komórki\n(bin 0° = krawędź pionowa)",
fontsize=FS,
fontweight="bold",
)
ax.set_ylabel("siła", fontsize=FS_SMALL)
# Step 4: Block normalization
ax = axes[3]
# 2x2 block of cells
for i in range(2):
for j in range(2):
rect = mpatches.Rectangle(
(j * 1.2, (1 - i) * 1.2),
1.0,
1.0,
lw=1.2,
edgecolor=LN,
facecolor=GRAY4,
)
ax.add_patch(rect)
ax.text(
j * 1.2 + 0.5,
(1 - i) * 1.2 + 0.5,
f"hist\n{i * 2 + j + 1}",
ha="center",
va="center",
fontsize=FS_SMALL,
)
ax.add_patch(
mpatches.Rectangle(
(-0.1, -0.1), 2.6, 2.6, lw=2, edgecolor=LN, facecolor="none", linestyle="--"
)
)
ax.text(
1.2,
-0.4,
"blok 2x2 → L2-norm",
ha="center",
fontsize=FS_SMALL,
fontweight="bold",
)
ax.set_xlim(-0.3, 2.8)
ax.set_ylim(-0.7, 2.8)
ax.set_aspect("equal")
ax.axis("off")
ax.set_title(
"④ Normalizacja bloków\n(odporność na oświetlenie)",
fontsize=FS,
fontweight="bold",
)
fig.tight_layout()
save_fig(fig, "q24_hog_gradient_steps.png")
# ============================================================
# 3. Viola-Jones Cascade
# ============================================================
def draw_viola_jones_cascade() -> None:
"""Draw viola jones cascade."""
fig, ax = plt.subplots(figsize=(10, 5))
ax.set_xlim(-0.5, 10.5)
ax.set_ylim(-1.5, 5)
ax.set_aspect("equal")
ax.axis("off")
ax.set_title(
"Viola-Jones — kaskada klasyfikatorów (SITO)",
fontsize=FS_TITLE,
fontweight="bold",
pad=12,
)
# Input
draw_box(
ax,
-0.3,
2.5,
1.5,
1.2,
"500 000\nokien",
fill=GRAY1,
fontsize=FS,
fontweight="bold",
)
stages = [
("Etap 1\n2 cechy", "50%\nodrzucone", "250 000", GRAY4),
("Etap 2\n10 cech", "80%\nodrzucone", "50 000", GRAY4),
("Etap 3\n25 cech", "90%\nodrzucone", "5 000", GRAY4),
("Etap 25\n200 cech", "99%\nodrzucone", "50", GRAY3),
]
x_pos = 1.6
for i, (label, reject, remain, col) in enumerate(stages):
# Stage box
draw_box(
ax, x_pos, 2.5, 1.6, 1.2, label, fill=col, fontsize=FS, fontweight="bold"
)
# Arrow from previous
draw_arrow(ax, x_pos - 0.3, 3.1, x_pos - 0.05, 3.1, lw=1.5)
# Reject arrow down
draw_arrow(ax, x_pos + 0.8, 2.45, x_pos + 0.8, 1.6, lw=1.2)
ax.text(
x_pos + 0.8,
1.3,
reject,
ha="center",
fontsize=FS_SMALL,
color="black",
style="italic",
)
ax.text(
x_pos + 0.8,
0.8,
"✗ NIE-TWARZ",
ha="center",
fontsize=FS_SMALL,
fontweight="bold",
)
# Remaining count above
if i < len(stages) - 1:
ax.text(
x_pos + 2.0,
3.9,
f"{remain}",
ha="center",
fontsize=FS_SMALL,
style="italic",
)
# Dots between stage 3 and stage 25
if i == _DOTS_STAGE_IDX:
ax.text(
x_pos + 2.0, 3.1, "· · ·", ha="center", fontsize=12, fontweight="bold"
)
x_pos += 2.5
else:
x_pos += 2.1
# Final output
draw_arrow(ax, x_pos + 0.3, 3.1, x_pos + 0.9, 3.1, lw=1.5)
draw_box(
ax,
x_pos + 0.5,
2.5,
1.3,
1.2,
"~50\nTWARZE\n",
fill=GRAY2,
fontsize=FS,
fontweight="bold",
)
# Timing info
ax.text(
5.0,
-0.5,
"Czas: 99% okien odrzucone w etapach 1-3 (~5 μs każde)\n"
"Tylko 0.01% dochodzi do etapu 25 → cały obraz w ~30 ms = 30+ fps",
ha="center",
fontsize=FS,
style="italic",
bbox={"boxstyle": "round,pad=0.4", "facecolor": GRAY4, "edgecolor": GRAY3},
)
save_fig(fig, "q24_viola_jones_cascade.png")

View File

@ -0,0 +1,413 @@
"""IoU diagram, NMS steps, and detector-from-classifier diagrams."""
from __future__ import annotations
from _q24_common import (
FS,
FS_LABEL,
FS_SMALL,
FS_TITLE,
GRAY1,
GRAY2,
GRAY3,
GRAY4,
LN,
draw_arrow,
draw_box,
plt,
save_fig,
)
import matplotlib.patches as mpatches
# ============================================================
# 8. IoU Diagram
# ============================================================
def draw_iou_diagram() -> None:
"""Draw iou diagram."""
fig, axes = plt.subplots(1, 3, figsize=(11, 3.5))
fig.suptitle(
"IoU (Intersection over Union) — miara nakładania bboxów",
fontsize=FS_TITLE,
fontweight="bold",
y=1.02,
)
# Low IoU
ax = axes[0]
ax.add_patch(
mpatches.Rectangle(
(0, 0), 3, 3, facecolor=GRAY4, edgecolor=LN, lw=1.5, label="A"
)
)
ax.add_patch(
mpatches.Rectangle(
(2.5, 2.5),
3,
3,
facecolor=GRAY2,
edgecolor=LN,
lw=1.5,
alpha=0.7,
label="B",
)
)
# Intersection
ax.add_patch(
mpatches.Rectangle((2.5, 2.5), 0.5, 0.5, facecolor=GRAY3, edgecolor=LN, lw=2)
)
ax.text(1.5, 1.5, "A", ha="center", va="center", fontsize=12, fontweight="bold")
ax.text(4, 4, "B", ha="center", va="center", fontsize=12, fontweight="bold")
ax.set_xlim(-0.5, 6)
ax.set_ylim(-0.5, 6)
ax.set_aspect("equal")
ax.axis("off")
ax.set_title(
"IoU ≈ 0.04\n(prawie się nie nakładają)", fontsize=FS, fontweight="bold"
)
# Medium IoU
ax = axes[1]
ax.add_patch(
mpatches.Rectangle((0, 0), 3, 3, facecolor=GRAY4, edgecolor=LN, lw=1.5)
)
ax.add_patch(
mpatches.Rectangle(
(1.5, 1.5), 3, 3, facecolor=GRAY2, edgecolor=LN, lw=1.5, alpha=0.7
)
)
ax.add_patch(
mpatches.Rectangle((1.5, 1.5), 1.5, 1.5, facecolor=GRAY3, edgecolor=LN, lw=2)
)
ax.text(0.7, 0.7, "A", ha="center", va="center", fontsize=12, fontweight="bold")
ax.text(3.5, 3.5, "B", ha="center", va="center", fontsize=12, fontweight="bold")
ax.text(2.25, 2.25, "", ha="center", va="center", fontsize=14, fontweight="bold")
ax.set_xlim(-0.5, 5)
ax.set_ylim(-0.5, 5)
ax.set_aspect("equal")
ax.axis("off")
ax.set_title("IoU ≈ 0.14\n(częściowe nakładanie)", fontsize=FS, fontweight="bold")
# High IoU
ax = axes[2]
ax.add_patch(
mpatches.Rectangle((0, 0), 3, 3, facecolor=GRAY4, edgecolor=LN, lw=1.5)
)
ax.add_patch(
mpatches.Rectangle(
(0.3, 0.3), 3, 3, facecolor=GRAY2, edgecolor=LN, lw=1.5, alpha=0.7
)
)
ax.add_patch(
mpatches.Rectangle((0.3, 0.3), 2.7, 2.7, facecolor=GRAY3, edgecolor=LN, lw=2)
)
ax.text(-0.3, -0.3, "A", ha="center", va="center", fontsize=12, fontweight="bold")
ax.text(3.5, 3.5, "B", ha="center", va="center", fontsize=12, fontweight="bold")
ax.text(1.65, 1.65, "", ha="center", va="center", fontsize=14, fontweight="bold")
ax.set_xlim(-0.8, 4)
ax.set_ylim(-0.8, 4)
ax.set_aspect("equal")
ax.axis("off")
ax.set_title(
"IoU ≈ 0.74\n(duże nakładanie → duplikat!)", fontsize=FS, fontweight="bold"
)
fig.tight_layout()
save_fig(fig, "q24_iou_diagram.png")
# ============================================================
# 9. NMS Step-by-Step
# ============================================================
def draw_nms_steps() -> None:
"""Draw nms steps."""
fig, axes = plt.subplots(1, 3, figsize=(12, 4))
fig.suptitle(
"NMS (Non-Maximum Suppression) — usuwanie duplikatów",
fontsize=FS_TITLE,
fontweight="bold",
y=1.02,
)
# Before NMS
ax = axes[0]
ax.add_patch(mpatches.Rectangle((0, 0), 6, 5, facecolor=GRAY4, edgecolor=LN, lw=1))
# Multiple overlapping boxes for same object
ax.add_patch(
mpatches.Rectangle((1, 1), 2.5, 3, facecolor="none", edgecolor=LN, lw=2)
)
ax.text(2.25, 4.2, "conf=0.95", ha="center", fontsize=FS_SMALL, fontweight="bold")
ax.add_patch(
mpatches.Rectangle(
(1.2, 1.3), 2.3, 2.8, facecolor="none", edgecolor=LN, lw=1.5, linestyle="--"
)
)
ax.text(2.35, 1.1, "conf=0.90", ha="center", fontsize=FS_SMALL)
ax.add_patch(
mpatches.Rectangle(
(0.8, 0.8), 2.7, 3.2, facecolor="none", edgecolor=LN, lw=1, linestyle=":"
)
)
ax.text(2.15, 0.6, "conf=0.85", ha="center", fontsize=FS_SMALL)
# Different object
ax.add_patch(
mpatches.Rectangle((4, 2), 1.5, 1.5, facecolor="none", edgecolor=LN, lw=1.5)
)
ax.text(4.75, 3.7, "conf=0.80", ha="center", fontsize=FS_SMALL)
ax.text(
2,
0.2,
"⚠ 4 detekcje (3 duplikaty!)",
ha="center",
fontsize=FS_SMALL,
fontweight="bold",
)
ax.set_xlim(-0.3, 6.3)
ax.set_ylim(-0.3, 5.3)
ax.set_aspect("equal")
ax.axis("off")
ax.set_title(
"① Przed NMS\n(wiele nakładających się)", fontsize=FS, fontweight="bold"
)
# NMS process
ax = axes[1]
ax.axis("off")
ax.set_xlim(0, 6)
ax.set_ylim(0, 5)
steps = [
("1. Sortuj: [0.95, 0.90, 0.85, 0.80]", 4.5),
("2. Weź najlepszą (0.95) → ZACHOWAJ", 3.7),
("3. IoU(0.95, 0.90)=0.82 > 0.5 → USUŃ", 2.9),
("4. IoU(0.95, 0.85)=0.75 > 0.5 → USUŃ", 2.1),
("5. IoU(0.95, 0.80)=0.10 < 0.5 → ZACHOWAJ", 1.3),
]
colors = [GRAY4, GRAY2, GRAY4, GRAY4, GRAY2]
for (text, yp), c in zip(steps, colors, strict=False):
ax.text(
3.0,
yp,
text,
ha="center",
fontsize=FS,
bbox={"boxstyle": "round,pad=0.2", "facecolor": c, "edgecolor": GRAY3},
)
ax.set_title("② Algorytm NMS\n(próg IoU = 0.5)", fontsize=FS, fontweight="bold")
# After NMS
ax = axes[2]
ax.add_patch(mpatches.Rectangle((0, 0), 6, 5, facecolor=GRAY4, edgecolor=LN, lw=1))
# Only best box for each object
ax.add_patch(
mpatches.Rectangle((1, 1), 2.5, 3, facecolor="none", edgecolor=LN, lw=2.5)
)
ax.text(2.25, 4.2, "conf=0.95 ✓", ha="center", fontsize=FS_SMALL, fontweight="bold")
ax.add_patch(
mpatches.Rectangle((4, 2), 1.5, 1.5, facecolor="none", edgecolor=LN, lw=2.5)
)
ax.text(4.75, 3.7, "conf=0.80 ✓", ha="center", fontsize=FS_SMALL, fontweight="bold")
ax.text(
3,
0.2,
"✓ 2 unikalne obiekty",
ha="center",
fontsize=FS_SMALL,
fontweight="bold",
)
ax.set_xlim(-0.3, 6.3)
ax.set_ylim(-0.3, 5.3)
ax.set_aspect("equal")
ax.axis("off")
ax.set_title("③ Po NMS\n(1 bbox na obiekt)", fontsize=FS, fontweight="bold")
fig.tight_layout()
save_fig(fig, "q24_nms_steps.png")
# ============================================================
# 10. Detector from Classifier — 3 approaches
# ============================================================
def draw_detector_from_classifier() -> None:
"""Draw detector from classifier."""
fig, ax = plt.subplots(figsize=(11, 9))
ax.set_xlim(-0.5, 11)
ax.set_ylim(-1, 9.5)
ax.set_aspect("equal")
ax.axis("off")
ax.set_title(
"Jak zbudować detektor z klasyfikatora? — 3 podejścia",
fontsize=FS_TITLE,
fontweight="bold",
pad=12,
)
# ---- Approach 1: Sliding Window ----
y = 7.0
ax.text(
0,
y + 1.5,
"① Sliding Window (NAJWOLNIEJSZE)",
fontsize=FS_LABEL,
fontweight="bold",
bbox={"boxstyle": "round,pad=0.2", "facecolor": GRAY4, "edgecolor": GRAY3},
)
# Image with sliding window
ax.add_patch(
mpatches.Rectangle(
(0, y - 0.6), 1.8, 1.8, facecolor=GRAY1, edgecolor=LN, lw=1.5
)
)
ax.text(0.9, y + 0.3, "obraz", ha="center", fontsize=FS_SMALL)
# Sliding windows
for dx, dy in [(0.1, 0.1), (0.4, 0.1), (0.7, 0.1), (0.1, 0.5), (0.4, 0.5)]:
ax.add_patch(
mpatches.Rectangle(
(dx, y - 0.5 + dy),
0.5,
0.5,
facecolor="none",
edgecolor=LN,
lw=0.8,
linestyle="--",
)
)
draw_arrow(ax, 2.0, y + 0.3, 2.7, y + 0.3, lw=1.2)
ax.text(2.35, y + 0.6, "xmiliony", fontsize=FS_SMALL, style="italic")
draw_box(
ax,
2.8,
y - 0.3,
1.8,
1.2,
'Klasyfikator\n(ResNet)\n"kot? pies? tło?"',
fill=GRAY4,
fontsize=FS,
)
draw_arrow(ax, 4.7, y + 0.3, 5.3, y + 0.3, lw=1.2)
draw_box(ax, 5.4, y - 0.3, 1.2, 1.2, "NMS", fill=GRAY1, fontsize=FS)
draw_arrow(ax, 6.7, y + 0.3, 7.3, y + 0.3, lw=1.2)
ax.text(
8.5,
y + 0.3,
"~3.3h / obraz!\n⚠ NIEPRAKTYCZNE",
ha="center",
fontsize=FS,
fontweight="bold",
bbox={"boxstyle": "round,pad=0.3", "facecolor": GRAY4, "edgecolor": GRAY3},
)
# ---- Approach 2: Region Proposals ----
y = 3.8
ax.text(
0,
y + 1.5,
"② Region Proposals + Klasyfikator (= R-CNN)",
fontsize=FS_LABEL,
fontweight="bold",
bbox={"boxstyle": "round,pad=0.2", "facecolor": GRAY4, "edgecolor": GRAY3},
)
ax.add_patch(
mpatches.Rectangle(
(0, y - 0.6), 1.8, 1.8, facecolor=GRAY1, edgecolor=LN, lw=1.5
)
)
ax.text(0.9, y + 0.3, "obraz", ha="center", fontsize=FS_SMALL)
# A few smart regions
ax.add_patch(
mpatches.Rectangle(
(0.1, y - 0.4), 0.7, 0.9, facecolor="none", edgecolor=LN, lw=1.5
)
)
ax.add_patch(
mpatches.Rectangle(
(0.9, y + 0.0), 0.7, 0.6, facecolor="none", edgecolor=LN, lw=1.5
)
)
draw_arrow(ax, 2.0, y + 0.3, 2.7, y + 0.3, lw=1.2)
draw_box(
ax,
2.8,
y - 0.3,
1.6,
1.2,
"Selective\nSearch\n~2000 regionów",
fill=GRAY2,
fontsize=FS,
)
draw_arrow(ax, 4.5, y + 0.3, 5.1, y + 0.3, lw=1.2)
ax.text(4.8, y + 0.6, "x2000", fontsize=FS_SMALL, style="italic")
draw_box(ax, 5.2, y - 0.3, 1.5, 1.2, "Klasyfikator\n(CNN)", fill=GRAY4, fontsize=FS)
draw_arrow(ax, 6.8, y + 0.3, 7.4, y + 0.3, lw=1.2)
draw_box(ax, 7.5, y - 0.3, 1.0, 1.2, "NMS", fill=GRAY1, fontsize=FS)
draw_arrow(ax, 8.6, y + 0.3, 9.0, y + 0.3, lw=1.2)
ax.text(
10.0,
y + 0.3,
"~20-50 s/obraz\n(250x szybciej)",
ha="center",
fontsize=FS,
fontweight="bold",
bbox={"boxstyle": "round,pad=0.3", "facecolor": GRAY4, "edgecolor": GRAY3},
)
# ---- Approach 3: Fine-tune backbone ----
y = 0.5
ax.text(
0,
y + 1.5,
"③ Fine-tune backbone + detection head (NAJLEPSZE)",
fontsize=FS_LABEL,
fontweight="bold",
bbox={"boxstyle": "round,pad=0.2", "facecolor": GRAY2, "edgecolor": GRAY3},
)
ax.add_patch(
mpatches.Rectangle(
(0, y - 0.6), 1.8, 1.8, facecolor=GRAY1, edgecolor=LN, lw=1.5
)
)
ax.text(0.9, y + 0.3, "obraz", ha="center", fontsize=FS_SMALL)
draw_arrow(ax, 2.0, y + 0.3, 2.7, y + 0.3, lw=1.2)
draw_box(
ax,
2.8,
y - 0.3,
1.8,
1.2,
"Pretrained\nbackbone\n(ResNet)",
fill=GRAY3,
fontsize=FS,
fontweight="bold",
)
draw_arrow(ax, 4.7, y + 0.3, 5.3, y + 0.3, lw=1.2)
# Two heads from feature map
draw_box(ax, 5.4, y + 0.3, 1.6, 0.6, "cls head\nP(klasa)", fill=GRAY4, fontsize=FS)
draw_box(
ax, 5.4, y - 0.5, 1.6, 0.6, "bbox head\nΔx,Δy,Δw,Δh", fill=GRAY4, fontsize=FS
)
draw_arrow(ax, 7.1, y + 0.6, 7.7, y + 0.6, lw=1.0)
draw_arrow(ax, 7.1, y - 0.2, 7.7, y - 0.2, lw=1.0)
draw_box(ax, 7.8, y - 0.3, 1.0, 1.2, "NMS", fill=GRAY1, fontsize=FS)
draw_arrow(ax, 8.9, y + 0.3, 9.3, y + 0.3, lw=1.2)
ax.text(
10.2,
y + 0.3,
"5-155 fps!\n✓ NAJLEPSZE",
ha="center",
fontsize=FS,
fontweight="bold",
bbox={"boxstyle": "round,pad=0.3", "facecolor": GRAY2, "edgecolor": GRAY3},
)
save_fig(fig, "q24_detector_from_classifier.png")

View File

@ -0,0 +1,365 @@
"""Two-stage vs one-stage table, ROI pooling, DETR, and sliding window."""
from __future__ import annotations
from _q24_common import (
_DATA_BRIGHT_THRESH,
FS,
FS_SMALL,
FS_TITLE,
GRAY1,
GRAY2,
GRAY3,
GRAY4,
GRAY5,
LN,
draw_arrow,
draw_box,
draw_table,
np,
plt,
rng,
save_fig,
)
import matplotlib.patches as mpatches
# ============================================================
# 12. Two-stage vs One-stage comparison table
# ============================================================
def draw_two_vs_one_stage() -> None:
"""Draw two vs one stage."""
fig, ax = plt.subplots(figsize=(10, 3.5))
ax.set_xlim(0, 10)
ax.set_ylim(-0.5, 4.5)
ax.set_aspect("equal")
ax.axis("off")
ax.set_title(
"Two-stage vs One-stage — porównanie",
fontsize=FS_TITLE,
fontweight="bold",
pad=8,
)
headers = ["Cecha", "Two-stage\n(Faster R-CNN)", "One-stage\n(YOLO)"]
rows = [
["Szybkość", "~5 fps", "45-155 fps"],
["Dokładność (mAP)", "wyższa (historycznie)", "dorównuje (YOLOv8)"],
["Małe obiekty", "lepszy", "gorszy (SSD/FPN pomaga)"],
["Architektura", "2 etapy + NMS", "1 etap + NMS"],
["Real-time?", "NIE", "TAK"],
]
col_widths = [2.5, 3.5, 3.5]
draw_table(
ax,
headers,
rows,
0.2,
3.8,
col_widths,
row_h=0.65,
fontsize=FS,
header_fontsize=FS,
)
save_fig(fig, "q24_two_vs_one_stage.png")
# ============================================================
# 13. ROI Pooling illustration
# ============================================================
def draw_roi_pooling() -> None:
"""Draw roi pooling."""
fig, axes = plt.subplots(1, 3, figsize=(12, 4))
fig.suptitle(
"ROI Pooling — dowolny rozmiar → stały rozmiar",
fontsize=FS_TITLE,
fontweight="bold",
y=1.02,
)
# Feature map with ROI
ax = axes[0]
# Draw feature map grid
fm = rng.integers(0, 10, (8, 8))
ax.imshow(fm, cmap="gray", vmin=0, vmax=10, alpha=0.3)
for i in range(9):
ax.axhline(y=i - 0.5, color=LN, lw=0.3)
ax.axvline(x=i - 0.5, color=LN, lw=0.3)
# ROI rectangle
ax.add_patch(
mpatches.Rectangle(
(1.5, 1.5), 4, 4, facecolor="none", edgecolor=LN, lw=3, linestyle="-"
)
)
ax.text(3.5, 0.8, "ROI", ha="center", fontsize=FS, fontweight="bold")
ax.set_xlim(-0.5, 7.5)
ax.set_ylim(7.5, -0.5)
ax.set_title("① Feature map\nz zaznaczonym ROI", fontsize=FS, fontweight="bold")
ax.set_xticks([])
ax.set_yticks([])
# ROI divided into grid
ax = axes[1]
roi_data = np.array(
[
[1, 3, 2, 1],
[0, 5, 1, 6],
[0, 4, 1, 0],
[7, 2, 9, 1],
]
)
ax.imshow(roi_data, cmap="gray", vmin=0, vmax=10)
for i in range(5):
ax.axhline(y=i - 0.5, color=LN, lw=1)
ax.axvline(x=i - 0.5, color=LN, lw=1)
# Grid lines for 2x2 pooling
ax.axhline(y=0.5, color=LN, lw=3, linestyle="--")
ax.axvline(x=0.5, color=LN, lw=3, linestyle="--")
for i in range(4):
for j in range(4):
ax.text(
j,
i,
str(roi_data[i, j]),
ha="center",
va="center",
fontsize=10,
fontweight="bold",
color="white" if roi_data[i, j] > _DATA_BRIGHT_THRESH else "black",
)
ax.set_title("② ROI podzielony\nna siatkę 2x2", fontsize=FS, fontweight="bold")
ax.set_xticks([])
ax.set_yticks([])
# Output after pooling
ax = axes[2]
out = np.array([[5, 6], [7, 9]])
ax.imshow(out, cmap="gray", vmin=0, vmax=10)
for i in range(3):
ax.axhline(y=i - 0.5, color=LN, lw=1.5)
ax.axvline(x=i - 0.5, color=LN, lw=1.5)
for i in range(2):
for j in range(2):
ax.text(
j,
i,
str(out[i, j]),
ha="center",
va="center",
fontsize=14,
fontweight="bold",
color="white" if out[i, j] > _DATA_BRIGHT_THRESH else "black",
)
ax.set_title(
"③ Po ROI Pool 2x2\n(max z każdej komórki)", fontsize=FS, fontweight="bold"
)
ax.set_xticks([])
ax.set_yticks([])
fig.tight_layout()
save_fig(fig, "q24_roi_pooling.png")
# ============================================================
# 14. DETR Pipeline
# ============================================================
def draw_detr_pipeline() -> None:
"""Draw detr pipeline."""
fig, ax = plt.subplots(figsize=(11, 4.5))
ax.set_xlim(-0.5, 11.5)
ax.set_ylim(-1, 4.5)
ax.set_aspect("equal")
ax.axis("off")
ax.set_title(
"DETR — Transformer do detekcji (bez NMS, bez anchorów)",
fontsize=FS_TITLE,
fontweight="bold",
pad=12,
)
# Pipeline
draw_box(ax, 0, 1.5, 1.5, 1.5, "Obraz\nwejściowy", fill=GRAY1, fontsize=FS)
draw_arrow(ax, 1.6, 2.25, 2.1, 2.25, lw=1.5)
draw_box(
ax,
2.2,
1.5,
1.5,
1.5,
"CNN\nBackbone\n(ResNet)",
fill=GRAY3,
fontsize=FS,
fontweight="bold",
)
draw_arrow(ax, 3.8, 2.25, 4.3, 2.25, lw=1.5)
draw_box(
ax,
4.4,
1.5,
1.8,
1.5,
"Transformer\nEncoder\n(self-attention)",
fill=GRAY2,
fontsize=FS,
)
draw_arrow(ax, 6.3, 2.25, 6.8, 2.25, lw=1.5)
draw_box(
ax,
6.9,
1.5,
1.8,
1.5,
"Transformer\nDecoder\n(N=100 queries)",
fill=GRAY2,
fontsize=FS,
fontweight="bold",
)
# Output branches
draw_arrow(ax, 8.8, 2.5, 9.5, 3.0, lw=1.2)
draw_box(ax, 9.6, 2.7, 1.5, 0.7, "klasa₁...klasa₁₀₀", fill=GRAY4, fontsize=FS_SMALL)
draw_arrow(ax, 8.8, 2.0, 9.5, 1.5, lw=1.2)
draw_box(ax, 9.6, 1.2, 1.5, 0.7, "bbox₁...bbox₁₀₀", fill=GRAY4, fontsize=FS_SMALL)
# Annotations
ax.text(
7.8,
0.5,
'100 object queries → 5 obiektów + 95x "brak"',
ha="center",
fontsize=FS,
style="italic",
bbox={"boxstyle": "round,pad=0.3", "facecolor": GRAY4, "edgecolor": GRAY3},
)
ax.text(
5.5,
0.0,
"Hungarian matching (trening): optymalne dopasowanie predykcji do GT",
ha="center",
fontsize=FS_SMALL,
style="italic",
bbox={"boxstyle": "round,pad=0.2", "facecolor": GRAY4, "edgecolor": GRAY5},
)
# Big benefit box
ax.text(
5.5,
4.0,
"BEZ anchorów • BEZ NMS • end-to-end • prosty pipeline",
ha="center",
fontsize=FS,
fontweight="bold",
bbox={"boxstyle": "round,pad=0.3", "facecolor": GRAY2, "edgecolor": GRAY3},
)
save_fig(fig, "q24_detr_pipeline.png")
# ============================================================
# 15. Sliding Window illustration
# ============================================================
def draw_sliding_window() -> None:
"""Draw sliding window."""
fig, axes = plt.subplots(1, 3, figsize=(12, 4))
fig.suptitle(
"Sliding Window — najprostsze podejście do detekcji",
fontsize=FS_TITLE,
fontweight="bold",
y=1.02,
)
# Multi-position
ax = axes[0]
ax.add_patch(
mpatches.Rectangle((0, 0), 8, 6, facecolor=GRAY4, edgecolor=LN, lw=1.5)
)
# Grid of sliding windows
for i in range(4):
for j in range(3):
ax.add_patch(
mpatches.Rectangle(
(i * 1.8 + 0.2, j * 1.8 + 0.2),
1.5,
1.5,
facecolor="none",
edgecolor=LN,
lw=0.6,
linestyle="--",
)
)
# Highlight current window
ax.add_patch(
mpatches.Rectangle((2.0, 2.0), 1.5, 1.5, facecolor="none", edgecolor=LN, lw=2.5)
)
ax.set_xlim(-0.5, 8.5)
ax.set_ylim(-0.5, 6.5)
ax.set_aspect("equal")
ax.axis("off")
ax.set_title("① Wiele pozycji\n(krok co 8 px)", fontsize=FS, fontweight="bold")
# Multi-scale
ax = axes[1]
ax.add_patch(
mpatches.Rectangle((0, 0), 6, 5, facecolor=GRAY4, edgecolor=LN, lw=1.5)
)
sizes = [(0.8, 0.8), (1.5, 1.5), (2.5, 2.5), (3.5, 3.5)]
for i, (w, h) in enumerate(sizes):
ax.add_patch(
mpatches.Rectangle(
(0.3 + i * 0.3, 0.3 + i * 0.3),
w,
h,
facecolor="none",
edgecolor=LN,
lw=1 + i * 0.3,
linestyle=[":", "--", "-.", "-"][i],
)
)
ax.text(3, 0, "4+ skal", ha="center", fontsize=FS_SMALL, fontweight="bold")
ax.set_xlim(-0.5, 6.5)
ax.set_ylim(-0.5, 5.5)
ax.set_aspect("equal")
ax.axis("off")
ax.set_title(
"② Wiele skal\n(obiekty mają różne rozmiary)", fontsize=FS, fontweight="bold"
)
# Count
ax = axes[2]
ax.axis("off")
ax.set_xlim(0, 6)
ax.set_ylim(0, 5)
lines = [
("Obraz: 640 x 480 px", 4.5),
("Okno: 64 x 64 px, krok 8 px", 3.8),
("Pozycje: ~72 x 52 = 3 744", 3.1),
("x 5 skal = 18 720 okien", 2.4),
("x klasyfikacja = WOLNE!", 1.7),
("→ ~3h na jeden obraz", 0.8),
]
for text, yp in lines:
fw = "bold" if "~3h" in text or "WOLNE" in text else "normal"
col = GRAY2 if "WOLNE" in text or "~3h" in text else GRAY4
ax.text(
3.0,
yp,
text,
ha="center",
fontsize=FS,
fontweight=fw,
bbox={"boxstyle": "round,pad=0.2", "facecolor": col, "edgecolor": GRAY3},
)
ax.set_title(
"③ Dlaczego wolne?\n(miliony klasyfikacji)", fontsize=FS, fontweight="bold"
)
fig.tight_layout()
save_fig(fig, "q24_sliding_window.png")

View File

@ -0,0 +1,344 @@
"""R-CNN evolution and YOLO grid diagrams."""
from __future__ import annotations
from typing import TYPE_CHECKING
from _q24_common import (
FS,
FS_LABEL,
FS_SMALL,
FS_TITLE,
GRAY1,
GRAY2,
GRAY3,
GRAY4,
LN,
draw_arrow,
draw_box,
plt,
save_fig,
)
import matplotlib.patches as mpatches
if TYPE_CHECKING:
from matplotlib.axes import Axes
def _draw_yolo_cell_prediction(ax: Axes) -> None:
"""Draw YOLO cell prediction vector panel."""
ax.axis("off")
ax.set_xlim(0, 6)
ax.set_ylim(-1, 5)
labels = [
"x",
"y",
"w",
"h",
"conf",
"x",
"y",
"w",
"h",
"conf",
"P(c₁)",
"...",
"P(c₂₀)",
]
colors_vec = [GRAY4] * 5 + [GRAY2] * 5 + [GRAY1] * 3
bw = 0.42
for i, (label, col) in enumerate(
zip(labels, colors_vec, strict=False),
):
x_pos = 0.3 + i * bw
ax.add_patch(
mpatches.Rectangle(
(x_pos, 2.5),
bw - 0.02,
0.6,
facecolor=col,
edgecolor=LN,
lw=0.8,
)
)
ax.text(
x_pos + bw / 2,
2.8,
label,
ha="center",
va="center",
fontsize=5,
fontweight="bold",
)
ax.annotate(
"",
xy=(0.3, 2.4),
xytext=(2.4, 2.4),
arrowprops={"arrowstyle": "-", "lw": 1},
)
ax.text(1.35, 2.15, "bbox 1 (5 wartości)", ha="center", fontsize=FS_SMALL)
ax.annotate(
"",
xy=(2.4, 2.4),
xytext=(4.5, 2.4),
arrowprops={"arrowstyle": "-", "lw": 1},
)
ax.text(3.45, 2.15, "bbox 2 (5 wartości)", ha="center", fontsize=FS_SMALL)
ax.annotate(
"",
xy=(4.5, 2.4),
xytext=(5.8, 2.4),
arrowprops={"arrowstyle": "-", "lw": 1},
)
ax.text(5.15, 2.15, "20 klas", ha="center", fontsize=FS_SMALL)
ax.text(
3.0,
3.5,
"Każda komórka → 30 wartości\n= 2x(x,y,w,h,conf) + 20 klas",
ha="center",
fontsize=FS,
fontweight="bold",
bbox={"boxstyle": "round,pad=0.3", "facecolor": GRAY4, "edgecolor": GRAY3},
)
ax.set_title(
"② Predykcja jednej komórki\n(S=7, B=2, C=20)",
fontsize=FS,
fontweight="bold",
)
# ============================================================
# 6. R-CNN Evolution
# ============================================================
def draw_rcnn_evolution() -> None:
"""Draw rcnn evolution."""
fig, ax = plt.subplots(figsize=(11, 7))
ax.set_xlim(-0.5, 11)
ax.set_ylim(-0.5, 7.5)
ax.set_aspect("equal")
ax.axis("off")
ax.set_title(
"Ewolucja R-CNN: od 50s do 0.2s na obraz",
fontsize=FS_TITLE,
fontweight="bold",
pad=12,
)
y_positions = [5.5, 3.0, 0.5]
labels = [
"R-CNN (2014) — 50 s/obraz",
"Fast R-CNN (2015) — 2 s/obraz",
"Faster R-CNN (2015) — 0.2 s/obraz",
]
# R-CNN
y = y_positions[0]
ax.text(0, y + 1.3, labels[0], fontsize=FS_LABEL, fontweight="bold")
draw_box(ax, 0, y, 2, 0.9, "Selective\nSearch", fill=GRAY2, fontsize=FS)
draw_arrow(ax, 2.1, y + 0.45, 2.5, y + 0.45)
ax.text(2.3, y + 0.8, "~2000", ha="center", fontsize=FS_SMALL, style="italic")
draw_box(ax, 2.6, y, 1.5, 0.9, "Resize\n224x224", fill=GRAY4, fontsize=FS)
draw_arrow(ax, 4.2, y + 0.45, 4.6, y + 0.45)
draw_box(
ax, 4.7, y, 1.5, 0.9, "CNN\nx2000!", fill=GRAY3, fontsize=FS, fontweight="bold"
)
draw_arrow(ax, 6.3, y + 0.45, 6.7, y + 0.45)
draw_box(ax, 6.8, y, 1.3, 0.9, "SVM\nklasyf.", fill=GRAY4, fontsize=FS)
draw_arrow(ax, 8.2, y + 0.45, 8.6, y + 0.45)
draw_box(ax, 8.7, y, 1.0, 0.9, "NMS", fill=GRAY1, fontsize=FS)
# Problem annotation
ax.text(
5.5,
y - 0.4,
"⚠ CNN uruchamiane 2000x → 50 sek!",
ha="center",
fontsize=FS_SMALL,
fontweight="bold",
bbox={"boxstyle": "round,pad=0.2", "facecolor": GRAY4, "edgecolor": GRAY3},
)
# Fast R-CNN
y = y_positions[1]
ax.text(0, y + 1.3, labels[1], fontsize=FS_LABEL, fontweight="bold")
draw_box(ax, 0, y, 2, 0.9, "Selective\nSearch", fill=GRAY2, fontsize=FS)
draw_arrow(ax, 2.1, y + 0.45, 2.5, y + 0.45)
draw_box(
ax,
2.6,
y,
1.5,
0.9,
"CNN\nx1 (RAZ!)",
fill=GRAY3,
fontsize=FS,
fontweight="bold",
)
draw_arrow(ax, 4.2, y + 0.45, 4.6, y + 0.45)
draw_box(
ax, 4.7, y, 1.5, 0.9, "ROI\nPooling", fill=GRAY1, fontsize=FS, fontweight="bold"
)
draw_arrow(ax, 6.3, y + 0.45, 6.7, y + 0.45)
draw_box(ax, 6.8, y, 1.3, 0.9, "FC\nklasa+bbox", fill=GRAY4, fontsize=FS)
draw_arrow(ax, 8.2, y + 0.45, 8.6, y + 0.45)
draw_box(ax, 8.7, y, 1.0, 0.9, "NMS", fill=GRAY1, fontsize=FS)
ax.text(
3.8,
y - 0.4,
"✓ CNN RAZ na cały obraz → 25x szybciej",
ha="center",
fontsize=FS_SMALL,
fontweight="bold",
bbox={"boxstyle": "round,pad=0.2", "facecolor": GRAY4, "edgecolor": GRAY3},
)
# Faster R-CNN
y = y_positions[2]
ax.text(0, y + 1.3, labels[2], fontsize=FS_LABEL, fontweight="bold")
draw_box(
ax,
0.5,
y,
1.5,
0.9,
"CNN\nBackbone",
fill=GRAY3,
fontsize=FS,
fontweight="bold",
)
draw_arrow(ax, 2.1, y + 0.45, 2.5, y + 0.45)
draw_box(ax, 2.6, y, 1.5, 0.9, "Feature\nMap", fill=GRAY1, fontsize=FS)
draw_arrow(ax, 4.2, y + 0.45, 4.6, y + 0.45)
draw_box(
ax,
4.7,
y,
1.3,
0.9,
"RPN\n(w sieci!)",
fill=GRAY2,
fontsize=FS,
fontweight="bold",
)
draw_arrow(ax, 6.1, y + 0.45, 6.5, y + 0.45)
draw_box(ax, 6.6, y, 1.3, 0.9, "ROI\nPooling", fill=GRAY1, fontsize=FS)
draw_arrow(ax, 8.0, y + 0.45, 8.4, y + 0.45)
draw_box(ax, 8.5, y, 1.3, 0.9, "FC\nklasa+bbox", fill=GRAY4, fontsize=FS)
ax.text(
5.0,
y - 0.4,
"✓ RPN zastępuje Selective Search → end-to-end",
ha="center",
fontsize=FS_SMALL,
fontweight="bold",
bbox={"boxstyle": "round,pad=0.2", "facecolor": GRAY4, "edgecolor": GRAY3},
)
save_fig(fig, "q24_rcnn_evolution.png")
# ============================================================
# 7. YOLO Grid
# ============================================================
def draw_yolo_grid() -> None:
"""Draw yolo grid."""
fig, axes = plt.subplots(1, 3, figsize=(12, 4))
fig.suptitle(
"YOLO — detekcja jednoetapowa (siatka SxS)",
fontsize=FS_TITLE,
fontweight="bold",
y=1.02,
)
# Grid on image
ax = axes[0]
grid_size = 7
ax.set_xlim(0, grid_size)
ax.set_ylim(0, grid_size)
for i in range(grid_size + 1):
ax.axhline(y=i, color=LN, lw=0.5, alpha=0.5)
ax.axvline(x=i, color=LN, lw=0.5, alpha=0.5)
ax.add_patch(
mpatches.Rectangle(
(0, 0),
grid_size,
grid_size,
facecolor=GRAY4,
edgecolor=LN,
lw=1.5,
)
)
# Highlight one cell
ax.add_patch(mpatches.Rectangle((3, 3), 1, 1, facecolor=GRAY2, edgecolor=LN, lw=2))
# Object center dot
ax.plot(3.5, 3.5, "ko", markersize=8)
# Bounding box from that cell
ax.add_patch(
mpatches.Rectangle(
(2.0, 2.2), 3.0, 2.6, facecolor="none", edgecolor=LN, lw=2, linestyle="--"
)
)
ax.text(
3.5,
1.8,
"bbox z komórki (3,3)",
ha="center",
fontsize=FS_SMALL,
fontweight="bold",
)
ax.set_aspect("equal")
ax.invert_yaxis()
ax.set_title("① Siatka 7x7\nna obrazie", fontsize=FS, fontweight="bold")
ax.set_xticks([])
ax.set_yticks([])
_draw_yolo_cell_prediction(axes[1])
# Speed comparison
ax = axes[2]
ax.axis("off")
ax.set_xlim(0, 5)
ax.set_ylim(0, 5)
methods = ["R-CNN", "Fast R-CNN", "Faster R-CNN", "YOLO", "YOLOv8"]
fps_vals = [0.02, 0.5, 5, 45, 100]
bar_colors = [GRAY3, GRAY3, GRAY3, GRAY2, GRAY1]
for i, (m, f, c) in enumerate(zip(methods, fps_vals, bar_colors, strict=False)):
bar_w = f / 100 * 4.0
y_pos = 4.0 - i * 0.8
ax.add_patch(
mpatches.Rectangle(
(0.5, y_pos), max(bar_w, 0.1), 0.5, facecolor=c, edgecolor=LN, lw=0.8
)
)
ax.text(
0.4,
y_pos + 0.25,
m,
ha="right",
va="center",
fontsize=FS,
fontweight="bold",
)
ax.text(
max(0.7, 0.5 + bar_w + 0.1),
y_pos + 0.25,
f"{f} fps",
ha="left",
va="center",
fontsize=FS,
)
ax.set_title(
"③ Porównanie szybkości\n(fps = klatki/sek)", fontsize=FS, fontweight="bold"
)
fig.tight_layout()
save_fig(fig, "q24_yolo_grid.png")

View File

@ -0,0 +1,102 @@
"""Common constants and utilities for Q31 diagrams."""
from __future__ import annotations
import logging
from pathlib import Path
from typing import TYPE_CHECKING
import matplotlib.patches as mpatches
from matplotlib.patches import FancyBboxPatch
if TYPE_CHECKING:
from matplotlib.axes import Axes
_logger = logging.getLogger(__name__)
DPI = 300
BG = "white"
LN = "black"
FS = 8
FS_TITLE = 11
OUTPUT_DIR = str(Path(__file__).resolve().parent / "img")
Path(OUTPUT_DIR).mkdir(parents=True, exist_ok=True)
GRAY1 = "#E8E8E8"
GRAY2 = "#D0D0D0"
GRAY3 = "#B8B8B8"
GRAY4 = "#F5F5F5"
GRAY5 = "#C0C0C0"
# Number of regret table header columns before the max-regret column
_REGRET_HEADER_COLS = 4
# Number of data state columns
_DATA_STATE_COLS = 3
# Expected-value for the winning alternative
_WINNING_EV = 95
def draw_box(
ax: Axes,
x: float,
y: float,
w: float,
h: float,
text: str,
*,
fill: str = "white",
lw: float = 1.2,
fontsize: float = FS,
fontweight: str = "normal",
ha: str = "center",
va: str = "center",
rounded: bool = True,
) -> None:
"""Draw a labeled box on the axes."""
if rounded:
rect = FancyBboxPatch(
(x, y),
w,
h,
boxstyle="round,pad=0.05",
lw=lw,
edgecolor=LN,
facecolor=fill,
)
else:
rect = mpatches.Rectangle((x, y), w, h, lw=lw, edgecolor=LN, facecolor=fill)
ax.add_patch(rect)
ax.text(
x + w / 2,
y + h / 2,
text,
ha=ha,
va=va,
fontsize=fontsize,
fontweight=fontweight,
wrap=True,
)
def draw_arrow(
ax: Axes,
x1: float,
y1: float,
x2: float,
y2: float,
*,
lw: float = 1.2,
style: str = "->",
color: str = LN,
) -> None:
"""Draw an arrow between two points."""
ax.annotate(
"",
xy=(x2, y2),
xytext=(x1, y1),
arrowprops={
"arrowstyle": style,
"color": color,
"lw": lw,
},
)

View File

@ -0,0 +1,256 @@
"""Q31 Diagram 1: Payoff matrix + all criteria bar chart."""
from __future__ import annotations
from pathlib import Path
from typing import TYPE_CHECKING
import matplotlib.patches as mpatches
import matplotlib.pyplot as plt
import numpy as np
from python_pkg.praca_magisterska_video.generate_images._q31_common import (
_DATA_STATE_COLS,
BG,
DPI,
FS,
FS_TITLE,
GRAY1,
GRAY2,
GRAY3,
GRAY4,
GRAY5,
LN,
OUTPUT_DIR,
_logger,
)
if TYPE_CHECKING:
from matplotlib.axes import Axes
def _draw_payoff_table(ax: Axes) -> None:
"""Draw the payoff matrix table on the left panel."""
ax.axis("off")
ax.set_xlim(0, 6)
ax.set_ylim(0, 6)
ax.set_title(
"Macierz wypłat (tys. zł)",
fontsize=FS_TITLE,
fontweight="bold",
pad=8,
)
headers_col = ["", "S₁\n(dobra)", "S₂\n(średnia)", "S₃\n(zła)"]
rows = [
["A₁ (fabryka)", "200", "50", "-100"],
["A₂ (sklep)", "80", "70", "40"],
["A₃ (obligacje)", "30", "30", "30"],
]
col_w = [1.8, 1.2, 1.2, 1.2]
row_h = 0.7
start_y = 4.5
start_x = 0.2
# Draw header row
x = start_x
for j, h in enumerate(headers_col):
fill = GRAY2 if j > 0 else GRAY3
rect = mpatches.Rectangle(
(x, start_y),
col_w[j],
row_h,
lw=1,
edgecolor=LN,
facecolor=fill,
)
ax.add_patch(rect)
ax.text(
x + col_w[j] / 2,
start_y + row_h / 2,
h,
ha="center",
va="center",
fontsize=FS,
fontweight="bold",
)
x += col_w[j]
# Draw data rows
for i, row in enumerate(rows):
x = start_x
y = start_y - (i + 1) * row_h
for j, val in enumerate(row):
fill = GRAY4 if j == 0 else ("white" if i % 2 == 0 else GRAY1)
if val.startswith("-"):
fill = "#D8D8D8"
rect = mpatches.Rectangle(
(x, y),
col_w[j],
row_h,
lw=1,
edgecolor=LN,
facecolor=fill,
)
ax.add_patch(rect)
fw = "bold" if j == 0 else "normal"
ax.text(
x + col_w[j] / 2,
y + row_h / 2,
val,
ha="center",
va="center",
fontsize=FS,
fontweight=fw,
)
x += col_w[j]
# Probability row for EV
x = start_x
y = start_y - 4 * row_h
probs = ["p (dla E[X]):", "0.5", "0.3", "0.2"]
for j, val in enumerate(probs):
fill = GRAY5 if j > 0 else GRAY3
rect = mpatches.Rectangle(
(x, y),
col_w[j],
row_h * 0.7,
lw=1,
edgecolor=LN,
facecolor=fill,
)
ax.add_patch(rect)
ax.text(
x + col_w[j] / 2,
y + row_h * 0.35,
val,
ha="center",
va="center",
fontsize=7,
fontweight="bold",
style="italic",
)
x += col_w[j]
def _draw_criteria_bars(ax2: Axes) -> None:
"""Draw the criteria comparison bar chart on the right panel."""
criteria = [
"E[X]",
"Laplace",
"Maximax",
"Maximin",
"Hurwicz\n\u03b1=0.6",
"Savage",
]
ev = [95, 69, 30]
laplace = [50, 63.3, 30]
maximax = [200, 80, 30]
maximin = [-100, 40, 30]
hurwicz = [80, 64, 30]
savage_maxregret = [140, 120, 170]
winners = [0, 1, 0, 1, 0, 1]
x_pos = np.arange(len(criteria))
width = 0.22
hatches = ["///", "...", "xxx"]
labels = ["A₁ (fabryka)", "A₂ (sklep)", "A₃ (obligacje)"]
all_vals = [
[
ev[0],
laplace[0],
maximax[0],
maximin[0],
hurwicz[0],
savage_maxregret[0],
],
[
ev[1],
laplace[1],
maximax[1],
maximin[1],
hurwicz[1],
savage_maxregret[1],
],
[
ev[2],
laplace[2],
maximax[2],
maximin[2],
hurwicz[2],
savage_maxregret[2],
],
]
for i in range(_DATA_STATE_COLS):
ax2.bar(
x_pos + (i - 1) * width,
all_vals[i],
width,
label=labels[i],
color="white",
edgecolor=LN,
hatch=hatches[i],
lw=0.8,
)
for c_idx in range(len(criteria)):
w = winners[c_idx]
val = all_vals[w][c_idx]
ax2.text(
x_pos[c_idx] + (w - 1) * width,
val + 5,
"",
ha="center",
va="bottom",
fontsize=10,
fontweight="bold",
)
ax2.set_xticks(x_pos)
ax2.set_xticklabels(criteria, fontsize=7)
ax2.set_ylabel("Wartość kryterium", fontsize=8)
ax2.set_title(
"Porównanie kryteriów",
fontsize=FS_TITLE,
fontweight="bold",
pad=8,
)
ax2.legend(fontsize=7, loc="upper right")
ax2.axhline(y=0, color=LN, lw=0.5, ls="-")
ax2.spines["top"].set_visible(False)
ax2.spines["right"].set_visible(False)
ax2.tick_params(labelsize=7)
ax2.text(
5,
-30,
"(Savage: niżej\n= lepiej)",
fontsize=6,
ha="center",
va="top",
style="italic",
)
def draw_criteria_comparison() -> None:
"""Draw payoff matrix and criteria comparison chart."""
fig, axes = plt.subplots(
1,
2,
figsize=(8.27, 4.5),
gridspec_kw={"width_ratios": [1.2, 1]},
)
_draw_payoff_table(axes[0])
_draw_criteria_bars(axes[1])
plt.tight_layout()
outpath = str(Path(OUTPUT_DIR) / "q31_criteria_comparison.png")
fig.savefig(outpath, dpi=DPI, bbox_inches="tight", facecolor=BG)
plt.close(fig)
_logger.info(" Saved: %s", outpath)

View File

@ -0,0 +1,289 @@
"""Q31 Diagrams 5 & 6: Expected value + decision conditions spectrum."""
from __future__ import annotations
from pathlib import Path
import matplotlib.patches as mpatches
from matplotlib.patches import FancyBboxPatch
import matplotlib.pyplot as plt
from python_pkg.praca_magisterska_video.generate_images._q31_common import (
_WINNING_EV,
BG,
DPI,
FS_TITLE,
GRAY1,
GRAY2,
GRAY3,
LN,
OUTPUT_DIR,
_logger,
)
def draw_expected_value() -> None:
"""Draw expected value criterion with probability-weighted bars."""
fig, axes = plt.subplots(1, 3, figsize=(8.27, 3.5), sharey=True)
fig.suptitle(
"Kryterium wartości oczekiwanej E[X]" " \u2014 rozkład wyników per alternatywa",
fontsize=FS_TITLE,
fontweight="bold",
y=1.02,
)
probs = [0.5, 0.3, 0.2]
alts = [
("A₁ (fabryka)", [200, 50, -100], 95),
("A₂ (sklep)", [80, 70, 40], 69),
("A₃ (obligacje)", [30, 30, 30], 30),
]
hatches = ["///", "...", "xxx"]
for _idx, (ax, (name, vals, ev)) in enumerate(zip(axes, alts, strict=False)):
x_positions = [0, 0.6, 1.0]
widths = [p * 0.9 for p in probs]
for i, (v, p, h) in enumerate(zip(vals, probs, hatches, strict=False)):
color = "white" if v >= 0 else GRAY2
ax.bar(
x_positions[i],
v,
width=widths[i],
color=color,
edgecolor=LN,
hatch=h,
lw=0.8,
align="edge",
)
offset = 8 if v >= 0 else -12
ax.text(
x_positions[i] + widths[i] / 2,
v + offset,
f"{v}",
ha="center",
va="center",
fontsize=8,
fontweight="bold",
)
contrib = v * p
ax.text(
x_positions[i] + widths[i] / 2,
v / 2,
f"{v}x{p}\n={contrib:.0f}",
ha="center",
va="center",
fontsize=6,
style="italic",
)
# Expected value line
ax.axhline(y=ev, color=LN, lw=2, ls="--")
ax.text(
1.35,
ev,
f"E[X]={ev}",
fontsize=8,
fontweight="bold",
va="center",
ha="left",
bbox={
"boxstyle": "round,pad=0.15",
"facecolor": GRAY1,
"edgecolor": LN,
},
)
ax.set_title(name, fontsize=9, fontweight="bold")
ax.set_xticks([0.225, 0.735, 1.09])
ax.set_xticklabels(["S₁", "S₂", "S₃"], fontsize=7)
ax.axhline(y=0, color=LN, lw=0.5)
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)
ax.tick_params(labelsize=7)
# Star on winner
if ev == _WINNING_EV:
ax.text(
0.7,
ev + 20,
"★ MAX",
fontsize=9,
fontweight="bold",
ha="center",
va="bottom",
)
axes[0].set_ylabel("Wypłata (tys. zł)", fontsize=8)
plt.tight_layout()
outpath = str(Path(OUTPUT_DIR) / "q31_expected_value.png")
fig.savefig(outpath, dpi=DPI, bbox_inches="tight", facecolor=BG)
plt.close(fig)
_logger.info(" Saved: %s", outpath)
def draw_conditions_spectrum() -> None:
"""Draw decision conditions spectrum diagram."""
fig, ax = plt.subplots(1, 1, figsize=(8.27, 3.5))
ax.set_xlim(0, 10)
ax.set_ylim(0, 5)
ax.set_aspect("equal")
ax.axis("off")
ax.set_title(
"Warunki decyzyjne" " \u2014 spektrum wiedzy decydenta",
fontsize=FS_TITLE + 1,
fontweight="bold",
pad=10,
)
# Three zones
zones = [
(
0.3,
1.5,
2.8,
2.5,
"PEWNOŚĆ",
"white",
[
"Znamy dokładny wynik",
"Przykład: lokata 5%",
"Metoda: po prostu wybierz",
"najlepszy wynik",
],
),
(
3.5,
1.5,
2.8,
2.5,
"RYZYKO",
GRAY1,
[
"Znamy wyniki I prawdop.",
"Przykład: gra w kości",
"Metoda: wartość",
"oczekiwana E[X]",
],
),
(
6.7,
1.5,
2.8,
2.5,
"NIEPEWNOŚĆ",
GRAY3,
[
"Znamy wyniki, ale",
"NIE znamy prawdop.",
"Metody: Laplace, maximax,",
"maximin, Hurwicz, Savage",
],
),
]
for x, y, w, h, title, fill, lines in zones:
rect = FancyBboxPatch(
(x, y),
w,
h,
boxstyle="round,pad=0.1",
lw=2,
edgecolor=LN,
facecolor=fill,
)
ax.add_patch(rect)
ax.text(
x + w / 2,
y + h - 0.3,
title,
ha="center",
va="center",
fontsize=11,
fontweight="bold",
)
for i, line in enumerate(lines):
ax.text(
x + w / 2,
y + h - 0.7 - i * 0.4,
line,
ha="center",
va="center",
fontsize=7,
)
# Arrows between zones
ax.annotate(
"",
xy=(3.4, 2.75),
xytext=(3.15, 2.75),
arrowprops={"arrowstyle": "->", "color": LN, "lw": 2},
)
ax.annotate(
"",
xy=(6.6, 2.75),
xytext=(6.35, 2.75),
arrowprops={"arrowstyle": "->", "color": LN, "lw": 2},
)
# Bottom: knowledge gradient bar
gradient_y = 0.5
gradient_h = 0.5
n_steps = 50
for i in range(n_steps):
x = 0.3 + i * (9.2 / n_steps)
w = 9.2 / n_steps + 0.01
gray_val = 1 - (i / n_steps) * 0.7
rect = mpatches.Rectangle(
(x, gradient_y),
w,
gradient_h,
lw=0,
facecolor=str(gray_val),
)
ax.add_patch(rect)
rect = mpatches.Rectangle(
(0.3, gradient_y),
9.2,
gradient_h,
lw=1.5,
edgecolor=LN,
facecolor="none",
)
ax.add_patch(rect)
ax.text(
0.3,
gradient_y - 0.15,
"Dużo wiedzy",
fontsize=7,
ha="left",
va="top",
)
ax.text(
9.5,
gradient_y - 0.15,
"Mało wiedzy",
fontsize=7,
ha="right",
va="top",
)
ax.text(
4.95,
gradient_y + gradient_h / 2,
"POZIOM WIEDZY DECYDENTA",
fontsize=8,
fontweight="bold",
ha="center",
va="center",
color="white",
)
plt.tight_layout()
outpath = str(Path(OUTPUT_DIR) / "q31_conditions_spectrum.png")
fig.savefig(outpath, dpi=DPI, bbox_inches="tight", facecolor=BG)
plt.close(fig)
_logger.info(" Saved: %s", outpath)

View File

@ -0,0 +1,344 @@
"""Q31 Diagrams 3 & 4: Hurwicz interpolation + criteria mnemonic map."""
from __future__ import annotations
from pathlib import Path
from typing import TYPE_CHECKING
from matplotlib.patches import FancyBboxPatch
import matplotlib.pyplot as plt
import numpy as np
from python_pkg.praca_magisterska_video.generate_images._q31_common import (
BG,
DPI,
FS_TITLE,
GRAY1,
GRAY2,
GRAY3,
LN,
OUTPUT_DIR,
_logger,
draw_box,
)
if TYPE_CHECKING:
from matplotlib.axes import Axes
def draw_hurwicz_interpolation() -> None:
"""Draw Hurwicz alpha interpolation diagram."""
fig, ax = plt.subplots(1, 1, figsize=(8.27, 4))
ax.set_title(
"Kryterium Hurwicza" " \u2014 wpływ \u03b1 na wybór alternatywy",
fontsize=FS_TITLE + 1,
fontweight="bold",
pad=10,
)
alphas = np.linspace(0, 1, 200)
v1 = alphas * 200 + (1 - alphas) * (-100)
v2 = alphas * 80 + (1 - alphas) * 40
v3 = alphas * 30 + (1 - alphas) * 30
ax.plot(
alphas,
v1,
"k-",
lw=2,
label="A₁ (fabryka): V = 300\u03b1 - 100",
)
ax.plot(
alphas,
v2,
"k--",
lw=2,
label="A₂ (sklep): V = 40\u03b1 + 40",
)
ax.plot(
alphas,
v3,
"k:",
lw=2,
label="A₃ (obligacje): V = 30",
)
# Crossover A2=A1
alpha_cross_12 = 140 / 260
v_cross_12 = 40 * alpha_cross_12 + 40
ax.plot(alpha_cross_12, v_cross_12, "ko", markersize=8, zorder=5)
ax.annotate(
f"\u03b1{alpha_cross_12:.2f}\nA₁ = A₂",
xy=(alpha_cross_12, v_cross_12),
xytext=(alpha_cross_12 + 0.12, v_cross_12 - 30),
fontsize=8,
fontweight="bold",
arrowprops={
"arrowstyle": "->",
"color": LN,
"lw": 1,
},
)
# Shade winning regions
ax.axvspan(0, alpha_cross_12, alpha=0.08, color="black")
ax.axvspan(alpha_cross_12, 1, alpha=0.15, color="black")
ax.text(
alpha_cross_12 / 2,
-60,
"A₂ wygrywa\n(pesymistycznie)",
fontsize=8,
ha="center",
va="center",
bbox={
"boxstyle": "round",
"facecolor": "white",
"edgecolor": LN,
},
)
ax.text(
(alpha_cross_12 + 1) / 2,
160,
"A₁ wygrywa\n(optymistycznie)",
fontsize=8,
ha="center",
va="center",
bbox={
"boxstyle": "round",
"facecolor": "white",
"edgecolor": LN,
},
)
# Special alpha values
ax.axvline(x=0, color=LN, lw=0.5, ls=":")
ax.axvline(x=1, color=LN, lw=0.5, ls=":")
ax.text(
0,
-115,
"\u03b1=0\nmaximin",
fontsize=7,
ha="center",
va="top",
fontweight="bold",
)
ax.text(
1,
-115,
"\u03b1=1\nmaximax",
fontsize=7,
ha="center",
va="top",
fontweight="bold",
)
ax.set_xlabel("Współczynnik optymizmu \u03b1", fontsize=9)
ax.set_ylabel("V(Aᵢ) = \u03b1·max + (1-\u03b1)·min", fontsize=9)
ax.legend(fontsize=8, loc="upper left")
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)
ax.set_xlim(-0.05, 1.05)
ax.axhline(y=0, color=LN, lw=0.3, ls="-")
ax.tick_params(labelsize=8)
plt.tight_layout()
outpath = str(Path(OUTPUT_DIR) / "q31_hurwicz_alpha.png")
fig.savefig(outpath, dpi=DPI, bbox_inches="tight", facecolor=BG)
plt.close(fig)
_logger.info(" Saved: %s", outpath)
def _draw_mnemonic_criteria_boxes(ax: Axes) -> None:
"""Draw the 6 criteria boxes around the center."""
criteria = [
(
0,
6.5,
3,
1.2,
"WARTOŚĆ OCZEKIWANA",
"\u201eMam prawdopodobieństwa\u201d",
"E[Aᵢ] = Σ pⱼ·aᵢⱼ",
),
(
3.5,
6.5,
3,
1.2,
"LAPLACE",
"\u201eWszystko po równo\u201d",
"V = Σaᵢⱼ / n",
),
(
7,
6.5,
3,
1.2,
"MAXIMAX",
"\u201eOptymista: max z max\u201d",
"max maxⱼ aᵢⱼ",
),
(
0,
0.5,
3,
1.2,
"MAXIMIN (Wald)",
"\u201ePesymista: max z min\u201d",
"max minⱼ aᵢⱼ",
),
(
3.5,
0.5,
3,
1.2,
"HURWICZ",
"\u201e\u03b1 pomiędzy\u201d",
"\u03b1·max + (1-\u03b1)·min",
),
(
7,
0.5,
3,
1.2,
"SAVAGE",
"\u201eMin max żalu\u201d",
"min maxⱼ rᵢⱼ",
),
]
fills = [GRAY3, GRAY1, "white", "white", GRAY1, GRAY3]
for i, (x, y, w, h, title, mnem, formula) in enumerate(criteria):
rect = FancyBboxPatch(
(x, y),
w,
h,
boxstyle="round,pad=0.08",
lw=1.5,
edgecolor=LN,
facecolor=fills[i],
)
ax.add_patch(rect)
ax.text(
x + w / 2,
y + h * 0.78,
title,
ha="center",
va="center",
fontsize=8,
fontweight="bold",
)
ax.text(
x + w / 2,
y + h * 0.45,
mnem,
ha="center",
va="center",
fontsize=7,
style="italic",
)
ax.text(
x + w / 2,
y + h * 0.15,
formula,
ha="center",
va="center",
fontsize=7,
fontweight="bold",
family="monospace",
)
# Arrows from center to each box
cx, cy = 5, 4
bx = x + w / 2
by_center = y + h / 2
if by_center > cy:
ax.annotate(
"",
xy=(bx, y),
xytext=(cx, 4.5),
arrowprops={
"arrowstyle": "->",
"color": LN,
"lw": 1,
"connectionstyle": "arc3,rad=0",
},
)
else:
ax.annotate(
"",
xy=(bx, y + h),
xytext=(cx, 3.5),
arrowprops={
"arrowstyle": "->",
"color": LN,
"lw": 1,
"connectionstyle": "arc3,rad=0",
},
)
# Labels on arrows
arrow_labels = [
(1.2, 5.6, "znane p"),
(5, 5.6, "p = 1/n"),
(8.7, 5.6, "max ↑"),
(1.2, 2.5, "min ↑"),
(5, 2.5, "podaj \u03b1"),
(8.7, 2.5, "macierz\nżalu"),
]
for lx, ly, ltext in arrow_labels:
ax.text(
lx,
ly,
ltext,
fontsize=7,
ha="center",
va="center",
bbox={
"boxstyle": "round,pad=0.15",
"facecolor": "white",
"edgecolor": GRAY3,
"lw": 0.5,
},
)
def draw_criteria_mnemonic() -> None:
"""Draw decision criteria mnemonic map diagram."""
fig, ax = plt.subplots(1, 1, figsize=(8.27, 6))
ax.set_xlim(0, 10)
ax.set_ylim(0, 8)
ax.set_aspect("equal")
ax.axis("off")
ax.set_title(
"Mapa mnemoniczna \u2014 6 kryteriów decyzyjnych",
fontsize=FS_TITLE + 2,
fontweight="bold",
pad=10,
)
# Central node
draw_box(
ax,
3.5,
3.5,
3,
1,
"MACIERZ\nWYPŁAT",
fill=GRAY2,
lw=2,
fontsize=11,
fontweight="bold",
)
_draw_mnemonic_criteria_boxes(ax)
plt.tight_layout()
outpath = str(Path(OUTPUT_DIR) / "q31_criteria_mnemonic.png")
fig.savefig(outpath, dpi=DPI, bbox_inches="tight", facecolor=BG)
plt.close(fig)
_logger.info(" Saved: %s", outpath)

View File

@ -0,0 +1,322 @@
"""Q31 Diagram 2: Regret matrix construction step-by-step."""
from __future__ import annotations
from pathlib import Path
from typing import TYPE_CHECKING
import matplotlib.patches as mpatches
import matplotlib.pyplot as plt
from python_pkg.praca_magisterska_video.generate_images._q31_common import (
_DATA_STATE_COLS,
_REGRET_HEADER_COLS,
BG,
DPI,
FS,
FS_TITLE,
GRAY1,
GRAY2,
GRAY3,
GRAY4,
LN,
OUTPUT_DIR,
_logger,
)
if TYPE_CHECKING:
from matplotlib.axes import Axes
def _draw_original_payoff(
ax: Axes,
start_y: float,
row_h: float,
) -> None:
"""Draw the original payoff matrix (left side of regret fig)."""
ax.text(
2.2,
6.3,
"Krok 1: Macierz wypłat",
fontsize=9,
fontweight="bold",
ha="center",
va="center",
)
col_w = 1.0
headers = ["", "S₁", "S₂", "S₃"]
data = [
["A₁", "200", "50", "-100"],
["A₂", "80", "70", "40"],
["A₃", "30", "30", "30"],
]
start_x = 0.3
for j, h in enumerate(headers):
w = 0.7 if j == 0 else col_w
x = start_x + (0 if j == 0 else 0.7 + (j - 1) * col_w)
rect = mpatches.Rectangle(
(x, start_y),
w,
row_h,
lw=1,
edgecolor=LN,
facecolor=GRAY2,
)
ax.add_patch(rect)
ax.text(
x + w / 2,
start_y + row_h / 2,
h,
ha="center",
va="center",
fontsize=FS,
fontweight="bold",
)
for i, row in enumerate(data):
y = start_y - (i + 1) * row_h
for j, val in enumerate(row):
w = 0.7 if j == 0 else col_w
x = start_x + (0 if j == 0 else 0.7 + (j - 1) * col_w)
fill = GRAY4 if j == 0 else "white"
rect = mpatches.Rectangle(
(x, y),
w,
row_h,
lw=1,
edgecolor=LN,
facecolor=fill,
)
ax.add_patch(rect)
ax.text(
x + w / 2,
y + row_h / 2,
val,
ha="center",
va="center",
fontsize=FS,
)
# Max per column annotation
max_y = start_y - _DATA_STATE_COLS * row_h - 0.1
col_maxes = ["max=200", "max=70", "max=40"]
for idx, label in enumerate(col_maxes):
ax.text(
start_x + 0.7 + (idx + 0.5) * col_w,
max_y,
label,
fontsize=7,
ha="center",
va="top",
fontweight="bold",
color="#333",
)
# Arrow
ax.annotate(
"",
xy=(5.0, 4.8),
xytext=(4.2, 4.8),
arrowprops={"arrowstyle": "->", "color": LN, "lw": 2},
)
ax.text(
4.6,
5.0,
"rᵢⱼ = max - aᵢⱼ",
fontsize=8,
ha="center",
va="bottom",
fontweight="bold",
)
def _draw_regret_table(
ax: Axes,
start_y: float,
row_h: float,
) -> None:
"""Draw the regret matrix (right side of regret fig)."""
ax.text(
7.5,
6.3,
"Krok 2: Macierz żalu",
fontsize=9,
fontweight="bold",
ha="center",
va="center",
)
regret_data = [
["A₁", "0", "20", "140"],
["A₂", "120", "0", "0"],
["A₃", "170", "40", "10"],
]
headers2 = ["", "S₁", "S₂", "S₃", "max rᵢ"]
start_x2 = 5.3
for j, h in enumerate(headers2):
w = 0.7 if j == 0 else (0.9 if j < _REGRET_HEADER_COLS else 1.0)
x = start_x2
if j == 0:
x = start_x2
elif j <= _DATA_STATE_COLS:
x = start_x2 + 0.7 + (j - 1) * 0.9
else:
x = start_x2 + 0.7 + _DATA_STATE_COLS * 0.9
rect = mpatches.Rectangle(
(x, start_y),
w,
row_h,
lw=1,
edgecolor=LN,
facecolor=(GRAY2 if j < _REGRET_HEADER_COLS else GRAY3),
)
ax.add_patch(rect)
ax.text(
x + w / 2,
start_y + row_h / 2,
h,
ha="center",
va="center",
fontsize=FS,
fontweight="bold",
)
max_regrets = [140, 120, 170]
for i, row in enumerate(regret_data):
y = start_y - (i + 1) * row_h
for j, val in enumerate(row):
w = 0.7 if j == 0 else 0.9
x = start_x2 + (0 if j == 0 else 0.7 + (j - 1) * 0.9)
fill = GRAY4 if j == 0 else "white"
if j > 0 and int(val) == max_regrets[i]:
fill = GRAY2
rect = mpatches.Rectangle(
(x, y),
w,
row_h,
lw=1,
edgecolor=LN,
facecolor=fill,
)
ax.add_patch(rect)
fw = "bold" if (j > 0 and int(val) == max_regrets[i]) else "normal"
ax.text(
x + w / 2,
y + row_h / 2,
val,
ha="center",
va="center",
fontsize=FS,
fontweight=fw,
)
# Max regret column
x = start_x2 + 0.7 + _DATA_STATE_COLS * 0.9
w = 1.0
is_winner = max_regrets[i] == min(max_regrets)
fill = "#C8C8C8" if is_winner else GRAY1
rect = mpatches.Rectangle(
(x, y),
w,
row_h,
lw=1.5 if is_winner else 1,
edgecolor=LN,
facecolor=fill,
)
ax.add_patch(rect)
marker = "" if is_winner else ""
ax.text(
x + w / 2,
y + row_h / 2,
f"{max_regrets[i]}{marker}",
ha="center",
va="center",
fontsize=FS,
fontweight="bold",
)
def draw_regret_matrix() -> None:
"""Draw the regret matrix construction diagram."""
fig, ax = plt.subplots(1, 1, figsize=(8.27, 5))
ax.axis("off")
ax.set_xlim(0, 10)
ax.set_ylim(0, 7)
ax.set_title(
"Kryterium Savage'a \u2014 budowa macierzy żalu",
fontsize=FS_TITLE + 1,
fontweight="bold",
pad=10,
)
start_y = 5.5
row_h = 0.55
_draw_original_payoff(ax, start_y, row_h)
_draw_regret_table(ax, start_y, row_h)
# Bottom conclusion
ax.text(
5.0,
2.8,
"Krok 3: Wybierz min z max żalu" " → A₂ (max żal = 120)",
fontsize=10,
ha="center",
va="center",
fontweight="bold",
bbox={
"boxstyle": "round,pad=0.3",
"facecolor": GRAY1,
"edgecolor": LN,
"lw": 1.5,
},
)
# Interpretation examples
ax.text(
5.0,
2.0,
"Interpretacja żalu: r₁₃ = 140 oznacza:\n"
"\u201eGdyby nastąpił S₃ (zła koniunktura),"
" a wybrałbym A₁,\n"
"żałowałbym, bo najlepszą opcją byłoby"
" A₂ z wynikiem 40 \u2014 traciłbym 140\u201d",
fontsize=7.5,
ha="center",
va="center",
style="italic",
bbox={
"boxstyle": "round,pad=0.3",
"facecolor": GRAY4,
"edgecolor": GRAY3,
"lw": 0.8,
},
)
# Mnemonic
ax.text(
5.0,
0.8,
"Mnemonik: Savage = \u201eŻal jak nóż\u201d\n"
"Maksymalny żal to nóż "
"\u2014 wybierz opcję z NAJMNIEJSZYM nożem",
fontsize=8,
ha="center",
va="center",
fontweight="bold",
bbox={
"boxstyle": "round,pad=0.3",
"facecolor": "white",
"edgecolor": LN,
"lw": 1,
},
)
plt.tight_layout()
outpath = str(Path(OUTPUT_DIR) / "q31_regret_matrix.png")
fig.savefig(outpath, dpi=DPI, bbox_inches="tight", facecolor=BG)
plt.close(fig)
_logger.info(" Saved: %s", outpath)

View File

@ -0,0 +1,448 @@
"""Q9 diagrams 1-6: process/thread basics, memory, states, PCB, speed."""
from __future__ import annotations
from _q9_common import (
FS,
FS_LABEL,
FS_SMALL,
FS_TITLE,
GRAY1,
GRAY2,
GRAY3,
GRAY4,
GRAY5,
LN,
draw_arrow,
draw_box,
draw_table,
save_fig,
)
import matplotlib.pyplot as plt
# ============================================================
# 1. Process vs Thread comparison table
# ============================================================
def gen_process_vs_thread() -> None:
"""Gen process vs thread."""
fig, ax = plt.subplots(figsize=(7.5, 4.5))
ax.set_xlim(0, 10)
ax.set_ylim(-4.5, 1.5)
ax.set_aspect("auto")
ax.axis("off")
ax.set_title(
"Proces vs Wątek — porównanie", fontsize=FS_TITLE, fontweight="bold", pad=10
)
headers = ["Cecha", "Proces", "Wątek"]
col_w = [2.5, 3.5, 3.5]
rows = [
["Pamięć", "Własna, izolowana", "Współdzielona (heap)"],
["Tworzenie", "~1-10 ms", "~10-100 μs (100x szybciej)"],
["Przełączanie", "~1-5 μs (TLB flush)", "~0.1-0.5 μs (10x)"],
["Komunikacja", "IPC (pipe, socket, shm)", "Bezpośrednia (wspólna pam.)"],
["Izolacja", "Pełna — awaria izolowana", "Brak — może zabić proces"],
["Zastosowanie", "Bezpieczeństwo, izolacja", "Wydajność, współdzielenie"],
]
draw_table(
ax,
headers,
rows,
x0=0.25,
y0=0.8,
col_widths=col_w,
row_h=0.55,
fontsize=7.5,
header_fontsize=FS_LABEL,
)
# Analogy at bottom
ax.text(
5.0,
-4.2,
"Analogia: Proces = mieszkanie (własny adres) "
"Wątek = pokój w mieszkaniu (wspólna kuchnia = heap)",
ha="center",
fontsize=FS,
style="italic",
bbox={"boxstyle": "round,pad=0.3", "facecolor": GRAY4, "edgecolor": GRAY3},
)
save_fig(fig, "q9_process_vs_thread.png")
# ============================================================
# 2. Memory segments layout
# ============================================================
def gen_memory_layout() -> None:
"""Gen memory layout."""
fig, ax = plt.subplots(figsize=(6, 5))
ax.set_xlim(0, 10)
ax.set_ylim(0, 8)
ax.set_aspect("auto")
ax.axis("off")
ax.set_title(
"Segmenty pamięci procesu", fontsize=FS_TITLE, fontweight="bold", pad=10
)
segments = [
("STACK ↓", "zmienne lokalne, adresy\npowrotu (każdy wątek WŁASNY)", GRAY1),
("...", "(wolna przestrzeń)", "white"),
("HEAP ↑", "malloc/new — dynamiczna\nalokacja (współdzielony)", GRAY4),
("BSS", "zmienne globalne\nniezainicjalizowane (zerowane)", GRAY2),
("DATA", "zmienne globalne\nzainicjalizowane", GRAY3),
("TEXT", "kod maszynowy\n(read-only, współdzielony)", GRAY5),
]
bx, bw = 2.0, 2.5
seg_h = 0.9
gap = 0.05
top_y = 7.0
for i, (name, desc, color) in enumerate(segments):
y = top_y - i * (seg_h + gap)
draw_box(
ax,
bx,
y,
bw,
seg_h,
name,
fill=color,
fontsize=FS_LABEL,
fontweight="bold",
rounded=False,
)
ax.text(bx + bw + 0.3, y + seg_h / 2, desc, fontsize=7.5, va="center")
# Address labels
ax.text(
bx - 0.2,
top_y + seg_h / 2,
"wysoki\nadres",
fontsize=FS_SMALL,
va="center",
ha="right",
style="italic",
)
bottom_y = top_y - 5 * (seg_h + gap)
ax.text(
bx - 0.2,
bottom_y + seg_h / 2,
"niski\nadres",
fontsize=FS_SMALL,
va="center",
ha="right",
style="italic",
)
# Arrows for growth
ax.annotate(
"",
xy=(bx - 0.5, top_y - 0.1),
xytext=(bx - 0.5, top_y + seg_h + 0.1),
arrowprops={"arrowstyle": "->", "lw": 1.5, "color": LN},
)
ax.text(bx - 0.9, top_y + 0.4, "rośnie\nw dół", fontsize=FS_SMALL, ha="center")
heap_y = top_y - 2 * (seg_h + gap)
ax.annotate(
"",
xy=(bx - 0.5, heap_y + seg_h + 0.1),
xytext=(bx - 0.5, heap_y - 0.1),
arrowprops={"arrowstyle": "->", "lw": 1.5, "color": LN},
)
ax.text(bx - 0.9, heap_y + 0.5, "rośnie\nw górę", fontsize=FS_SMALL, ha="center")
save_fig(fig, "q9_memory_layout.png")
# ============================================================
# 3. Process states diagram
# ============================================================
def gen_process_states() -> None:
"""Gen process states."""
fig, ax = plt.subplots(figsize=(7, 3.5))
ax.set_xlim(0, 12)
ax.set_ylim(0, 5)
ax.set_aspect("auto")
ax.axis("off")
ax.set_title(
"Stany procesu — diagram przejść", fontsize=FS_TITLE, fontweight="bold", pad=10
)
states = {
"NEW": (1.0, 2.5),
"READY": (3.5, 2.5),
"RUNNING": (6.5, 2.5),
"BLOCKED": (6.5, 0.5),
"TERMINATED": (10.0, 2.5),
}
fills = {
"NEW": GRAY4,
"READY": GRAY1,
"RUNNING": GRAY3,
"BLOCKED": GRAY2,
"TERMINATED": GRAY5,
}
bw, bh = 1.8, 0.9
for name, (x, y) in states.items():
draw_box(
ax,
x,
y,
bw,
bh,
name,
fill=fills[name],
fontsize=FS_LABEL,
fontweight="bold",
)
# Transitions
transitions = [
("NEW", "READY", "admit"),
("READY", "RUNNING", "dispatch\n(scheduler)"),
("RUNNING", "TERMINATED", "exit"),
("RUNNING", "BLOCKED", "I/O wait"),
]
for src, dst, label in transitions:
sx, sy = states[src]
dx, dy = states[dst]
if sy == dy: # horizontal
draw_arrow(ax, sx + bw, sy + bh / 2, dx, dy + bh / 2, lw=1.5)
mx = (sx + bw + dx) / 2
ax.text(
mx,
sy + bh / 2 + 0.25,
label,
fontsize=FS_SMALL,
ha="center",
va="bottom",
)
else: # vertical
draw_arrow(ax, sx + bw / 2, sy, dx + bw / 2, dy + bh, lw=1.5)
ax.text(
sx + bw + 0.2,
(sy + dy + bh) / 2,
label,
fontsize=FS_SMALL,
ha="left",
va="center",
)
# BLOCKED → READY
bx, by = states["BLOCKED"]
rx, ry = states["READY"]
ax.annotate(
"",
xy=(rx + bw / 2, ry),
xytext=(bx - 0.3, by + bh / 2),
arrowprops={
"arrowstyle": "->",
"lw": 1.5,
"color": LN,
"connectionstyle": "arc3,rad=0.3",
},
)
ax.text(3.5, 0.7, "I/O done", fontsize=FS_SMALL, ha="center")
# RUNNING → READY (preemption)
rux, ruy = states["RUNNING"]
draw_arrow(ax, rux, ruy + bh, rx + bw, ry + bh, lw=1.2)
ax.text(5.0, 3.7, "preempt /\ntimeout", fontsize=FS_SMALL, ha="center")
save_fig(fig, "q9_process_states.png")
# ============================================================
# 4. Thread structure within process
# ============================================================
def gen_thread_structure() -> None:
"""Gen thread structure."""
fig, ax = plt.subplots(figsize=(8, 4.5))
ax.set_xlim(0, 10)
ax.set_ylim(0, 6)
ax.set_aspect("auto")
ax.axis("off")
ax.set_title(
"Wątki wewnątrz procesu (PID=42)", fontsize=FS_TITLE, fontweight="bold", pad=10
)
# Shared memory region
draw_box(ax, 0.5, 3.5, 9.0, 1.8, "", fill=GRAY1, rounded=False, lw=2)
ax.text(5.0, 5.0, "WSPÓŁDZIELONE", fontsize=FS, fontweight="bold", ha="center")
labels_shared = ["TEXT", "DATA", "BSS", "HEAP", "pliki", "PID"]
for i, lab in enumerate(labels_shared):
x = 1.0 + i * 1.4
draw_box(
ax,
x,
3.8,
1.1,
0.6,
lab,
fill=GRAY3,
fontsize=FS,
fontweight="bold",
rounded=False,
)
ax.text(x + 0.55, 4.6, lab, fontsize=FS_SMALL, ha="center", color="#555555")
# Per-thread regions
draw_box(
ax, 0.5, 0.5, 9.0, 2.7, "", fill="white", rounded=False, lw=2, linestyle="--"
)
ax.text(
5.0, 2.95, "PRYWATNE (każdy wątek)", fontsize=FS, fontweight="bold", ha="center"
)
for i in range(3):
x = 1.0 + i * 3.0
tid = i + 1
draw_box(ax, x, 0.7, 2.3, 2.0, "", fill=GRAY4, rounded=False)
ax.text(
x + 1.15,
2.4,
f"Wątek {tid}",
fontsize=FS_LABEL,
fontweight="bold",
ha="center",
)
items = [f"stos_{tid}", f"rejestry_{tid}", f"PC_{tid}", f"TID={40 + tid}"]
for j, item in enumerate(items):
ax.text(
x + 1.15,
2.0 - j * 0.35,
item,
fontsize=FS_SMALL,
ha="center",
family="monospace",
)
save_fig(fig, "q9_thread_structure.png")
# ============================================================
# 5. PCB structure
# ============================================================
def gen_pcb_structure() -> None:
"""Gen pcb structure."""
fig, ax = plt.subplots(figsize=(5, 3.5))
ax.set_xlim(0, 8)
ax.set_ylim(0, 5.5)
ax.set_aspect("auto")
ax.axis("off")
ax.set_title(
"PCB (Process Control Block)", fontsize=FS_TITLE, fontweight="bold", pad=10
)
fields = [
("PID", "42"),
("Stan", "READY / RUNNING / BLOCKED"),
("Rejestry CPU", "EAX, EBX, ESP, EIP ..."),
("Tablice stron", "mapowanie wirtualne → fizyczne"),
("Otwarte pliki", "fd[0], fd[1], fd[2] ..."),
("Priorytety", "nice value, scheduling class"),
("Statystyki", "CPU time, I/O count"),
]
top_y = 4.8
for i, (field, value) in enumerate(fields):
y = top_y - i * 0.55
draw_box(
ax,
0.5,
y,
2.2,
0.45,
field,
fill=GRAY2,
fontsize=FS,
fontweight="bold",
rounded=False,
)
draw_box(ax, 2.7, y, 4.5, 0.45, value, fill=GRAY4, fontsize=FS, rounded=False)
ax.text(
4.0,
0.3,
"Context switch = zapisz PCB starego → wczytaj PCB nowego",
fontsize=FS_SMALL,
ha="center",
style="italic",
)
save_fig(fig, "q9_pcb_structure.png")
# ============================================================
# 6. Speed comparison
# ============================================================
def gen_speed_comparison() -> None:
"""Gen speed comparison."""
fig, axes = plt.subplots(1, 2, figsize=(9, 3.5))
fig.suptitle(
"Szybkość — procesy vs wątki (benchmarki Linux)",
fontsize=FS_TITLE,
fontweight="bold",
)
# Creation time panel
ax = axes[0]
ops = ["fork()\n(nowy proces)", "pthread_create()\n(nowy wątek)"]
times = [3.0, 0.05] # ms
colors = [GRAY3, GRAY1]
bars = ax.barh(ops, times, color=colors, edgecolor=LN, height=0.5, linewidth=1.2)
ax.set_xlabel("Czas [ms]", fontsize=FS)
ax.set_title("Tworzenie", fontsize=FS_LABEL, fontweight="bold")
ax.set_xlim(0, 4.5)
for bar, t in zip(bars, times, strict=False):
ax.text(
bar.get_width() + 0.1,
bar.get_y() + bar.get_height() / 2,
f"{t} ms",
va="center",
fontsize=FS,
)
ax.text(
2.5,
-0.6,
"~100x szybciej",
fontsize=FS,
ha="center",
fontweight="bold",
transform=ax.get_xaxis_transform(),
)
ax.tick_params(labelsize=FS)
# Right: context switch
ax = axes[1]
ops2 = ["Proces→Proces\n(TLB flush)", "Wątek→Wątek\n(TLB warm)"]
times2 = [3000, 300] # ns
bars2 = ax.barh(ops2, times2, color=colors, edgecolor=LN, height=0.5, linewidth=1.2)
ax.set_xlabel("Czas [ns]", fontsize=FS)
ax.set_title("Przełączanie kontekstu", fontsize=FS_LABEL, fontweight="bold")
ax.set_xlim(0, 4500)
for bar, t in zip(bars2, times2, strict=False):
ax.text(
bar.get_width() + 50,
bar.get_y() + bar.get_height() / 2,
f"{t} ns",
va="center",
fontsize=FS,
)
ax.text(
2500,
-0.6,
"~10x szybciej",
fontsize=FS,
ha="center",
fontweight="bold",
transform=ax.get_xaxis_transform(),
)
ax.tick_params(labelsize=FS)
fig.tight_layout(rect=[0, 0.05, 1, 0.92])
save_fig(fig, "q9_speed_comparison.png")

View File

@ -0,0 +1,420 @@
"""Q9 diagrams 14-16: classic sync problems, mechanism comparison, semaphore."""
from __future__ import annotations
from _q9_common import (
FS,
FS_LABEL,
FS_SMALL,
FS_TITLE,
GRAY1,
GRAY2,
GRAY3,
GRAY4,
GRAY5,
LN,
OCCUPIED_SLOTS,
draw_arrow,
draw_box,
draw_table,
save_fig,
)
import matplotlib.pyplot as plt
import numpy as np
# ============================================================
# 14. Bounded buffer + readers-writers + philosophers
# ============================================================
def _draw_bounded_buffer_panel(ax: plt.Axes) -> None:
"""Draw the bounded-buffer (producer-consumer) panel."""
ax.set_xlim(0, 8)
ax.set_ylim(0, 7)
ax.set_aspect("auto")
ax.axis("off")
ax.set_title(
"Producent-Konsument\n(Bounded Buffer, N=4)", fontsize=FS, fontweight="bold"
)
draw_box(
ax,
0.2,
4.0,
2.0,
1.2,
"Producent\nP(empty)\nP(mutex)\nwstaw()\nV(mutex)\nV(full)",
fill=GRAY1,
fontsize=5.5,
)
items = ["A", "B", "", ""]
for i, item in enumerate(items):
x = 2.8 + i * 0.9
fill = GRAY3 if item else "white"
draw_box(
ax,
x,
4.3,
0.9,
0.7,
item,
fill=fill,
fontsize=FS,
fontweight="bold",
rounded=False,
)
ax.text(4.6, 5.2, "Bufor (N=4)", fontsize=FS_SMALL, ha="center", fontweight="bold")
draw_box(
ax,
6.0,
4.0,
2.0,
1.2,
"Konsument\nP(full)\nP(mutex)\npobierz()\nV(mutex)\nV(empty)",
fill=GRAY4,
fontsize=5.5,
)
draw_arrow(ax, 2.2, 4.6, 2.8, 4.65, lw=1.2)
draw_arrow(ax, 6.4, 4.65, 6.0, 4.6, lw=1.2)
sems = [("mutex = 1", GRAY2), ("empty = N", GRAY1), ("full = 0", GRAY3)]
for i, (s, c) in enumerate(sems):
draw_box(
ax,
2.0,
2.5 - i * 0.6,
4.0,
0.45,
s,
fill=c,
fontsize=FS_SMALL,
fontweight="bold",
)
ax.text(
4.0,
0.5,
"KOLEJNOŚĆ: P(empty/full)\nPRZED P(mutex)!\nOdwrotnie = DEADLOCK",
fontsize=5.5,
ha="center",
fontweight="bold",
color="#C62828",
bbox={"boxstyle": "round", "facecolor": "#F8D7DA", "edgecolor": "#C62828"},
)
def _draw_readers_writers_panel(ax: plt.Axes) -> None:
"""Draw the readers-writers panel."""
ax.set_xlim(0, 8)
ax.set_ylim(0, 7)
ax.set_aspect("auto")
ax.axis("off")
ax.set_title(
"Czytelnicy-Pisarze\n(Readers-Writers)", fontsize=FS, fontweight="bold"
)
draw_box(
ax,
2.5,
3.5,
3.0,
1.5,
"Dane\n(współdzielone)",
fill=GRAY2,
fontsize=FS,
fontweight="bold",
)
for i in range(3):
x = 0.3 + i * 1.0
draw_box(
ax,
x,
5.5,
0.8,
0.7,
f"R{i + 1}",
fill=GRAY4,
fontsize=FS,
fontweight="bold",
)
draw_arrow(ax, x + 0.4, 5.5, 3.0 + i * 0.5, 5.0, lw=1)
ax.text(
1.5,
6.5,
"Czytelnicy (wielu naraz)",
fontsize=FS_SMALL,
ha="center",
fontweight="bold",
)
draw_box(
ax, 5.5, 5.5, 1.5, 0.7, "Pisarz", fill=GRAY5, fontsize=FS, fontweight="bold"
)
draw_arrow(ax, 6.25, 5.5, 5.0, 5.0, lw=1.5)
ax.text(
6.25,
6.5,
"WYŁĄCZNY",
fontsize=FS_SMALL,
ha="center",
fontweight="bold",
color="#C62828",
)
rules = [
"Wielu czytelników = OK",
"Jeden pisarz = wyłączny",
"Czytelnik + Pisarz = ✗",
"Problem: pisarze głodują",
]
for i, r in enumerate(rules):
ax.text(4.0, 2.5 - i * 0.45, r, fontsize=FS_SMALL, ha="center")
ax.text(
4.0,
0.5,
"Rozwiązanie:\nrw_mutex + count_mutex\n+ zmienna readers",
fontsize=5.5,
ha="center",
bbox={"boxstyle": "round,pad=0.2", "facecolor": GRAY4, "edgecolor": GRAY3},
)
def _draw_philosophers_panel(ax: plt.Axes) -> None:
"""Draw the dining-philosophers panel."""
ax.set_xlim(0, 8)
ax.set_ylim(0, 7)
ax.set_aspect("auto")
ax.axis("off")
ax.set_title(
"Ucztujący filozofowie\n(Dining Philosophers)", fontsize=FS, fontweight="bold"
)
cx, cy, r = 4.0, 3.8, 1.8
table = plt.Circle((cx, cy), 0.8, fill=True, facecolor=GRAY2, edgecolor=LN, lw=1.5)
ax.add_patch(table)
ax.text(cx, cy, "Stół", fontsize=FS, ha="center", fontweight="bold")
for i in range(5):
angle = np.pi / 2 + i * 2 * np.pi / 5
px = cx + r * np.cos(angle)
py = cy + r * np.sin(angle)
circle = plt.Circle(
(px, py), 0.35, fill=True, facecolor=GRAY1, edgecolor=LN, lw=1.2
)
ax.add_patch(circle)
ax.text(
px, py, f"F{i}", ha="center", va="center", fontsize=FS, fontweight="bold"
)
fork_angle = np.pi / 2 + (i + 0.5) * 2 * np.pi / 5
fx = cx + (r * 0.6) * np.cos(fork_angle)
fy = cy + (r * 0.6) * np.sin(fork_angle)
ax.plot(
[fx - 0.1, fx + 0.1],
[fy - 0.15, fy + 0.15],
color=LN,
lw=2.5,
solid_capstyle="round",
)
ax.text(fx + 0.2, fy + 0.15, f"w{i}", fontsize=5, color="#555555")
rules = [
"Jedzenie = 2 widelce",
"Naiwne → DEADLOCK",
"Fix: F4 bierze odwrotnie",
"Alt: semafor(4)",
]
for i, r in enumerate(rules):
ax.text(4.0, 1.2 - i * 0.35, r, fontsize=FS_SMALL, ha="center")
def gen_classic_problems() -> None:
"""Gen classic problems."""
fig, axes = plt.subplots(1, 3, figsize=(12, 5))
fig.suptitle(
"Klasyczne problemy synchronizacji", fontsize=FS_TITLE, fontweight="bold"
)
_draw_bounded_buffer_panel(axes[0])
_draw_readers_writers_panel(axes[1])
_draw_philosophers_panel(axes[2])
fig.tight_layout(rect=[0, 0, 1, 0.88])
save_fig(fig, "q9_classic_problems.png")
# ============================================================
# 15. Sync mechanisms comparison + mutex/sem/spinlock
# ============================================================
def gen_sync_comparison() -> None:
"""Gen sync comparison."""
fig, axes = plt.subplots(2, 1, figsize=(9, 7))
# Top: comparison table
ax = axes[0]
ax.set_xlim(0, 11.5)
ax.set_ylim(-5, 1)
ax.set_aspect("auto")
ax.axis("off")
ax.set_title(
"Mechanizmy synchronizacji — porównanie",
fontsize=FS_TITLE,
fontweight="bold",
pad=10,
)
headers = ["Mechanizm", "Opis", "Kiedy używać"]
col_w = [2.5, 4.5, 4.0]
rows = [
["Mutex", "Zamek: 1 wątek w sekcji", "Sekcja krytyczna"],
["Semafor(n)", "Licznik: max n wątków", "Ograniczone zasoby (n miejsc)"],
["Monitor", "Obiekt z wbudowanym mutex", "Java synchronized"],
["Cond. Variable", "wait()/signal() na warunek", "Producent-konsument"],
["Spinlock", "Aktywne czekanie (busy-wait)", "Bardzo krótkie sekcje (<1 μs)"],
["RW Lock", "Wielu czytelników LUB 1 pisarz", "Bazy danych, cache"],
["Barrier", "Czekaj aż wszyscy dotrą", "Obliczenia równoległe"],
]
draw_table(
ax, headers, rows, x0=0.25, y0=0.5, col_widths=col_w, row_h=0.5, fontsize=7
)
# Bottom: mutex vs semafor vs spinlock
ax = axes[1]
ax.set_xlim(0, 12)
ax.set_ylim(0, 5)
ax.set_aspect("auto")
ax.axis("off")
ax.set_title(
"Mutex vs Semafor vs Spinlock", fontsize=FS_TITLE, fontweight="bold", pad=5
)
# Mutex
draw_box(ax, 0.3, 2.5, 3.5, 2.0, "", fill=GRAY4)
ax.text(2.05, 4.2, "MUTEX", fontsize=FS_LABEL, fontweight="bold", ha="center")
ax.text(2.05, 3.6, "= klucz do łazienki\n(1 osoba)", fontsize=FS, ha="center")
ax.text(
2.05,
2.8,
"Wątek ZASYPIA gdy czeka\nOS go obudzi (~μs)",
fontsize=FS_SMALL,
ha="center",
style="italic",
)
# Semafor
draw_box(ax, 4.3, 2.5, 3.5, 2.0, "", fill=GRAY1)
ax.text(6.05, 4.2, "SEMAFOR(n)", fontsize=FS_LABEL, fontweight="bold", ha="center")
ax.text(
6.05, 3.6, "= parking na n miejsc\n(n wątków naraz)", fontsize=FS, ha="center"
)
ax.text(
6.05,
2.8,
"Semafor(1) = mutex\nP() = zmniejsz, V() = zwiększ",
fontsize=FS_SMALL,
ha="center",
style="italic",
)
# Spinlock
draw_box(ax, 8.3, 2.5, 3.5, 2.0, "", fill=GRAY2)
ax.text(10.05, 4.2, "SPINLOCK", fontsize=FS_LABEL, fontweight="bold", ha="center")
ax.text(
10.05, 3.6, "= obrotowe drzwi\n(kręcisz się w kółko)", fontsize=FS, ha="center"
)
ax.text(
10.05,
2.8,
"Wątek KRĘCI się w pętli\nLepszy gdy sekcja < 1 μs",
fontsize=FS_SMALL,
ha="center",
style="italic",
)
# Dividing rule
ax.text(
6.0,
1.5,
"Reguła kciuka: sekcja > 1 μs → MUTEX | "
"sekcja < 1 μs → SPINLOCK | n jednocześnie → SEMAFOR(n)",
fontsize=FS,
ha="center",
fontweight="bold",
bbox={"boxstyle": "round,pad=0.3", "facecolor": GRAY4, "edgecolor": GRAY3},
)
fig.tight_layout()
save_fig(fig, "q9_sync_comparison.png")
# ============================================================
# 16. Semaphore concept diagram
# ============================================================
def gen_semaphore_concept() -> None:
"""Gen semaphore concept."""
fig, ax = plt.subplots(figsize=(6, 3))
ax.set_xlim(0, 10)
ax.set_ylim(0, 5)
ax.set_aspect("auto")
ax.axis("off")
ax.set_title(
"Semafor — koncepcja (parking na 3 miejsca)",
fontsize=FS_TITLE,
fontweight="bold",
pad=10,
)
# Parking slots
for i in range(3):
x = 2.0 + i * 2.0
occupied = i < OCCUPIED_SLOTS
fill = GRAY3 if occupied else "white"
label = f"Wątek {i + 1}" if occupied else "(wolne)"
draw_box(
ax,
x,
2.5,
1.5,
1.2,
label,
fill=fill,
fontsize=FS,
fontweight="bold" if occupied else "normal",
rounded=False,
)
ax.text(
5.0,
4.2,
"semafor(3): counter = 1 (jedno wolne miejsce)",
fontsize=FS,
ha="center",
fontweight="bold",
)
# Waiting thread
draw_box(
ax,
0.2,
0.5,
1.5,
0.8,
"Wątek 4\nP() → czeka",
fill="#F8D7DA",
fontsize=FS_SMALL,
)
draw_arrow(ax, 1.7, 0.9, 2.0, 2.5, lw=1.2, color="#C62828")
ax.text(
5.0,
0.6,
"P() = counter-- (jeśli 0 → czekaj)\nV() = counter++ (obudź czekającego)",
fontsize=FS,
ha="center",
family="monospace",
bbox={"boxstyle": "round,pad=0.2", "facecolor": GRAY4, "edgecolor": GRAY3},
)
save_fig(fig, "q9_semaphore_concept.png")

View File

@ -0,0 +1,200 @@
"""Common utilities and constants for Q9 diagram generation.
Monochrome, A4-printable PNGs (300 DPI).
"""
from __future__ import annotations
import logging
from pathlib import Path
from typing import TYPE_CHECKING
import matplotlib as mpl
mpl.use("Agg")
import matplotlib.patches as mpatches
from matplotlib.patches import FancyBboxPatch
import matplotlib.pyplot as plt
if TYPE_CHECKING:
from matplotlib.axes import Axes
from matplotlib.figure import Figure
_logger = logging.getLogger(__name__)
DPI = 300
BG = "white"
LN = "black"
FS = 8
FS_TITLE = 11
FS_SMALL = 6.5
FS_LABEL = 9
OUTPUT_DIR = str(Path(__file__).resolve().parent / "img")
Path(OUTPUT_DIR).mkdir(parents=True, exist_ok=True)
GRAY1 = "#E8E8E8"
GRAY2 = "#D0D0D0"
GRAY3 = "#B8B8B8"
GRAY4 = "#F5F5F5"
GRAY5 = "#C0C0C0"
OCCUPIED_SLOTS = 2
def draw_box(
ax: Axes,
x: float,
y: float,
w: float,
h: float,
text: str,
fill: str = "white",
lw: float = 1.2,
fontsize: float = FS,
fontweight: str = "normal",
ha: str = "center",
va: str = "center",
*,
rounded: bool = True,
edgecolor: str = LN,
linestyle: str = "-",
) -> None:
"""Draw box."""
if rounded:
rect = FancyBboxPatch(
(x, y),
w,
h,
boxstyle="round,pad=0.05",
lw=lw,
edgecolor=edgecolor,
facecolor=fill,
linestyle=linestyle,
)
else:
rect = mpatches.Rectangle(
(x, y),
w,
h,
lw=lw,
edgecolor=edgecolor,
facecolor=fill,
linestyle=linestyle,
)
ax.add_patch(rect)
ax.text(
x + w / 2,
y + h / 2,
text,
ha=ha,
va=va,
fontsize=fontsize,
fontweight=fontweight,
wrap=True,
)
def draw_arrow(
ax: Axes,
x1: float,
y1: float,
x2: float,
y2: float,
lw: float = 1.2,
style: str = "->",
color: str = LN,
) -> None:
"""Draw arrow."""
ax.annotate(
"",
xy=(x2, y2),
xytext=(x1, y1),
arrowprops={"arrowstyle": style, "color": color, "lw": lw},
)
def draw_double_arrow(
ax: Axes,
x1: float,
y1: float,
x2: float,
y2: float,
lw: float = 1.2,
color: str = LN,
) -> None:
"""Draw double arrow."""
ax.annotate(
"",
xy=(x2, y2),
xytext=(x1, y1),
arrowprops={"arrowstyle": "<->", "color": color, "lw": lw},
)
def save_fig(fig: Figure, name: str) -> None:
"""Save fig."""
path = str(Path(OUTPUT_DIR) / name)
fig.savefig(path, dpi=DPI, bbox_inches="tight", facecolor=BG, pad_inches=0.15)
plt.close(fig)
_logger.info(" Saved: %s", path)
def draw_table(
ax: Axes,
headers: list[str],
rows: list[list[str]],
x0: float,
y0: float,
col_widths: list[float],
row_h: float = 0.4,
header_fill: str = GRAY2,
row_fills: list[str] | None = None,
fontsize: float = FS,
header_fontsize: float | None = None,
) -> None:
"""Draw a clean table on axes."""
if header_fontsize is None:
header_fontsize = fontsize
len(headers)
len(rows)
sum(col_widths)
# Header
cx = x0
for j, hdr in enumerate(headers):
draw_box(
ax,
cx,
y0,
col_widths[j],
row_h,
hdr,
fill=header_fill,
fontsize=header_fontsize,
fontweight="bold",
rounded=False,
)
cx += col_widths[j]
# Rows
for i, row in enumerate(rows):
cy = y0 - (i + 1) * row_h
cx = x0
fill = GRAY4 if (i % 2 == 0) else "white"
if row_fills and i < len(row_fills):
fill = row_fills[i]
for j, cell in enumerate(row):
fw = "bold" if j == 0 else "normal"
draw_box(
ax,
cx,
cy,
col_widths[j],
row_h,
cell,
fill=fill,
fontsize=fontsize,
fontweight=fw,
rounded=False,
)
cx += col_widths[j]

View File

@ -0,0 +1,212 @@
"""Q9 diagrams 7-9: IPC mechanisms and scenario tables."""
from __future__ import annotations
from _q9_common import (
FS,
FS_LABEL,
FS_SMALL,
FS_TITLE,
GRAY1,
GRAY2,
GRAY3,
GRAY4,
draw_arrow,
draw_box,
draw_double_arrow,
draw_table,
save_fig,
)
import matplotlib.pyplot as plt
# ============================================================
# 7. Scenario table (when to use process vs thread)
# ============================================================
def gen_scenario_table() -> None:
"""Gen scenario table."""
fig, ax = plt.subplots(figsize=(8.5, 4.5))
ax.set_xlim(0, 11)
ax.set_ylim(-5.5, 1)
ax.set_aspect("auto")
ax.axis("off")
ax.set_title(
"Kiedy proces, kiedy wątek? — typowe scenariusze",
fontsize=FS_TITLE,
fontweight="bold",
pad=10,
)
headers = ["Scenariusz", "Wybór", "Dlaczego?"]
col_w = [3.5, 2.5, 4.5]
rows = [
["Serwer WWW (Apache)", "Proces", "izolacja klientów"],
["Serwer WWW (nginx)", "Wątek / async", "szybkość, cooperacja"],
["Przeglądarka (karty)", "Proces", "crash isolation"],
["Przeglądarka (JS+render)", "Wątek", "współdzielony DOM"],
["Gra (fizyka+rendering)", "Wątek", "współdzielony świat gry"],
["Kompilacja (make -j8)", "Proces", "izolacja, prostota"],
["Baza danych (zapytania)", "Wątek", "współdzielony cache"],
["Microservices", "Proces (kontener)", "izolacja, deployment"],
]
draw_table(
ax, headers, rows, x0=0.25, y0=0.5, col_widths=col_w, row_h=0.5, fontsize=7
)
save_fig(fig, "q9_scenario_table.png")
# ============================================================
# 8. IPC details: pipe, shared memory, socket (3-panel)
# ============================================================
def gen_ipc_details() -> None:
"""Gen ipc details."""
fig, axes = plt.subplots(1, 3, figsize=(11, 3.5))
fig.suptitle("Mechanizmy IPC — szczegóły", fontsize=FS_TITLE, fontweight="bold")
# Panel 1: Pipe
ax = axes[0]
ax.set_xlim(0, 8)
ax.set_ylim(0, 5)
ax.set_aspect("auto")
ax.axis("off")
ax.set_title("Pipe (potok)", fontsize=FS_LABEL, fontweight="bold")
draw_box(ax, 0.2, 2.0, 1.8, 1.2, "Proces A\n(ls)\nstdout", fill=GRAY1, fontsize=FS)
draw_box(
ax,
3.0,
2.0,
1.8,
1.2,
"Bufor\njądra\n(4 KB)",
fill=GRAY2,
fontsize=FS,
fontweight="bold",
)
draw_box(ax, 5.8, 2.0, 1.8, 1.2, "Proces B\n(grep)\nstdin", fill=GRAY1, fontsize=FS)
draw_arrow(ax, 2.0, 2.6, 3.0, 2.6, lw=1.5)
ax.text(2.5, 3.0, "write()\nfd[1]", fontsize=FS_SMALL, ha="center")
draw_arrow(ax, 4.8, 2.6, 5.8, 2.6, lw=1.5)
ax.text(5.3, 3.0, "read()\nfd[0]", fontsize=FS_SMALL, ha="center")
ax.text(
4.0,
0.8,
"Jednokierunkowy\nBufor pełny → write() blokuje",
fontsize=FS_SMALL,
ha="center",
style="italic",
bbox={"boxstyle": "round,pad=0.2", "facecolor": GRAY4, "edgecolor": GRAY3},
)
# Panel 2: Shared Memory
ax = axes[1]
ax.set_xlim(0, 8)
ax.set_ylim(0, 5)
ax.set_aspect("auto")
ax.axis("off")
ax.set_title("Shared Memory", fontsize=FS_LABEL, fontweight="bold")
draw_box(ax, 0.3, 3.0, 2.2, 1.2, "Proces A\nstrona 7", fill=GRAY1, fontsize=FS)
draw_box(ax, 5.5, 3.0, 2.2, 1.2, "Proces B\nstrona 3", fill=GRAY1, fontsize=FS)
draw_box(
ax,
2.8,
1.0,
2.4,
1.2,
"RAM\nramka 42",
fill=GRAY3,
fontsize=FS,
fontweight="bold",
)
draw_arrow(ax, 2.0, 3.0, 3.5, 2.2, lw=1.5)
draw_arrow(ax, 6.0, 3.0, 4.5, 2.2, lw=1.5)
ax.text(
4.0,
0.3,
"Zero kopiowania!\nA pisze → B widzi od razu\nWymaga synchronizacji (semafor)",
fontsize=FS_SMALL,
ha="center",
style="italic",
bbox={"boxstyle": "round,pad=0.2", "facecolor": GRAY4, "edgecolor": GRAY3},
)
# Panel 3: Socket
ax = axes[2]
ax.set_xlim(0, 8)
ax.set_ylim(0, 5)
ax.set_aspect("auto")
ax.axis("off")
ax.set_title("Socket", fontsize=FS_LABEL, fontweight="bold")
# Network socket
draw_box(
ax, 0.3, 3.2, 1.8, 0.9, "Klient", fill=GRAY1, fontsize=FS, fontweight="bold"
)
draw_box(
ax, 5.5, 3.2, 1.8, 0.9, "Serwer", fill=GRAY1, fontsize=FS, fontweight="bold"
)
draw_double_arrow(ax, 2.1, 3.65, 5.5, 3.65, lw=1.5)
ax.text(3.8, 4.3, "TCP/IP (sieciowy)", fontsize=FS, ha="center", fontweight="bold")
# Unix socket
draw_box(
ax, 0.3, 1.3, 1.8, 0.9, "Proces A", fill=GRAY4, fontsize=FS, fontweight="bold"
)
draw_box(
ax, 5.5, 1.3, 1.8, 0.9, "Proces B", fill=GRAY4, fontsize=FS, fontweight="bold"
)
draw_double_arrow(ax, 2.1, 1.75, 5.5, 1.75, lw=1.5)
ax.text(
3.8,
2.4,
"Unix domain socket\n(/tmp/app.sock)",
fontsize=FS,
ha="center",
fontweight="bold",
)
ax.text(
3.8,
0.5,
"Dwukierunkowy\nNajbardziej uniwersalny IPC",
fontsize=FS_SMALL,
ha="center",
style="italic",
bbox={"boxstyle": "round,pad=0.2", "facecolor": GRAY4, "edgecolor": GRAY3},
)
fig.tight_layout(rect=[0, 0, 1, 0.9])
save_fig(fig, "q9_ipc_details.png")
# ============================================================
# 9. IPC comparison table
# ============================================================
def gen_ipc_table() -> None:
"""Gen ipc table."""
fig, ax = plt.subplots(figsize=(8.5, 3.5))
ax.set_xlim(0, 11)
ax.set_ylim(-4.5, 1)
ax.set_aspect("auto")
ax.axis("off")
ax.set_title(
"Porównanie mechanizmów IPC", fontsize=FS_TITLE, fontweight="bold", pad=10
)
headers = ["Mechanizm", "Kierunek", "Szybkość", "Zastosowanie"]
col_w = [2.5, 2.0, 2.5, 3.5]
rows = [
["Pipe", "jednokierunkowy", "średnia", "ls | grep"],
["Named Pipe", "jednokierunkowy", "średnia", "demon → klient"],
["Shared Memory", "dwukierunkowy", "NAJSZYBSZA", "video, bazy danych"],
["Message Queue", "dwukierunkowy", "średnia", "wieloproducentowe"],
["Socket", "dwukierunkowy", "wolna (sieć)", "klient-serwer"],
["Signal", "jednokierunkowy", "natychmiastowa", "powiadomienia (nr)"],
]
draw_table(
ax, headers, rows, x0=0.25, y0=0.5, col_widths=col_w, row_h=0.5, fontsize=7.5
)
save_fig(fig, "q9_ipc_table.png")

View File

@ -0,0 +1,404 @@
"""Q9 diagrams 10-13: race conditions, deadlock, Coffman, starvation."""
from __future__ import annotations
from _q9_common import (
FS,
FS_LABEL,
FS_SMALL,
FS_TITLE,
GRAY1,
GRAY2,
GRAY3,
GRAY4,
GRAY5,
LN,
draw_arrow,
draw_box,
draw_table,
save_fig,
)
import matplotlib.pyplot as plt
# ============================================================
# 10. Race condition (simple x + bank timeline)
# ============================================================
def gen_race_condition() -> None:
"""Gen race condition."""
fig, axes = plt.subplots(1, 2, figsize=(11, 5))
fig.suptitle(
"Wyścig (Race Condition) — przykłady", fontsize=FS_TITLE, fontweight="bold"
)
# Panel 1: simple x increment
ax = axes[0]
ax.set_xlim(0, 8)
ax.set_ylim(0, 7)
ax.set_aspect("auto")
ax.axis("off")
ax.set_title("Prosty wyścig: x = x + 1", fontsize=FS_LABEL, fontweight="bold")
# Timeline
steps_a = ["czytaj x (=0)", "dodaj 1", "zapisz x (=1)"]
steps_b = ["czytaj x (=0)", "dodaj 1", "zapisz x (=1)"]
ax.text(2.0, 6.3, "Wątek A", fontsize=FS_LABEL, ha="center", fontweight="bold")
ax.text(6.0, 6.3, "Wątek B", fontsize=FS_LABEL, ha="center", fontweight="bold")
ax.plot([2, 2], [0.8, 6.0], color=LN, lw=1)
ax.plot([6, 6], [0.8, 6.0], color=LN, lw=1)
for i, (sa, sb) in enumerate(zip(steps_a, steps_b, strict=False)):
y = 5.3 - i * 1.2
draw_box(ax, 0.5, y, 3.0, 0.6, sa, fill=GRAY4, fontsize=FS)
draw_box(ax, 4.5, y - 0.3, 3.0, 0.6, sb, fill=GRAY1, fontsize=FS)
ax.text(
4.0,
0.4,
"Wynik: x = 1 (powinno 2!)",
fontsize=FS,
ha="center",
fontweight="bold",
color="#C62828",
bbox={"boxstyle": "round", "facecolor": "#F8D7DA", "edgecolor": "#C62828"},
)
# Panel 2: bank account
ax = axes[1]
ax.set_xlim(0, 10)
ax.set_ylim(0, 7)
ax.set_aspect("auto")
ax.axis("off")
ax.set_title("Konto bankowe: saldo = 1000 zł", fontsize=FS_LABEL, fontweight="bold")
ax.text(2.5, 6.3, "Wątek A (+500)", fontsize=FS, ha="center", fontweight="bold")
ax.text(7.5, 6.3, "Wątek B (-200)", fontsize=FS, ha="center", fontweight="bold")
ax.plot([2.5, 2.5], [0.8, 6.0], color=LN, lw=1)
ax.plot([7.5, 7.5], [0.8, 6.0], color=LN, lw=1)
events = [
("t1", "czytaj → 1000", "", 5.3),
("t2", "", "czytaj → 1000", 4.6),
("t3", "1000+500=1500", "", 3.9),
("t4", "", "1000-200=800", 3.2),
("t5", "zapisz 1500", "", 2.5),
("t6", "", "zapisz 800 ✗", 1.8),
]
for t, a, b, y in events:
ax.text(0.3, y + 0.15, t, fontsize=FS_SMALL, fontweight="bold", va="center")
if a:
draw_box(ax, 1.0, y, 3.0, 0.45, a, fill=GRAY4, fontsize=FS_SMALL)
if b:
fill = "#F8D7DA" if "" in b else GRAY1
draw_box(ax, 6.0, y, 3.0, 0.45, b, fill=fill, fontsize=FS_SMALL)
ax.text(
5.0,
0.4,
"Wynik: 800 zł (powinno 1300!)",
fontsize=FS,
ha="center",
fontweight="bold",
color="#C62828",
bbox={"boxstyle": "round", "facecolor": "#F8D7DA", "edgecolor": "#C62828"},
)
fig.tight_layout(rect=[0, 0, 1, 0.9])
save_fig(fig, "q9_race_condition.png")
# ============================================================
# 11. Deadlock scenario + cycle
# ============================================================
def gen_deadlock_scenario() -> None:
"""Gen deadlock scenario."""
fig, axes = plt.subplots(1, 2, figsize=(11, 4.5))
fig.suptitle("Zakleszczenie (Deadlock)", fontsize=FS_TITLE, fontweight="bold")
# Panel 1: timeline
ax = axes[0]
ax.set_xlim(0, 8)
ax.set_ylim(0, 6)
ax.set_aspect("auto")
ax.axis("off")
ax.set_title("Scenariusz z 2 mutexami", fontsize=FS_LABEL, fontweight="bold")
ax.text(2.5, 5.3, "Wątek A", fontsize=FS_LABEL, ha="center", fontweight="bold")
ax.text(6.0, 5.3, "Wątek B", fontsize=FS_LABEL, ha="center", fontweight="bold")
steps = [
("lock(mutex1) OK", "", "trzyma", False, 4.5),
("", "lock(mutex2) OK", "trzyma", False, 3.7),
("lock(mutex2) ...WAIT", "", "CZEKA!", True, 2.9),
("", "lock(mutex1) ...WAIT", "CZEKA!", True, 2.1),
]
for a_text, b_text, _note, is_wait, y in steps:
if a_text:
fill = "#F8D7DA" if is_wait else GRAY4
draw_box(ax, 0.5, y, 3.3, 0.55, a_text, fill=fill, fontsize=FS_SMALL)
if b_text:
fill = "#F8D7DA" if is_wait else GRAY4
draw_box(ax, 4.3, y, 3.3, 0.55, b_text, fill=fill, fontsize=FS_SMALL)
ax.text(
4.0,
1.2,
"DEADLOCK!\nŻaden nie odpuści",
fontsize=FS,
ha="center",
fontweight="bold",
color="#C62828",
bbox={"boxstyle": "round", "facecolor": "#F8D7DA", "edgecolor": "#C62828"},
)
# Panel 2: cycle diagram
ax = axes[1]
ax.set_xlim(0, 8)
ax.set_ylim(0, 6)
ax.set_aspect("auto")
ax.axis("off")
ax.set_title("Cykl oczekiwania", fontsize=FS_LABEL, fontweight="bold")
# Thread boxes
draw_box(
ax,
0.5,
3.5,
2.2,
1.2,
"Wątek A\ntrzyma Mutex 1",
fill=GRAY1,
fontsize=FS,
fontweight="bold",
)
draw_box(
ax,
5.3,
3.5,
2.2,
1.2,
"Wątek B\ntrzyma Mutex 2",
fill=GRAY1,
fontsize=FS,
fontweight="bold",
)
# Mutex boxes
draw_box(
ax, 0.5, 1.0, 2.2, 1.0, "Mutex 1", fill=GRAY3, fontsize=FS, fontweight="bold"
)
draw_box(
ax, 5.3, 1.0, 2.2, 1.0, "Mutex 2", fill=GRAY3, fontsize=FS, fontweight="bold"
)
# holds arrows (down)
draw_arrow(ax, 1.6, 3.5, 1.6, 2.0, lw=2)
ax.text(0.9, 2.7, "trzyma", fontsize=FS_SMALL, rotation=90, va="center")
draw_arrow(ax, 6.4, 3.5, 6.4, 2.0, lw=2)
ax.text(7.0, 2.7, "trzyma", fontsize=FS_SMALL, rotation=90, va="center")
# waits-for arrows (across, red)
draw_arrow(ax, 2.7, 4.3, 5.3, 4.3, lw=2.5, color="#C62828")
ax.text(
4.0,
4.7,
"czeka na Mutex 2",
fontsize=FS_SMALL,
ha="center",
fontweight="bold",
color="#C62828",
)
draw_arrow(ax, 5.3, 3.7, 2.7, 3.7, lw=2.5, color="#C62828")
ax.text(
4.0,
3.1,
"czeka na Mutex 1",
fontsize=FS_SMALL,
ha="center",
fontweight="bold",
color="#C62828",
)
fig.tight_layout(rect=[0, 0, 1, 0.9])
save_fig(fig, "q9_deadlock_scenario.png")
# ============================================================
# 12. Coffman conditions + prevention strategies
# ============================================================
def gen_coffman_strategies() -> None:
"""Gen coffman strategies."""
fig, ax = plt.subplots(figsize=(9, 4))
ax.set_xlim(0, 11.5)
ax.set_ylim(-3.5, 1)
ax.set_aspect("auto")
ax.axis("off")
ax.set_title(
"Warunki Coffmana — zapobieganie deadlockowi",
fontsize=FS_TITLE,
fontweight="bold",
pad=10,
)
headers = ["Warunek", "Opis", "Jak złamać", "Przykład"]
col_w = [2.5, 2.5, 3.0, 3.0]
rows = [
[
"1. Mutual Exclusion",
"zasób wyłączny",
"współdzielony zasób",
"Read-write lock",
],
[
"2. Hold and Wait",
"trzymaj + czekaj",
"bierz WSZYSTKIE naraz",
"lock(m1,m2) atomowo",
],
[
"3. No Preemption",
"nie zabierzesz siłą",
"timeout / trylock",
"pthread_mutex_trylock()",
],
[
"4. Circular Wait",
"cykliczne oczekiw.",
"porządek liniowy",
"zawsze m1 przed m2",
],
]
draw_table(
ax, headers, rows, x0=0.25, y0=0.5, col_widths=col_w, row_h=0.6, fontsize=7
)
ax.text(
5.75,
-3.1,
"▸ Najczęstsza strategia: PORZĄDEK LINIOWY — "
"numeruj mutexy, zawsze blokuj rosnąco",
fontsize=FS,
ha="center",
fontweight="bold",
bbox={"boxstyle": "round,pad=0.2", "facecolor": GRAY4, "edgecolor": GRAY3},
)
save_fig(fig, "q9_coffman_strategies.png")
# ============================================================
# 13. Starvation + Priority Inversion (2-panel)
# ============================================================
def gen_starvation_priority() -> None:
"""Gen starvation priority."""
fig, axes = plt.subplots(1, 2, figsize=(11, 4.5))
fig.suptitle(
"Zagłodzenie i Inwersja priorytetów", fontsize=FS_TITLE, fontweight="bold"
)
# Panel 1: Starvation + aging
ax = axes[0]
ax.set_xlim(0, 8)
ax.set_ylim(0, 6)
ax.set_aspect("auto")
ax.axis("off")
ax.set_title("Zagłodzenie (Starvation)", fontsize=FS_LABEL, fontweight="bold")
threads = [
("Wątek HIGH", "prio=10", GRAY5, 3.0),
("Wątek HIGH", "prio=9", GRAY3, 2.2),
("Wątek MED", "prio=5", GRAY2, 1.4),
("Wątek LOW", "prio=1 → głoduje!", "#F8D7DA", 0.6),
]
for name, prio, color, y in threads:
draw_box(
ax, 0.5, y, 2.0, 0.6, name, fill=color, fontsize=FS_SMALL, fontweight="bold"
)
ax.text(2.8, y + 0.3, prio, fontsize=FS_SMALL, va="center")
ax.text(
1.5,
4.2,
"CPU zawsze\ndostaje HIGH!",
fontsize=FS,
ha="center",
fontweight="bold",
)
draw_arrow(ax, 1.5, 3.9, 1.5, 3.65, lw=1.5)
# Aging solution
draw_box(ax, 4.5, 1.5, 3.2, 2.5, "", fill=GRAY4, rounded=True)
ax.text(6.1, 3.7, "Rozwiązanie: AGING", fontsize=FS, fontweight="bold", ha="center")
aging = [
"t=0: prio=1",
"t=100ms: prio=2",
"t=200ms: prio=3",
"...",
"w końcu → CPU!",
]
for i, line in enumerate(aging):
ax.text(
6.1, 3.2 - i * 0.4, line, fontsize=FS_SMALL, ha="center", family="monospace"
)
# Panel 2: Priority Inversion
ax = axes[1]
ax.set_xlim(0, 10)
ax.set_ylim(0, 6)
ax.set_aspect("auto")
ax.axis("off")
ax.set_title("Inwersja priorytetów", fontsize=FS_LABEL, fontweight="bold")
# Timeline
labels = ["H (wysoki)", "M (średni)", "L (niski)"]
ys = [4.2, 2.8, 1.4]
for label, y in zip(labels, ys, strict=False):
ax.text(0.3, y + 0.2, label, fontsize=FS, fontweight="bold", va="center")
# L runs and locks mutex
draw_box(ax, 2.0, ys[2], 1.2, 0.5, "lock(m)", fill=GRAY1, fontsize=FS_SMALL)
# M preempts L
draw_box(ax, 3.5, ys[1], 3.0, 0.5, "M pracuje...", fill=GRAY3, fontsize=FS_SMALL)
# H waits for mutex
draw_box(
ax,
3.5,
ys[0],
3.0,
0.5,
"CZEKA na mutex!",
fill="#F8D7DA",
fontsize=FS_SMALL,
fontweight="bold",
)
# M finishes, L continues, unlocks
draw_box(ax, 6.8, ys[2], 1.5, 0.5, "unlock(m)", fill=GRAY1, fontsize=FS_SMALL)
draw_box(ax, 8.5, ys[0], 1.2, 0.5, "H runs", fill=GRAY4, fontsize=FS_SMALL)
# Explanation
ax.text(
5.0,
0.5,
"H czeka na M (mimo H > M)!\n"
"Rozwiązanie: Priority Inheritance\n"
"L dziedziczy priorytet H → M nie wypycha L",
fontsize=FS_SMALL,
ha="center",
style="italic",
bbox={"boxstyle": "round,pad=0.3", "facecolor": GRAY4, "edgecolor": GRAY3},
)
ax.text(
5.0,
0.0,
"Mars Pathfinder (1997) — klasyczny bug!",
fontsize=FS_SMALL,
ha="center",
fontweight="bold",
)
fig.tight_layout(rect=[0, 0, 1, 0.9])
save_fig(fig, "q9_starvation_priority.png")

View File

@ -0,0 +1,87 @@
"""Common constants and utilities for scheduling diagrams."""
from __future__ import annotations
import logging
from pathlib import Path
from typing import TYPE_CHECKING
import matplotlib.patches as mpatches
from matplotlib.patches import FancyBboxPatch
if TYPE_CHECKING:
from matplotlib.axes import Axes
_logger = logging.getLogger(__name__)
DPI = 300
BG = "white"
LN = "black"
FS = 8
FS_TITLE = 11
OUTPUT_DIR = str(Path(__file__).resolve().parent / "img")
Path(OUTPUT_DIR).mkdir(parents=True, exist_ok=True)
GRAY1 = "#E8E8E8"
GRAY2 = "#D0D0D0"
GRAY3 = "#B8B8B8"
GRAY4 = "#F5F5F5"
GRAY5 = "#C0C0C0"
MIN_COLUMN_INDEX = 3
FONTWEIGHT_THRESHOLD = 3
def draw_box(
ax: Axes,
x: float,
y: float,
w: float,
h: float,
text: str,
fill: str = "white",
lw: float = 1.2,
fontsize: float = FS,
fontweight: str = "normal",
ha: str = "center",
va: str = "center",
*,
rounded: bool = True,
) -> None:
"""Draw box."""
if rounded:
rect = FancyBboxPatch(
(x, y), w, h, boxstyle="round,pad=0.05", lw=lw, edgecolor=LN, facecolor=fill
)
else:
rect = mpatches.Rectangle((x, y), w, h, lw=lw, edgecolor=LN, facecolor=fill)
ax.add_patch(rect)
ax.text(
x + w / 2,
y + h / 2,
text,
ha=ha,
va=va,
fontsize=fontsize,
fontweight=fontweight,
wrap=True,
)
def draw_arrow(
ax: Axes,
x1: float,
y1: float,
x2: float,
y2: float,
lw: float = 1.2,
style: str = "->",
color: str = LN,
) -> None:
"""Draw arrow."""
ax.annotate(
"",
xy=(x2, y2),
xytext=(x1, y1),
arrowprops={"arrowstyle": style, "color": color, "lw": lw},
)

View File

@ -0,0 +1,309 @@
"""Scheduling complexity landscape and EDD example diagrams."""
from __future__ import annotations
import logging
from pathlib import Path
import matplotlib.patches as mpatches
from matplotlib.patches import FancyBboxPatch
import matplotlib.pyplot as plt
from python_pkg.praca_magisterska_video.generate_images._sched_common import (
BG,
DPI,
FS_TITLE,
GRAY1,
GRAY2,
GRAY3,
GRAY4,
GRAY5,
LN,
OUTPUT_DIR,
draw_arrow,
)
_logger = logging.getLogger(__name__)
# ============================================================
# SCHEDULING COMPLEXITY LANDSCAPE
# ============================================================
def draw_complexity_map() -> None:
"""Draw complexity map."""
_fig, ax = plt.subplots(1, 1, figsize=(8.27, 5))
ax.set_xlim(0, 10)
ax.set_ylim(0, 7)
ax.set_aspect("equal")
ax.axis("off")
ax.set_title(
"Złożoność problemów szeregowania — od łatwych do NP-trudnych",
fontsize=FS_TITLE,
fontweight="bold",
pad=10,
)
# Gradient arrow at the top
ax.annotate(
"",
xy=(9.5, 6.2),
xytext=(0.5, 6.2),
arrowprops={"arrowstyle": "->", "color": LN, "lw": 2},
)
ax.text(5, 6.5, "Rosnąca złożoność", ha="center", fontsize=9, fontweight="bold")
# Easy (polynomial) region
easy_rect = FancyBboxPatch(
(0.3, 2.8),
4.0,
3.0,
boxstyle="round,pad=0.15",
lw=1.5,
edgecolor="#666666",
facecolor=GRAY4,
linestyle="-",
)
ax.add_patch(easy_rect)
ax.text(
2.3,
5.5,
"WIELOMIANOWE O(n log n)",
ha="center",
fontsize=9,
fontweight="bold",
color="#444444",
)
easy_problems = [
("1 || ΣCⱼ", "SPT", GRAY1, 4.8),
("1 || Lmax", "EDD", GRAY2, 4.0),
("F2 || Cmax", "Johnson", GRAY1, 3.2),
]
for prob, method, fill, y in easy_problems:
rect = FancyBboxPatch(
(0.6, y),
3.5,
0.6,
boxstyle="round,pad=0.05",
lw=1,
edgecolor=LN,
facecolor=fill,
)
ax.add_patch(rect)
ax.text(
1.2,
y + 0.3,
prob,
ha="center",
va="center",
fontsize=8,
fontweight="bold",
fontfamily="monospace",
)
ax.text(3.0, y + 0.3, f"{method}", ha="center", va="center", fontsize=8)
# Hard (NP) region
hard_rect = FancyBboxPatch(
(5.3, 2.8),
4.3,
3.0,
boxstyle="round,pad=0.15",
lw=1.5,
edgecolor="#444444",
facecolor=GRAY3,
linestyle="-",
)
ax.add_patch(hard_rect)
ax.text(
7.45,
5.5,
"NP-TRUDNE",
ha="center",
fontsize=9,
fontweight="bold",
color="#333333",
)
hard_problems = [
("Pm || Cmax\n(m≥2)", "LPT heuryst.", GRAY2, 4.5),
("1 || ΣTⱼ", "branch&bound", GRAY4, 3.7),
("Jm || Cmax\n(m≥3)", "metaheuryst.", GRAY5, 2.9),
]
for prob, method, fill, y in hard_problems:
rect = FancyBboxPatch(
(5.6, y),
3.7,
0.7,
boxstyle="round,pad=0.05",
lw=1,
edgecolor=LN,
facecolor=fill,
)
ax.add_patch(rect)
ax.text(
6.5,
y + 0.35,
prob,
ha="center",
va="center",
fontsize=7,
fontweight="bold",
fontfamily="monospace",
)
ax.text(8.2, y + 0.35, f"{method}", ha="center", va="center", fontsize=7)
# Arrow connecting
draw_arrow(ax, 4.4, 4.0, 5.2, 4.0, lw=2, color="#888888")
ax.text(4.8, 4.25, "+1\nmaszyna", ha="center", fontsize=6, color="#888888")
# Bottom: key insight
ax.text(
5.0,
1.8,
"„Dodanie jednej maszyny lub jednego ograniczenia\n"
'może zmienić problem z łatwego na NP-trudny!"',
ha="center",
fontsize=8,
fontweight="bold",
style="italic",
bbox={
"boxstyle": "round,pad=0.3",
"facecolor": GRAY4,
"edgecolor": GRAY3,
"lw": 1,
},
)
# Bottom examples
ax.text(
5.0,
0.8,
"1 maszyna → łatwe (sortuj) | ≥2 maszyny równoległe → NP-trudne\n"
"Flow shop 2 maszyny → Johnson O(n log n) | Flow shop 3 maszyny → NP-trudne",
ha="center",
fontsize=7,
color="#555555",
)
plt.tight_layout()
plt.savefig(
str(Path(OUTPUT_DIR) / "scheduling_complexity_map.png"),
dpi=DPI,
bbox_inches="tight",
facecolor=BG,
)
plt.close()
_logger.info(" ✓ scheduling_complexity_map.png")
# ============================================================
# EDD EXAMPLE (1 || Lmax)
# ============================================================
def draw_edd_example() -> None:
"""Draw edd example."""
_fig, ax = plt.subplots(1, 1, figsize=(8.27, 4))
ax.set_xlim(-2, 28)
ax.set_ylim(-2, 4)
ax.axis("off")
ax.set_title(
"EDD (Earliest Due Date) — 1 || Lmax — Przykład",
fontsize=FS_TITLE,
fontweight="bold",
pad=8,
)
# Tasks: name, processing time, due date
tasks = [("J1", 4, 10), ("J2", 2, 6), ("J3", 6, 15), ("J4", 3, 8), ("J5", 5, 18)]
# EDD: sort by due date
edd_order = sorted(tasks, key=lambda x: x[2])
bar_y = 1.5
bar_h = 0.8
t = 0
fills_edd = [GRAY1, GRAY2, GRAY4, GRAY3, GRAY5]
lateness_vals = []
for i, (name, p, d) in enumerate(edd_order):
rect = mpatches.Rectangle(
(t, bar_y), p, bar_h, lw=1.2, edgecolor=LN, facecolor=fills_edd[i]
)
ax.add_patch(rect)
ax.text(
t + p / 2,
bar_y + bar_h / 2,
f"{name}\np={p}, d={d}",
ha="center",
va="center",
fontsize=6.5,
fontweight="bold",
)
t += p
lateness = t - d
lateness_vals.append(lateness)
# Due date marker
ax.plot(
[d, d], [bar_y - 0.4, bar_y - 0.1], color="#888888", lw=0.8, linestyle="--"
)
ax.text(
d,
bar_y - 0.5,
f"d={d}",
ha="center",
va="top",
fontsize=5.5,
color="#888888",
)
# Completion + lateness
ax.plot([t, t], [bar_y + bar_h, bar_y + bar_h + 0.15], color=LN, lw=0.8)
ax.text(
t,
bar_y + bar_h + 0.2,
f"C={t}\nL={lateness}",
ha="center",
va="bottom",
fontsize=5.5,
)
# Time axis
ax.plot([0, 22], [bar_y - 0.05, bar_y - 0.05], color=LN, lw=0.5)
lmax = max(lateness_vals)
ax.text(
22,
bar_y + bar_h / 2,
f"Lmax = {lmax}",
ha="left",
va="center",
fontsize=10,
fontweight="bold",
bbox={"boxstyle": "round,pad=0.2", "facecolor": GRAY1, "edgecolor": LN},
)
# Bottom mnemonic
ax.text(
10,
-1.3,
'„Early Due Date Does it first" — najpilniejszy deadline idzie pierwszy',
ha="center",
fontsize=8,
fontweight="bold",
style="italic",
bbox={
"boxstyle": "round,pad=0.3",
"facecolor": GRAY4,
"edgecolor": GRAY3,
"lw": 0.8,
},
)
plt.tight_layout()
plt.savefig(
str(Path(OUTPUT_DIR) / "scheduling_edd_example.png"),
dpi=DPI,
bbox_inches="tight",
facecolor=BG,
)
plt.close()
_logger.info(" ✓ scheduling_edd_example.png")

View File

@ -0,0 +1,484 @@
"""Graham notation α|β|γ visual mnemonic map diagram."""
from __future__ import annotations
import logging
from pathlib import Path
from typing import TYPE_CHECKING
from matplotlib.patches import FancyBboxPatch
import matplotlib.pyplot as plt
from python_pkg.praca_magisterska_video.generate_images._sched_common import (
BG,
DPI,
FS_TITLE,
GRAY1,
GRAY2,
GRAY3,
GRAY4,
GRAY5,
LN,
OUTPUT_DIR,
)
if TYPE_CHECKING:
from matplotlib.axes import Axes
_logger = logging.getLogger(__name__)
def draw_graham_notation() -> None:
"""Draw graham notation."""
_fig, ax = plt.subplots(1, 1, figsize=(8.27, 10))
ax.set_xlim(0, 10)
ax.set_ylim(0, 14)
ax.set_aspect("equal")
ax.axis("off")
ax.set_title(
"Notacja Grahama \u03b1 | β | \u03b3 — Mapa mnemoniczna",
fontsize=FS_TITLE + 1,
fontweight="bold",
pad=12,
)
_draw_graham_formula_bar(ax)
_draw_graham_alpha_beta(ax)
_draw_graham_lower(ax)
plt.tight_layout()
plt.savefig(
str(Path(OUTPUT_DIR) / "scheduling_graham_notation.png"),
dpi=DPI,
bbox_inches="tight",
facecolor=BG,
)
plt.close()
_logger.info(" ✓ scheduling_graham_notation.png")
def _draw_graham_formula_bar(ax: Axes) -> None:
"""Draw the top alpha|beta|gamma formula bar."""
bar_y = 12.5
bar_h = 1.0
# alpha box
rect = FancyBboxPatch(
(0.5, bar_y),
2.5,
bar_h,
boxstyle="round,pad=0.08",
lw=2,
edgecolor=LN,
facecolor=GRAY1,
)
ax.add_patch(rect)
ax.text(
1.75,
bar_y + bar_h / 2,
"\u03b1",
fontsize=20,
fontweight="bold",
ha="center",
va="center",
)
ax.text(
1.75,
bar_y - 0.25,
"MASZYNY",
fontsize=8,
fontweight="bold",
ha="center",
va="top",
color="#444444",
)
# separator |
ax.text(
3.3,
bar_y + bar_h / 2,
"|",
fontsize=24,
fontweight="bold",
ha="center",
va="center",
)
# β box
rect = FancyBboxPatch(
(3.7, bar_y),
2.5,
bar_h,
boxstyle="round,pad=0.08",
lw=2,
edgecolor=LN,
facecolor=GRAY2,
)
ax.add_patch(rect)
ax.text(
4.95,
bar_y + bar_h / 2,
"β",
fontsize=20,
fontweight="bold",
ha="center",
va="center",
)
ax.text(
4.95,
bar_y - 0.25,
"OGRANICZENIA",
fontsize=8,
fontweight="bold",
ha="center",
va="top",
color="#444444",
)
# separator |
ax.text(
6.5,
bar_y + bar_h / 2,
"|",
fontsize=24,
fontweight="bold",
ha="center",
va="center",
)
# gamma box
rect = FancyBboxPatch(
(6.9, bar_y),
2.5,
bar_h,
boxstyle="round,pad=0.08",
lw=2,
edgecolor=LN,
facecolor=GRAY3,
)
ax.add_patch(rect)
ax.text(
8.15,
bar_y + bar_h / 2,
"\u03b3",
fontsize=20,
fontweight="bold",
ha="center",
va="center",
)
ax.text(
8.15,
bar_y - 0.25,
"CEL",
fontsize=8,
fontweight="bold",
ha="center",
va="top",
color="#444444",
)
def _draw_graham_alpha_beta(ax: Axes) -> None:
"""Draw alpha (machines) and beta (constraints) sections."""
start_x = 0.3
col_w = 1.28
# === SECTION alpha: MACHINES ===
sec_y = 11.5
ax.text(
0.3,
sec_y,
'\u03b1 — „1 Prawdziwy Quasi-Rycerz Forsuje Jaskinię Orków"',
fontsize=8,
fontweight="bold",
va="top",
style="italic",
color="#333333",
)
alpha_items = [
("1", "jedna maszyna", "", GRAY4),
("Pm", "identyczne Parallel", "●●●", GRAY1),
("Qm", "Quasi-uniform\n(różne prędkości)", "●●◐", GRAY4),
("Rm", "Random unrelated\n(czasy per para)", "●◆▲", GRAY1),
("Fm", "Flow shop\n(ta sama kolejność)", "→→→", GRAY2),
("Jm", "Job shop\n(indyw. trasy)", "↗↙↘", GRAY4),
("Om", "Open shop\n(dowolna kolej.)", "?→?", GRAY1),
]
col_w = 1.28
box_h_a = 1.1
start_x = 0.3
start_y = 9.6
for i, (symbol, desc, icon, fill) in enumerate(alpha_items):
x = start_x + i * col_w
y = start_y
rect = FancyBboxPatch(
(x, y),
col_w - 0.1,
box_h_a,
boxstyle="round,pad=0.04",
lw=1,
edgecolor=LN,
facecolor=fill,
)
ax.add_patch(rect)
ax.text(
x + (col_w - 0.1) / 2,
y + box_h_a - 0.15,
symbol,
ha="center",
va="top",
fontsize=9,
fontweight="bold",
)
ax.text(
x + (col_w - 0.1) / 2,
y + box_h_a / 2 - 0.1,
desc,
ha="center",
va="center",
fontsize=5.5,
)
ax.text(
x + (col_w - 0.1) / 2, y + 0.12, icon, ha="center", va="bottom", fontsize=7
)
# Complexity arrow under alpha
arr_y = 9.35
ax.annotate(
"",
xy=(9.0, arr_y),
xytext=(0.5, arr_y),
arrowprops={"arrowstyle": "->", "color": "#666666", "lw": 1.5},
)
ax.text(
4.8,
arr_y - 0.18,
"rosnąca złożoność →",
ha="center",
fontsize=6,
color="#666666",
)
# === SECTION β: CONSTRAINTS ===
sec_y2 = 8.9
ax.text(
0.3,
sec_y2,
"β — „Robak Daje Deadline: Przerwy Poprzedzają Pojedyncze Setup'y\"",
fontsize=8,
fontweight="bold",
va="top",
style="italic",
color="#333333",
)
beta_items = [
("rⱼ", "release\ndates", "Robak\ndostępne\nod czasu rⱼ", GRAY1),
("dⱼ", "due\ndates", "Daje\ntermin soft\n(kara za spóźn.)", GRAY4),
("d̄ⱼ", "dead-\nlines", "Deadline\ntermin hard\n(musi dotrzymać)", GRAY1),
("pmtn", "preemp-\ntion", "Przerwy\nmożna\nprzerwać", GRAY2),
("prec", "prece-\ndencje", "Poprzedzają\nA->B (DAG)", GRAY4),
("pⱼ=1", "unit\ntime", "Pojedyncze\nwszystkie = 1", GRAY1),
("sⱼₖ", "setup\ntimes", "Setup'y\nprzezbrojenie\nmiędzy j->k", GRAY4),
]
start_y2 = 7.0
box_h_b = 1.4
for i, (symbol, _label, desc, fill) in enumerate(beta_items):
x = start_x + i * col_w
y = start_y2
rect = FancyBboxPatch(
(x, y),
col_w - 0.1,
box_h_b,
boxstyle="round,pad=0.04",
lw=1,
edgecolor=LN,
facecolor=fill,
)
ax.add_patch(rect)
ax.text(
x + (col_w - 0.1) / 2,
y + box_h_b - 0.12,
symbol,
ha="center",
va="top",
fontsize=9,
fontweight="bold",
)
ax.text(
x + (col_w - 0.1) / 2,
y + box_h_b / 2 - 0.05,
desc,
ha="center",
va="center",
fontsize=5,
)
def _draw_graham_lower(ax: Axes) -> None:
"""Draw gamma criteria, examples, and footer sections."""
start_x = 0.3
# === SECTION gamma: CRITERIA ===
sec_y3 = 6.5
ax.text(
0.3,
sec_y3,
'\u03b3 — „Ciężki Sum Ważony Lata, Tardiness Uderza"',
fontsize=8,
fontweight="bold",
va="top",
style="italic",
color="#333333",
)
gamma_items = [
("Cmax", "makespan\nmax(Cⱼ)", "Jak długo\ntrwa WSZYSTKO?", GRAY2),
("ΣCⱼ", "suma\nukończeń", "Średni czas\noczekiwania?", GRAY4),
("ΣwⱼCⱼ", "ważona\nsuma", "Priorytety\nzadań?", GRAY1),
("Lmax", "max\nopóźnienie", "Najgorsze\nspóźnienie?", GRAY2),
("ΣTⱼ", "suma\nspóźnień", "Łączne\nspóźnienia?", GRAY4),
("ΣUⱼ", "liczba\nspóźnionych", "Ile spóźnionych\nzadań?", GRAY1),
]
start_y3 = 4.5
box_h_g = 1.4
col_w_g = 1.5
for i, (symbol, label, question, fill) in enumerate(gamma_items):
x = start_x + i * col_w_g
y = start_y3
rect = FancyBboxPatch(
(x, y),
col_w_g - 0.1,
box_h_g,
boxstyle="round,pad=0.04",
lw=1,
edgecolor=LN,
facecolor=fill,
)
ax.add_patch(rect)
ax.text(
x + (col_w_g - 0.1) / 2,
y + box_h_g - 0.1,
symbol,
ha="center",
va="top",
fontsize=9,
fontweight="bold",
)
ax.text(
x + (col_w_g - 0.1) / 2,
y + box_h_g / 2 - 0.05,
label,
ha="center",
va="center",
fontsize=6,
)
ax.text(
x + (col_w_g - 0.1) / 2,
y + 0.15,
f'{question}"',
ha="center",
va="bottom",
fontsize=5,
style="italic",
)
# === BOTTOM: Example + Optimal methods ===
ex_y = 3.5
ax.text(
0.3,
ex_y,
"Przykłady zapisu i optymalne metody:",
fontsize=8,
fontweight="bold",
va="top",
)
examples = [
("1 || ΣCⱼ", "SPT (najkrótsze\nnajpierw)", "O(n log n)", GRAY1),
("1 || Lmax", "EDD (najwcześniejszy\ntermin)", "O(n log n)", GRAY4),
("F2 || Cmax", "Algorytm\nJohnsona", "O(n log n)", GRAY2),
("Pm || Cmax", "LPT heurystyka\n(NP-trudny!)", "NP-hard", GRAY3),
("Jm || Cmax", "Branch & Bound\n(NP-trudny!)", "NP-hard", GRAY5),
]
ex_start_y = 1.8
ex_box_w = 1.72
ex_box_h = 1.4
for i, (notation, method, complexity, fill) in enumerate(examples):
x = start_x + i * (ex_box_w + 0.1)
y = ex_start_y
rect = FancyBboxPatch(
(x, y),
ex_box_w,
ex_box_h,
boxstyle="round,pad=0.04",
lw=1.2,
edgecolor=LN,
facecolor=fill,
)
ax.add_patch(rect)
ax.text(
x + ex_box_w / 2,
y + ex_box_h - 0.12,
notation,
ha="center",
va="top",
fontsize=8,
fontweight="bold",
fontfamily="monospace",
)
ax.text(
x + ex_box_w / 2,
y + ex_box_h / 2 - 0.05,
method,
ha="center",
va="center",
fontsize=6,
)
ax.text(
x + ex_box_w / 2,
y + 0.12,
complexity,
ha="center",
va="bottom",
fontsize=6.5,
fontweight="bold",
color="#555555",
)
# Footer mnemonic summary
ax.text(
5.0,
0.8,
'\u03b1|β|\u03b3 = Maszyny | Ograniczenia | Cel"',
ha="center",
fontsize=9,
fontweight="bold",
style="italic",
color="#333333",
bbox={
"boxstyle": "round,pad=0.3",
"facecolor": GRAY4,
"edgecolor": GRAY3,
"lw": 1,
},
)
ax.text(
5.0,
0.2,
"\u03b1: ILE maszyn i JAKIE? "
"β: JAKIE ograniczenia zadań? "
"\u03b3: CO minimalizujemy?",
ha="center",
fontsize=7,
color="#555555",
)

View File

@ -0,0 +1,318 @@
"""Johnson's algorithm Gantt chart diagram (F2||Cmax)."""
from __future__ import annotations
import logging
from pathlib import Path
from typing import TYPE_CHECKING
import matplotlib.patches as mpatches
import matplotlib.pyplot as plt
from python_pkg.praca_magisterska_video.generate_images._sched_common import (
BG,
DPI,
FONTWEIGHT_THRESHOLD,
FS_TITLE,
GRAY1,
GRAY2,
GRAY3,
GRAY4,
GRAY5,
LN,
MIN_COLUMN_INDEX,
OUTPUT_DIR,
)
if TYPE_CHECKING:
from matplotlib.axes import Axes
_logger = logging.getLogger(__name__)
def draw_johnson_gantt() -> None:
"""Draw johnson gantt."""
_fig, axes = plt.subplots(
2, 1, figsize=(8.27, 7), gridspec_kw={"height_ratios": [1, 1.8]}
)
_draw_johnson_decision_table(axes[0])
_draw_johnson_gantt_chart(axes[1])
plt.tight_layout()
plt.savefig(
str(Path(OUTPUT_DIR) / "scheduling_johnson_gantt.png"),
dpi=DPI,
bbox_inches="tight",
facecolor=BG,
)
plt.close()
_logger.info(" ✓ scheduling_johnson_gantt.png")
def _draw_johnson_decision_table(ax: Axes) -> None:
"""Draw the Johnson algorithm decision table."""
ax.set_xlim(0, 10)
ax.set_ylim(0, 5)
ax.set_aspect("equal")
ax.axis("off")
ax.set_title(
"Algorytm Johnsona (F2 || Cmax) — Decyzja + Diagram Gantta",
fontsize=FS_TITLE,
fontweight="bold",
pad=10,
)
# Task table
tasks = ["J1", "J2", "J3", "J4", "J5"]
a_times = [4, 2, 6, 1, 3]
b_times = [5, 3, 2, 7, 4]
min_vals = [min(a, b) for a, b in zip(a_times, b_times, strict=False)]
min_on = ["M1" if a <= b else "M2" for a, b in zip(a_times, b_times, strict=False)]
assign = ["POCZątek" if m == "M1" else "KONIEC" for m in min_on]
# Draw table
col_w_t = 1.3
row_h = 0.55
headers = ["Zadanie", "aⱼ (M1)", "bⱼ (M2)", "min", "min na", "Przydziel"]
table_x = 0.8
table_y = 3.8
for j, hdr in enumerate(headers):
x = table_x + j * col_w_t
rect = mpatches.Rectangle(
(x, table_y), col_w_t, row_h, lw=1, edgecolor=LN, facecolor=GRAY2
)
ax.add_patch(rect)
ax.text(
x + col_w_t / 2,
table_y + row_h / 2,
hdr,
ha="center",
va="center",
fontsize=6.5,
fontweight="bold",
)
for i in range(5):
row_data = [
tasks[i],
str(a_times[i]),
str(b_times[i]),
str(min_vals[i]),
min_on[i],
assign[i],
]
for j, val in enumerate(row_data):
x = table_x + j * col_w_t
y = table_y - (i + 1) * row_h
fill_c = GRAY1 if min_on[i] == "M1" else GRAY4
if j == MIN_COLUMN_INDEX: # min column - highlight
fill_c = GRAY3
rect = mpatches.Rectangle(
(x, y), col_w_t, row_h, lw=0.8, edgecolor=LN, facecolor=fill_c
)
ax.add_patch(rect)
fw = "bold" if j >= FONTWEIGHT_THRESHOLD else "normal"
ax.text(
x + col_w_t / 2,
y + row_h / 2,
val,
ha="center",
va="center",
fontsize=6.5,
fontweight=fw,
)
# Sorting result
result_y = 0.7
ax.text(
5.0,
result_y + 0.4,
"Sortuj → POCZĄTEK ↑aⱼ: J4(1), J2(2), J5(3), J1(4) | KONIEC ↓bⱼ: J3(2)",
ha="center",
fontsize=7,
color="#333333",
)
ax.text(
5.0,
result_y,
"Optymalna kolejność: J4 → J2 → J5 → J1 → J3",
ha="center",
fontsize=9,
fontweight="bold",
bbox={
"boxstyle": "round,pad=0.2",
"facecolor": GRAY1,
"edgecolor": LN,
"lw": 1.2,
},
)
def _draw_johnson_gantt_chart(ax2: Axes) -> None:
"""Draw the Johnson algorithm Gantt chart."""
ax2.set_xlim(-1, 24)
ax2.set_ylim(-1, 4)
ax2.axis("off")
# Machines labels
m1_y = 2.5
m2_y = 0.8
bar_h = 0.9
ax2.text(
-0.8,
m1_y + bar_h / 2,
"M1",
ha="center",
va="center",
fontsize=11,
fontweight="bold",
)
ax2.text(
-0.8,
m2_y + bar_h / 2,
"M2",
ha="center",
va="center",
fontsize=11,
fontweight="bold",
)
# Schedule: J4 → J2 → J5 → J1 → J3
order = ["J4", "J2", "J5", "J1", "J3"]
a_ord = [1, 2, 3, 4, 6] # M1 times in order
b_ord = [7, 3, 4, 5, 2] # M2 times in order
fills = [GRAY1, GRAY2, GRAY4, GRAY3, GRAY5]
hatches = ["", "///", "", "\\\\\\", "xxx"]
# M1 schedule
m1_starts = []
t = 0
for a in a_ord:
m1_starts.append(t)
t += a
m1_ends = [s + a for s, a in zip(m1_starts, a_ord, strict=False)]
# M2 schedule (must wait for M1 finish AND previous M2 finish)
m2_starts = []
m2_ends = []
prev_m2_end = 0
for i, b in enumerate(b_ord):
start = max(m1_ends[i], prev_m2_end)
m2_starts.append(start)
m2_ends.append(start + b)
prev_m2_end = start + b
# Draw M1 bars
for i in range(5):
rect = mpatches.Rectangle(
(m1_starts[i], m1_y),
a_ord[i],
bar_h,
lw=1.2,
edgecolor=LN,
facecolor=fills[i],
hatch=hatches[i],
)
ax2.add_patch(rect)
ax2.text(
m1_starts[i] + a_ord[i] / 2,
m1_y + bar_h / 2,
f"{order[i]}\n({a_ord[i]})",
ha="center",
va="center",
fontsize=7,
fontweight="bold",
)
# Draw M2 bars
for i in range(5):
rect = mpatches.Rectangle(
(m2_starts[i], m2_y),
b_ord[i],
bar_h,
lw=1.2,
edgecolor=LN,
facecolor=fills[i],
hatch=hatches[i],
)
ax2.add_patch(rect)
ax2.text(
m2_starts[i] + b_ord[i] / 2,
m2_y + bar_h / 2,
f"{order[i]}\n({b_ord[i]})",
ha="center",
va="center",
fontsize=7,
fontweight="bold",
)
# Draw idle regions on M2
idle_starts = [0]
idle_ends = [m2_starts[0]]
for i in range(1, 5):
if m2_starts[i] > m2_ends[i - 1]:
idle_starts.append(m2_ends[i - 1])
idle_ends.append(m2_starts[i])
for s, e in zip(idle_starts, idle_ends, strict=False):
if e > s:
rect = mpatches.Rectangle(
(s, m2_y),
e - s,
bar_h,
lw=0.5,
edgecolor="#AAAAAA",
facecolor="white",
linestyle="--",
)
ax2.add_patch(rect)
ax2.text(
s + (e - s) / 2,
m2_y + bar_h / 2,
"idle",
ha="center",
va="center",
fontsize=5,
color="#999999",
)
# Time axis
ax_y = m2_y - 0.15
ax2.plot([0, 23], [ax_y, ax_y], color=LN, lw=0.8)
for t in range(0, 24, 2):
ax2.plot([t, t], [ax_y - 0.08, ax_y + 0.08], color=LN, lw=0.8)
ax2.text(t, ax_y - 0.25, str(t), ha="center", va="top", fontsize=6)
ax2.text(11.5, ax_y - 0.55, "czas", ha="center", fontsize=7)
# Cmax annotation
ax2.annotate(
f"Cmax = {m2_ends[-1]}",
xy=(m2_ends[-1], m2_y + bar_h),
xytext=(m2_ends[-1] + 0.5, m2_y + bar_h + 0.6),
fontsize=10,
fontweight="bold",
color="#333333",
arrowprops={"arrowstyle": "->", "color": "#333333", "lw": 1.5},
)
# Mnemonic at bottom
ax2.text(
11,
-0.7,
"„Krótki na M1 → START (szybko karmi M2)"
" Krótki na M2 → KONIEC"
' (szybko kończy)"',
ha="center",
fontsize=7.5,
fontweight="bold",
style="italic",
bbox={
"boxstyle": "round,pad=0.3",
"facecolor": GRAY4,
"edgecolor": GRAY3,
"lw": 0.8,
},
)

View File

@ -0,0 +1,352 @@
"""SPT vs LPT comparison and Flow Shop vs Job Shop diagrams."""
from __future__ import annotations
import logging
from pathlib import Path
from typing import TYPE_CHECKING
import matplotlib.patches as mpatches
import matplotlib.pyplot as plt
from python_pkg.praca_magisterska_video.generate_images._sched_common import (
BG,
DPI,
GRAY1,
GRAY2,
GRAY3,
GRAY4,
GRAY5,
LN,
OUTPUT_DIR,
draw_arrow,
)
if TYPE_CHECKING:
from matplotlib.axes import Axes
_logger = logging.getLogger(__name__)
# ============================================================
# SPT vs LPT COMPARISON (1 || ΣCⱼ)
# ============================================================
def draw_spt_comparison() -> None:
"""Draw spt comparison."""
fig, axes = plt.subplots(2, 1, figsize=(8.27, 5.5))
tasks_orig = [("J1", 5), ("J2", 3), ("J3", 8), ("J4", 2), ("J5", 6)]
spt_order = sorted(tasks_orig, key=lambda x: x[1])
lpt_order = sorted(tasks_orig, key=lambda x: -x[1])
fills_map = {"J1": GRAY1, "J2": GRAY2, "J3": GRAY3, "J4": GRAY4, "J5": GRAY5}
hatch_map = {"J1": "", "J2": "///", "J3": "xxx", "J4": "", "J5": "\\\\\\"}
for _idx, (ax, order_list, title, is_optimal) in enumerate(
[
(axes[0], spt_order, "SPT (Shortest Processing Time) — OPTYMALNE", True),
(axes[1], lpt_order, "LPT (Longest Processing Time) — gorsze!", False),
]
):
ax.set_xlim(-2, 26)
ax.set_ylim(-0.5, 2.5)
ax.axis("off")
color = "#222222" if is_optimal else "#666666"
marker = "" if is_optimal else ""
ax.set_title(
f"{marker} {title}",
fontsize=9,
fontweight="bold",
loc="left",
color=color,
pad=5,
)
bar_y = 1.0
bar_h = 0.8
t = 0
completions = []
for name, duration in order_list:
rect = mpatches.Rectangle(
(t, bar_y),
duration,
bar_h,
lw=1.2,
edgecolor=LN,
facecolor=fills_map[name],
hatch=hatch_map[name],
)
ax.add_patch(rect)
ax.text(
t + duration / 2,
bar_y + bar_h / 2,
f"{name}\n({duration})",
ha="center",
va="center",
fontsize=7,
fontweight="bold",
)
t += duration
completions.append(t)
# Completion time marker
ax.plot([t, t], [bar_y - 0.15, bar_y], color=LN, lw=0.8)
ax.text(
t,
bar_y - 0.25,
f"C={t}",
ha="center",
va="top",
fontsize=6,
color="#555555",
)
total = sum(completions)
# Time axis
ax.plot([0, 25], [bar_y - 0.05, bar_y - 0.05], color=LN, lw=0.5)
# Sum annotation
comp_str = " + ".join(str(c) for c in completions)
ax.text(
25,
bar_y + bar_h / 2,
f"ΣCⱼ = {comp_str}\n = {total}",
ha="left",
va="center",
fontsize=7,
fontweight="bold" if is_optimal else "normal",
color=color,
bbox={
"boxstyle": "round,pad=0.2",
"facecolor": GRAY1 if is_optimal else "white",
"edgecolor": color,
"lw": 1,
},
)
# Bottom annotation
fig.text(
0.5,
0.02,
'„Short People To the front"'
" — krótkie najpierw,"
" jak niskie osoby w zdjęciu klasowym",
ha="center",
fontsize=8,
fontweight="bold",
style="italic",
color="#444444",
)
plt.tight_layout(rect=[0, 0.05, 1, 1])
plt.savefig(
str(Path(OUTPUT_DIR) / "scheduling_spt_comparison.png"),
dpi=DPI,
bbox_inches="tight",
facecolor=BG,
)
plt.close()
_logger.info(" ✓ scheduling_spt_comparison.png")
# ============================================================
# FLOW SHOP vs JOB SHOP
# ============================================================
def draw_flow_vs_job() -> None:
"""Draw flow vs job."""
_fig, axes = plt.subplots(1, 2, figsize=(8.27, 4.5))
_draw_flow_shop(axes[0])
_draw_job_shop(axes[1])
plt.tight_layout()
plt.savefig(
str(Path(OUTPUT_DIR) / "scheduling_flow_vs_job.png"),
dpi=DPI,
bbox_inches="tight",
facecolor=BG,
)
plt.close()
_logger.info(" ✓ scheduling_flow_vs_job.png")
def _draw_flow_shop(ax: Axes) -> None:
"""Draw the Flow Shop diagram."""
ax.set_xlim(0, 6)
ax.set_ylim(0, 6)
ax.set_aspect("equal")
ax.axis("off")
ax.set_title("Flow Shop (Fm)", fontsize=10, fontweight="bold", pad=8)
# Machines in a row
machines_x = [1, 3, 5]
machines_y = 3
mach_r = 0.4
for i, mx in enumerate(machines_x):
circle = plt.Circle(
(mx, machines_y), mach_r, facecolor=GRAY2, edgecolor=LN, lw=1.5
)
ax.add_patch(circle)
ax.text(
mx,
machines_y,
f"M{i + 1}",
ha="center",
va="center",
fontsize=9,
fontweight="bold",
)
# Arrows between machines
for i in range(len(machines_x) - 1):
draw_arrow(
ax,
machines_x[i] + mach_r + 0.05,
machines_y,
machines_x[i + 1] - mach_r - 0.05,
machines_y,
lw=2,
)
# Jobs all flowing the same way
jobs_flow = ["J1", "J2", "J3"]
for _j, (job, y_off) in enumerate(zip(jobs_flow, [0.8, 0, -0.8], strict=False)):
ax.text(
0.2,
machines_y + y_off,
job,
ha="center",
va="center",
fontsize=7,
fontweight="bold",
bbox={"boxstyle": "round,pad=0.15", "facecolor": GRAY1, "edgecolor": LN},
)
# Dashed flow line
ax.annotate(
"",
xy=(5.5, machines_y + y_off * 0.3),
xytext=(0.5, machines_y + y_off),
arrowprops={
"arrowstyle": "->",
"color": "#888888",
"lw": 0.8,
"linestyle": "dashed",
},
)
ax.text(
3,
1.2,
"Wszystkie zadania:\nM1 → M2 → M3",
ha="center",
va="center",
fontsize=8,
bbox={"boxstyle": "round,pad=0.3", "facecolor": GRAY4, "edgecolor": GRAY3},
)
ax.text(
3,
0.4,
"Jak taśma montażowa",
ha="center",
fontsize=7,
style="italic",
color="#666666",
)
def _draw_job_shop(ax: Axes) -> None:
"""Draw the Job Shop diagram."""
mach_r = 0.4
ax.set_xlim(0, 6)
ax.set_ylim(0, 6)
ax.set_aspect("equal")
ax.axis("off")
ax.set_title("Job Shop (Jm)", fontsize=10, fontweight="bold", pad=8)
# Machines scattered
m_positions = [(1.5, 4.2), (4.5, 4.2), (3, 2.5)]
for i, (mx, my) in enumerate(m_positions):
circle = plt.Circle((mx, my), mach_r, facecolor=GRAY2, edgecolor=LN, lw=1.5)
ax.add_patch(circle)
ax.text(
mx, my, f"M{i + 1}", ha="center", va="center", fontsize=9, fontweight="bold"
)
# J1: M1 → M2 → M3 (solid)
route1 = [(1.5, 4.2), (4.5, 4.2), (3, 2.5)]
for i in range(len(route1) - 1):
x1, y1 = route1[i]
x2, y2 = route1[i + 1]
dx = x2 - x1
dy = y2 - y1
d = (dx**2 + dy**2) ** 0.5
draw_arrow(
ax,
x1 + mach_r * dx / d + 0.05,
y1 + mach_r * dy / d,
x2 - mach_r * dx / d - 0.05,
y2 - mach_r * dy / d,
lw=1.5,
)
ax.text(
0.3,
4.8,
"J1: M1→M2→M3",
fontsize=7,
fontweight="bold",
bbox={"boxstyle": "round,pad=0.1", "facecolor": GRAY1, "edgecolor": LN},
)
# J2: M2 → M3 → M1 (dashed)
route2 = [(4.5, 4.2), (3, 2.5), (1.5, 4.2)]
for i in range(len(route2) - 1):
x1, y1 = route2[i]
x2, y2 = route2[i + 1]
dx = x2 - x1
dy = y2 - y1
d = (dx**2 + dy**2) ** 0.5
off = 0.15 # offset to avoid overlap
ax.annotate(
"",
xy=(x2 - mach_r * dx / d - 0.05, y2 - mach_r * dy / d + off),
xytext=(x1 + mach_r * dx / d + 0.05, y1 + mach_r * dy / d + off),
arrowprops={
"arrowstyle": "->",
"color": "#555555",
"lw": 1.5,
"linestyle": "dashed",
},
)
ax.text(
3.8,
5.2,
"J2: M2→M3→M1",
fontsize=7,
fontweight="bold",
bbox={"boxstyle": "round,pad=0.1", "facecolor": GRAY4, "edgecolor": LN},
)
ax.text(
3,
1.2,
"Każde zadanie:\nwłasna trasa!",
ha="center",
va="center",
fontsize=8,
bbox={"boxstyle": "round,pad=0.3", "facecolor": GRAY4, "edgecolor": GRAY3},
)
ax.text(
3,
0.4,
"NP-trudny już dla 3 maszyn",
ha="center",
fontsize=7,
style="italic",
color="#666666",
)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff