Skip to content
Open
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
Binary file added .DS_Store
Binary file not shown.
60 changes: 32 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,58 +10,62 @@ Download a video of a Synthesia song, run it through the program, and receive th
### Basic Tutorial
> This program is in its early stages so don't expect very high quality output at this time

> If the beat doesn't sound quite right, remove all occurrences of 'z/8' and it might sound better

Program Arguments
```text
(venv) >python -m copycat.main --help

usage: main.py [-h] -f PATH [-k NOTE] [-t TEMPO] [--skip-frames SKIP_FRAMES] [--debug] {manual} ...
usage: main.py [-h] --file PATH [--tempo TEMPO] {manual} ...

positional arguments:
{manual}

required arguments:
-f PATH, --file PATH The path of the mp4 synthesia video
-t TEMPO, --tempo TEMPO The tempo of the piece in BPM

optional arguments:
-h, --help show this help message and exit
```

At the moment, the only supported parsing format is manual,
meaning the program requires more basic information

```text
(venv) >python -m copycat.main manual --help
usage: main.py manual [-h] --bounds X Y WIDTH HEIGHT --first-key NOTE [--skip-frames SKIP_FRAMES] [--detector-line-offset DETECTOR_LINE_OFFSET]
[--min-speed MIN_SPEED] [--debug]

optional arguments:
-h, --help show this help message and exit
-f PATH, --file PATH The path of the mp4 synthesia video
-k NOTE, --first-key NOTE
The first white key in the bounds.
-t TEMPO, --tempo TEMPO
The tempo of the piece in BPM
--bounds X Y WIDTH HEIGHT
The boundaries around the piano keys space separated
--first-key NOTE The first white key in the bounds.
--skip-frames SKIP_FRAMES
How many frame to skip in case there is an introduction
--detector-line-offset DETECTOR_LINE_OFFSET
Number of pixels from the top of the boundary to offset the detector line
--min-speed MIN_SPEED
Defines a minimum note duration
--debug Show debugged version

(venv) F:\Development\copycat>
```

The only required arguments are **bounds** and **first key**.

### Example
Here is an example of a song with the correct parameters running in debug mode (`--debug`)

`main.py --file /tmp/taylor_swift.mp4 --first-key D2 --debug`
`main.py --file /tmp/taylor_swift.mp4 manual --bounds "20 632 1780 200" --first-key A2 --skip-frames 150 --debug`

![](public/debug_example.png)

##### Explained
1. The orange line is the detection line. It is where the key detection actually happens. Make sure no *special effects* are seen at this line.
2. The purple key text has been placed above the respective key.
1. The green rectangle is the `--bounds` property. It should begin at a white key.
2. The orange line is the detection line `--detector-line-offset`. It is where the key detection actually happens. Make sure no *special effects* are seen at this line.
3. The purple key text has been placed above the respective key.

If everything looks good you can run without the `--debug` flag

Example output:
```text
T:
C:
Q: 120
[B,1] [D1] [A1] [G1] [A1] [G1] [D1] [G1] [B,1] %
[D1] [A1] [G1] [A1] [G1] [D1] [G1] [A,1] [D1] [A1] %
[G1] [A1] [G1] [D1] [G1] [A,1] [D1] [A1] [G1] [A1] %
[G1] [D1] [G1] [B,1] [E1] [A1] [G1] [A1] [G1] [E1] %
[G1] [B,1] [E1] [A1] [G1] [A1] [G1] [E1] [G1] [C1] %
[D1] [A1] [G1] [A1] [G1] [D1] [G1] [C1] [D1] [A1] %
[G1] [A1] [G1] [D1] [G1] [^F1] [G1] [G2] [G3] %
[G1] [^F1] [G2] [A2] [G2] [G1] [^F1] [G2] %
[G3] [G1] [G1] [^F1] [G2] [A2] [G1] [^F1] %
[G1] [E8] z2 [G1] [G1] [G1] [^F1] [^F1] [D1] %
```

Take the output string when the program finishes and paste it into your favorite
[abc notation editor](https://www.abcjs.net/abcjs-editor.html)
Empty file added catsite/__init__.py
Empty file.
16 changes: 16 additions & 0 deletions catsite/asgi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""
ASGI config for catsite project.

It exposes the ASGI callable as a module-level variable named ``application``.

For more information on this file, see
https://docs.djangoproject.com/en/4.1/howto/deployment/asgi/
"""

import os

from django.core.asgi import get_asgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'catsite.settings')

application = get_asgi_application()
131 changes: 131 additions & 0 deletions catsite/settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
"""
Django settings for catsite project.

Generated by 'django-admin startproject' using Django 4.1.3.

For more information on this file, see
https://docs.djangoproject.com/en/4.1/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.1/ref/settings/
"""

from pathlib import Path

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-3ptyh_lmj^$0s(+@q^2sougb$aj=aa3mygqdj06^bs5$w81l@3'

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = []


# Application definition

INSTALLED_APPS = [
'copycat.apps.CopycatConfig',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]

MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'catsite.urls'

TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates'],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]

WSGI_APPLICATION = 'catsite.wsgi.application'


# Database
# https://docs.djangoproject.com/en/4.1/ref/settings/#databases

DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}


# Password validation
# https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]


# Internationalization
# https://docs.djangoproject.com/en/4.1/topics/i18n/

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.1/howto/static-files/

import os

STATIC_URL = '/static/'
STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]

MEDIA_URL = '/media/'

MEDIA_ROOT = BASE_DIR / 'media'

# Default primary key field type
# https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
11 changes: 11 additions & 0 deletions catsite/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from django.contrib import admin
from django.urls import path,include
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
path('admin/', admin.site.urls),
path('', include('copycat.urls')),
]

urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
16 changes: 16 additions & 0 deletions catsite/wsgi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""
WSGI config for catsite project.

It exposes the WSGI callable as a module-level variable named ``application``.

For more information on this file, see
https://docs.djangoproject.com/en/4.1/howto/deployment/wsgi/
"""

import os

from django.core.wsgi import get_wsgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'catsite.settings')

application = get_wsgi_application()
4 changes: 4 additions & 0 deletions copycat/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from django.contrib import admin
from .models import Uploadmp4

admin.site.register(Uploadmp4)
6 changes: 6 additions & 0 deletions copycat/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig


class CopycatConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'copycat'
29 changes: 8 additions & 21 deletions copycat/debugging/debugging.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import cv2

from globals.global_types import Image, Bounds
from globals.paino_key import PianoKey, PianoKeyContour
from globals.paino_key import PianoKey


def show_video(video: Iterable[Image]):
Expand All @@ -28,22 +28,13 @@ def show_contours(original_image, contours: List, slideshow=0):
cv2.imshow("contoured", image)


def draw_contours_for_keys(original_image, keys: Iterable[PianoKeyContour]):
def draw_contours_for_keys(original_image, keys: Iterable[PianoKey]):
image = original_image.copy()
for key in keys:
bounds = Bounds(*cv2.boundingRect(key.__contour))
bounds = Bounds(*cv2.boundingRect(key.contour))
cv2.putText(image, key.note, (bounds.x + round(bounds.width / 10), bounds.y + round(bounds.height / 2)),
cv2.FONT_HERSHEY_SIMPLEX, fontScale=0.4, color=(255, 105, 180))
__outline_contour(image, key.__contour)
return image


def draw_notes_for_keys(original_image, keys: Iterable[PianoKey], detection_height):
image = original_image.copy()
for key in keys:
cv2.putText(image, key.note,
(key.section.start + round((key.section.end - key.section.start) / 10), detection_height - 50),
cv2.FONT_HERSHEY_SIMPLEX, fontScale=0.4, color=(255, 105, 180))
__outline_contour(image, key.contour)
return image


Expand Down Expand Up @@ -78,14 +69,10 @@ def __outline_contour(base_image, contour):
return image


def debug_params(control_frame, keys, detector, bounds=None):
image = control_frame.copy()
if bounds is not None:
image = draw_rectangle(image, bounds.x, bounds.y, bounds.width, bounds.height)
def debug_params(control_frame, frames, keys, detector, bounds):
original_image = control_frame.copy()
image = draw_rectangle(original_image, bounds.x, bounds.y, bounds.width, bounds.height)
image = draw_line(image, detector._detection_height)
if isinstance(keys[0], PianoKeyContour):
image = draw_contours_for_keys(image, keys.values())
else:
image = draw_notes_for_keys(image, keys, detector._detection_height)
image = draw_contours_for_keys(image, keys.values())
cv2.imshow("debug", image)
cv2.waitKey()
7 changes: 7 additions & 0 deletions copycat/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from django import forms
from .models import Uploadmp4

class Mp4form(forms.ModelForm):
class Meta:
model = Uploadmp4
fields = ('files',)
10 changes: 2 additions & 8 deletions copycat/globals/color.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,7 @@ def __sub__(self, other):
return sqrt(self.r ** 2 + self.g ** 2 + self.b ** 2) - sqrt(other.r ** 2 + other.g ** 2 + other.b ** 2)

def __str__(self):
try:
return rgb_to_name((self.r, self.g, self.b))
except ValueError:
return f"{self.r},{self.g},{self.b}"

def __repr__(self):
return str(self)
return f"{self.r},{self.g},{self.b}"

def to_tuple(self):
return self.b, self.g, self.r
Expand All @@ -59,5 +53,5 @@ def from_bgr(b: int, g: int, r: int):
return Color(b=b, g=g, r=r)

@staticmethod
def from_rgb(r: int, g: int, b: int):
def from_rbg(r: int, b: int, g: int):
return Color(b=b, g=g, r=r)
2 changes: 1 addition & 1 deletion copycat/globals/global_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from dataclasses import dataclass
from typing import NewType, Tuple

from cv2 import cv2
import cv2
from numpy import ndarray

Image = NewType("Image", ndarray)
Expand Down
2 changes: 1 addition & 1 deletion copycat/globals/math_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,4 @@ def take_closest(thing_list: List[T], thing: T):
if after - thing <= thing - before:
return pos
else:
return pos - 1
return pos - 1
2 changes: 1 addition & 1 deletion copycat/image_processing/image_manipulations.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from cv2 import cv2
import cv2
import numpy as np

from globals.global_types import Image
Expand Down
Loading