Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 45 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,47 @@ Want to predict the result of a wheel spin before the winner is announced? Use t

![preview.gif](./preview.gif)

## How it works

The solution is entirely built on collected heuristics and on standard human behavioral patterns. The implementation relies on screen parsing via OpenCV, text recognition via Tesseract OCR engine and basic math computations.

The program workflow:

1. Detecting the initial spin frame

- The program processes frames sequentially until it finds one that meets the following criteria:
- The current frame contains a wheel (identified as a large circle) and has the text "Winner" above it.
- The next frame shows the wheel in motion (determined by comparing the angular difference between frames).

2. Determining the total spin duration

- The simplest approach is detecting a user-entered duration on the screen (by parsing an image and extracting a numerical value)
- If the duration box is unrecognized, hidden, or spin duration is randomized, a mathematical approach is applied. The program estimates the duration based on observed wheel movement. The wheel spin follows a custom easing function implemented with [GSAP](https://gsap.com/docs/v3/Eases/CustomEase/), which optimizes Bézier curves by generating interpolated `x` and `y` values for improved calculation accuracy. However, this implementation introduces slight deviations near the interpolated points. These deviations help eliminate unsuitable duration candidates, typically narrowing the range to a 1-4 second window. Post-processing further refines the estimate. This approximation is sufficient for our needs at this stage
- The fallback strategy: if all else fails, the program prompts the user for input

3. Calculating the destination angle

Picking a frame from the spinning sequence:

- $x_{i} = \frac{d_{i}}{d}$, where `dᵢ` is the elapsed time, and `d` is the total spin duration
- $y_{i} = \frac{a_{i}}{a}$, where `aᵢ` is the elapsed angular displacement (including full rotations) from initial state, and `a` is the target angle
- $y_{i} = bezier(x_{i})$, where the `bezier` function maps the elapsed time to the corresponding elapsed angle, both normalized to a scale from 0 to 1
- The target angle is computed as: $a = \frac{a_{i}}{bezier(\frac{d_{i}}{d})}$

4. Collecting lot names and their positions on the wheel

- The initial frame is used to determine sector boundaries (collected in format start_angle and end_angle)
- The initial wheel spin is analyzed to extract sector names (obtained from the text displayed above the wheel)

5. Refining calculations for greater accuracy

- Avoiding early-stage angle measurements due to high error margins
- Smoothing data by filtering out spikes, duplicates, and inconsistencies
- Discarding invalid duration candidates based on discrepancies between computed and expected target angles
- Converting the elliptical wheel projection to a circular model for more precise measurements (not yet implemented)
- Extending the spin analysis window to improve text recognition of lot names
- Implementing a voting system to enhance accuracy in determining the winning sector

## Installation

1. Install the project:
Expand All @@ -34,14 +75,17 @@ Want to predict the result of a wheel spin before the winner is announced? Use t

## Run

Run the program before the wheel spin starts:
Run the program before the wheel starts to spin:


```shell
streamlink --twitch-low-latency --stdout <channel link> best | python main.py winner

# or you can use a shorthand
./utils run <channel link>

# or you can analyze your video snippet
cat vod.ts | python main.py winner
```

## Lint
Expand Down
46 changes: 22 additions & 24 deletions config/__init__.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
from .config import (
ANGLE_WINDOW_LEN,
ASK_LENGTH,
ANGLE_WINDOW_SIZE,
PROMPT_FOR_DURATION,
CALCULATION_STEP,
EXCLUDE_SECONDS_RANGE,
FRAMES_STEP_FOR_LENGTH_DETECTION,
LOOK_BEHIND_FRAMES_FOR_LENGTH_DETECTION,
MAX_MEAN_ANGLE_DELTA,
MIN_SKIP_OF_WHEEL_SPIN,
MIN_SKIP_SEC,
NASTY_OPTIMIZATION,
DURATION_DETECTION_TIME_RANGE,
DURATION_DETECTION_FRAME_STEP,
MAX_ANGLE_DELTA,
MIN_WHEEL_SPIN_SKIP_RATIO,
MIN_SKIP_DURATION,
SPECULATIVE_OPTIMIZATION,
READ_STEP,
SPIN_BUFFER_SIZE,
SPIN_BUFFER_SIZE_FOR_LENGTH_DETECTION,
SPIN_DETECT_LENGTH,
TESSERACT_LANG,
DURATION_DETECTION_MAX_SPINS,
DURATION_DETECTION_MAX_FRAMES,
TESSERACT_LANGUAGE,
VISUALIZATION_ENABLED,
)
from .logger_config import add_global_event_time, setup_logger, update_global_event_time
Expand All @@ -23,19 +22,18 @@
"update_global_event_time",
"add_global_event_time",
"READ_STEP",
"ANGLE_WINDOW_LEN",
"ANGLE_WINDOW_SIZE",
"CALCULATION_STEP",
"SPIN_BUFFER_SIZE",
"MIN_SKIP_OF_WHEEL_SPIN",
"MIN_SKIP_SEC",
"MAX_MEAN_ANGLE_DELTA",
"NASTY_OPTIMIZATION",
"SPIN_DETECT_LENGTH",
"SPIN_BUFFER_SIZE_FOR_LENGTH_DETECTION",
"FRAMES_STEP_FOR_LENGTH_DETECTION",
"LOOK_BEHIND_FRAMES_FOR_LENGTH_DETECTION",
"EXCLUDE_SECONDS_RANGE",
"ASK_LENGTH",
"TESSERACT_LANG",
"MIN_WHEEL_SPIN_SKIP_RATIO",
"MIN_SKIP_DURATION",
"MAX_ANGLE_DELTA",
"SPECULATIVE_OPTIMIZATION",
"DURATION_DETECTION_MAX_SPINS",
"DURATION_DETECTION_MAX_FRAMES",
"DURATION_DETECTION_FRAME_STEP",
"DURATION_DETECTION_TIME_RANGE",
"PROMPT_FOR_DURATION",
"TESSERACT_LANGUAGE",
"VISUALIZATION_ENABLED",
]
28 changes: 14 additions & 14 deletions config/config.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
READ_STEP = 2
ANGLE_WINDOW_LEN = 30
CALCULATION_STEP = ANGLE_WINDOW_LEN * 3
ANGLE_WINDOW_SIZE = 30
CALCULATION_STEP = ANGLE_WINDOW_SIZE * 3
SPIN_BUFFER_SIZE = 3
# The close to accurate is 1/4. The most accurate 1/3.
MIN_SKIP_OF_WHEEL_SPIN = 1 / 3
MIN_SKIP_SEC = 10
MAX_MEAN_ANGLE_DELTA = 10.0
TESSERACT_LANG = "rus+eng" # "eng"
# The calculations are most accurate at 1/3 and fairly accurate at 1/4.
MIN_WHEEL_SPIN_SKIP_RATIO = 1 / 3
MIN_SKIP_DURATION = 10
MAX_ANGLE_DELTA = 10.0

NASTY_OPTIMIZATION = True
TESSERACT_LANGUAGE = "rus+eng" # "eng"

SPIN_DETECT_LENGTH = True
SPIN_BUFFER_SIZE_FOR_LENGTH_DETECTION = 10
FRAMES_STEP_FOR_LENGTH_DETECTION = 10
LOOK_BEHIND_FRAMES_FOR_LENGTH_DETECTION = 5
EXCLUDE_SECONDS_RANGE = (30, 180)
ASK_LENGTH = True
SPECULATIVE_OPTIMIZATION = True

DURATION_DETECTION_MAX_SPINS = 10
DURATION_DETECTION_MAX_FRAMES = 600
DURATION_DETECTION_FRAME_STEP = 10
DURATION_DETECTION_TIME_RANGE = (30, 180)

PROMPT_FOR_DURATION = True

VISUALIZATION_ENABLED = False
Loading