Start version control like a real programmer

This commit is contained in:
544146
2023-09-16 04:35:10 +01:00
parent 7aeb50ebdf
commit bee8f85af6
20 changed files with 12102 additions and 2 deletions
+171
View File
@@ -0,0 +1,171 @@
vite-project/public/output_video.mp4
clicked_points.json
annotated_drs.json
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
DanceDashLaunchHelper/bin/
DanceDashLaunchHelper/.idea/
DanceDashLaunchHelper/obj/
.idea
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
+34
View File
@@ -0,0 +1,34 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: debug-statements
- id: double-quote-string-fixer
- repo: https://github.com/asottile/reorder_python_imports
rev: v3.9.0
hooks:
- id: reorder-python-imports
args: [--py37-plus, --add-import, "from __future__ import annotations"]
- repo: https://github.com/asottile/add-trailing-comma
rev: v2.4.0
hooks:
- id: add-trailing-comma
args: [--py36-plus]
- repo: https://github.com/asottile/pyupgrade
rev: v3.3.2
hooks:
- id: pyupgrade
args: [--py37-plus]
- repo: https://github.com/pre-commit/mirrors-autopep8
rev: v2.0.2
hooks:
- id: autopep8
- repo: https://github.com/PyCQA/flake8
rev: 6.0.0
hooks:
- id: flake8
args:
- --max-line-length=120
@@ -0,0 +1,36 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net46</TargetFramework>
<AssemblyName>DanceDashLaunchHelper</AssemblyName>
<Description>Simple plugin to assist launching Dance Dash without VR</Description>
<Version>1.0.0</Version>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BepInEx.Analyzers" Version="1.*" PrivateAssets="all" />
<PackageReference Include="BepInEx.Core" Version="5.*" />
<PackageReference Include="BepInEx.PluginInfoProps" Version="1.*" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework.TrimEnd(`0123456789`))' == 'net'">
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.2" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<Reference Include="DDVR">
<HintPath>..\Dance Dash_Data\Managed\DDVR.dll</HintPath>
</Reference>
<Reference Include="UnityEngine">
<HintPath>..\Dance Dash_Data\Managed\UnityEngine.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.CoreModule">
<HintPath>..\Dance Dash_Data\Managed\UnityEngine.CoreModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.UI">
<HintPath>..\Dance Dash_Data\Managed\UnityEngine.UI.dll</HintPath>
</Reference>
</ItemGroup>
</Project>
@@ -0,0 +1,4 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/AddReferences/RecentPaths/=D_003A_005Cgames_005Csteamapps_005Ccommon_005CDance_0020Dash_005CDance_0020Dash_005FData_005CManaged_005CDDVR_002Edll/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/AddReferences/RecentPaths/=D_003A_005Cgames_005Csteamapps_005Ccommon_005CDance_0020Dash_005CDance_0020Dash_005FData_005CManaged_005CUnityEngine_002Edll/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/Environment/AssemblyExplorer/XmlDocument/@EntryValue">&lt;AssemblyExplorer /&gt;</s:String></wpf:ResourceDictionary>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="BepInEx" value="https://nuget.bepinex.dev/v3/index.json" />
</packageSources>
</configuration>
+139
View File
@@ -0,0 +1,139 @@
using BepInEx;
using BepInEx.Logging;
using Controller;
using CustomEventHandler;
using HarmonyLib;
using UI.Panel;
using UI.SelectPanel;
using UnityEngine;
using UnityEngine.UI;
namespace DanceDashLaunchHelper
{
[BepInPlugin(PluginInfo.PLUGIN_GUID, PluginInfo.PLUGIN_NAME, PluginInfo.PLUGIN_VERSION)]
public class Plugin : BaseUnityPlugin
{
private void Awake()
{
Logger.LogInfo($"Plugin {PluginInfo.PLUGIN_GUID} is loaded!");
var harmony = new Harmony(PluginInfo.PLUGIN_GUID);
harmony.PatchAll();
}
}
// when we first start the game, we set the ready button to be interactable & click it programmatically
[HarmonyPatch(typeof(ControllerSwitchUI), "Start")]
class ControllerSwitchUI_Start_Patch
{
static bool Prefix(ControllerSwitchUI __instance)
{
Button btn = (Button)Traverse.Create(__instance).Field("readyBTN").GetValue();
Traverse.Create(btn).Field("m_Interactable").SetValue(true);
GameObject hintText = (GameObject)Traverse.Create(__instance).Field("hintTextObj").GetValue();
hintText.SetActive(false);
btn.onClick.Invoke();
return true;
}
}
// we trigger PanelGameSetting.OnToggleHpChange with true to force infinite hp (dont wanna die)
[HarmonyPatch(typeof(PanelGameSetting), "Awake")]
class PanelGameSetting_Awake_Patch
{
static void Postfix(PanelGameSetting __instance)
{
Traverse.Create(__instance).Method("OnToggleHpChange", true).GetValue();
}
}
// this patch is bad, it hijacks init and toggles the albums and invokes them as true, happens to
// toggle on the last album (custom songs) purely based on the order of execution, but again, this sucks
[HarmonyPatch(typeof(VolListItem), "Init")]
class VolListItem_Init_Patch
{
private static ManualLogSource logger = BepInEx.Logging.Logger.CreateLogSource("VolListItem_Init_Patch");
static void Postfix(VolListItem __instance)
{
logger.LogInfo($"Invoking on {__instance} inside VolListItem.Init");
__instance.Invoke(true);
}
}
// hijacks the ost selection happening as a side effect of VolListItem_Init_Patch, and clicks the play button
[HarmonyPatch(typeof(PanelSelect), "OnSelectOst")]
class PanelSelect_OnSelectOst_Patch
{
static void Postfix(PanelSelect __instance)
{
Traverse.Create(__instance).Method("OnClickPlay").GetValue();
}
}
// all of the patches below are purely to avoid errors related to not having a HMD or controllers/trackers
[HarmonyPatch(typeof(PanelSelect), "OnClickPlay")]
class PanelSelect_OnClickPlay_Patch
{
private static ManualLogSource logger = BepInEx.Logging.Logger.CreateLogSource("PanelSelect_OnClickPlay_Patch");
static bool Prefix(PanelSelect __instance)
{
logger.LogInfo($"Inside prefix of PanelSelect.OnClickPlay");
// this is copied from the source, with player controller related things excluded (comments)
PlayerDataManager.Instance.rtr(true);
if (PlayerDataManager.Instance.IsDRSMode)
EventHandlerProxy<EvtCenter, dep>.sck(dep.DRSTriggerChange);
// PlayerController.instance.AllowLaserSwich = false;
// PlayerController.instance.interactiveControlManagement.rsn();
SfxManager.tba();
dhd.tab();
Traverse.Create(__instance).Method("AddStatByMode").GetValue();
dhj.tcf("Play");
return false;
}
}
[HarmonyPatch(typeof(PlayerDataManager), "rtr")]
class PlayerDataManager_rtr_Patch
{
static bool Prefix(PanelSelect __instance, bool a)
{
Traverse.Create(__instance).Field("bvxp").SetValue(a);
return false;
}
}
[HarmonyPatch(typeof(PlayerController), "sze")]
class PlayerController_sze_Patch
{
static bool Prefix(PanelSelect __instance)
{
return false;
}
}
[HarmonyPatch(typeof(PlayerController), "syy")]
class PlayerController_syy_Patch
{
static bool Prefix(PanelSelect __instance, deo a)
{
return false;
}
}
[HarmonyPatch(typeof(dhj), "tcc")]
public static class DhjTccPatch
{
public static bool Prefix(string a, dhj.dhi[] analyticsParams)
{
return false;
}
}
}
+36 -2
View File
@@ -1,2 +1,36 @@
# drs2dd
dance rush stardom chart -> dance dash beat map
# DANCERUSH STARDOM Track Annotator
Convert videos of DANCERUSH STARDOM tracks into a defined data schema.
## Stuff
### Completed:
- [x] Detect right & left notes, including position & timing
- [x] Define models based on dd & drs schemas
- [x] Fetch metadata from data store (https://arcade-songs.zetaraku.dev/drs/)
- [x] Simple BepinEx plugin to launch Dance Dash w/o VR
### To-Do:
- [ ] Detect down and jump notes (?)
- [ ] Determine final data schema
- [ ] Write tests based on total note count (https://remywiki.com/AC_DRS)
---
Setup:
- Install Python 3.10 (pyenv recommended)
- Install the required Python packages
```bash
python -m venv .venv
. .venv/bin/activate
python -m pip install -r requirements.txt
```
- Install pre-commit hooks
```bash
pre-commit install
```
+127
View File
@@ -0,0 +1,127 @@
from __future__ import annotations
from dataclasses import dataclass
from dataclasses import field
LEFT_NOTE = 8
RIGHT_NOTE = 9
@dataclass
class X_Y:
x: float | int
y: float | int
@dataclass
class X_Y_Z(X_Y):
z: float | int
@dataclass
class DDLineNode:
lineGroupId: int
indexInLine: int
isSliding: bool
noteOrder: int
time: float
position: X_Y
position2D: X_Y
size: X_Y_Z
noteType: int
postionOffset: X_Y_Z | None # Yes, postionOffset - not positionOffset.
isPlayAudio: bool
Ticks: int
@dataclass
class DDLSphereNode:
noteOrder: int
time: float
position: X_Y
position2D: X_Y
size: X_Y_Z
noteType: int
postionOffset: dict | None
isPlayAudio: bool
def create_note_sphere(
time: float,
position: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9,
note_order: int,
note_type: LEFT_NOTE | RIGHT_NOTE,
):
return DDLSphereNode(
noteOrder=note_order,
time=time,
position=X_Y(x=position, y=0),
position2D=X_Y(x=0, y=0),
size=X_Y_Z(x=1, y=1, z=1),
noteType=note_type,
postionOffset=None,
isPlayAudio=False,
)
@dataclass
class DDBeatMapData:
name: str
intervalPerSecond: float
gridSize: X_Y
planeSize: X_Y
orderCountPerBeat: int
sphereNodes: list[DDLSphereNode] = field(default_factory=list)
lineNodes: list[DDLineNode] = field(default_factory=list)
effectNodes: list = field(default_factory=list)
roadBlockNodes: list = field(default_factory=list)
trapNodes: list = field(default_factory=list)
@dataclass
class DDBeatMap:
data: DDBeatMapData
beatSubs: int
BPM: int
songStartOffset: float
NPS: str
developerMode: bool
noteSpeed: float
noteJumpOffset: float
interval: float
info: str
@classmethod
def create(
cls,
name: str,
interval_per_second: float,
order_count_per_beat: int,
sphere_nodes: list[DDLSphereNode],
beat_subs: int,
bpm: int,
info: str,
):
return cls(
data=DDBeatMapData(
name=name,
intervalPerSecond=interval_per_second,
gridSize=X_Y(x=0, y=0),
planeSize=X_Y(x=0, y=0),
orderCountPerBeat=order_count_per_beat,
sphereNodes=sphere_nodes,
lineNodes=[],
effectNodes=[],
roadBlockNodes=[],
trapNodes=[],
),
beatSubs=beat_subs,
BPM=bpm,
songStartOffset=-0.4, # -0.4 is arbitrary, figure out a better value. (because we detect notes early)
NPS='0.0',
developerMode=False,
noteSpeed=1.0,
noteJumpOffset=0.0,
interval=1.0,
info=info,
)
+33
View File
@@ -0,0 +1,33 @@
from __future__ import annotations
from dataclasses import dataclass
from dataclasses import field
@dataclass
class DRSSheetData:
type: str
difficulty: str
level: str
levelValue: int
@dataclass
class DRSSongData:
songId: str
category: str
title: str
artist: str
bpm: int
imageName: str
version: str | None = None
releaseDate: str | None = None
isNew: bool | None = None
isLocked: bool = False
sheets: list[DRSSheetData] = field(default_factory=list)
@classmethod
def from_dict(cls, data_dict):
sheets_data = data_dict.get('sheets') or []
sheets = [DRSSheetData(**sheet) for sheet in sheets_data]
return cls(**{**data_dict, 'sheets': sheets})
+156
View File
@@ -0,0 +1,156 @@
from __future__ import annotations
import argparse
import json
import os
from dataclasses import asdict
import cv2
from loguru import logger
from tqdm import tqdm
from model.dancedash import create_note_sphere
from model.dancedash import DDBeatMap
from model.dancedash import LEFT_NOTE
from model.dancedash import RIGHT_NOTE
from util import ORDER_COUNT_PER_BEAT
from util import crop_frame
from util import find_l
from util import find_r
from util import find_stage
from util import get_song_metadata_remote
from util import map_position_to_dd_x
from util import NOTE_SEARCH_AREA
from util import SIGN_SEARCH_AREA
from util import WORKING_RESOLUTION
@logger.catch
def extract_spheres_from_drs_video(video_path: str, bpm: int): # noqa
cap = cv2.VideoCapture(video_path)
if not cap.isOpened():
raise ValueError(f'Unable to open video file: {video_path}')
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
video_fps = int(cap.get(cv2.CAP_PROP_FPS))
duration_seconds = total_frames / video_fps
bps = bpm / 60
pbar = tqdm(total=total_frames, desc='Finding stage')
logger.info(f'Starting to process video: {video_path}')
logger.info(f'Total frames: {total_frames}')
stage_found = False
while not stage_found:
pbar.update(1)
ret, frame = cap.read()
if not ret:
raise ValueError('Unable to find stage.')
frame_number = cap.get(cv2.CAP_PROP_POS_FRAMES)
if int(frame_number % 5) != 0:
continue
frame = cv2.resize(frame, WORKING_RESOLUTION)
search_area = crop_frame(frame, *SIGN_SEARCH_AREA)
stage_found = find_stage(search_area)
pbar.set_description('Finding notes')
frames_since_l = 0
frames_since_r = 0
spheres = []
while True:
pbar.update(1)
ret, frame = cap.read()
if not ret:
break
frame = cv2.resize(frame, WORKING_RESOLUTION)
# frame_number = cap.get(cv2.CAP_PROP_POS_FRAMES)
current_position_seconds = cap.get(cv2.CAP_PROP_POS_MSEC) / 1000.0
# float between 0 and 1 representing the time in the song
progress_percentage = current_position_seconds / duration_seconds
frames_since_l -= 1
frames_since_r -= 1
note_order = int(bps * current_position_seconds * ORDER_COUNT_PER_BEAT)
search_area = crop_frame(frame, *NOTE_SEARCH_AREA)
if frames_since_l < 1:
if left := find_l(search_area):
frames_since_l = 5
note = create_note_sphere( # noqa
progress_percentage,
map_position_to_dd_x(left[0]),
note_order,
LEFT_NOTE,
)
spheres.append(note)
if frames_since_r < 1:
if right := find_r(search_area):
frames_since_r = 5
note = create_note_sphere( # noqa
progress_percentage,
map_position_to_dd_x(right[0]),
note_order,
RIGHT_NOTE,
)
spheres.append(note)
pbar.close()
cap.release()
return spheres
if __name__ == '__main__':
parser = argparse.ArgumentParser(
description='Image and video processing tools',
)
parser.add_argument('video_path', type=str, help='DRS Video path')
parser.add_argument('--song-id', type=str, help='BeatMap ID')
parser.add_argument('--bpm', type=int, help='BPM of the song')
args = parser.parse_args()
if not args.song_id and not args.bpm:
raise ValueError('Must provide either song_id or bpm')
raw_video_name = os.path.basename(args.video_path)
logger.remove() # remove default logger (stdout)
logger.add(
f'{raw_video_name}.log',
backtrace=True,
diagnose=True,
level='DEBUG' if os.getenv('DEBUG') else 'INFO',
)
if song_metadata := get_song_metadata_remote(args.song_id):
name = song_metadata.title
bpm = song_metadata.bpm
elif args.bpm:
name = f'Unknown Song - {raw_video_name}'
bpm = args.bpm
else:
raise ValueError('Unable to find song metadata')
spheres = extract_spheres_from_drs_video(args.video_path, bpm)
beatmap = DDBeatMap.create(
name=name,
interval_per_second=0.0, # todo: what is this.
order_count_per_beat=ORDER_COUNT_PER_BEAT,
sphere_nodes=spheres,
beat_subs=1, # todo: what is this.
bpm=bpm,
info='',
)
beatmap_dict = asdict(beatmap)
with open(f'{raw_video_name}.json', 'w') as f:
json.dump(beatmap_dict, f, indent=4)
logger.info(f'Finished processing video: {args.video_path}')
+10
View File
@@ -0,0 +1,10 @@
pre-commit
pytest~=7.4.0
pytest-mock
python-dotenv
opencv-python~=4.8.0.74
numpy~=1.25.2
Pillow~=10.0.0
loguru
tqdm
requests
+11094
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

View File
View File
+5
View File
@@ -0,0 +1,5 @@
from __future__ import annotations
def test_extract_lines_from_drs_video():
assert True # todo: implement your test here
+171
View File
@@ -0,0 +1,171 @@
from __future__ import annotations
import os
from unittest.mock import Mock
import numpy as np
import pytest
from util import crop_frame
from util import get_song_metadata_remote
from util import L_TEMPLATE
from util import map_position_to_dd_x
from util import match_template
from util import R_TEMPLATE
@pytest.mark.parametrize(
'image, template, threshold, expected_result',
[
(
np.array(L_TEMPLATE),
np.array(L_TEMPLATE),
0.8,
(0, 0, 60, 30),
),
(
np.array(L_TEMPLATE),
np.array(R_TEMPLATE),
0.8,
None,
),
],
)
def test_match_template(image, template, threshold, expected_result):
result = match_template(image, template, threshold)
assert result == expected_result
@pytest.mark.parametrize(
'input_number, expected_output', [
(30, 1), # Below minimum
(31, 1), # Minimum value
(100, 2), # Somewhere in between
(155, 3), # Somewhere in between
(522, 9), # Maximum value
(523, 9), # Above maximum
],
)
def test_map_position_to_dd_x(input_number, expected_output):
result = map_position_to_dd_x(input_number)
assert result == expected_output
def test_get_song_metadata_remote_remote_data(mocker):
mocker.patch.dict(os.environ, {'REMOTE_DATA': 'True'})
mock_requests_get = mocker.patch('requests.get')
response_mock = Mock()
response_mock.json.return_value = _SAMPLE_DRS_JSON_DATA
mock_requests_get.return_value = response_mock
result = get_song_metadata_remote('1')
assert result is not None
assert result.songId == '1'
assert result.title == 'Song 1'
_SAMPLE_DRS_JSON_DATA = {
'songs': [
{
'songId': '1',
'title': 'Song 1',
'category': 'Category 1',
'artist': 'Artist 1',
'bpm': 120,
'imageName': 'song1.jpg',
},
{
'songId': '2',
'title': 'Song 2',
'category': 'Category 2',
'artist': 'Artist 2',
'bpm': 140,
'imageName': 'song2.jpg',
},
],
}
def test_get_song_metadata_remote_local_data(mocker):
mocker.patch.dict(os.environ, {'REMOTE_DATA': 'False'})
result = get_song_metadata_remote('50th Memorial Songs -Beginning Story-')
assert result is not None
assert result.songId == '50th Memorial Songs -Beginning Story-'
assert result.title == '50th Memorial Songs -Beginning Story-'
def test_get_song_metadata_remote_song_id_none(mocker):
mocker.patch.dict(os.environ, {'REMOTE_DATA': 'False'})
result = get_song_metadata_remote('does not exist')
assert result is None
_TEST_FRAME = np.array(
[
[1, 2, 3, 4, 5],
[6, 7, 8, 9, 10],
[11, 12, 13, 14, 15],
[16, 17, 18, 19, 20],
[21, 22, 23, 24, 25],
],
)
@pytest.mark.parametrize(
'x, y, width, height, expected_result', [
(
1, 1, 3, 3, np.array(
[
[7, 8, 9],
[12, 13, 14],
[17, 18, 19],
],
),
),
(
0, 0, 2, 2, np.array(
[
[1, 2],
[6, 7],
],
),
),
(
2, 2, 2, 2, np.array(
[
[13, 14],
[18, 19],
],
),
),
(
3, 3, 3, 3, np.array(
[
[19, 20],
[24, 25],
],
),
),
],
)
def test_crop_frame(x, y, width, height, expected_result):
result = crop_frame(_TEST_FRAME, x, y, width, height)
np.testing.assert_array_equal(result, expected_result)
def test_find_l():
assert True # todo
def test_find_r():
assert True # todo
def test_find_stage():
assert True # todo
+80
View File
@@ -0,0 +1,80 @@
from __future__ import annotations
import json
import os
from functools import partial
import cv2
import numpy as np
import requests
from PIL import Image
from model.dancerush import DRSSongData
ORDER_COUNT_PER_BEAT = 24
DEFAULT_THRESHOLD = 0.8
def match_template(image, template, threshold=DEFAULT_THRESHOLD) -> tuple[int, int, int, int]:
gray_image = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2GRAY)
gray_template = cv2.cvtColor(np.array(template), cv2.COLOR_RGB2GRAY)
result = cv2.matchTemplate(gray_image, gray_template, cv2.TM_CCOEFF_NORMED)
loc = np.where(result >= threshold)
if loc[0].size:
x, y = loc[1][0], loc[0][0]
width, height = gray_template.shape[::-1]
return int(x), int(y), int(width), int(height)
def map_position_to_dd_x(number):
minimum, maximum = 31, 522 # 31 is the leftmost, 522 is the rightmost
if number < minimum:
return 1
elif number > maximum:
return 9
percentage = (number - minimum) / (maximum - minimum)
return int(percentage * 8) + 1
def _get_song_metadata_json():
if os.getenv('REMOTE_DATA'):
return requests.get(DRS_DATA_LINK).json()
return json.load(open('resources/data.json', encoding='utf-8'))
def get_song_metadata_remote(song_id: str) -> DRSSongData | None:
if not song_id:
return None
drs_data = _get_song_metadata_json()
song_id_to_song = {s['songId']: s for s in drs_data['songs']}
if metadata := song_id_to_song.get(song_id):
return DRSSongData.from_dict(metadata)
def crop_frame(frame, x, y, width, height):
cropped_frame = frame[y:y + height, x:x + width]
return cropped_frame
DRS_DATA_LINK = 'https://dp4p6x0xfi5o9.cloudfront.net/drs/data.json'
WORKING_RESOLUTION = 1920, 1080
L_TEMPLATE = Image.open(
'resources/l_template.png',
)
R_TEMPLATE = Image.open(
'resources/r_template.png',
)
NOTE_SEARCH_AREA = 650, 475, 618, 105
find_l = partial(match_template, template=L_TEMPLATE)
find_r = partial(match_template, template=R_TEMPLATE)
SIGN_TEMPLATE = Image.open(
'resources/drs_sign_template.png',
)
SIGN_SEARCH_AREA = 850, 143, 223, 49
find_stage = partial(match_template, template=SIGN_TEMPLATE)