mirror of
https://github.com/kuhyx/testsAndMisc-archive.git
synced 2026-07-04 13:43:02 +02:00
WIP: Enforce 500-line limit - split batch 1
Split 16+ files. 27 files still need splitting. See session notes.
This commit is contained in:
parent
3139333f24
commit
27a1ef634c
14
python_pkg/keyboard_coop/tests/conftest.py
Normal file
14
python_pkg/keyboard_coop/tests/conftest.py
Normal 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
|
||||
148
python_pkg/keyboard_coop/tests/test_constants.py
Normal file
148
python_pkg/keyboard_coop/tests/test_constants.py
Normal 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
|
||||
371
python_pkg/keyboard_coop/tests/test_game_logic.py
Normal file
371
python_pkg/keyboard_coop/tests/test_game_logic.py
Normal 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
|
||||
426
python_pkg/keyboard_coop/tests/test_game_loop.py
Normal file
426
python_pkg/keyboard_coop/tests/test_game_loop.py
Normal 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
311
python_pkg/keyboard_coop/tests/test_ui.py
Normal file
311
python_pkg/keyboard_coop/tests/test_ui.py
Normal 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
409
python_pkg/lichess_bot/tests/test_main_analysis.py
Normal file
409
python_pkg/lichess_bot/tests/test_main_analysis.py
Normal 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
|
||||
474
python_pkg/lichess_bot/tests/test_main_bot_loop.py
Normal file
474
python_pkg/lichess_bot/tests/test_main_bot_loop.py
Normal 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)
|
||||
403
python_pkg/lichess_bot/tests/test_main_game_state.py
Normal file
403
python_pkg/lichess_bot/tests/test_main_game_state.py
Normal 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()
|
||||
445
python_pkg/praca_magisterska_video/_q23_classical.py
Normal file
445
python_pkg/praca_magisterska_video/_q23_classical.py
Normal 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
|
||||
248
python_pkg/praca_magisterska_video/_q23_deeplab.py
Normal file
248
python_pkg/praca_magisterska_video/_q23_deeplab.py
Normal 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
|
||||
116
python_pkg/praca_magisterska_video/_q23_helpers.py
Normal file
116
python_pkg/praca_magisterska_video/_q23_helpers.py
Normal 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)]
|
||||
)
|
||||
430
python_pkg/praca_magisterska_video/_q23_transformer.py
Normal file
430
python_pkg/praca_magisterska_video/_q23_transformer.py
Normal 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)]
|
||||
)
|
||||
399
python_pkg/praca_magisterska_video/_q23_unet_fcn.py
Normal file
399
python_pkg/praca_magisterska_video/_q23_unet_fcn.py
Normal 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
|
||||
332
python_pkg/praca_magisterska_video/_q24_classical.py
Normal file
332
python_pkg/praca_magisterska_video/_q24_classical.py
Normal 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
|
||||
115
python_pkg/praca_magisterska_video/_q24_common.py
Normal file
115
python_pkg/praca_magisterska_video/_q24_common.py
Normal 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)]
|
||||
)
|
||||
239
python_pkg/praca_magisterska_video/_q24_nms_final.py
Normal file
239
python_pkg/praca_magisterska_video/_q24_nms_final.py
Normal 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)]
|
||||
)
|
||||
405
python_pkg/praca_magisterska_video/_q24_rcnn.py
Normal file
405
python_pkg/praca_magisterska_video/_q24_rcnn.py
Normal 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
|
||||
383
python_pkg/praca_magisterska_video/_q24_rpn_yolo.py
Normal file
383
python_pkg/praca_magisterska_video/_q24_rpn_yolo.py
Normal 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
|
||||
459
python_pkg/praca_magisterska_video/_q24_yolo_arch_detr.py
Normal file
459
python_pkg/praca_magisterska_video/_q24_yolo_arch_detr.py
Normal 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
|
||||
@ -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)
|
||||
@ -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")
|
||||
@ -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")
|
||||
@ -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")
|
||||
@ -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")
|
||||
@ -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")
|
||||
@ -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]
|
||||
@ -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")
|
||||
@ -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")
|
||||
@ -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")
|
||||
@ -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")
|
||||
@ -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
|
||||
@ -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")
|
||||
@ -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")
|
||||
@ -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")
|
||||
@ -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")
|
||||
@ -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")
|
||||
@ -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")
|
||||
@ -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")
|
||||
@ -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]
|
||||
@ -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")
|
||||
@ -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")
|
||||
@ -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 = ["0°", "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")
|
||||
@ -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")
|
||||
@ -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")
|
||||
@ -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")
|
||||
@ -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,
|
||||
},
|
||||
)
|
||||
@ -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)
|
||||
@ -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)
|
||||
@ -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)
|
||||
@ -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)
|
||||
448
python_pkg/praca_magisterska_video/generate_images/_q9_basics.py
Normal file
448
python_pkg/praca_magisterska_video/generate_images/_q9_basics.py
Normal 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")
|
||||
@ -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")
|
||||
200
python_pkg/praca_magisterska_video/generate_images/_q9_common.py
Normal file
200
python_pkg/praca_magisterska_video/generate_images/_q9_common.py
Normal 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]
|
||||
212
python_pkg/praca_magisterska_video/generate_images/_q9_ipc.py
Normal file
212
python_pkg/praca_magisterska_video/generate_images/_q9_ipc.py
Normal 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")
|
||||
@ -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")
|
||||
@ -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},
|
||||
)
|
||||
@ -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")
|
||||
@ -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",
|
||||
)
|
||||
@ -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,
|
||||
},
|
||||
)
|
||||
@ -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
2153
python_pkg/praca_magisterska_video/generate_images/generate_q20_diagrams.py
Executable file → Normal file
2153
python_pkg/praca_magisterska_video/generate_images/generate_q20_diagrams.py
Executable file → Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
2312
python_pkg/praca_magisterska_video/generate_images/generate_q24_diagrams.py
Executable file → Normal file
2312
python_pkg/praca_magisterska_video/generate_images/generate_q24_diagrams.py
Executable file → Normal file
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
1473
python_pkg/praca_magisterska_video/generate_images/generate_scheduling_diagrams.py
Executable file → Normal file
1473
python_pkg/praca_magisterska_video/generate_images/generate_scheduling_diagrams.py
Executable file → Normal file
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
Loading…
Reference in New Issue
Block a user