|
|
""" |
|
|
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ |
|
|
β Models: gesture.py β |
|
|
β Core classes for representing hand gestures β |
|
|
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ |
|
|
|
|
|
This module contains: |
|
|
β’ GestureRanking - The ranking system for gestures |
|
|
β’ GestureImage - Represents a captured gesture with its data |
|
|
|
|
|
π WHY SEPARATE FILES? |
|
|
In the procedural style, you might put everything in one big file. |
|
|
In OOP, we organize related classes into modules (files). |
|
|
|
|
|
Benefits: |
|
|
β’ Easier to find code (gesture stuff is in gesture.py) |
|
|
β’ Easier to test (can test gesture.py independently) |
|
|
β’ Easier to reuse (import just what you need) |
|
|
β’ Easier to collaborate (different people work on different files) |
|
|
""" |
|
|
|
|
|
from dataclasses import dataclass |
|
|
from typing import List, Optional |
|
|
from PIL import Image |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class GestureRanking: |
|
|
""" |
|
|
Defines the ordering of hand gestures for sorting purposes. |
|
|
|
|
|
This class encapsulates (bundles together): |
|
|
- The ranking of each gesture (which comes first in sorted order) |
|
|
- The emoji representation of each gesture |
|
|
- Methods to compare gestures |
|
|
|
|
|
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ |
|
|
β π CONCEPT: Class Attributes vs Instance Attributes β |
|
|
β β |
|
|
β CLASS ATTRIBUTES: Shared by ALL instances (objects) of the class β |
|
|
β - Defined directly in the class body β |
|
|
β - Like a shared resource everyone can read β |
|
|
β - Here: RANKINGS and EMOJIS are class attributes β |
|
|
β β |
|
|
β INSTANCE ATTRIBUTES: Unique to EACH instance β |
|
|
β - Defined in __init__ using self.attribute_name β |
|
|
β - Like personal belongings each person carries β |
|
|
β - Here: GestureRanking doesn't have instance attributes β |
|
|
β (it's a utility class with shared data) β |
|
|
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ |
|
|
""" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
RANKINGS = { |
|
|
"fist": 1, |
|
|
"one": 2, |
|
|
"two_up": 3, |
|
|
"peace": 3, |
|
|
"three": 4, |
|
|
"four": 5, |
|
|
"palm": 6, |
|
|
"stop": 6, |
|
|
"ok": 7, |
|
|
"like": 8, |
|
|
"dislike": 9, |
|
|
"rock": 10, |
|
|
"call": 11, |
|
|
"mute": 12, |
|
|
"no_gesture": 99, |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
EMOJIS = { |
|
|
"fist": "β", |
|
|
"one": "βοΈ", |
|
|
"two_up": "βοΈ", |
|
|
"peace": "βοΈ", |
|
|
"three": "π€", |
|
|
"four": "π", |
|
|
"palm": "ποΈ", |
|
|
"stop": "ποΈ", |
|
|
"ok": "π", |
|
|
"like": "π", |
|
|
"dislike": "π", |
|
|
"rock": "π€", |
|
|
"call": "π€", |
|
|
"mute": "π€«", |
|
|
"no_gesture": "β", |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod |
|
|
def get_rank(cls, gesture_name: str) -> int: |
|
|
""" |
|
|
Get the sorting rank of a gesture. |
|
|
|
|
|
Args: |
|
|
gesture_name: The name of the gesture (e.g., "peace", "fist") |
|
|
|
|
|
Returns: |
|
|
The rank (1-99) of the gesture. Lower = earlier in sorted order. |
|
|
Returns 99 if gesture is unknown. |
|
|
|
|
|
Example: |
|
|
>>> GestureRanking.get_rank("peace") |
|
|
3 |
|
|
>>> GestureRanking.get_rank("fist") |
|
|
1 |
|
|
""" |
|
|
|
|
|
|
|
|
return cls.RANKINGS.get(gesture_name.lower(), 99) |
|
|
|
|
|
@classmethod |
|
|
def get_emoji(cls, gesture_name: str) -> str: |
|
|
""" |
|
|
Get the emoji representation of a gesture. |
|
|
|
|
|
Args: |
|
|
gesture_name: The name of the gesture |
|
|
|
|
|
Returns: |
|
|
The emoji string for this gesture, or β if unknown. |
|
|
""" |
|
|
return cls.EMOJIS.get(gesture_name.lower(), "β") |
|
|
|
|
|
@classmethod |
|
|
def compare(cls, gesture_a: str, gesture_b: str) -> int: |
|
|
""" |
|
|
Compare two gestures for sorting order. |
|
|
|
|
|
This follows the standard comparison convention: |
|
|
- Returns NEGATIVE if a < b (a comes before b) |
|
|
- Returns ZERO if a == b (same rank) |
|
|
- Returns POSITIVE if a > b (a comes after b) |
|
|
|
|
|
Args: |
|
|
gesture_a: First gesture name |
|
|
gesture_b: Second gesture name |
|
|
|
|
|
Returns: |
|
|
Negative, zero, or positive integer. |
|
|
|
|
|
Example: |
|
|
>>> GestureRanking.compare("fist", "peace") |
|
|
-2 # Negative: fist comes before peace |
|
|
>>> GestureRanking.compare("peace", "fist") |
|
|
2 # Positive: peace comes after fist |
|
|
""" |
|
|
return cls.get_rank(gesture_a) - cls.get_rank(gesture_b) |
|
|
|
|
|
@classmethod |
|
|
def get_all_gestures(cls) -> List[str]: |
|
|
""" |
|
|
Get a list of all known gestures, sorted by rank. |
|
|
|
|
|
Returns: |
|
|
List of gesture names in sorted order. |
|
|
""" |
|
|
|
|
|
|
|
|
sorted_gestures = sorted( |
|
|
cls.RANKINGS.keys(), |
|
|
key=lambda name: cls.RANKINGS[name] |
|
|
) |
|
|
|
|
|
seen = set() |
|
|
unique = [] |
|
|
for gesture in sorted_gestures: |
|
|
if gesture not in seen: |
|
|
seen.add(gesture) |
|
|
unique.append(gesture) |
|
|
return unique |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
""" |
|
|
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ |
|
|
β π CONCEPT: What is a @dataclass? β |
|
|
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ£ |
|
|
β β |
|
|
β A @dataclass is a shortcut for creating classes that mainly hold DATA. β |
|
|
β β |
|
|
β WITHOUT @dataclass (the long way): β |
|
|
β βββββββββββββββββββββββββββββββββ β |
|
|
β class GestureImage: β |
|
|
β def __init__(self, gesture, rank, emoji, image, capture_id): β |
|
|
β self.gesture = gesture β |
|
|
β self.rank = rank β |
|
|
β self.emoji = emoji β |
|
|
β self.image = image β |
|
|
β self.capture_id = capture_id β |
|
|
β β |
|
|
β def __repr__(self): β |
|
|
β return f"GestureImage(gesture={self.gesture}, ...)" β |
|
|
β β |
|
|
β def __eq__(self, other): β |
|
|
β return self.gesture == other.gesture and ... β |
|
|
β β |
|
|
β WITH @dataclass (the shortcut): β |
|
|
β βββββββββββββββββββββββββββββββ β |
|
|
β @dataclass β |
|
|
β class GestureImage: β |
|
|
β gesture: str β |
|
|
β rank: int β |
|
|
β emoji: str β |
|
|
β image: Image β |
|
|
β capture_id: int β |
|
|
β β |
|
|
β The @dataclass automatically generates __init__, __repr__, __eq__, etc! β |
|
|
β β |
|
|
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ |
|
|
""" |
|
|
|
|
|
@dataclass |
|
|
class GestureImage: |
|
|
""" |
|
|
Represents a captured hand gesture image with its classification. |
|
|
|
|
|
This is the CORE DATA STRUCTURE of our application. |
|
|
Each GestureImage bundles together: |
|
|
- The actual image (pixels) |
|
|
- The AI's prediction of what gesture it shows |
|
|
- A unique ID for tracking (important for stability testing) |
|
|
- Visual representations (emoji, rank) |
|
|
|
|
|
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ |
|
|
β π‘ WHY THIS MATTERS: Encapsulation β |
|
|
β β |
|
|
β In procedural code, you'd pass around separate variables: β |
|
|
β process_gesture(image, name, rank, emoji, id) # 5 parameters! β |
|
|
β β |
|
|
β With OOP, you pass ONE object that contains everything: β |
|
|
β process_gesture(gesture_image) # 1 parameter! β |
|
|
β β |
|
|
β Benefits: β |
|
|
β β Less room for errors (can't mix up parameter order) β |
|
|
β β Easier to add new attributes later β |
|
|
β β Methods travel WITH the data they operate on β |
|
|
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ |
|
|
|
|
|
Attributes: |
|
|
gesture: The name of the detected gesture (e.g., "peace", "fist") |
|
|
rank: Numeric rank for sorting (lower = comes first) |
|
|
emoji: Visual emoji representation |
|
|
image: The actual PIL Image (can be None if not needed) |
|
|
capture_id: Unique ID from capture order (for stability testing) |
|
|
thumbnail: Smaller version for display (generated automatically) |
|
|
""" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
gesture: str |
|
|
rank: int |
|
|
emoji: str |
|
|
capture_id: int |
|
|
image: Optional[Image.Image] = None |
|
|
thumbnail: Optional[Image.Image] = None |
|
|
confidence: float = 0.0 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def __post_init__(self): |
|
|
""" |
|
|
Called automatically after the object is created. |
|
|
Generates a thumbnail if an image is provided. |
|
|
""" |
|
|
if self.image is not None and self.thumbnail is None: |
|
|
self._create_thumbnail() |
|
|
|
|
|
def _create_thumbnail(self, max_size: int = 80): |
|
|
""" |
|
|
Create a smaller version of the image for display. |
|
|
|
|
|
The underscore prefix (_create_thumbnail) is a Python convention |
|
|
meaning "this is an internal method, not meant to be called from outside". |
|
|
|
|
|
Args: |
|
|
max_size: Maximum width/height of the thumbnail |
|
|
""" |
|
|
if self.image is not None: |
|
|
|
|
|
thumb = self.image.copy() |
|
|
|
|
|
thumb.thumbnail((max_size, max_size), Image.Resampling.LANCZOS) |
|
|
self.thumbnail = thumb |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
""" |
|
|
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ |
|
|
β π CONCEPT: Magic Methods (Dunder Methods) β |
|
|
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ£ |
|
|
β β |
|
|
β Python has special method names surrounded by double underscores. β |
|
|
β These are called "magic methods" or "dunder methods" (double under). β |
|
|
β β |
|
|
β They let your objects work with Python's built-in operations: β |
|
|
β β |
|
|
β __lt__(self, other) β enables: object1 < object2 β |
|
|
β __le__(self, other) β enables: object1 <= object2 β |
|
|
β __eq__(self, other) β enables: object1 == object2 β |
|
|
β __gt__(self, other) β enables: object1 > object2 β |
|
|
β __ge__(self, other) β enables: object1 >= object2 β |
|
|
β __str__(self) β enables: str(object) or print(object) β |
|
|
β __repr__(self) β enables: repr(object) (for debugging) β |
|
|
β β |
|
|
β π‘ WHY THIS MATTERS: β |
|
|
β With these methods, Python's built-in sorted() function β |
|
|
β automatically works with our GestureImage objects! β |
|
|
β β |
|
|
β gestures = [gesture1, gesture2, gesture3] β |
|
|
β sorted_gestures = sorted(gestures) # Just works! β¨ β |
|
|
β β |
|
|
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ |
|
|
""" |
|
|
|
|
|
def __lt__(self, other: 'GestureImage') -> bool: |
|
|
""" |
|
|
Less than comparison. Enables: gesture1 < gesture2 |
|
|
|
|
|
Compares by rank. If ranks are equal, maintains stability |
|
|
by comparing capture_id (earlier captured = smaller). |
|
|
""" |
|
|
if self.rank != other.rank: |
|
|
return self.rank < other.rank |
|
|
|
|
|
return self.capture_id < other.capture_id |
|
|
|
|
|
def __le__(self, other: 'GestureImage') -> bool: |
|
|
"""Less than or equal. Enables: gesture1 <= gesture2""" |
|
|
return self.rank <= other.rank |
|
|
|
|
|
def __gt__(self, other: 'GestureImage') -> bool: |
|
|
"""Greater than. Enables: gesture1 > gesture2""" |
|
|
if self.rank != other.rank: |
|
|
return self.rank > other.rank |
|
|
return self.capture_id > other.capture_id |
|
|
|
|
|
def __ge__(self, other: 'GestureImage') -> bool: |
|
|
"""Greater than or equal. Enables: gesture1 >= gesture2""" |
|
|
return self.rank >= other.rank |
|
|
|
|
|
def __eq__(self, other: object) -> bool: |
|
|
""" |
|
|
Equality comparison. Enables: gesture1 == gesture2 |
|
|
|
|
|
Two gestures are equal if they have the same rank. |
|
|
Note: We compare RANKS, not capture_ids, for sorting purposes. |
|
|
""" |
|
|
if not isinstance(other, GestureImage): |
|
|
return False |
|
|
return self.rank == other.rank |
|
|
|
|
|
def __hash__(self) -> int: |
|
|
""" |
|
|
Hash function. Required for using objects in sets or as dict keys. |
|
|
We hash by capture_id since it's unique. |
|
|
""" |
|
|
return hash(self.capture_id) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def __str__(self) -> str: |
|
|
""" |
|
|
Human-readable string representation. |
|
|
Called by print() and str(). |
|
|
|
|
|
Example: "βοΈβ" (peace sign, capture #1) |
|
|
""" |
|
|
|
|
|
subscripts = "ββββββ
ββββ" |
|
|
sub_id = ''.join(subscripts[int(d)] for d in str(self.capture_id)) |
|
|
return f"{self.emoji}{sub_id}" |
|
|
|
|
|
def __repr__(self) -> str: |
|
|
""" |
|
|
Developer-friendly representation (for debugging). |
|
|
Called by repr() and shown in interactive Python. |
|
|
""" |
|
|
return f"GestureImage(gesture='{self.gesture}', rank={self.rank}, id={self.capture_id})" |
|
|
|
|
|
def display_label(self) -> str: |
|
|
""" |
|
|
Get a label for UI display. |
|
|
Shows emoji, gesture name, and capture ID. |
|
|
""" |
|
|
return f"{self.emoji} {self.gesture} (#{self.capture_id})" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
""" |
|
|
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ |
|
|
β π CONCEPT: Factory Methods β |
|
|
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ£ |
|
|
β β |
|
|
β A Factory Method is a class method that CREATES instances. β |
|
|
β β |
|
|
β Instead of: β |
|
|
β gesture = GestureImage( β |
|
|
β gesture="peace", β |
|
|
β rank=GestureRanking.get_rank("peace"), β |
|
|
β emoji=GestureRanking.get_emoji("peace"), β |
|
|
β capture_id=1, β |
|
|
β image=my_image, β |
|
|
β confidence=0.95 β |
|
|
β ) β |
|
|
β β |
|
|
β You can use: β |
|
|
β gesture = GestureImage.create_from_prediction( β |
|
|
β gesture_name="peace", β |
|
|
β capture_id=1, β |
|
|
β image=my_image, β |
|
|
β confidence=0.95 β |
|
|
β ) β |
|
|
β β |
|
|
β The factory method handles the details of looking up rank/emoji! β |
|
|
β β |
|
|
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ |
|
|
""" |
|
|
|
|
|
@classmethod |
|
|
def create_from_prediction( |
|
|
cls, |
|
|
gesture_name: str, |
|
|
capture_id: int, |
|
|
image: Optional[Image.Image] = None, |
|
|
confidence: float = 0.0 |
|
|
) -> 'GestureImage': |
|
|
""" |
|
|
Factory method to create a GestureImage from an AI prediction. |
|
|
|
|
|
This is a convenient way to create GestureImage objects without |
|
|
needing to manually look up ranks and emojis. |
|
|
|
|
|
Args: |
|
|
gesture_name: The predicted gesture name |
|
|
capture_id: Unique identifier for this capture |
|
|
image: The original image (optional) |
|
|
confidence: AI confidence score (0.0 to 1.0) |
|
|
|
|
|
Returns: |
|
|
A new GestureImage instance |
|
|
""" |
|
|
return cls( |
|
|
gesture=gesture_name.lower(), |
|
|
rank=GestureRanking.get_rank(gesture_name), |
|
|
emoji=GestureRanking.get_emoji(gesture_name), |
|
|
capture_id=capture_id, |
|
|
image=image, |
|
|
confidence=confidence |
|
|
) |
|
|
|
|
|
@classmethod |
|
|
def create_manual( |
|
|
cls, |
|
|
gesture_name: str, |
|
|
capture_id: int, |
|
|
image: Optional[Image.Image] = None |
|
|
) -> 'GestureImage': |
|
|
""" |
|
|
Create a GestureImage with manual gesture assignment (no AI). |
|
|
Same as create_from_prediction but with 100% confidence. |
|
|
""" |
|
|
return cls.create_from_prediction( |
|
|
gesture_name=gesture_name, |
|
|
capture_id=capture_id, |
|
|
image=image, |
|
|
confidence=1.0 |
|
|
) |
|
|
|