Compare commits
6 Commits
f26a722d95
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 17f51e757a | |||
| 4c486199ed | |||
| 1ff888cf34 | |||
| b77383681e | |||
|
|
aaa80bd07d | ||
|
|
723e10ccb3 |
1
.cache
@@ -1 +0,0 @@
|
||||
{"access_token": "BQDeVV_yylewo72n9ZtSMTn1WNhJKSbUBDKv18Cl-QY7dphidwC9jDuGuZWKaIWZv-fVIG1uuP1XvJFKwcj3GOxgRxYJbaWOLtogCHkAxC-iOaZe6uphn-vCy5rbX6Xlx9XVUG-jHNu0jaCf1-ueTRUQuyF0Btu4_aAcIktTnYyofSUJuk_Mdi1J6V3hmARkCl34Rv-4hgfXcIqQ_rpdCFHGheEU1rSL5oME", "token_type": "Bearer", "expires_in": 3600, "scope": "user-modify-playback-state user-read-playback-state", "expires_at": 1757966041, "refresh_token": "AQAxD3wm_8aDlK9w2ZsibKsrPY0SxUJb2UaqiT2ygxK-pEq2ojxPzgLa6DNt9EpP3mjFDCLmGO5glj5fSpR3ZnqEa26belG4EQv5FOeee38ix31o2xuxPbDsY44KR5Uj1KE"}
|
||||
2
boonepeter.github.io-code/.gitignore
vendored
@@ -1,2 +0,0 @@
|
||||
__pycache__
|
||||
.venv
|
||||
@@ -1,12 +0,0 @@
|
||||
# Code snippets
|
||||
|
||||
This repo contains the code snippets I reference in [my blog](https://boonepeter.github.io/).
|
||||
|
||||
## Index
|
||||
|
||||
Code|Blog Post|
|
||||
---|---|
|
||||
[spotify-codes](./spotify-codes)|[How do Spotify Codes Work?](https://boonepeter.github.io/posts/2020-11-10-spotify-codes/)|
|
||||
[spotify-codes-part-2](./spotify-codes-part-2)|[Spotify Codes Part 2](https://boonepeter.github.io/posts/spotify-codes-part-2/)
|
||||
[snapcodes](./snapcodes)|No post yet
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
# Snapcodes
|
||||
|
||||
Using Python 3.10.1
|
||||
|
||||
```bash
|
||||
pip3 install -r requirements.txt
|
||||
```
|
||||
|
||||
Running this command
|
||||
|
||||
```bash
|
||||
python3 img_to_points.py snapcode.png
|
||||
```
|
||||
|
||||
Gets the points from this image:
|
||||
|
||||

|
||||
|
||||
Points:
|
||||
|
||||
```python
|
||||
[1, 2, 3, 4, 6, 9, 14, 18, 20, 21, 22, 23, 27, 28, 29, 30, 32, 38, 39, 41, 43, 44, 45, 49, 53, 58, 59, 62, 64, 65, 66, 67, 70, 71, 73, 76, 77, 83, 87, 90, 93, 94, 96, 99, 100, 101, 103, 104, 105, 106, 107, 108, 109, 110, 112, 113, 115, 118, 119, 120, 130, 131, 134, 140, 141, 144, 145, 146, 150, 153, 155, 157, 159, 161, 163, 169, 170, 171, 172, 173, 174, 175, 178, 182, 183, 184, 185, 188, 190, 192, 193, 196, 197, 202, 204, 206, 207, 209]
|
||||
```
|
||||
|
||||
Points are numbered left to right, top to bottom, starting with 0 and ending at 211.
|
||||
|
||||
And this command
|
||||
|
||||
```bash
|
||||
python3 img_to_svg.py -i snapcode.png -o output.svg
|
||||
```
|
||||
|
||||
Outputs an SVG file:
|
||||
|
||||

|
||||
|
||||
The svg is useful for checking that the point parsing was correct.
|
||||
@@ -1,10 +0,0 @@
|
||||
imageio==2.13.5
|
||||
networkx==2.6.3
|
||||
numpy==1.22.1
|
||||
packaging==21.3
|
||||
Pillow==9.0.0
|
||||
pyparsing==3.0.6
|
||||
PyWavelets==1.2.0
|
||||
scikit-image==0.19.1
|
||||
scipy==1.7.3
|
||||
tifffile==2021.11.2
|
||||
@@ -1,2 +0,0 @@
|
||||
/imgs
|
||||
.ipynb_checkpoints/
|
||||
|
Before Width: | Height: | Size: 200 KiB |
|
Before Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 119 KiB |
@@ -1,305 +0,0 @@
|
||||
import argparse
|
||||
from skimage import io
|
||||
import numpy as np
|
||||
from skimage import io, img_as_ubyte
|
||||
from skimage.filters import threshold_otsu
|
||||
from skimage.color import rgb2gray, rgba2rgb
|
||||
from skimage.measure import label, regionprops
|
||||
|
||||
## BBOX of points from all_dots_yellow.png
|
||||
ALL_BBOX = [
|
||||
[0.025, 0.076, 0.0583, 0.1094],
|
||||
[0.025, 0.1271, 0.0583, 0.1604],
|
||||
[0.025, 0.1781, 0.0583, 0.2115],
|
||||
[0.025, 0.2292, 0.0583, 0.2625],
|
||||
[0.025, 0.2802, 0.0583, 0.3135],
|
||||
[0.025, 0.3302, 0.0583, 0.3635],
|
||||
[0.025, 0.3812, 0.0583, 0.4146],
|
||||
[0.025, 0.4323, 0.0583, 0.4656],
|
||||
[0.025, 0.4833, 0.0583, 0.5167],
|
||||
[0.025, 0.5344, 0.0583, 0.5677],
|
||||
[0.025, 0.5854, 0.0583, 0.6188],
|
||||
[0.025, 0.6365, 0.0583, 0.6698],
|
||||
[0.025, 0.6865, 0.0583, 0.7198],
|
||||
[0.025, 0.7375, 0.0583, 0.7708],
|
||||
[0.025, 0.7885, 0.0583, 0.8219],
|
||||
[0.025, 0.8396, 0.0583, 0.8729],
|
||||
[0.025, 0.8906, 0.0583, 0.924],
|
||||
[0.076, 0.025, 0.1094, 0.0583],
|
||||
[0.076, 0.076, 0.1094, 0.1094],
|
||||
[0.076, 0.1271, 0.1094, 0.1604],
|
||||
[0.076, 0.1781, 0.1094, 0.2115],
|
||||
[0.076, 0.2292, 0.1094, 0.2625],
|
||||
[0.076, 0.2802, 0.1094, 0.3135],
|
||||
[0.076, 0.3302, 0.1094, 0.3635],
|
||||
[0.076, 0.3812, 0.1094, 0.4146],
|
||||
[0.076, 0.4323, 0.1094, 0.4656],
|
||||
[0.076, 0.4833, 0.1094, 0.5167],
|
||||
[0.076, 0.5344, 0.1094, 0.5677],
|
||||
[0.076, 0.5854, 0.1094, 0.6188],
|
||||
[0.076, 0.6365, 0.1094, 0.6698],
|
||||
[0.076, 0.6865, 0.1094, 0.7198],
|
||||
[0.076, 0.7375, 0.1094, 0.7708],
|
||||
[0.076, 0.7885, 0.1094, 0.8219],
|
||||
[0.076, 0.8396, 0.1094, 0.8729],
|
||||
[0.076, 0.8906, 0.1094, 0.924],
|
||||
[0.076, 0.9417, 0.1094, 0.975],
|
||||
[0.1271, 0.025, 0.1604, 0.0583],
|
||||
[0.1271, 0.076, 0.1604, 0.1094],
|
||||
[0.1271, 0.1271, 0.1604, 0.1604],
|
||||
[0.1271, 0.1781, 0.1604, 0.2115],
|
||||
[0.1271, 0.2292, 0.1604, 0.2625],
|
||||
[0.1271, 0.2802, 0.1604, 0.3135],
|
||||
[0.1271, 0.3312, 0.1604, 0.3635],
|
||||
[0.1271, 0.3812, 0.1604, 0.4146],
|
||||
[0.1271, 0.4323, 0.1604, 0.4656],
|
||||
[0.1271, 0.4833, 0.1604, 0.5167],
|
||||
[0.1271, 0.5344, 0.1604, 0.5677],
|
||||
[0.1271, 0.5854, 0.1604, 0.6188],
|
||||
[0.1271, 0.6365, 0.1604, 0.6698],
|
||||
[0.1271, 0.6865, 0.1604, 0.7198],
|
||||
[0.1271, 0.7375, 0.1604, 0.7708],
|
||||
[0.1271, 0.7885, 0.1604, 0.8219],
|
||||
[0.1271, 0.8396, 0.1604, 0.8729],
|
||||
[0.1271, 0.8906, 0.1604, 0.924],
|
||||
[0.1271, 0.9417, 0.1604, 0.975],
|
||||
[0.1781, 0.025, 0.2115, 0.0583],
|
||||
[0.1781, 0.076, 0.2115, 0.1094],
|
||||
[0.1781, 0.1271, 0.2115, 0.1604],
|
||||
[0.1781, 0.1781, 0.2115, 0.2115],
|
||||
[0.1781, 0.2292, 0.2115, 0.2625],
|
||||
[0.1781, 0.2802, 0.2115, 0.3135],
|
||||
[0.1781, 0.6865, 0.2115, 0.7198],
|
||||
[0.1781, 0.7375, 0.2115, 0.7708],
|
||||
[0.1781, 0.7885, 0.2115, 0.8219],
|
||||
[0.1781, 0.8396, 0.2115, 0.8729],
|
||||
[0.1781, 0.8906, 0.2115, 0.924],
|
||||
[0.1781, 0.9417, 0.2115, 0.975],
|
||||
[0.2292, 0.025, 0.2625, 0.0583],
|
||||
[0.2292, 0.076, 0.2625, 0.1094],
|
||||
[0.2292, 0.1271, 0.2625, 0.1604],
|
||||
[0.2292, 0.1781, 0.2625, 0.2115],
|
||||
[0.2292, 0.2292, 0.2625, 0.2625],
|
||||
[0.2292, 0.7375, 0.2625, 0.7708],
|
||||
[0.2292, 0.7885, 0.2625, 0.8219],
|
||||
[0.2292, 0.8396, 0.2625, 0.8729],
|
||||
[0.2292, 0.8906, 0.2625, 0.924],
|
||||
[0.2292, 0.9417, 0.2625, 0.975],
|
||||
[0.2802, 0.025, 0.3135, 0.0583],
|
||||
[0.2802, 0.076, 0.3135, 0.1094],
|
||||
[0.2802, 0.1271, 0.3135, 0.1604],
|
||||
[0.2802, 0.1781, 0.3135, 0.2115],
|
||||
[0.2802, 0.7885, 0.3135, 0.8219],
|
||||
[0.2802, 0.8396, 0.3135, 0.8729],
|
||||
[0.2802, 0.8906, 0.3135, 0.924],
|
||||
[0.2802, 0.9417, 0.3135, 0.975],
|
||||
[0.3302, 0.025, 0.3635, 0.0583],
|
||||
[0.3302, 0.076, 0.3635, 0.1094],
|
||||
[0.3302, 0.1271, 0.3635, 0.1604],
|
||||
[0.3302, 0.8396, 0.3635, 0.8729],
|
||||
[0.3302, 0.8906, 0.3635, 0.924],
|
||||
[0.3302, 0.9417, 0.3635, 0.975],
|
||||
[0.3812, 0.025, 0.4146, 0.0583],
|
||||
[0.3812, 0.076, 0.4146, 0.1094],
|
||||
[0.3812, 0.1271, 0.4146, 0.1604],
|
||||
[0.3812, 0.8396, 0.4146, 0.8729],
|
||||
[0.3812, 0.8906, 0.4146, 0.924],
|
||||
[0.3812, 0.9417, 0.4146, 0.975],
|
||||
[0.4323, 0.025, 0.4656, 0.0583],
|
||||
[0.4323, 0.076, 0.4656, 0.1094],
|
||||
[0.4323, 0.1271, 0.4656, 0.1604],
|
||||
[0.4323, 0.8396, 0.4656, 0.8729],
|
||||
[0.4323, 0.8906, 0.4656, 0.924],
|
||||
[0.4323, 0.9417, 0.4656, 0.975],
|
||||
[0.4833, 0.025, 0.5167, 0.0583],
|
||||
[0.4833, 0.076, 0.5167, 0.1094],
|
||||
[0.4833, 0.1271, 0.5167, 0.1604],
|
||||
[0.4833, 0.8396, 0.5167, 0.8729],
|
||||
[0.4833, 0.8906, 0.5167, 0.924],
|
||||
[0.4833, 0.9417, 0.5167, 0.975],
|
||||
[0.5344, 0.025, 0.5677, 0.0583],
|
||||
[0.5344, 0.076, 0.5677, 0.1094],
|
||||
[0.5344, 0.1271, 0.5677, 0.1604],
|
||||
[0.5344, 0.8396, 0.5677, 0.8729],
|
||||
[0.5344, 0.8906, 0.5677, 0.924],
|
||||
[0.5344, 0.9417, 0.5677, 0.975],
|
||||
[0.5854, 0.025, 0.6188, 0.0583],
|
||||
[0.5854, 0.076, 0.6188, 0.1094],
|
||||
[0.5854, 0.1271, 0.6188, 0.1604],
|
||||
[0.5854, 0.8396, 0.6188, 0.8729],
|
||||
[0.5854, 0.8906, 0.6188, 0.924],
|
||||
[0.5854, 0.9417, 0.6188, 0.975],
|
||||
[0.6365, 0.025, 0.6698, 0.0583],
|
||||
[0.6365, 0.076, 0.6698, 0.1094],
|
||||
[0.6365, 0.1271, 0.6698, 0.1604],
|
||||
[0.6365, 0.8396, 0.6698, 0.8729],
|
||||
[0.6365, 0.8906, 0.6698, 0.924],
|
||||
[0.6365, 0.9417, 0.6698, 0.975],
|
||||
[0.6865, 0.025, 0.7198, 0.0583],
|
||||
[0.6865, 0.076, 0.7198, 0.1094],
|
||||
[0.6865, 0.1271, 0.7198, 0.1604],
|
||||
[0.6865, 0.1781, 0.7198, 0.2115],
|
||||
[0.6865, 0.7885, 0.7198, 0.8219],
|
||||
[0.6865, 0.8396, 0.7198, 0.8729],
|
||||
[0.6865, 0.8906, 0.7198, 0.924],
|
||||
[0.6865, 0.9417, 0.7198, 0.975],
|
||||
[0.7375, 0.025, 0.7708, 0.0583],
|
||||
[0.7375, 0.076, 0.7708, 0.1094],
|
||||
[0.7375, 0.1271, 0.7708, 0.1604],
|
||||
[0.7375, 0.1781, 0.7708, 0.2115],
|
||||
[0.7375, 0.2292, 0.7708, 0.2625],
|
||||
[0.7375, 0.7375, 0.7708, 0.7708],
|
||||
[0.7375, 0.7885, 0.7708, 0.8219],
|
||||
[0.7375, 0.8396, 0.7708, 0.8729],
|
||||
[0.7375, 0.8906, 0.7708, 0.924],
|
||||
[0.7375, 0.9417, 0.7708, 0.975],
|
||||
[0.7885, 0.025, 0.8219, 0.0583],
|
||||
[0.7885, 0.076, 0.8219, 0.1094],
|
||||
[0.7885, 0.1271, 0.8219, 0.1604],
|
||||
[0.7885, 0.1781, 0.8219, 0.2115],
|
||||
[0.7885, 0.2292, 0.8219, 0.2625],
|
||||
[0.7885, 0.2802, 0.8219, 0.3135],
|
||||
[0.7885, 0.6865, 0.8219, 0.7198],
|
||||
[0.7885, 0.7375, 0.8219, 0.7708],
|
||||
[0.7885, 0.7885, 0.8219, 0.8219],
|
||||
[0.7885, 0.8396, 0.8219, 0.8729],
|
||||
[0.7885, 0.8906, 0.8219, 0.924],
|
||||
[0.7885, 0.9417, 0.8219, 0.975],
|
||||
[0.8396, 0.025, 0.8729, 0.0583],
|
||||
[0.8396, 0.076, 0.8729, 0.1094],
|
||||
[0.8396, 0.1271, 0.8729, 0.1604],
|
||||
[0.8396, 0.1781, 0.8729, 0.2115],
|
||||
[0.8396, 0.2292, 0.8729, 0.2625],
|
||||
[0.8396, 0.2802, 0.8729, 0.3135],
|
||||
[0.8396, 0.3312, 0.8729, 0.3635],
|
||||
[0.8396, 0.3812, 0.8729, 0.4146],
|
||||
[0.8396, 0.4323, 0.8729, 0.4656],
|
||||
[0.8396, 0.4833, 0.8729, 0.5167],
|
||||
[0.8396, 0.5344, 0.8729, 0.5677],
|
||||
[0.8396, 0.5854, 0.8729, 0.6188],
|
||||
[0.8396, 0.6365, 0.8729, 0.6698],
|
||||
[0.8396, 0.6865, 0.8729, 0.7198],
|
||||
[0.8396, 0.7375, 0.8729, 0.7708],
|
||||
[0.8396, 0.7885, 0.8729, 0.8219],
|
||||
[0.8396, 0.8396, 0.8729, 0.8729],
|
||||
[0.8396, 0.8906, 0.8729, 0.924],
|
||||
[0.8396, 0.9417, 0.8729, 0.975],
|
||||
[0.8906, 0.025, 0.924, 0.0583],
|
||||
[0.8906, 0.076, 0.924, 0.1094],
|
||||
[0.8906, 0.1271, 0.924, 0.1604],
|
||||
[0.8906, 0.1781, 0.924, 0.2115],
|
||||
[0.8906, 0.2292, 0.924, 0.2625],
|
||||
[0.8906, 0.2802, 0.924, 0.3135],
|
||||
[0.8906, 0.3302, 0.924, 0.3635],
|
||||
[0.8906, 0.3812, 0.924, 0.4146],
|
||||
[0.8906, 0.4323, 0.924, 0.4656],
|
||||
[0.8906, 0.4833, 0.924, 0.5167],
|
||||
[0.8906, 0.5344, 0.924, 0.5677],
|
||||
[0.8906, 0.5854, 0.924, 0.6188],
|
||||
[0.8906, 0.6365, 0.924, 0.6698],
|
||||
[0.8906, 0.6865, 0.924, 0.7198],
|
||||
[0.8906, 0.7375, 0.924, 0.7708],
|
||||
[0.8906, 0.7885, 0.924, 0.8219],
|
||||
[0.8906, 0.8396, 0.924, 0.8729],
|
||||
[0.8906, 0.8906, 0.924, 0.924],
|
||||
[0.8906, 0.9417, 0.924, 0.975],
|
||||
[0.9417, 0.076, 0.975, 0.1094],
|
||||
[0.9417, 0.1271, 0.975, 0.1604],
|
||||
[0.9417, 0.1781, 0.975, 0.2115],
|
||||
[0.9417, 0.2292, 0.975, 0.2625],
|
||||
[0.9417, 0.2802, 0.975, 0.3135],
|
||||
[0.9417, 0.3302, 0.975, 0.3635],
|
||||
[0.9417, 0.3812, 0.975, 0.4146],
|
||||
[0.9417, 0.4323, 0.975, 0.4656],
|
||||
[0.9417, 0.4833, 0.975, 0.5167],
|
||||
[0.9417, 0.5344, 0.975, 0.5677],
|
||||
[0.9417, 0.5854, 0.975, 0.6188],
|
||||
[0.9417, 0.6365, 0.975, 0.6698],
|
||||
[0.9417, 0.6865, 0.975, 0.7198],
|
||||
[0.9417, 0.7375, 0.975, 0.7708],
|
||||
[0.9417, 0.7885, 0.975, 0.8219],
|
||||
[0.9417, 0.8396, 0.975, 0.8729],
|
||||
[0.9417, 0.8906, 0.975, 0.924],
|
||||
]
|
||||
|
||||
def color_crop(im, color=[(250, 255), (245, 255), (0, 10)]):
|
||||
"""Crop an image to the min and max occurances of a color range.
|
||||
Accepts an image and list of colors (channels and colors must match).
|
||||
"""
|
||||
assert len(color) == im.shape[-1]
|
||||
binary = np.ones(im.shape[:-1], dtype=bool)
|
||||
for i in range(3):
|
||||
binary &= (im[:,:,i] >= color[i][0]) & (im[:,:,i] <= color[i][1])
|
||||
labeled = label(binary, background=0)
|
||||
rp = regionprops(labeled)
|
||||
bbox = max(rp, key=lambda x: x.bbox_area).bbox
|
||||
return im[bbox[0]:bbox[2], bbox[1]:bbox[3], :]
|
||||
|
||||
def _get_points(image):
|
||||
im = rgb2gray(image)
|
||||
t = threshold_otsu(im)
|
||||
return im > t
|
||||
|
||||
|
||||
def get_bbox_and_centers(im, color=[(250, 255), (245, 255), (0, 10)]):
|
||||
im = color_crop(im, color=color)
|
||||
p = _get_points(im)
|
||||
l = label(p, background=1)
|
||||
rp = regionprops(l)
|
||||
(maxx, maxy) = l.shape
|
||||
max_size = l.shape[0] * l.shape[1] // 500
|
||||
min_size = l.shape[0] * l.shape[1] // 2000
|
||||
|
||||
bbs = []
|
||||
centers = []
|
||||
for props in rp:
|
||||
minr, minc, maxr, maxc = props.bbox
|
||||
if props.bbox_area < max_size and props.bbox_area > min_size:
|
||||
bbs.append((props.bbox[0] / maxx, props.bbox[1] / maxx, props.bbox[2] / maxx, props.bbox[3] / maxx))
|
||||
centers.append(
|
||||
(
|
||||
(((minr / maxy) + (maxr / maxy)) / 2),
|
||||
(((minc / maxx) + (maxc / maxx)) / 2)
|
||||
),
|
||||
)
|
||||
return bbs, centers
|
||||
|
||||
def get_numbers(centers, bbs):
|
||||
points = []
|
||||
for i, b in enumerate(bbs):
|
||||
for x, y in centers:
|
||||
if x > b[0] and x < b[2] and y > b[1] and y < b[3]:
|
||||
points.append(i)
|
||||
return points
|
||||
|
||||
def img_to_points(filepath, check_counts=False):
|
||||
im = io.imread(filepath)
|
||||
color = [(250, 255), (245, 255), (0, 10)]
|
||||
# convert to 3 channel
|
||||
if im.shape[-1] == 4:
|
||||
im = rgba2rgb(im)
|
||||
# convert to uint8 (0-255)
|
||||
im = img_as_ubyte(im)
|
||||
try:
|
||||
_, centers = get_bbox_and_centers(im, color)
|
||||
except:
|
||||
try:
|
||||
# for some reason, some of the colors are off
|
||||
# when the images are read in
|
||||
color = [(250, 255), (245, 255), (75, 90)]
|
||||
_, centers = get_bbox_and_centers(im, color)
|
||||
except Exception as e:
|
||||
raise e
|
||||
if check_counts and (len(centers) < 50 or len(centers) > 120):
|
||||
raise Exception("Too many or too few points")
|
||||
return get_numbers(centers, ALL_BBOX)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("filepath", type=str)
|
||||
parser.add_argument("--check_counts", action="store_true")
|
||||
args = parser.parse_args()
|
||||
print(img_to_points(args.filepath, args.check_counts))
|
||||
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import argparse
|
||||
from img_to_points import img_to_points
|
||||
from points_to_svg import points_to_svg
|
||||
|
||||
def img_to_svg(filepath, output_filepath):
|
||||
"""
|
||||
Converts an image to an SVG file.
|
||||
"""
|
||||
points = img_to_points(filepath, True)
|
||||
print(points)
|
||||
svg = points_to_svg(points)
|
||||
with open(output_filepath, "w") as f:
|
||||
f.write(svg)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Convert an image to an SVG file.")
|
||||
parser.add_argument("-i", "--input", help="Input filepath", required=True)
|
||||
parser.add_argument("-o", "--output", help="Output filepath", required=True)
|
||||
args = parser.parse_args()
|
||||
img_to_svg(args.input, args.output)
|
||||
@@ -1,300 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns:xlink="http://www.w3.org/1999/xlink" height="1024" version="1.1" viewBox="0 0 1024 1024" width="1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d=" M749.88,641.75 C763.31,649.46,754.85,644.24,749.88,641.75 M0,860.16 C0,950.64,73.36,1024,163.84,1024 L860.16,1024 C950.64,1024,1024,950.64,1024,860.16 L1024,163.84 C1024,73.36,950.64,0,860.16,0 L163.84,0 C73.36,0,0,73.36,0,163.84 L0,860.16" fill="#000" fill-rule="evenodd"/>
|
||||
<path d="M32,163.84 C32,91.03,91.03,32,163.84,32 L860.16,32 C932.97,32,992,91.03,992,163.84 L992,860.16 C992,932.97,932.97,992,860.16,992 L163.84,992 C91.03,992,32,932.97,32,860.16 L32,163.84
|
||||
M170.10500000000002,56.42
|
||||
A16,16,0,0,0,170.10500000000002,88.42
|
||||
A16,16,0,0,0,170.10500000000002,56.42
|
||||
M218.9475,56.42
|
||||
A16,16,0,0,0,218.9475,88.42
|
||||
A16,16,0,0,0,218.9475,56.42
|
||||
M267.79,56.42
|
||||
A16,16,0,0,0,267.79,88.42
|
||||
A16,16,0,0,0,267.79,56.42
|
||||
M316.6325,56.42
|
||||
A16,16,0,0,0,316.6325,88.42
|
||||
A16,16,0,0,0,316.6325,56.42
|
||||
M414.31750000000005,56.42
|
||||
A16,16,0,0,0,414.31750000000005,88.42
|
||||
A16,16,0,0,0,414.31750000000005,56.42
|
||||
M560.845,56.42
|
||||
A16,16,0,0,0,560.845,88.42
|
||||
A16,16,0,0,0,560.845,56.42
|
||||
M805.0575,56.42
|
||||
A16,16,0,0,0,805.0575,88.42
|
||||
A16,16,0,0,0,805.0575,56.42
|
||||
M121.2625,105.2625
|
||||
A16,16,0,0,0,121.2625,137.2625
|
||||
A16,16,0,0,0,121.2625,105.2625
|
||||
M218.9475,105.2625
|
||||
A16,16,0,0,0,218.9475,137.2625
|
||||
A16,16,0,0,0,218.9475,105.2625
|
||||
M267.79,105.2625
|
||||
A16,16,0,0,0,267.79,137.2625
|
||||
A16,16,0,0,0,267.79,105.2625
|
||||
M316.6325,105.2625
|
||||
A16,16,0,0,0,316.6325,137.2625
|
||||
A16,16,0,0,0,316.6325,105.2625
|
||||
M365.475,105.2625
|
||||
A16,16,0,0,0,365.475,137.2625
|
||||
A16,16,0,0,0,365.475,105.2625
|
||||
M560.845,105.2625
|
||||
A16,16,0,0,0,560.845,137.2625
|
||||
A16,16,0,0,0,560.845,105.2625
|
||||
M609.6875,105.2625
|
||||
A16,16,0,0,0,609.6875,137.2625
|
||||
A16,16,0,0,0,609.6875,105.2625
|
||||
M658.53,105.2625
|
||||
A16,16,0,0,0,658.53,137.2625
|
||||
A16,16,0,0,0,658.53,105.2625
|
||||
M707.3725,105.2625
|
||||
A16,16,0,0,0,707.3725,137.2625
|
||||
A16,16,0,0,0,707.3725,105.2625
|
||||
M805.0575,105.2625
|
||||
A16,16,0,0,0,805.0575,137.2625
|
||||
A16,16,0,0,0,805.0575,105.2625
|
||||
M170.10500000000002,154.10500000000002
|
||||
A16,16,0,0,0,170.10500000000002,186.10500000000002
|
||||
A16,16,0,0,0,170.10500000000002,154.10500000000002
|
||||
M218.9475,154.10500000000002
|
||||
A16,16,0,0,0,218.9475,186.10500000000002
|
||||
A16,16,0,0,0,218.9475,154.10500000000002
|
||||
M316.6325,154.10500000000002
|
||||
A16,16,0,0,0,316.6325,186.10500000000002
|
||||
A16,16,0,0,0,316.6325,154.10500000000002
|
||||
M414.31750000000005,154.10500000000002
|
||||
A16,16,0,0,0,414.31750000000005,186.10500000000002
|
||||
A16,16,0,0,0,414.31750000000005,154.10500000000002
|
||||
M463.16,154.10500000000002
|
||||
A16,16,0,0,0,463.16,186.10500000000002
|
||||
A16,16,0,0,0,463.16,154.10500000000002
|
||||
M512.0024999999999,154.10500000000002
|
||||
A16,16,0,0,0,512.0024999999999,186.10500000000002
|
||||
A16,16,0,0,0,512.0024999999999,154.10500000000002
|
||||
M707.3725,154.10500000000002
|
||||
A16,16,0,0,0,707.3725,186.10500000000002
|
||||
A16,16,0,0,0,707.3725,154.10500000000002
|
||||
M902.7425,154.10500000000002
|
||||
A16,16,0,0,0,902.7425,186.10500000000002
|
||||
A16,16,0,0,0,902.7425,154.10500000000002
|
||||
M218.9475,202.9475
|
||||
A16,16,0,0,0,218.9475,234.9475
|
||||
A16,16,0,0,0,218.9475,202.9475
|
||||
M267.79,202.9475
|
||||
A16,16,0,0,0,267.79,234.9475
|
||||
A16,16,0,0,0,267.79,202.9475
|
||||
M756.215,202.9475
|
||||
A16,16,0,0,0,756.215,234.9475
|
||||
A16,16,0,0,0,756.215,202.9475
|
||||
M853.9,202.9475
|
||||
A16,16,0,0,0,853.9,234.9475
|
||||
A16,16,0,0,0,853.9,202.9475
|
||||
M902.7425,202.9475
|
||||
A16,16,0,0,0,902.7425,234.9475
|
||||
A16,16,0,0,0,902.7425,202.9475
|
||||
M951.5849999999999,202.9475
|
||||
A16,16,0,0,0,951.5849999999999,234.9475
|
||||
A16,16,0,0,0,951.5849999999999,202.9475
|
||||
M72.42,251.79000000000002
|
||||
A16,16,0,0,0,72.42,283.79
|
||||
A16,16,0,0,0,72.42,251.79000000000002
|
||||
M218.9475,251.79000000000002
|
||||
A16,16,0,0,0,218.9475,283.79
|
||||
A16,16,0,0,0,218.9475,251.79000000000002
|
||||
M267.79,251.79000000000002
|
||||
A16,16,0,0,0,267.79,283.79
|
||||
A16,16,0,0,0,267.79,251.79000000000002
|
||||
M805.0575,251.79000000000002
|
||||
A16,16,0,0,0,805.0575,283.79
|
||||
A16,16,0,0,0,805.0575,251.79000000000002
|
||||
M951.5849999999999,251.79000000000002
|
||||
A16,16,0,0,0,951.5849999999999,283.79
|
||||
A16,16,0,0,0,951.5849999999999,251.79000000000002
|
||||
M72.42,300.6325
|
||||
A16,16,0,0,0,72.42,332.6325
|
||||
A16,16,0,0,0,72.42,300.6325
|
||||
M902.7425,300.6325
|
||||
A16,16,0,0,0,902.7425,332.6325
|
||||
A16,16,0,0,0,902.7425,300.6325
|
||||
M170.10500000000002,349.475
|
||||
A16,16,0,0,0,170.10500000000002,381.475
|
||||
A16,16,0,0,0,170.10500000000002,349.475
|
||||
M951.5849999999999,349.475
|
||||
A16,16,0,0,0,951.5849999999999,381.475
|
||||
A16,16,0,0,0,951.5849999999999,349.475
|
||||
M170.10500000000002,398.31750000000005
|
||||
A16,16,0,0,0,170.10500000000002,430.31750000000005
|
||||
A16,16,0,0,0,170.10500000000002,398.31750000000005
|
||||
M853.9,398.31750000000005
|
||||
A16,16,0,0,0,853.9,430.31750000000005
|
||||
A16,16,0,0,0,853.9,398.31750000000005
|
||||
M951.5849999999999,398.31750000000005
|
||||
A16,16,0,0,0,951.5849999999999,430.31750000000005
|
||||
A16,16,0,0,0,951.5849999999999,398.31750000000005
|
||||
M170.10500000000002,447.16
|
||||
A16,16,0,0,0,170.10500000000002,479.16
|
||||
A16,16,0,0,0,170.10500000000002,447.16
|
||||
M853.9,447.16
|
||||
A16,16,0,0,0,853.9,479.16
|
||||
A16,16,0,0,0,853.9,447.16
|
||||
M902.7425,447.16
|
||||
A16,16,0,0,0,902.7425,479.16
|
||||
A16,16,0,0,0,902.7425,447.16
|
||||
M72.42,496.0025
|
||||
A16,16,0,0,0,72.42,528.0025
|
||||
A16,16,0,0,0,72.42,496.0025
|
||||
M121.2625,496.0025
|
||||
A16,16,0,0,0,121.2625,528.0025
|
||||
A16,16,0,0,0,121.2625,496.0025
|
||||
M170.10500000000002,496.0025
|
||||
A16,16,0,0,0,170.10500000000002,528.0025
|
||||
A16,16,0,0,0,170.10500000000002,496.0025
|
||||
M853.9,496.0025
|
||||
A16,16,0,0,0,853.9,528.0025
|
||||
A16,16,0,0,0,853.9,496.0025
|
||||
M902.7425,496.0025
|
||||
A16,16,0,0,0,902.7425,528.0025
|
||||
A16,16,0,0,0,902.7425,496.0025
|
||||
M951.5849999999999,496.0025
|
||||
A16,16,0,0,0,951.5849999999999,528.0025
|
||||
A16,16,0,0,0,951.5849999999999,496.0025
|
||||
M72.42,544.845
|
||||
A16,16,0,0,0,72.42,576.845
|
||||
A16,16,0,0,0,72.42,544.845
|
||||
M121.2625,544.845
|
||||
A16,16,0,0,0,121.2625,576.845
|
||||
A16,16,0,0,0,121.2625,544.845
|
||||
M853.9,544.845
|
||||
A16,16,0,0,0,853.9,576.845
|
||||
A16,16,0,0,0,853.9,544.845
|
||||
M902.7425,544.845
|
||||
A16,16,0,0,0,902.7425,576.845
|
||||
A16,16,0,0,0,902.7425,544.845
|
||||
M72.42,593.6875
|
||||
A16,16,0,0,0,72.42,625.6875
|
||||
A16,16,0,0,0,72.42,593.6875
|
||||
M853.9,593.6875
|
||||
A16,16,0,0,0,853.9,625.6875
|
||||
A16,16,0,0,0,853.9,593.6875
|
||||
M902.7425,593.6875
|
||||
A16,16,0,0,0,902.7425,625.6875
|
||||
A16,16,0,0,0,902.7425,593.6875
|
||||
M951.5849999999999,593.6875
|
||||
A16,16,0,0,0,951.5849999999999,625.6875
|
||||
A16,16,0,0,0,951.5849999999999,593.6875
|
||||
M218.9475,691.3725
|
||||
A16,16,0,0,0,218.9475,723.3725
|
||||
A16,16,0,0,0,218.9475,691.3725
|
||||
M805.0575,691.3725
|
||||
A16,16,0,0,0,805.0575,723.3725
|
||||
A16,16,0,0,0,805.0575,691.3725
|
||||
M951.5849999999999,691.3725
|
||||
A16,16,0,0,0,951.5849999999999,723.3725
|
||||
A16,16,0,0,0,951.5849999999999,691.3725
|
||||
M756.215,740.215
|
||||
A16,16,0,0,0,756.215,772.215
|
||||
A16,16,0,0,0,756.215,740.215
|
||||
M805.0575,740.215
|
||||
A16,16,0,0,0,805.0575,772.215
|
||||
A16,16,0,0,0,805.0575,740.215
|
||||
M951.5849999999999,740.215
|
||||
A16,16,0,0,0,951.5849999999999,772.215
|
||||
A16,16,0,0,0,951.5849999999999,740.215
|
||||
M72.42,789.0575
|
||||
A16,16,0,0,0,72.42,821.0575
|
||||
A16,16,0,0,0,72.42,789.0575
|
||||
M121.2625,789.0575
|
||||
A16,16,0,0,0,121.2625,821.0575
|
||||
A16,16,0,0,0,121.2625,789.0575
|
||||
M316.6325,789.0575
|
||||
A16,16,0,0,0,316.6325,821.0575
|
||||
A16,16,0,0,0,316.6325,789.0575
|
||||
M805.0575,789.0575
|
||||
A16,16,0,0,0,805.0575,821.0575
|
||||
A16,16,0,0,0,805.0575,789.0575
|
||||
M902.7425,789.0575
|
||||
A16,16,0,0,0,902.7425,821.0575
|
||||
A16,16,0,0,0,902.7425,789.0575
|
||||
M72.42,837.9
|
||||
A16,16,0,0,0,72.42,869.9
|
||||
A16,16,0,0,0,72.42,837.9
|
||||
M170.10500000000002,837.9
|
||||
A16,16,0,0,0,170.10500000000002,869.9
|
||||
A16,16,0,0,0,170.10500000000002,837.9
|
||||
M267.79,837.9
|
||||
A16,16,0,0,0,267.79,869.9
|
||||
A16,16,0,0,0,267.79,837.9
|
||||
M365.475,837.9
|
||||
A16,16,0,0,0,365.475,869.9
|
||||
A16,16,0,0,0,365.475,837.9
|
||||
M658.53,837.9
|
||||
A16,16,0,0,0,658.53,869.9
|
||||
A16,16,0,0,0,658.53,837.9
|
||||
M707.3725,837.9
|
||||
A16,16,0,0,0,707.3725,869.9
|
||||
A16,16,0,0,0,707.3725,837.9
|
||||
M756.215,837.9
|
||||
A16,16,0,0,0,756.215,869.9
|
||||
A16,16,0,0,0,756.215,837.9
|
||||
M805.0575,837.9
|
||||
A16,16,0,0,0,805.0575,869.9
|
||||
A16,16,0,0,0,805.0575,837.9
|
||||
M853.9,837.9
|
||||
A16,16,0,0,0,853.9,869.9
|
||||
A16,16,0,0,0,853.9,837.9
|
||||
M902.7425,837.9
|
||||
A16,16,0,0,0,902.7425,869.9
|
||||
A16,16,0,0,0,902.7425,837.9
|
||||
M951.5849999999999,837.9
|
||||
A16,16,0,0,0,951.5849999999999,869.9
|
||||
A16,16,0,0,0,951.5849999999999,837.9
|
||||
M170.10500000000002,886.7425
|
||||
A16,16,0,0,0,170.10500000000002,918.7425
|
||||
A16,16,0,0,0,170.10500000000002,886.7425
|
||||
M365.475,886.7425
|
||||
A16,16,0,0,0,365.475,918.7425
|
||||
A16,16,0,0,0,365.475,886.7425
|
||||
M414.31750000000005,886.7425
|
||||
A16,16,0,0,0,414.31750000000005,918.7425
|
||||
A16,16,0,0,0,414.31750000000005,886.7425
|
||||
M463.16,886.7425
|
||||
A16,16,0,0,0,463.16,918.7425
|
||||
A16,16,0,0,0,463.16,886.7425
|
||||
M512.0024999999999,886.7425
|
||||
A16,16,0,0,0,512.0024999999999,918.7425
|
||||
A16,16,0,0,0,512.0024999999999,886.7425
|
||||
M658.53,886.7425
|
||||
A16,16,0,0,0,658.53,918.7425
|
||||
A16,16,0,0,0,658.53,886.7425
|
||||
M756.215,886.7425
|
||||
A16,16,0,0,0,756.215,918.7425
|
||||
A16,16,0,0,0,756.215,886.7425
|
||||
M853.9,886.7425
|
||||
A16,16,0,0,0,853.9,918.7425
|
||||
A16,16,0,0,0,853.9,886.7425
|
||||
M902.7425,886.7425
|
||||
A16,16,0,0,0,902.7425,918.7425
|
||||
A16,16,0,0,0,902.7425,886.7425
|
||||
M170.10500000000002,935.5849999999999
|
||||
A16,16,0,0,0,170.10500000000002,967.5849999999999
|
||||
A16,16,0,0,0,170.10500000000002,935.5849999999999
|
||||
M218.9475,935.5849999999999
|
||||
A16,16,0,0,0,218.9475,967.5849999999999
|
||||
A16,16,0,0,0,218.9475,935.5849999999999
|
||||
M463.16,935.5849999999999
|
||||
A16,16,0,0,0,463.16,967.5849999999999
|
||||
A16,16,0,0,0,463.16,935.5849999999999
|
||||
M560.845,935.5849999999999
|
||||
A16,16,0,0,0,560.845,967.5849999999999
|
||||
A16,16,0,0,0,560.845,935.5849999999999
|
||||
M658.53,935.5849999999999
|
||||
A16,16,0,0,0,658.53,967.5849999999999
|
||||
A16,16,0,0,0,658.53,935.5849999999999
|
||||
M707.3725,935.5849999999999
|
||||
A16,16,0,0,0,707.3725,967.5849999999999
|
||||
A16,16,0,0,0,707.3725,935.5849999999999
|
||||
M805.0575,935.5849999999999
|
||||
A16,16,0,0,0,805.0575,967.5849999999999
|
||||
A16,16,0,0,0,805.0575,935.5849999999999
|
||||
" fill="#FFFC00"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 10 KiB |
@@ -1,78 +0,0 @@
|
||||
import argparse
|
||||
|
||||
positions = [
|
||||
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17],
|
||||
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18],
|
||||
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18],
|
||||
[0, 1, 2, 3, 4, 5, 13, 14, 15, 16, 17, 18],
|
||||
[0, 1, 2, 3, 4, 14, 15, 16, 17, 18],
|
||||
[0, 1, 2, 3, 15, 16, 17, 18],
|
||||
[0, 1, 2, 16, 17, 18],
|
||||
[0, 1, 2, 16, 17, 18],
|
||||
[0, 1, 2, 16, 17, 18],
|
||||
[0, 1, 2, 16, 17, 18],
|
||||
[0, 1, 2, 16, 17, 18],
|
||||
[0, 1, 2, 16, 17, 18],
|
||||
[0, 1, 2, 16, 17, 18],
|
||||
[0, 1, 2, 3, 15, 16, 17, 18],
|
||||
[0, 1, 2, 3, 4, 14, 15, 16, 17, 18],
|
||||
[0, 1, 2, 3, 4, 5, 13, 14, 15, 16, 17, 18],
|
||||
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18],
|
||||
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18],
|
||||
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17],
|
||||
]
|
||||
|
||||
BORDER = 56.42
|
||||
BORDER_C = 72.42
|
||||
MULT = 48.8425
|
||||
DIAMETER = 32
|
||||
SVG = '''<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns:xlink="http://www.w3.org/1999/xlink" height="1024" version="1.1" viewBox="0 0 1024 1024" width="1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d=" M749.88,641.75 C763.31,649.46,754.85,644.24,749.88,641.75 M0,860.16 C0,950.64,73.36,1024,163.84,1024 L860.16,1024 C950.64,1024,1024,950.64,1024,860.16 L1024,163.84 C1024,73.36,950.64,0,860.16,0 L163.84,0 C73.36,0,0,73.36,0,163.84 L0,860.16" fill="#000" fill-rule="evenodd"/>
|
||||
<path d="M32,163.84 C32,91.03,91.03,32,163.84,32 L860.16,32 C932.97,32,992,91.03,992,163.84 L992,860.16 C992,932.97,932.97,992,860.16,992 L163.84,992 C91.03,992,32,932.97,32,860.16 L32,163.84
|
||||
{path}" fill="#FFFC00"/>
|
||||
</svg>
|
||||
'''
|
||||
|
||||
def points_to_svg(points):
|
||||
counter = 0
|
||||
path = ""
|
||||
points.sort()
|
||||
for r in range(len(positions)):
|
||||
for c in range(len(positions[r])):
|
||||
col = positions[r][c]
|
||||
if len(points) == 0:
|
||||
break
|
||||
if points[0] == counter:
|
||||
path += f"""M{col * MULT + BORDER_C},{r * MULT + BORDER}
|
||||
A16,16,0,0,0,{col * MULT + BORDER_C},{r * MULT + BORDER + DIAMETER}
|
||||
A16,16,0,0,0,{col * MULT + BORDER_C},{r * MULT + BORDER}
|
||||
"""
|
||||
points.pop(0)
|
||||
counter += 1
|
||||
return SVG.format(path=path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--points", type=str, help="Points to draw")
|
||||
parser.add_argument("--output", type=str, help="Output file")
|
||||
parser.add_argument("--show", action="store_true", help="Show SVG")
|
||||
parser.set_defaults(points=",".join([str(i) for i in range(212)]))
|
||||
args = parser.parse_args()
|
||||
points = args.points.split(",")
|
||||
points = [int(i) for i in points]
|
||||
svg = points_to_svg(points)
|
||||
if args.output:
|
||||
with open(args.output, "w") as f:
|
||||
f.write(svg)
|
||||
if args.show:
|
||||
import subprocess, os, platform
|
||||
if platform.system() == 'Darwin': # macOS
|
||||
subprocess.call(('open', args.output))
|
||||
elif platform.system() == 'Windows': # Windows
|
||||
os.startfile(args.output)
|
||||
else: # linux variants
|
||||
subprocess.call(('xdg-open', args.output))
|
||||
if not args.show or not args.output:
|
||||
print(svg)
|
||||
@@ -1,15 +0,0 @@
|
||||
import os
|
||||
|
||||
from img_to_svg import img_to_svg
|
||||
|
||||
|
||||
files = os.listdir("./imgs/twitter")
|
||||
files = [os.path.join("./imgs/twitter", f) for f in files if f.endswith(".jpeg")]
|
||||
|
||||
for f in files:
|
||||
output_path = f.replace(".jpeg", ".svg")
|
||||
try:
|
||||
img_to_svg(f, output_path)
|
||||
except Exception as e:
|
||||
print(f"Error processing {f}")
|
||||
print(e)
|
||||
|
Before Width: | Height: | Size: 85 KiB |
@@ -1,78 +0,0 @@
|
||||
from skimage import io
|
||||
from skimage.color import rgb2gray
|
||||
from skimage.filters import threshold_otsu
|
||||
from skimage.measure import label, regionprops
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib.patches as patches
|
||||
from skimage import io
|
||||
from skimage.color import rgb2gray
|
||||
from skimage.filters import threshold_otsu, gaussian
|
||||
from skimage.measure import label, regionprops
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib.patches as patches
|
||||
def get_heights(filename: str) -> list:
|
||||
"""Open an image and return a list of the bar heights.
|
||||
Also saves three images: the processed binary image, the original image with drawings, and a smoothed binary image.
|
||||
"""
|
||||
image = io.imread(filename)
|
||||
im = rgb2gray(image)
|
||||
binary_im = im > threshold_otsu(im)
|
||||
|
||||
# Apply Gaussian smoothing to binary image to make bars look rounder
|
||||
smooth_im = gaussian(binary_im, sigma=2)
|
||||
# Threshold again to obtain a smoothed binary
|
||||
smooth_binary = smooth_im > 0.5
|
||||
labeled = label(smooth_binary)
|
||||
bar_dimensions = [r.bbox for r in regionprops(labeled)]
|
||||
bar_dimensions.sort(key=lambda x: x[1]) # Sort left to right
|
||||
# The first object (spotify logo) is the max height of the bars
|
||||
logo = bar_dimensions[0]
|
||||
max_height = logo[2] - logo[0]
|
||||
sequence = []
|
||||
# Create figure and axis for drawing
|
||||
fig, ax = plt.subplots()
|
||||
ax.imshow(smooth_binary, cmap='gray')
|
||||
# Draw rectangle around logo
|
||||
logo_rect = patches.Rectangle(
|
||||
(logo[1], logo[0]),
|
||||
logo[3]-logo[1],
|
||||
logo[2]-logo[0],
|
||||
linewidth=2,
|
||||
edgecolor='yellow',
|
||||
facecolor='none'
|
||||
)
|
||||
ax.add_patch(logo_rect)
|
||||
# Add 'Logo' text near the rectangle
|
||||
ax.text(logo[1], logo[0] - 10, 'Logo', color='yellow', fontsize=12, weight='bold')
|
||||
# Draw bars and center markers
|
||||
for bar in bar_dimensions[1:]:
|
||||
height = bar[2] - bar[0]
|
||||
ratio = height / max_height
|
||||
ratio *= 8
|
||||
ratio //= 1
|
||||
val = int(ratio - 1)
|
||||
sequence.append(val)
|
||||
# Draw rectangle around bar
|
||||
rect = patches.Rectangle(
|
||||
(bar[1], bar[0]),
|
||||
bar[3]-bar[1],
|
||||
bar[2]-bar[0],
|
||||
linewidth=1,
|
||||
edgecolor='red',
|
||||
facecolor='none'
|
||||
)
|
||||
ax.add_patch(rect)
|
||||
# Draw center marker (white circle)
|
||||
center_x = (bar[1] + bar[3]) / 2
|
||||
center_y = (bar[0] + bar[2]) / 2
|
||||
center_marker = patches.Circle((center_x, center_y), radius=5, color='black')
|
||||
ax.add_patch(center_marker)
|
||||
# Save processed binary image used for calculations
|
||||
plt.imsave('processed_binary_image.png', binary_im, cmap='gray')
|
||||
# Save smoothed binary image
|
||||
plt.imsave('img_smooth.png', smooth_binary, cmap='gray')
|
||||
# Save image with drawings
|
||||
plt.axis('off')
|
||||
plt.savefig('image_with_drawings.png', bbox_inches='tight', pad_inches=0)
|
||||
plt.close()
|
||||
return sequence
|
||||
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
@@ -1,11 +0,0 @@
|
||||
import crc8
|
||||
|
||||
hash = crc8.crc8()
|
||||
media_ref = 67775490487
|
||||
ref_bytes = media_ref.to_bytes(5, byteorder="big")
|
||||
print(ref_bytes)
|
||||
# b'\x0f\xc7\xbb\xe9\xb7'
|
||||
hash.update(ref_bytes)
|
||||
check_bits = hash.digest()
|
||||
print(check_bits)
|
||||
# b'\x0c'
|
||||
@@ -1,52 +0,0 @@
|
||||
from skimage import io
|
||||
from skimage.measure import label, regionprops
|
||||
from skimage.filters import threshold_otsu
|
||||
from skimage.color import rgb2gray
|
||||
|
||||
|
||||
from skimage import io
|
||||
from skimage.color import rgb2gray
|
||||
from skimage.filters import threshold_sauvola
|
||||
from skimage.exposure import equalize_adapthist
|
||||
from skimage.measure import label, regionprops
|
||||
from skimage.morphology import closing, square
|
||||
|
||||
def get_heights(filename: str) -> list:
|
||||
"""Open an image and return a list of the bar heights."""
|
||||
# Load image and convert to grayscale
|
||||
image = io.imread(filename)
|
||||
im = rgb2gray(image)
|
||||
|
||||
# Apply adaptive histogram equalization to enhance contrast
|
||||
im_eq = equalize_adapthist(im, clip_limit=0.03)
|
||||
|
||||
# Adaptive thresholding using Sauvola method
|
||||
window_size = 25 # Adjust window size if needed
|
||||
thresh_sauvola = threshold_sauvola(im_eq, window_size=window_size)
|
||||
binary_im = im_eq > thresh_sauvola
|
||||
|
||||
# Morphological closing to fill gaps and clean noise
|
||||
binary_im = closing(binary_im, square(3))
|
||||
|
||||
# Label connected components
|
||||
labeled = label(binary_im)
|
||||
|
||||
# Get bounding boxes for each detected object
|
||||
bar_dimensions = [r.bbox for r in regionprops(labeled)]
|
||||
|
||||
# Sort bounding boxes by horizontal position (xmin)
|
||||
bar_dimensions.sort(key=lambda x: x[1])
|
||||
|
||||
# Use the first detected object (logo) as max height reference
|
||||
logo = bar_dimensions[0]
|
||||
max_height = logo[2] - logo[0]
|
||||
|
||||
sequence = []
|
||||
for bar in bar_dimensions[1:]:
|
||||
height = bar[2] - bar[0]
|
||||
ratio = height / max_height
|
||||
ratio *= 8
|
||||
ratio = int(ratio) # truncate decimal part
|
||||
sequence.append(ratio - 1)
|
||||
|
||||
return sequence
|
||||
19
final-loop/Dockerfile
Normal file
@@ -0,0 +1,19 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install only packages that exist in Debian Trixie
|
||||
RUN apt-get update && apt-get install -y \
|
||||
libgl1 \
|
||||
libglib2.0-0 \
|
||||
libsm6 \
|
||||
libxext6 \
|
||||
libxrender1 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY requirements.txt ./
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY src/ .
|
||||
1
final-loop/client-cache/credentials.json
Normal file
@@ -0,0 +1 @@
|
||||
{"username":"mathias-120","auth_type":1,"auth_data":"QWdDZF9iOVZDTUltQ0Noc0lnSFdfNU5QOXBMdFdiQllFclJ3dHM3ZVRaY1lzUEpKd1Jwc1pOc3FzdjFwYnRYZ2ZRSXE2Q1ZiWTBjdzRDTUYzdkJMekdqOVliRTh1UldlZlJOdF94eEgydzJUWmhtT0RlX295ODdrTzZDYXFuMW5SVEQ4TV95S2R0aEJLTDZldkxPeDlva0wtcHJNY0p6Q3VzQl9YOGFhVHNYaw=="}
|
||||
1
final-loop/client-cache/volume
Normal file
@@ -0,0 +1 @@
|
||||
65535
|
||||
40
final-loop/compose.yml
Normal file
@@ -0,0 +1,40 @@
|
||||
services:
|
||||
librespot:
|
||||
container_name: spotvinyl-client
|
||||
image: giof71/librespot:latest
|
||||
network_mode: host
|
||||
devices:
|
||||
- /dev/snd:/dev/snd
|
||||
environment:
|
||||
- DEVICE=hw:x20,0
|
||||
- BACKEND=alsa
|
||||
- BITRATE=320
|
||||
- INITIAL_VOLUME=100
|
||||
- DEVICE_NAME=spotvinyl
|
||||
- ENABLE_OAUTH=headless # Enables OAuth headless mode
|
||||
- ENABLE_SYSTEM_CACHE=Y # Cache token for persistent login
|
||||
volumes:
|
||||
- ./client-cache:/data/system-cache
|
||||
stdin_open: true
|
||||
tty: true
|
||||
|
||||
spotvinyl:
|
||||
build: .
|
||||
container_name: spotvinyl
|
||||
devices:
|
||||
- /dev/video0:/dev/video0
|
||||
- /dev/video1:/dev/video1
|
||||
- /dev/video2:/dev/video2
|
||||
- /dev/video3:/dev/video3
|
||||
|
||||
volumes:
|
||||
- ./src:/app
|
||||
# - ./spotvinyl-cache
|
||||
working_dir: /app
|
||||
environment:
|
||||
- PYTHONUNBUFFERED=1
|
||||
env_file:
|
||||
- .env
|
||||
command: python3 -u decode_barcode.py
|
||||
ports:
|
||||
- "5000:5000"
|
||||
43
final-loop/requirements.txt
Normal file
@@ -0,0 +1,43 @@
|
||||
blinker==1.9.0
|
||||
certifi==2025.11.12
|
||||
chardet==5.2.0
|
||||
charset-normalizer==3.4.4
|
||||
click==8.3.1
|
||||
contourpy==1.3.3
|
||||
crccheck==1.3.1
|
||||
cycler==0.12.1
|
||||
decorator==5.2.1
|
||||
Flask==3.1.2
|
||||
fonttools==4.60.1
|
||||
greenlet==3.2.4
|
||||
idna==3.11
|
||||
ImageIO==2.37.2
|
||||
itsdangerous==2.2.0
|
||||
Jinja2==3.1.6
|
||||
joblib==1.5.2
|
||||
kiwisolver==1.4.9
|
||||
lazy_loader==0.4
|
||||
MarkupSafe==3.0.3
|
||||
matplotlib==3.10.7
|
||||
networkx==3.5
|
||||
numpy==2.2.6
|
||||
opencv-python==4.12.0.88
|
||||
packaging==25.0
|
||||
pillow==12.0.0
|
||||
playwright==1.56.0
|
||||
pyee==13.0.0
|
||||
pyparsing==3.2.5
|
||||
python-dateutil==2.9.0.post0
|
||||
PyWavelets==1.9.0
|
||||
redis==7.1.0
|
||||
requests==2.32.5
|
||||
scikit-image==0.25.2
|
||||
scikit-learn==1.7.2
|
||||
scipy==1.16.3
|
||||
six==1.17.0
|
||||
spotipy==2.25.1
|
||||
threadpoolctl==3.6.0
|
||||
tifffile==2025.10.16
|
||||
typing_extensions==4.15.0
|
||||
urllib3==2.5.0
|
||||
Werkzeug==3.1.3
|
||||
1
final-loop/src/.cache
Normal file
@@ -0,0 +1 @@
|
||||
{"access_token": "BQDrlnB0kiEzwgXmPTK1_c4IfsmaAxTBhIy6XdV8WLETUahlOzbqZjsCnt75YR-IyPUplWlHpcxStmm_yw3IyGhu6cLIuhX1A1tfndQhNERescv7pkW9vFOx2VNhSq_u4z66_C0cKeC1zxGrB5mP03YQfOgC1jTdT6qzfThHd1_XJ_c5LogWiWNH8B8lxaKEmVi-sVJKldynGTuj6nU3HA6G8jYef1YGK_34f1qwsLYOcYUN0tjfUA", "token_type": "Bearer", "expires_in": 3600, "scope": "user-modify-playback-state user-read-playback-state", "expires_at": 1765023017, "refresh_token": "AQAUKbWNL0mkEiOaoWfodsTnE2r8pqocU7XCRq6MvSLXlJGU5Wc6QaTRgL24EIVVOvjBzucYbMQ571v-GzbTCK02EDQfLBgWcslm9RnwtfVScGCYvl1mpzSJF0cK-I9OIUo"}
|
||||
85
final-loop/src/decode_barcode.py
Normal file
@@ -0,0 +1,85 @@
|
||||
import argparse
|
||||
import pprint
|
||||
from get_heights import get_heights
|
||||
from encode_decode import spotify_bar_decode
|
||||
from get_uri import get_uri, get_info
|
||||
import cv2
|
||||
from start_song import start_song
|
||||
import os
|
||||
from flask import Flask, Response
|
||||
import threading
|
||||
import time
|
||||
import numpy as np
|
||||
|
||||
os.environ["QT_QPA_PLATFORM"] = "offscreen"
|
||||
os.makedirs("/app/img_cache", exist_ok=True) # create folder if missing
|
||||
def process_frame(frame):
|
||||
|
||||
heights,preprocess = get_heights(frame)
|
||||
cv2.imwrite("/app/img_cache/frame.jpg", preprocess)
|
||||
if len(heights)!=23:
|
||||
return None # skip bad frames
|
||||
else:
|
||||
print("ON TROUVE UN CODE")
|
||||
if heights == [0, 4, 2, 6, 5, 7, 2, 3, 1, 5, 5, 7, 4, 6, 4, 1, 2, 0, 6, 7, 3, 5, 0]:
|
||||
print("VICTOIREEEEEEE")
|
||||
|
||||
heights = heights[1:11] + heights[12:-1]
|
||||
decoded = spotify_bar_decode(heights)
|
||||
if decoded == -1:
|
||||
return None
|
||||
uri = get_uri(decoded)
|
||||
print(uri)
|
||||
if uri:
|
||||
start_song(uri,os.getenv('DEVICE_ID'))
|
||||
return summary
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
|
||||
|
||||
cap = cv2.VideoCapture('/dev/video2')
|
||||
|
||||
|
||||
|
||||
|
||||
if not cap.isOpened():
|
||||
print("Error: Could not open camera.")
|
||||
exit(1)
|
||||
else:
|
||||
print("Camera is working and ready to capture frames.")
|
||||
|
||||
while True:
|
||||
ret, frame = cap.read()
|
||||
if not ret:
|
||||
break
|
||||
# Crop same as streaming for consistency
|
||||
height, width, _ = frame.shape
|
||||
margin = 100
|
||||
frame = frame[margin:height - margin, margin:width - margin]
|
||||
|
||||
try:
|
||||
|
||||
|
||||
summary = process_frame(frame)
|
||||
|
||||
except Exception as e:
|
||||
print(e)
|
||||
print("")
|
||||
continue
|
||||
# cv2.imshow("preframe", frame)
|
||||
|
||||
if cv2.waitKey(1) & 0xFF == ord('q'):
|
||||
break
|
||||
|
||||
cap.release()
|
||||
cv2.destroyAllWindows()
|
||||
70
final-loop/src/get_heights.py
Normal file
@@ -0,0 +1,70 @@
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
from skimage import io
|
||||
from skimage.filters import threshold_otsu, gaussian
|
||||
from skimage.morphology import closing, square
|
||||
from skimage.measure import label, regionprops
|
||||
from skimage.color import rgb2gray
|
||||
import matplotlib.patches as patches
|
||||
from sklearn.cluster import KMeans
|
||||
import warnings
|
||||
from skimage.morphology import remove_small_objects
|
||||
from skimage.transform import resize
|
||||
from skimage import exposure
|
||||
from skimage.morphology import erosion, square
|
||||
import cv2
|
||||
from skimage.filters import threshold_local
|
||||
from scipy import ndimage as ndi
|
||||
from skimage.segmentation import watershed
|
||||
from skimage.morphology import opening, rectangle
|
||||
from scipy.ndimage import gaussian_filter
|
||||
|
||||
warnings.filterwarnings("ignore", category=FutureWarning, message=".*square.*deprecated.*")
|
||||
|
||||
#### keep this. If does not work, remove black and reaply the logic only to not-black part of the picture
|
||||
def get_heights(image):
|
||||
# image = io.imread(filename)
|
||||
|
||||
|
||||
im = rgb2gray(image)
|
||||
im_eq = exposure.equalize_adapthist(im, clip_limit=0.01)
|
||||
# Filtrage gaussien léger pour diminuer bruit
|
||||
im_blur = gaussian_filter(im_eq, sigma=1)
|
||||
|
||||
# Calcul du seuil local avec block_size impair, ajuster offset si besoin
|
||||
th = threshold_local(im_blur, block_size=31, offset=-0.14)
|
||||
|
||||
binary_im = im_blur > th
|
||||
|
||||
separated = binary_im
|
||||
labeled = label(separated)
|
||||
|
||||
bar_dims = [r.bbox for r in regionprops(labeled)]
|
||||
bar_dims.sort(key=lambda x: x[1]) # left to right
|
||||
|
||||
bars = bar_dims[1:] # skip logo
|
||||
bar_heights_raw = []
|
||||
if(len(bars)!=23):
|
||||
print(len(bars))
|
||||
return [0], (separated.astype("uint8") * 255) # convert to 0/255 uint8
|
||||
|
||||
for bar in bars:
|
||||
top, left, bottom, right = bar
|
||||
effective_height = bottom - top # use bounding box height directly
|
||||
bar_heights_raw.append(effective_height)
|
||||
|
||||
# Cluster measured heights to 8 clusters representing discrete bar levels
|
||||
bar_heights_raw_np = np.array(bar_heights_raw).reshape(-1, 1)
|
||||
|
||||
kmeans = KMeans(n_clusters=8, random_state=0).fit(bar_heights_raw_np)
|
||||
cluster_centers = np.sort(kmeans.cluster_centers_.flatten())
|
||||
|
||||
# Assign each bar height to closest cluster center index (0 to 7)
|
||||
predicted_levels = []
|
||||
for h in bar_heights_raw:
|
||||
diffs = np.abs(cluster_centers - h)
|
||||
closest_cluster = np.argmin(diffs)
|
||||
predicted_levels.append(int(closest_cluster))
|
||||
|
||||
print(predicted_levels)
|
||||
return predicted_levels, (separated.astype("uint8") * 255)
|
||||
137
final-loop/src/get_uri.py
Normal file
@@ -0,0 +1,137 @@
|
||||
from typing import Tuple
|
||||
import requests
|
||||
import spotipy
|
||||
from spotipy.oauth2 import SpotifyOAuth
|
||||
import sys
|
||||
from urllib.parse import urlencode
|
||||
from urllib.parse import urlencode, urlparse, parse_qs
|
||||
import webbrowser
|
||||
import time
|
||||
import base64
|
||||
|
||||
|
||||
CLIENT_ID = 'a1b29f64bef643b5ade0944830637510'
|
||||
CLIENT_SECRET = '1d74196e6fec41f9986917afd57df3da'
|
||||
REDIRECT_URI = 'https://vinyly.couraud.xyz'
|
||||
SCOPE = 'user-modify-playback-state user-read-playback-state'
|
||||
TOKEN_FILE = 'spotify_token.json'
|
||||
import json
|
||||
|
||||
def get_spotify_token():
|
||||
# Try to load existing token
|
||||
try:
|
||||
with open(TOKEN_FILE) as f:
|
||||
data = json.load(f)
|
||||
if data['expires_at'] > time.time():
|
||||
return data['access_token']
|
||||
# refresh token
|
||||
resp = requests.post(
|
||||
'https://accounts.spotify.com/api/token',
|
||||
data={'grant_type': 'refresh_token', 'refresh_token': data['refresh_token']},
|
||||
headers={'Authorization': 'Basic ' + base64.b64encode(f"{CLIENT_ID}:{CLIENT_SECRET}".encode()).decode()}
|
||||
).json()
|
||||
data['access_token'] = resp['access_token']
|
||||
data['expires_at'] = time.time() + resp['expires_in']
|
||||
with open(TOKEN_FILE, 'w') as f2:
|
||||
json.dump(data, f2)
|
||||
return data['access_token']
|
||||
except FileNotFoundError:
|
||||
# Get authorization code
|
||||
auth_url = 'https://accounts.spotify.com/authorize?' + urlencode({
|
||||
'client_id': CLIENT_ID,
|
||||
'response_type': 'code',
|
||||
'redirect_uri': REDIRECT_URI,
|
||||
'scope': SCOPE
|
||||
})
|
||||
print("Open this URL and authorize:", auth_url)
|
||||
webbrowser.open(auth_url)
|
||||
code = input("Paste the full redirected URL here: ")
|
||||
code = parse_qs(urlparse(code).query)['code'][0]
|
||||
|
||||
# Exchange code for tokens
|
||||
resp = requests.post(
|
||||
'https://accounts.spotify.com/api/token',
|
||||
data={
|
||||
'grant_type': 'authorization_code',
|
||||
'code': code,
|
||||
'redirect_uri': REDIRECT_URI
|
||||
},
|
||||
headers={'Authorization': 'Basic ' + base64.b64encode(f"{CLIENT_ID}:{CLIENT_SECRET}".encode()).decode()}
|
||||
).json()
|
||||
|
||||
data = {
|
||||
'access_token': resp['access_token'],
|
||||
'refresh_token': resp['refresh_token'],
|
||||
'expires_at': time.time() + resp['expires_in']
|
||||
}
|
||||
with open(TOKEN_FILE, 'w') as f:
|
||||
json.dump(data, f)
|
||||
return data['access_token']
|
||||
|
||||
|
||||
HEADERS_LUT = {
|
||||
"X-Client-Id": "58bd3c95768941ea9eb4350aaa033eb3",
|
||||
"Accept-Encoding": "gzip, deflate",
|
||||
"Connection": "close",
|
||||
"App-Platform": "iOS",
|
||||
"Accept": "*/*",
|
||||
"User-Agent": "Spotify/8.5.68 iOS/13.4 (iPhone9,3)",
|
||||
"Accept-Language": "en",
|
||||
"Spotify-App-Version": "8.5.68",
|
||||
}
|
||||
|
||||
MEDIA_REF_LUT_URL = "https://spclient.wg.spotify.com:443/scannable-id/id"
|
||||
|
||||
auth_url = "https://accounts.spotify.com/authorize?" + urlencode({
|
||||
"client_id": CLIENT_ID,
|
||||
"response_type": "code",
|
||||
"redirect_uri": REDIRECT_URI,
|
||||
"scope": SCOPE
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
def get_uri(media_ref: int):
|
||||
|
||||
with open("/app/songs.json") as f:
|
||||
code_to_url = json.load(f)
|
||||
|
||||
uri = code_to_url.get(str(media_ref))
|
||||
if uri:
|
||||
return uri
|
||||
else:
|
||||
return -1
|
||||
|
||||
|
||||
def get_info(uri: str, token: str) -> Tuple[dict, dict]:
|
||||
"""Query the Spotify API to get information about a URI."""
|
||||
info_headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
"Authorization": f"Bearer {token}"
|
||||
}
|
||||
split = uri.split(":")
|
||||
content_type = split[-2] + "s"
|
||||
id = split[-1]
|
||||
response = requests.get(f"https://api.spotify.com/v1/{content_type}/{id}", headers=info_headers)
|
||||
response.raise_for_status()
|
||||
resp = response.json()
|
||||
result = {
|
||||
"name": resp["name"],
|
||||
"type": split[-2],
|
||||
"url": resp["external_urls"]["spotify"],
|
||||
}
|
||||
if "artists" in resp:
|
||||
result['artists'] = []
|
||||
for a in resp['artists']:
|
||||
result["artists"].append(a["name"])
|
||||
if "album" in resp:
|
||||
result['album'] = resp['album']['name']
|
||||
|
||||
if "description" in resp:
|
||||
result["description"] = resp['description']
|
||||
|
||||
return result, resp
|
||||
|
||||
|
||||
BIN
final-loop/src/img_cache/frame.jpg
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
2
final-loop/src/songs.json
Normal file
@@ -0,0 +1,2 @@
|
||||
{"6818913446" : "spotify:track:0RoA7ObU6phWpqhlC9zH4Z"}
|
||||
|
||||
@@ -9,7 +9,7 @@ REDIRECT_URI = 'https://vinyly.couraud.xyz'
|
||||
SCOPE = 'user-modify-playback-state user-read-playback-state'
|
||||
|
||||
|
||||
def main():
|
||||
def start_song(URI="spotify:track:0RoA7ObU6phWpqhlC9zH4Z",device_id="2f97220989834a8d84e8d93860444601fde25f44"):
|
||||
auth_manager = SpotifyOAuth(client_id=CLIENT_ID,
|
||||
client_secret=CLIENT_SECRET,
|
||||
redirect_uri=REDIRECT_URI,
|
||||
@@ -26,28 +26,21 @@ def main():
|
||||
|
||||
sp = spotipy.Spotify(auth_manager=auth_manager)
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
print('Usage: python play_spotify.py "song name"')
|
||||
sys.exit(1)
|
||||
|
||||
song_name = ' '.join(sys.argv[1:])
|
||||
|
||||
results = sp.search(q=song_name, type='track', limit=1)
|
||||
if not results['tracks']['items']:
|
||||
print(f'No track found for: {song_name}')
|
||||
sys.exit(1)
|
||||
|
||||
track_uri = results['tracks']['items'][0]['uri']
|
||||
|
||||
devices = sp.devices()
|
||||
if not devices['devices']:
|
||||
print('No active Spotify devices found. Please start playback on a device.')
|
||||
sys.exit(1)
|
||||
|
||||
active_device_id = devices['devices'][0]['id']
|
||||
print(devices['devices'])
|
||||
current = sp.current_user_playing_track()
|
||||
current_uri = ""
|
||||
if current and current.get("item"):
|
||||
current_uri = current["item"]["uri"]
|
||||
|
||||
sp.start_playback(device_id=active_device_id, uris=[track_uri])
|
||||
print(f'Playing "{song_name}" on device ID: {active_device_id}')
|
||||
if current_uri != URI:
|
||||
print(URI)
|
||||
sp.start_playback(device_id=device_id, uris=[URI])
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
start_song()
|
||||
BIN
mid_image.jpg
|
Before Width: | Height: | Size: 468 KiB |
1
old/.cache
Normal file
@@ -0,0 +1 @@
|
||||
{"access_token": "BQBR3Dnu7QMSOnq8G_8lOiz858WKJmMxiCgskqjp3QDu17EcQWx3Y8ALVQGSXzUcTgkJb-z5oHjJxEQJNfz8tSYREfJGGMKQfoCo0P2tCUbnB0wdGm7iSmWOlYuvrnDTVleJ9nJBGG6vVGc2ZEIl43T54_TmKbkPzampV8FuuGIATUvzbjRO6uEHqzc3ImuBELCZJjj0n8-h4zImXifkJP6pssEY76_GWgKo_HVx", "token_type": "Bearer", "expires_in": 3600, "scope": "user-modify-playback-state user-read-playback-state", "expires_at": 1763737778, "refresh_token": "AQAxD3wm_8aDlK9w2ZsibKsrPY0SxUJb2UaqiT2ygxK-pEq2ojxPzgLa6DNt9EpP3mjFDCLmGO5glj5fSpR3ZnqEa26belG4EQv5FOeee38ix31o2xuxPbDsY44KR5Uj1KE"}
|
||||
@@ -69,10 +69,11 @@ def image_for_mathias(name="1.jpg"):
|
||||
|
||||
# Show result
|
||||
cv2.imshow("Spotify Code Deskewed", warped)
|
||||
|
||||
cv2.waitKey(0)
|
||||
cv2.destroyAllWindows()
|
||||
|
||||
|
||||
l = ["1.jpg", "2.jpg", "3.jpg", "4.jpg"]
|
||||
l = ["rebirth.jpg"]
|
||||
for name in l:
|
||||
image_for_mathias(name)
|
||||
BIN
old/rebirth.jpg
Normal file
|
After Width: | Height: | Size: 240 KiB |
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 188 KiB After Width: | Height: | Size: 188 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 129 KiB After Width: | Height: | Size: 129 KiB |
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 9.5 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 3.1 MiB After Width: | Height: | Size: 3.1 MiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
31
old/spotify-codes-part-2/src/crc.py
Normal file
@@ -0,0 +1,31 @@
|
||||
|
||||
def crc(data, polynomial):
|
||||
n = len(polynomial) - 1
|
||||
initial_length = len(data)
|
||||
check_bits = data + [0] * n
|
||||
for i in range(initial_length):
|
||||
if check_bits[i] == 1:
|
||||
for j, p in enumerate(polynomial):
|
||||
check_bits[i + j] = check_bits[i + j] ^ p
|
||||
return check_bits[-n:]
|
||||
|
||||
def check_crc(data, polynomial, check_bits):
|
||||
full_data = data + check_bits
|
||||
for i in range(len(data)):
|
||||
if full_data[i] == 1:
|
||||
for j, p in enumerate(polynomial):
|
||||
full_data[i + j] = full_data[i + j] ^ p
|
||||
return 1 not in full_data
|
||||
data = [0,0,1,0,0,0,1,0,1,1,1,1,0,1,1,1,1,0,0,0,1,1,1,1,0,1,1,0,1,0,1,1,0,0,0,0,1,1,0,1]
|
||||
check = [1,1,0,0,1,1,0,0]
|
||||
poly = [1,0,0,0,0,0,1,1,1]
|
||||
print(check_crc(data, poly, check))
|
||||
|
||||
if __name__ == "__main__":
|
||||
example = "0010001011110111100011110110101100001101"
|
||||
long = [int(i) for i in example]
|
||||
polynomial = [1, 0, 0, 0, 0, 0, 1, 1, 1] # crc8 polynomial
|
||||
check = crc(long, polynomial)
|
||||
print(f"Check bits: {check}")
|
||||
checked = check_crc(long, polynomial, check)
|
||||
print(f"Checked: {checked}")
|
||||
@@ -17,7 +17,6 @@ if __name__ == "__main__":
|
||||
filename = args.filename
|
||||
token = args.token
|
||||
heights = get_heights(filename)
|
||||
print(f"Heights: {heights}")
|
||||
# drop the fist, last, and 12th bar
|
||||
heights = heights[1:11] + heights[12:-1]
|
||||
decoded = spotify_bar_decode(heights)
|
||||
118
old/spotify-codes-part-2/src/encode_decode.py
Normal file
@@ -0,0 +1,118 @@
|
||||
import numpy as np
|
||||
import crccheck
|
||||
|
||||
# This code was written by "Doyle" on Stack Overflow
|
||||
# https://stackoverflow.com/a/64950150/10703868
|
||||
|
||||
|
||||
# Utils for conversion between int, array of binary
|
||||
# and array of bytes (as ints)
|
||||
def int_to_bin(num, length, endian):
|
||||
if endian == 'l':
|
||||
return [num >> i & 1 for i in range(0, length)]
|
||||
elif endian == 'b':
|
||||
return [num >> i & 1 for i in range(length-1, -1, -1)]
|
||||
|
||||
def bin_to_int(bin,length):
|
||||
return int("".join([str(bin[i]) for i in range(length-1,-1,-1)]),2)
|
||||
|
||||
def bin_to_bytes(bin, length):
|
||||
b = bin[0:length] + [0] * (-length % 8)
|
||||
return [(b[i]<<7) + (b[i+1]<<6) + (b[i+2]<<5) + (b[i+3]<<4) +
|
||||
(b[i+4]<<3) + (b[i+5]<<2) + (b[i+6]<<1) + b[i+7] for i in range(0,len(b),8)]
|
||||
|
||||
# Return the circular right shift of an array by 'n' positions
|
||||
def shift_right(arr, n):
|
||||
return arr[-n % len(arr):len(arr):] + arr[0:-n % len(arr)]
|
||||
|
||||
gray_code = [0,1,3,2,7,6,4,5]
|
||||
gray_code_inv = [[0,0,0],[0,0,1],[0,1,1],[0,1,0],
|
||||
[1,1,0],[1,1,1],[1,0,1],[1,0,0]]
|
||||
|
||||
# CRC using Rocksoft model:
|
||||
# NOTE: this is not quite any of their predefined CRC's
|
||||
# 8: number of check bits (degree of poly)
|
||||
# 0x7: representation of poly without high term (x^8+x^2+x+1)
|
||||
# 0x0: initial fill of register
|
||||
# True: byte reverse data
|
||||
# True: byte reverse check
|
||||
# 0xff: Mask check (i.e. invert)
|
||||
spotify_crc = crccheck.crc.Crc(8, 0x7, 0x0, True, True, 0xff)
|
||||
|
||||
def calc_spotify_crc(bin37):
|
||||
bytes = bin_to_bytes(bin37, 37)
|
||||
return int_to_bin(spotify_crc.calc(bytes), 8, 'b')
|
||||
|
||||
def check_spotify_crc(bin45):
|
||||
data = bin_to_bytes(bin45,37)
|
||||
return spotify_crc.calc(data) == bin_to_bytes(bin45[37:], 8)[0]
|
||||
|
||||
# Simple convolutional encoder
|
||||
def encode_cc(dat):
|
||||
gen1 = [1,0,1,1,0,1,1]
|
||||
gen2 = [1,1,1,1,0,0,1]
|
||||
punct = [1,1,0]
|
||||
dat_pad = dat[-6:] + dat # 6 bits are needed to initialize
|
||||
# register for tail-biting
|
||||
stream1 = np.convolve(dat_pad, gen1, mode='valid') % 2
|
||||
stream2 = np.convolve(dat_pad, gen2, mode='valid') % 2
|
||||
enc = [val for pair in zip(stream1, stream2) for val in pair]
|
||||
return [enc[i] for i in range(len(enc)) if punct[i % 3]]
|
||||
|
||||
# To create a generator matrix for a code, we encode each row
|
||||
# of the identity matrix. Note that the CRC is not quite linear
|
||||
# because of the check mask so we apply the lamda function to
|
||||
# invert it. Given a 37 bit media reference we can encode by
|
||||
# ref * spotify_generator + spotify_mask (mod 2)
|
||||
_i37 = np.identity(37, dtype=bool)
|
||||
crc_generator = [_i37[r].tolist() +
|
||||
list(map(lambda x : 1-x, calc_spotify_crc(_i37[r].tolist())))
|
||||
for r in range(37)]
|
||||
spotify_generator = 1*np.array([encode_cc(crc_generator[r]) for r in range(37)], dtype=bool)
|
||||
del _i37
|
||||
|
||||
spotify_mask = 1*np.array(encode_cc(37*[0] + 8*[1]), dtype=bool)
|
||||
|
||||
# The following matrix is used to "invert" the convolutional code.
|
||||
# In particular, we choose a 45 vector basis for the columns of the
|
||||
# generator matrix (by deleting those in positions equal to 2 mod 4)
|
||||
# and then inverting the matrix. By selecting the corresponding 45
|
||||
# elements of the convolutionally encoded vector and multiplying
|
||||
# on the right by this matrix, we get back to the unencoded data,
|
||||
# assuming there are no errors.
|
||||
# Note: numpy does not invert binary matrices, i.e. GF(2), so we
|
||||
# hard code the following 3 row vectors to generate the matrix.
|
||||
conv_gen = [[0,1,0,1,1,1,1,0,1,1,0,0,0,1]+31*[0],
|
||||
[1,0,1,0,1,0,1,0,0,0,1,1,1] + 32*[0],
|
||||
[0,0,1,0,1,1,1,1,1,1,0,0,1] + 32*[0] ]
|
||||
|
||||
conv_generator_inv = 1*np.array([shift_right(conv_gen[(s-27) % 3],s) for s in range(27,72)], dtype=bool)
|
||||
|
||||
|
||||
# Given an integer media reference, returns list of 20 barcode levels
|
||||
def spotify_bar_code(ref):
|
||||
bin37 = np.array([int_to_bin(ref, 37, 'l')], dtype=bool)
|
||||
enc = (np.add(1*np.dot(bin37, spotify_generator), spotify_mask) % 2).flatten()
|
||||
perm = [enc[7*i % 60] for i in range(60)]
|
||||
return [gray_code[4*perm[i]+2*perm[i+1]+perm[i+2]] for i in range(0,len(perm),3)]
|
||||
|
||||
# Equivalent function but using CRC and CC encoders.
|
||||
def spotify_bar_code2(ref):
|
||||
bin37 = int_to_bin(ref, 37, 'l')
|
||||
enc_crc = bin37 + calc_spotify_crc(bin37)
|
||||
enc_cc = encode_cc(enc_crc)
|
||||
perm = [enc_cc[7*i % 60] for i in range(60)]
|
||||
return [gray_code[4*perm[i]+2*perm[i+1]+perm[i+2]] for i in range(0,len(perm),3)]
|
||||
|
||||
# Given 20 (clean) barcode levels, returns media reference
|
||||
def spotify_bar_decode(levels):
|
||||
level_bits = np.array([gray_code_inv[levels[i]] for i in range(20)], dtype=bool).flatten()
|
||||
conv_bits = [level_bits[43*i % 60] for i in range(60)]
|
||||
cols = [i for i in range(60) if i % 4 != 2] # columns to invert
|
||||
conv_bits45 = np.array([conv_bits[c] for c in cols], dtype=bool)
|
||||
bin45 = (1*np.dot(conv_bits45, conv_generator_inv) % 2).tolist()
|
||||
if check_spotify_crc(bin45):
|
||||
return bin_to_int(bin45, 37)
|
||||
else:
|
||||
print('Error in levels; Use real decoder!!!')
|
||||
return -1
|
||||
45
old/spotify-codes-part-2/src/get_heights.py
Normal file
@@ -0,0 +1,45 @@
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
from skimage import io
|
||||
from skimage.filters import threshold_otsu, gaussian
|
||||
from skimage.morphology import closing, square
|
||||
from skimage.measure import label, regionprops
|
||||
from skimage.color import rgb2gray
|
||||
import matplotlib.patches as patches
|
||||
from sklearn.cluster import KMeans
|
||||
|
||||
def get_heights(filename: str):
|
||||
image = io.imread(filename)
|
||||
im = rgb2gray(image)
|
||||
binary_im = im > threshold_otsu(im)
|
||||
smooth_im = gaussian(binary_im.astype(float), sigma=1)
|
||||
morph_im = closing(smooth_im > 0.5, square(3))
|
||||
|
||||
labeled = label(morph_im)
|
||||
bar_dims = [r.bbox for r in regionprops(labeled)]
|
||||
bar_dims.sort(key=lambda x: x[1]) # left to right
|
||||
|
||||
bars = bar_dims[1:] # skip logo
|
||||
bar_heights_raw = []
|
||||
print(len(bars))
|
||||
for bar in bars:
|
||||
top, left, bottom, right = bar
|
||||
effective_height = bottom - top # use bounding box height directly
|
||||
bar_heights_raw.append(effective_height)
|
||||
|
||||
# Cluster measured heights to 8 clusters representing discrete bar levels
|
||||
bar_heights_raw_np = np.array(bar_heights_raw).reshape(-1, 1)
|
||||
|
||||
kmeans = KMeans(n_clusters=8, random_state=0).fit(bar_heights_raw_np)
|
||||
cluster_centers = np.sort(kmeans.cluster_centers_.flatten())
|
||||
|
||||
# Assign each bar height to closest cluster center index (0 to 7)
|
||||
predicted_levels = []
|
||||
for h in bar_heights_raw:
|
||||
diffs = np.abs(cluster_centers - h)
|
||||
closest_cluster = np.argmin(diffs)
|
||||
predicted_levels.append(int(closest_cluster))
|
||||
|
||||
|
||||
print((predicted_levels))
|
||||
return predicted_levels
|
||||
11
old/spotify-codes-part-2/src/permute.py
Normal file
@@ -0,0 +1,11 @@
|
||||
|
||||
|
||||
|
||||
def permute(bits, step=7):
|
||||
for i in range(len(bits)):
|
||||
yield bits[(i * step) % len(bits)]
|
||||
|
||||
if __name__ == "__main__":
|
||||
bits = "111000111100101111101110111001011100110000100100011100110011"
|
||||
print("".join(permute(bits)))
|
||||
# 111100110001110101101000011110010110101100111111101000111000
|
||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
35
old/spotify-codes-part-2/src/simple_convolutional_code.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from typing import List
|
||||
|
||||
|
||||
def encode(bits: List[int], polynomial: List[int], tail_bite=False):
|
||||
"""Convolutionally encode the stream of bits using the generator polynomial.
|
||||
If tail_bite == True, prepend the tail of the input. Otherwise use 0s to fill.
|
||||
"""
|
||||
if tail_bite:
|
||||
tail = bits[-(len(polynomial) - 1):]
|
||||
else:
|
||||
tail = [0 for i in range(len(polynomial) - 1)]
|
||||
full = tail + bits
|
||||
polynomial.reverse() # Reverse since we're working the other direction
|
||||
parity_bits = []
|
||||
for i in range(len(bits)):
|
||||
parity = 0
|
||||
for j in range(len(polynomial)):
|
||||
parity ^= full[i + j] * polynomial[j]
|
||||
parity_bits.append(parity)
|
||||
return parity_bits
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
g0 = [1, 0, 1, 1, 0, 1, 1]
|
||||
g1 = [1, 1, 1, 1, 0, 0, 1]
|
||||
bits = "010001001110111111110001110101101011011001100"
|
||||
g0_expected = "100011100111110100110011110100000010001001011"
|
||||
g1_expected = "110011100010110110110100101101011100110011011"
|
||||
bits = [int(i) for i in bits]
|
||||
|
||||
p0 = encode(bits, g0, True)
|
||||
p1 = encode(bits, g1, True)
|
||||
|
||||
print(g0_expected == "".join(str(i) for i in p0))
|
||||
print(g1_expected == "".join(str(i) for i in p1))
|
||||
15
old/spotify-codes-part-2/src/step_by_step.py
Normal file
@@ -0,0 +1,15 @@
|
||||
|
||||
|
||||
|
||||
|
||||
media_ref = 57639171874
|
||||
binary = f"{bin(media_ref)[2:]:0>37}"
|
||||
print(binary)
|
||||
# pad with 3 bits to the right:
|
||||
binary = f"{binary:0<39}"
|
||||
print(binary)
|
||||
|
||||
a = "100011100111110100110011110100000010001001011"
|
||||
b = "110011100010110110110100101101011100110011011"
|
||||
c = zip(a, b)
|
||||
print("".join(i + j for i, j in c))
|
||||
|
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 59 KiB |
@@ -1,44 +0,0 @@
|
||||
import cv2
|
||||
from ultralytics import YOLO
|
||||
|
||||
model = YOLO('yolov5s.pt') # Replace with your trained logo detection model path
|
||||
cap = cv2.VideoCapture(0) # Webcam input
|
||||
|
||||
if not cap.isOpened():
|
||||
print("Error: Could not open video stream.")
|
||||
exit()
|
||||
|
||||
while True:
|
||||
ret, frame = cap.read()
|
||||
if not ret:
|
||||
break
|
||||
|
||||
results = model(frame)
|
||||
|
||||
for result in results:
|
||||
boxes = result.boxes.xyxy.cpu().numpy()
|
||||
scores = result.boxes.conf.cpu().numpy()
|
||||
class_ids = result.boxes.cls.cpu().numpy()
|
||||
|
||||
for box, score, cls_id in zip(boxes, scores, class_ids):
|
||||
if score < 0.3:
|
||||
continue
|
||||
|
||||
x1, y1, x2, y2 = map(int, box)
|
||||
label = model.names[int(cls_id)]
|
||||
|
||||
# Draw bounding box and label
|
||||
cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2)
|
||||
cv2.putText(frame, f'{label} {score:.2f}', (x1, y1 - 10),
|
||||
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2)
|
||||
|
||||
# Log detection to console
|
||||
print(f"Detected: {label} | Confidence: {score:.2f} | Box: ({x1}, {y1}), ({x2}, {y2})")
|
||||
|
||||
cv2.imshow('Logo Detection', frame)
|
||||
|
||||
if cv2.waitKey(1) & 0xFF == 27:
|
||||
break
|
||||
|
||||
cap.release()
|
||||
cv2.destroyAllWindows()
|
||||