From 6dc6dbfc4ead41a5848bc6bd667be58a1af20eab Mon Sep 17 00:00:00 2001 From: big-nacho Date: Fri, 30 May 2025 12:47:49 +0000 Subject: [PATCH 1/2] Remove polygons, introduce SKPath CU-86c3mbjx8 --- envs/dev.yaml | 93 +++++++------- envs/prod.yaml | 58 ++++----- requirements/dev.in | 2 +- requirements/dev.txt | 156 ++++++++++++----------- requirements/prod.in | 2 +- requirements/prod.txt | 44 +++---- vectorizing/__init__.py | 27 ++-- vectorizing/geometry/bounds.py | 73 ++++------- vectorizing/geometry/cubic_bezier.py | 121 ------------------ vectorizing/geometry/potrace.py | 71 ++++------- vectorizing/geometry/segment_list.py | 34 ----- vectorizing/solvers/color/ColorSolver.py | 30 ++--- vectorizing/solvers/color/clip.py | 45 ++----- vectorizing/svg/draw.py | 43 ------- vectorizing/svg/markup.py | 99 +++++++++++--- 15 files changed, 342 insertions(+), 556 deletions(-) delete mode 100644 vectorizing/geometry/cubic_bezier.py delete mode 100644 vectorizing/geometry/segment_list.py delete mode 100644 vectorizing/svg/draw.py diff --git a/envs/dev.yaml b/envs/dev.yaml index b165c52..9fdcb8f 100644 --- a/envs/dev.yaml +++ b/envs/dev.yaml @@ -5,90 +5,91 @@ dependencies: - _libgcc_mutex=0.1=main - _openmp_mutex=5.1=1_gnu - bzip2=1.0.8=h5eee18b_6 - - ca-certificates=2024.7.2=h06a4308_0 - - ld_impl_linux-64=2.38=h1181459_1 + - ca-certificates=2025.2.25=h06a4308_0 + - ld_impl_linux-64=2.40=h12ee557_0 - libffi=3.4.4=h6a678d5_1 - libgcc-ng=11.2.0=h1234567_1 - libgomp=11.2.0=h1234567_1 - libstdcxx-ng=11.2.0=h1234567_1 - libuuid=1.41.5=h5eee18b_0 - ncurses=6.4=h6a678d5_0 - - openssl=3.0.14=h5eee18b_0 - - pip=24.2=py311h06a4308_0 + - openssl=3.0.16=h5eee18b_0 + - pip=25.1=pyhc872135_2 - python=3.11.5=h955ad1f_0 - readline=8.2=h5eee18b_0 - - setuptools=72.1.0=py311h06a4308_0 + - setuptools=78.1.1=py311h06a4308_0 - sqlite=3.45.3=h5eee18b_0 - tk=8.6.14=h39e8969_0 - - tzdata=2024a=h04d1e81_0 - - xz=5.4.6=h5eee18b_1 + - tzdata=2025b=h04d1e81_0 + - wheel=0.45.1=py311h06a4308_0 + - xz=5.6.4=h5eee18b_1 - zlib=1.2.13=h5eee18b_1 - pip: - - blinker==1.8.2 - - boto3==1.35.2 - - botocore==1.35.2 - - build==1.2.1 + - blinker==1.9.0 + - boto3==1.35.99 + - botocore==1.35.99 + - build==1.2.2.post1 - cairocffi==1.7.1 - cairosvg==2.7.1 - - certifi==2024.7.4 - - cffi==1.17.0 + - certifi==2025.4.26 + - cffi==1.17.1 - cfgv==3.4.0 - - charset-normalizer==3.3.2 - - click==8.1.7 - - cssselect2==0.7.0 + - charset-normalizer==3.4.2 + - click==8.2.1 + - cssselect2==0.8.0 - cuid==0.4 - defusedxml==0.7.1 - - distlib==0.3.8 - - exceptiongroup==1.2.2 + - distlib==0.3.9 + - exceptiongroup==1.3.0 - faiss-cpu==1.7.4 - - filelock==3.15.4 + - filelock==3.18.0 - flask==2.3.3 - - flask-cors==4.0.1 + - flask-cors==4.0.2 - gunicorn==21.2.0 - - identify==2.6.0 - - idna==3.7 - - imageio==2.35.1 - - iniconfig==2.0.0 + - identify==2.6.12 + - idna==3.10 + - imageio==2.37.0 + - iniconfig==2.1.0 - itsdangerous==2.2.0 - - jinja2==3.1.4 + - jinja2==3.1.6 - jmespath==1.0.1 - lazy-loader==0.4 - llvmlite==0.40.1 - - markupsafe==2.1.5 - - networkx==3.3 + - markupsafe==3.0.2 + - networkx==3.4.2 - nodeenv==1.9.1 - numba==0.57.1 - numpy==1.24.4 - opencv-python-headless==4.8.0.76 - - packaging==24.1 + - packaging==25.0 - pillow==10.0.1 - pip-tools==7.4.1 - - platformdirs==4.2.2 - - pluggy==1.5.0 - - pre-commit==3.8.0 - - pyclipper==1.3.0.post5 + - platformdirs==4.3.8 + - pluggy==1.6.0 + - pre-commit==4.2.0 - pycparser==2.22 - pypotrace==0.3 - - pyproject-hooks==1.1.0 - - pytest==8.3.2 + - pyproject-hooks==1.2.0 + - pytest==8.3.5 - pytest-dotenv==0.5.2 - python-dateutil==2.9.0.post0 - - python-dotenv==1.0.1 - - pywavelets==1.7.0 + - python-dotenv==1.1.0 + - pywavelets==1.8.0 - pyyaml==6.0.2 - requests==2.31.0 - - s3transfer==0.10.2 + - s3transfer==0.10.4 - scikit-image==0.21.0 - - scipy==1.14.1 + - scipy==1.15.3 - sentry-sdk==1.30.0 - sewar==0.4.6 - - six==1.16.0 - - tifffile==2024.8.10 - - tinycss2==1.3.0 - - tomli==2.0.1 - - urllib3==2.2.2 - - virtualenv==20.26.3 + - six==1.17.0 + - skia-pathops==0.8.0.post2 + - tifffile==2025.5.10 + - tinycss2==1.4.0 + - tomli==2.2.1 + - typing-extensions==4.13.2 + - urllib3==2.4.0 + - virtualenv==20.31.2 - webencodings==0.5.1 - - werkzeug==3.0.3 - - wheel==0.44.0 + - werkzeug==3.1.3 prefix: /opt/conda/envs/dev diff --git a/envs/prod.yaml b/envs/prod.yaml index 792ddd0..f7eae44 100644 --- a/envs/prod.yaml +++ b/envs/prod.yaml @@ -5,62 +5,62 @@ dependencies: - _libgcc_mutex=0.1=main - _openmp_mutex=5.1=1_gnu - bzip2=1.0.8=h5eee18b_6 - - ca-certificates=2024.7.2=h06a4308_0 - - ld_impl_linux-64=2.38=h1181459_1 + - ca-certificates=2025.2.25=h06a4308_0 + - ld_impl_linux-64=2.40=h12ee557_0 - libffi=3.4.4=h6a678d5_1 - libgcc-ng=11.2.0=h1234567_1 - libgomp=11.2.0=h1234567_1 - libstdcxx-ng=11.2.0=h1234567_1 - libuuid=1.41.5=h5eee18b_0 - ncurses=6.4=h6a678d5_0 - - openssl=3.0.14=h5eee18b_0 - - pip=24.2=py311h06a4308_0 + - openssl=3.0.16=h5eee18b_0 + - pip=25.1=pyhc872135_2 - python=3.11.5=h955ad1f_0 - readline=8.2=h5eee18b_0 - - setuptools=72.1.0=py311h06a4308_0 + - setuptools=78.1.1=py311h06a4308_0 - sqlite=3.45.3=h5eee18b_0 - tk=8.6.14=h39e8969_0 - - tzdata=2024a=h04d1e81_0 - - wheel=0.43.0=py311h06a4308_0 - - xz=5.4.6=h5eee18b_1 + - tzdata=2025b=h04d1e81_0 + - wheel=0.45.1=py311h06a4308_0 + - xz=5.6.4=h5eee18b_1 - zlib=1.2.13=h5eee18b_1 - pip: - - blinker==1.8.2 - - boto3==1.35.2 - - botocore==1.35.2 - - certifi==2024.7.4 - - charset-normalizer==3.3.2 - - click==8.1.7 + - blinker==1.9.0 + - boto3==1.35.99 + - botocore==1.35.99 + - certifi==2025.4.26 + - charset-normalizer==3.4.2 + - click==8.2.1 - cuid==0.4 - faiss-cpu==1.7.4 - flask==2.3.3 - - flask-cors==4.0.1 + - flask-cors==4.0.2 - gunicorn==21.2.0 - - idna==3.7 - - imageio==2.35.1 + - idna==3.10 + - imageio==2.37.0 - itsdangerous==2.2.0 - - jinja2==3.1.4 + - jinja2==3.1.6 - jmespath==1.0.1 - lazy-loader==0.4 - llvmlite==0.40.1 - - markupsafe==2.1.5 - - networkx==3.3 + - markupsafe==3.0.2 + - networkx==3.4.2 - numba==0.57.1 - numpy==1.24.4 - opencv-python-headless==4.8.0.76 - - packaging==24.1 + - packaging==25.0 - pillow==10.0.1 - - pyclipper==1.3.0.post5 - pypotrace==0.3 - python-dateutil==2.9.0.post0 - - pywavelets==1.7.0 + - pywavelets==1.8.0 - requests==2.31.0 - - s3transfer==0.10.2 + - s3transfer==0.10.4 - scikit-image==0.21.0 - - scipy==1.14.1 + - scipy==1.15.3 - sentry-sdk==1.30.0 - - six==1.16.0 - - tifffile==2024.8.10 - - urllib3==2.2.2 - - werkzeug==3.0.3 + - six==1.17.0 + - skia-pathops==0.8.0.post2 + - tifffile==2025.5.10 + - urllib3==2.4.0 + - werkzeug==3.1.3 prefix: /opt/conda/envs/prod diff --git a/requirements/dev.in b/requirements/dev.in index 0fa3f0c..ff7431e 100644 --- a/requirements/dev.in +++ b/requirements/dev.in @@ -7,4 +7,4 @@ pre-commit pip-tools pytest cairosvg~=2.7.1 -sewar~=0.4.6 +sewar~=0.4.6 \ No newline at end of file diff --git a/requirements/dev.txt b/requirements/dev.txt index a60d12c..d9c7b5f 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -4,112 +4,112 @@ # # pip-compile /workspaces/vectorizing/scripts/../requirements/dev.in # -blinker==1.8.2 +blinker==1.9.0 # via - # -r /workspaces/vectorizing/scripts/../requirements/prod.txt + # -r /workspaces/vectorizing/requirements/prod.txt # flask -boto3==1.35.2 - # via -r /workspaces/vectorizing/scripts/../requirements/prod.txt -botocore==1.35.2 +boto3==1.35.99 + # via -r /workspaces/vectorizing/requirements/prod.txt +botocore==1.35.99 # via - # -r /workspaces/vectorizing/scripts/../requirements/prod.txt + # -r /workspaces/vectorizing/requirements/prod.txt # boto3 # s3transfer -build==1.2.1 +build==1.2.2.post1 # via pip-tools cairocffi==1.7.1 # via cairosvg cairosvg==2.7.1 # via -r /workspaces/vectorizing/scripts/../requirements/dev.in -certifi==2024.7.4 +certifi==2025.4.26 # via - # -r /workspaces/vectorizing/scripts/../requirements/prod.txt + # -r /workspaces/vectorizing/requirements/prod.txt # requests # sentry-sdk -cffi==1.17.0 +cffi==1.17.1 # via cairocffi cfgv==3.4.0 # via pre-commit -charset-normalizer==3.3.2 +charset-normalizer==3.4.2 # via - # -r /workspaces/vectorizing/scripts/../requirements/prod.txt + # -r /workspaces/vectorizing/requirements/prod.txt # requests -click==8.1.7 +click==8.2.1 # via - # -r /workspaces/vectorizing/scripts/../requirements/prod.txt + # -r /workspaces/vectorizing/requirements/prod.txt # flask # pip-tools -cssselect2==0.7.0 +cssselect2==0.8.0 # via cairosvg cuid==0.4 - # via -r /workspaces/vectorizing/scripts/../requirements/prod.txt + # via -r /workspaces/vectorizing/requirements/prod.txt defusedxml==0.7.1 # via cairosvg -distlib==0.3.8 +distlib==0.3.9 # via virtualenv -exceptiongroup==1.2.2 +exceptiongroup==1.3.0 # via pytest faiss-cpu==1.7.4 - # via -r /workspaces/vectorizing/scripts/../requirements/prod.txt -filelock==3.15.4 + # via -r /workspaces/vectorizing/requirements/prod.txt +filelock==3.18.0 # via virtualenv flask==2.3.3 # via - # -r /workspaces/vectorizing/scripts/../requirements/prod.txt + # -r /workspaces/vectorizing/requirements/prod.txt # flask-cors -flask-cors==4.0.1 - # via -r /workspaces/vectorizing/scripts/../requirements/prod.txt +flask-cors==4.0.2 + # via -r /workspaces/vectorizing/requirements/prod.txt gunicorn==21.2.0 - # via -r /workspaces/vectorizing/scripts/../requirements/prod.txt -identify==2.6.0 + # via -r /workspaces/vectorizing/requirements/prod.txt +identify==2.6.12 # via pre-commit -idna==3.7 +idna==3.10 # via - # -r /workspaces/vectorizing/scripts/../requirements/prod.txt + # -r /workspaces/vectorizing/requirements/prod.txt # requests -imageio==2.35.1 +imageio==2.37.0 # via - # -r /workspaces/vectorizing/scripts/../requirements/prod.txt + # -r /workspaces/vectorizing/requirements/prod.txt # scikit-image -iniconfig==2.0.0 +iniconfig==2.1.0 # via pytest itsdangerous==2.2.0 # via - # -r /workspaces/vectorizing/scripts/../requirements/prod.txt + # -r /workspaces/vectorizing/requirements/prod.txt # flask -jinja2==3.1.4 +jinja2==3.1.6 # via - # -r /workspaces/vectorizing/scripts/../requirements/prod.txt + # -r /workspaces/vectorizing/requirements/prod.txt # flask jmespath==1.0.1 # via - # -r /workspaces/vectorizing/scripts/../requirements/prod.txt + # -r /workspaces/vectorizing/requirements/prod.txt # boto3 # botocore lazy-loader==0.4 # via - # -r /workspaces/vectorizing/scripts/../requirements/prod.txt + # -r /workspaces/vectorizing/requirements/prod.txt # scikit-image llvmlite==0.40.1 # via - # -r /workspaces/vectorizing/scripts/../requirements/prod.txt + # -r /workspaces/vectorizing/requirements/prod.txt # numba -markupsafe==2.1.5 +markupsafe==3.0.2 # via - # -r /workspaces/vectorizing/scripts/../requirements/prod.txt + # -r /workspaces/vectorizing/requirements/prod.txt # jinja2 # werkzeug -networkx==3.3 +networkx==3.4.2 # via - # -r /workspaces/vectorizing/scripts/../requirements/prod.txt + # -r /workspaces/vectorizing/requirements/prod.txt # scikit-image nodeenv==1.9.1 # via pre-commit numba==0.57.1 - # via -r /workspaces/vectorizing/scripts/../requirements/prod.txt + # via -r /workspaces/vectorizing/requirements/prod.txt numpy==1.24.4 # via - # -r /workspaces/vectorizing/scripts/../requirements/prod.txt + # -r /workspaces/vectorizing/requirements/prod.txt # imageio # numba # opencv-python-headless @@ -120,10 +120,10 @@ numpy==1.24.4 # sewar # tifffile opencv-python-headless==4.8.0.76 - # via -r /workspaces/vectorizing/scripts/../requirements/prod.txt -packaging==24.1 + # via -r /workspaces/vectorizing/requirements/prod.txt +packaging==25.0 # via - # -r /workspaces/vectorizing/scripts/../requirements/prod.txt + # -r /workspaces/vectorizing/requirements/prod.txt # build # gunicorn # lazy-loader @@ -131,30 +131,28 @@ packaging==24.1 # scikit-image pillow==10.0.1 # via - # -r /workspaces/vectorizing/scripts/../requirements/prod.txt + # -r /workspaces/vectorizing/requirements/prod.txt # cairosvg # imageio # scikit-image # sewar pip-tools==7.4.1 # via -r /workspaces/vectorizing/scripts/../requirements/dev.in -platformdirs==4.2.2 +platformdirs==4.3.8 # via virtualenv -pluggy==1.5.0 +pluggy==1.6.0 # via pytest -pre-commit==3.8.0 +pre-commit==4.2.0 # via -r /workspaces/vectorizing/scripts/../requirements/dev.in -pyclipper==1.3.0.post5 - # via -r /workspaces/vectorizing/scripts/../requirements/prod.txt pycparser==2.22 # via cffi pypotrace==0.3 - # via -r /workspaces/vectorizing/scripts/../requirements/prod.txt -pyproject-hooks==1.1.0 + # via -r /workspaces/vectorizing/requirements/prod.txt +pyproject-hooks==1.2.0 # via # build # pip-tools -pytest==8.3.2 +pytest==8.3.5 # via # -r /workspaces/vectorizing/scripts/../requirements/dev.in # pytest-dotenv @@ -162,67 +160,71 @@ pytest-dotenv==0.5.2 # via -r /workspaces/vectorizing/scripts/../requirements/dev.in python-dateutil==2.9.0.post0 # via - # -r /workspaces/vectorizing/scripts/../requirements/prod.txt + # -r /workspaces/vectorizing/requirements/prod.txt # botocore -python-dotenv==1.0.1 +python-dotenv==1.1.0 # via pytest-dotenv -pywavelets==1.7.0 +pywavelets==1.8.0 # via - # -r /workspaces/vectorizing/scripts/../requirements/prod.txt + # -r /workspaces/vectorizing/requirements/prod.txt # scikit-image pyyaml==6.0.2 # via pre-commit requests==2.31.0 - # via -r /workspaces/vectorizing/scripts/../requirements/prod.txt -s3transfer==0.10.2 + # via -r /workspaces/vectorizing/requirements/prod.txt +s3transfer==0.10.4 # via - # -r /workspaces/vectorizing/scripts/../requirements/prod.txt + # -r /workspaces/vectorizing/requirements/prod.txt # boto3 scikit-image==0.21.0 - # via -r /workspaces/vectorizing/scripts/../requirements/prod.txt -scipy==1.14.1 + # via -r /workspaces/vectorizing/requirements/prod.txt +scipy==1.15.3 # via - # -r /workspaces/vectorizing/scripts/../requirements/prod.txt + # -r /workspaces/vectorizing/requirements/prod.txt # scikit-image # sewar sentry-sdk==1.30.0 - # via -r /workspaces/vectorizing/scripts/../requirements/prod.txt + # via -r /workspaces/vectorizing/requirements/prod.txt sewar==0.4.6 # via -r /workspaces/vectorizing/scripts/../requirements/dev.in -six==1.16.0 +six==1.17.0 # via - # -r /workspaces/vectorizing/scripts/../requirements/prod.txt + # -r /workspaces/vectorizing/requirements/prod.txt # python-dateutil -tifffile==2024.8.10 +skia-pathops==0.8.0.post2 + # via -r /workspaces/vectorizing/requirements/prod.txt +tifffile==2025.5.10 # via - # -r /workspaces/vectorizing/scripts/../requirements/prod.txt + # -r /workspaces/vectorizing/requirements/prod.txt # scikit-image -tinycss2==1.3.0 +tinycss2==1.4.0 # via # cairosvg # cssselect2 -tomli==2.0.1 +tomli==2.2.1 # via # build # pip-tools # pytest -urllib3==2.2.2 +typing-extensions==4.13.2 + # via exceptiongroup +urllib3==2.4.0 # via - # -r /workspaces/vectorizing/scripts/../requirements/prod.txt + # -r /workspaces/vectorizing/requirements/prod.txt # botocore # requests # sentry-sdk -virtualenv==20.26.3 +virtualenv==20.31.2 # via pre-commit webencodings==0.5.1 # via # cssselect2 # tinycss2 -werkzeug==3.0.3 +werkzeug==3.1.3 # via - # -r /workspaces/vectorizing/scripts/../requirements/prod.txt + # -r /workspaces/vectorizing/requirements/prod.txt # flask -wheel==0.44.0 +wheel==0.45.1 # via pip-tools # The following packages are considered to be unsafe in a requirements file: diff --git a/requirements/prod.in b/requirements/prod.in index adca285..d5b6003 100644 --- a/requirements/prod.in +++ b/requirements/prod.in @@ -8,9 +8,9 @@ sentry-sdk~=1.30.0 numba~=0.57.1 pillow~=10.0.0 faiss-cpu~=1.7.4 -pyclipper~=1.3.0.post4 pypotrace~=0.3 scikit-image~=0.21.0 opencv-python-headless~=4.8.0.76 requests~=2.31.0 gunicorn~=21.2.0 +skia-pathops~=0.8.0 diff --git a/requirements/prod.txt b/requirements/prod.txt index d6c739b..ce3be5e 100644 --- a/requirements/prod.txt +++ b/requirements/prod.txt @@ -4,21 +4,21 @@ # # pip-compile /workspaces/vectorizing/scripts/../requirements/prod.in # -blinker==1.8.2 +blinker==1.9.0 # via flask -boto3==1.35.2 +boto3==1.35.99 # via -r /workspaces/vectorizing/scripts/../requirements/prod.in -botocore==1.35.2 +botocore==1.35.99 # via # boto3 # s3transfer -certifi==2024.7.4 +certifi==2025.4.26 # via # requests # sentry-sdk -charset-normalizer==3.3.2 +charset-normalizer==3.4.2 # via requests -click==8.1.7 +click==8.2.1 # via flask cuid==0.4 # via -r /workspaces/vectorizing/scripts/../requirements/prod.in @@ -28,17 +28,17 @@ flask==2.3.3 # via # -r /workspaces/vectorizing/scripts/../requirements/prod.in # flask-cors -flask-cors==4.0.1 +flask-cors==4.0.2 # via -r /workspaces/vectorizing/scripts/../requirements/prod.in gunicorn==21.2.0 # via -r /workspaces/vectorizing/scripts/../requirements/prod.in -idna==3.7 +idna==3.10 # via requests -imageio==2.35.1 +imageio==2.37.0 # via scikit-image itsdangerous==2.2.0 # via flask -jinja2==3.1.4 +jinja2==3.1.6 # via flask jmespath==1.0.1 # via @@ -48,11 +48,11 @@ lazy-loader==0.4 # via scikit-image llvmlite==0.40.1 # via numba -markupsafe==2.1.5 +markupsafe==3.0.2 # via # jinja2 # werkzeug -networkx==3.3 +networkx==3.4.2 # via scikit-image numba==0.57.1 # via -r /workspaces/vectorizing/scripts/../requirements/prod.in @@ -68,7 +68,7 @@ numpy==1.24.4 # tifffile opencv-python-headless==4.8.0.76 # via -r /workspaces/vectorizing/scripts/../requirements/prod.in -packaging==24.1 +packaging==25.0 # via # gunicorn # lazy-loader @@ -78,32 +78,32 @@ pillow==10.0.1 # -r /workspaces/vectorizing/scripts/../requirements/prod.in # imageio # scikit-image -pyclipper==1.3.0.post5 - # via -r /workspaces/vectorizing/scripts/../requirements/prod.in pypotrace==0.3 # via -r /workspaces/vectorizing/scripts/../requirements/prod.in python-dateutil==2.9.0.post0 # via botocore -pywavelets==1.7.0 +pywavelets==1.8.0 # via scikit-image requests==2.31.0 # via -r /workspaces/vectorizing/scripts/../requirements/prod.in -s3transfer==0.10.2 +s3transfer==0.10.4 # via boto3 scikit-image==0.21.0 # via -r /workspaces/vectorizing/scripts/../requirements/prod.in -scipy==1.14.1 +scipy==1.15.3 # via scikit-image sentry-sdk==1.30.0 # via -r /workspaces/vectorizing/scripts/../requirements/prod.in -six==1.16.0 +six==1.17.0 # via python-dateutil -tifffile==2024.8.10 +skia-pathops==0.8.0.post2 + # via -r /workspaces/vectorizing/scripts/../requirements/prod.in +tifffile==2025.5.10 # via scikit-image -urllib3==2.2.2 +urllib3==2.4.0 # via # botocore # requests # sentry-sdk -werkzeug==3.0.3 +werkzeug==3.1.3 # via flask diff --git a/vectorizing/__init__.py b/vectorizing/__init__.py index bbd83ac..879184e 100644 --- a/vectorizing/__init__.py +++ b/vectorizing/__init__.py @@ -6,12 +6,12 @@ from vectorizing.server.timer import Timer from vectorizing.server.logs import setup_logs from vectorizing.server.s3 import upload_markup -from vectorizing.svg.markup import create_markup +from vectorizing.svg.markup import generate_SVG_markup from vectorizing.util.read import try_read_image_from_url from vectorizing.server.env import get_required, get_optional from vectorizing.solvers.color.ColorSolver import ColorSolver from vectorizing.solvers.binary.BinarySolver import BinarySolver -from vectorizing.geometry.bounds import compound_path_list_bounds +from vectorizing.geometry.bounds import compound_paths_bounds # 0 -> BinarySolver # 1 -> ColorSolver @@ -35,8 +35,8 @@ def process_binary(img): solver = BinarySolver(img) return solver.solve() -def process_color(img, color_count, tolerance, timer): - solver = ColorSolver(img, color_count, tolerance, timer) +def process_color(img, color_count, timer): + solver = ColorSolver(img, color_count, timer) return solver.solve() def validate_args(args): @@ -56,17 +56,12 @@ def validate_args(args): if not only_numbers: return False - tolerance = args.get("tolerance") - if tolerance is not None and tolerance < 0: - return False - return SimpleNamespace( crop_box=box, solver=solver, url=args.get("url"), raw=args.get("raw"), - color_count=args.get("color_count"), - tolerance=args.get("tolerance"), + color_count=args.get("color_count") ) def invalid_args(): @@ -92,10 +87,6 @@ def index(): color_count = args.color_count raw = args.raw crop_box = args.crop_box - tolerance = args.tolerance - - if tolerance is None: - tolerance = 0.2 try: timer = Timer() @@ -114,13 +105,13 @@ def index(): else: timer.start_timer('Color Solver - Total') - solved = process_color(img, color_count, tolerance, timer) + solved = process_color(img, color_count, timer) timer.end_timer() compound_paths, colors, width, height = solved timer.start_timer('Markup Creation') - markup = create_markup(compound_paths, colors, width, height) + markup = generate_SVG_markup(compound_paths, colors, width, height) timer.end_timer() if raw: @@ -131,7 +122,7 @@ def index(): timer.end_timer() timer.start_timer('Bounds Creation') - bounds = compound_path_list_bounds(compound_paths, tolerance) + bounds = compound_paths_bounds(compound_paths) timer.end_timer() app.logger.info(timer.timelog()) @@ -140,7 +131,7 @@ def index(): 'success': True, 'objectId': cuid_str, 'info': { - 'bounds': bounds.to_dict(), + 'bounds': bounds, 'image_width': width, 'image_height': height } diff --git a/vectorizing/geometry/bounds.py b/vectorizing/geometry/bounds.py index 00dd99f..0931cc9 100644 --- a/vectorizing/geometry/bounds.py +++ b/vectorizing/geometry/bounds.py @@ -1,53 +1,34 @@ import numpy as np -# Class that represents a bounds object -class Bounds: - def __init__(self, min_x, min_y, max_x, max_y): - self.min_x = min_x - self.min_y = min_y - self.max_x = max_x - self.max_y = max_y - - def to_dict(self): - return { - 'top': self.min_y, - 'left': self.min_x, - 'bottom': self.max_y, - 'right': self.max_x, - 'width': self.max_x - self.min_x, - 'height': self.max_y - self.min_y - } -# Given a list of bounds, it will compute the total bounds. -# Meaning, the tightest bounds that contain all the bounds in the list. -def compute_total_bounds(bounds_list): - min_x = [bounds.min_x for bounds in bounds_list] - min_y = [bounds.min_y for bounds in bounds_list] - max_x = [bounds.max_x for bounds in bounds_list] - max_y = [bounds.max_y for bounds in bounds_list] +# Calculates bounds of a list of compound paths +def compound_paths_bounds(compound_paths): + """ + Calculates the combined bounds of a list of compound paths. - return Bounds( - min(min_x), - min(min_y), - max(max_x), - max(max_y) - ) + Parameters: + compound_paths: The list of compound paths (SKPath). -# Calculates bounds of a path made out CubicBeziers and SegmentLists -def path_bounds(path, tolerance): - bounds = [item.bounds(tolerance) for item in path] - return compute_total_bounds(bounds) + Returns: + An object containing combined bounds info. + """ + min_x = np.inf + min_y = np.inf + max_x = -np.inf + max_y = -np.inf -# Calculates bounds of a compound path, meaning a list of paths -def compound_path_bounds(compound_path, tolerance): - bounds = [path_bounds(path, tolerance) for path in compound_path] - return compute_total_bounds(bounds) + for compound_path in compound_paths: + l, t, r, b = compound_path.bounds + min_x = min(min_x, l) + min_y = min(min_y, t) + max_x = max(max_x, r) + max_y = max(max_y, b) -# Calculates bounds of a list of compound paths -def compound_path_list_bounds(compound_path_list, tolerance): - bounds = [ - compound_path_bounds(compound_path, tolerance) - for compound_path in compound_path_list - if len(compound_path) > 0 - ] - return compute_total_bounds(bounds) \ No newline at end of file + return { + "top": min_y, + "left": min_x, + "bottom": max_y, + "right": max_x, + "width": max_x - min_x, + "height": max_y - min_y, + } diff --git a/vectorizing/geometry/cubic_bezier.py b/vectorizing/geometry/cubic_bezier.py deleted file mode 100644 index 4490d6b..0000000 --- a/vectorizing/geometry/cubic_bezier.py +++ /dev/null @@ -1,121 +0,0 @@ -import numpy as np -from vectorizing.geometry.segment_list import SegmentList -from numba import njit - -# Flattening approach is inspired in -# https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.86.162&rep=rep1&type=pdf - -@njit -def is_flat_enough(points, tol = .2): - p0 = points[0] - p1 = points[1] - p2 = points[2] - p3 = points[3] - - ux = 3 * p1[0] - 2 * p0[0] - p3[0] - uy = 3 * p1[1] - 2 * p0[1] - p3[1] - vx = 3 * p2[0] - 2 * p3[0] - p0[0] - vy = 3 * p2[1] - 2 * p3[1] - p0[1] - - ux = ux ** 2 - uy = uy ** 2 - vx = vx ** 2 - vy = vy ** 2 - - if ux < vx: - ux = vx - - if uy < vy: - uy = vy - - tolerance = 16 * tol ** 2 - return ux + uy <= tolerance - -@njit -def subdivide(points): - p0 = points[0] - p1 = points[1] - p2 = points[2] - p3 = points[3] - - l0 = p0 - r3 = p3 - - l1 = ( - (p0[0] + p1[0]) / 2, - (p0[1] + p1[1]) / 2 - ) - - r2 = ( - (p2[0] + p3[0]) / 2, - (p2[1] + p3[1]) / 2 - ) - - m = ( - (p1[0] + p2[0]) / 2, - (p1[1] + p2[1]) / 2 - ) - - l2 = ( - (m[0] + l1[0]) / 2, - (m[1] + l1[1]) / 2, - ) - - r1 = ( - (m[0] + r2[0]) / 2, - (m[1] + r2[1]) / 2 - ) - - j = ( - (l2[0] + r1[0]) / 2, - (l2[1] + r1[1]) / 2 - ) - - l3 = r0 = j - - return ( - (l0, l1, l2, l3), - (r0, r1, r2, r3) - ) - -@njit -def flatten(points, tolerance): - stack = [points] - flattened = [] - - while len(stack): - first = stack.pop() - - if is_flat_enough(first, tolerance): - flattened.append(first[0]) - flattened.append(first[1]) - - else: - subdivision = subdivide(first) - stack.append(subdivision[1]) - stack.append(subdivision[0]) - - return flattened - -# Class to represent a cubic bezier -class CubicBezier: - def __init__(self, p0, p1, p2, p3): - self.p0 = p0 - self.p1 = p1 - self.p2 = p2 - self.p3 = p3 - - def scaled(self, s): - return CubicBezier( - self.p0 * s, - self.p1 * s, - self.p2 * s, - self.p3 * s - ) - - def flattened(self, tolerance): - points = (tuple(self.p0), tuple(self.p1), tuple(self.p2), tuple(self.p3)) - return SegmentList(np.array(flatten(points, tolerance))) - - def bounds(self, tolerance): - return self.flattened(tolerance).bounds(tolerance) \ No newline at end of file diff --git a/vectorizing/geometry/potrace.py b/vectorizing/geometry/potrace.py index b3edabc..521cdb7 100644 --- a/vectorizing/geometry/potrace.py +++ b/vectorizing/geometry/potrace.py @@ -1,68 +1,41 @@ import numpy as np +from pathops import Path, FillType -from vectorizing.geometry.segment_list import SegmentList -from vectorizing.geometry.cubic_bezier import CubicBezier -# Given a potrace path, it constructs a compound path -# using SegmentList and CubicBezier def potrace_path_to_compound_path(potrace_path): - compound_path = [] + """ + Converts a potrace path into an SKPath. + This conversion is needed to later perform boolean operations + on compound paths. - for potrace_curve in potrace_path: - path = [] + Parameters: + potrace_path: The potrace path + + Returns: + The SKPath + """ + compound_path = Path(fillType=FillType.EVEN_ODD) + for potrace_curve in potrace_path: start_x, start_y = potrace_curve.start_point + compound_path.moveTo(start_x, start_y) + for segment in potrace_curve: end_x, end_y = segment.end_point - start = np.array([start_x, start_y]) - end = np.array([end_x, end_y]) if segment.is_corner: c_x, c_y = segment.c - c = np.array([c_x, c_y]) - path.append(SegmentList(np.array([start, c, end]))) - + compound_path.lineTo(c_x, c_y) + compound_path.lineTo(end_x, end_y) + else: c1_x, c1_y = segment.c1 c2_x, c2_y = segment.c2 - p1 = np.array([c1_x, c1_y]) - p2 = np.array([c2_x, c2_y]) - path.append(CubicBezier(start, p1, p2, end)) - + compound_path.cubicTo(c1_x, c1_y, c2_x, c2_y, end_x, end_y) + start_x = end_x start_y = end_y - compound_path.append(path) - - return compound_path - -# "Unfolds" a folded polygon, meaning: -# A polygon that can be written in the form p = [...SegmentList, ...SegmentList, ...] -# In the end, p can be written as p = [...SegmentList] -def unfold_polygon(folded_polygon): - unfolded = [] - poly_length = len(folded_polygon) - - for idx, segment_list in enumerate(folded_polygon): - point_list = segment_list.to_list() - if idx == poly_length - 1: - unfolded += point_list - else: - unfolded += point_list[:len(point_list) - 1] - - return unfolded + compound_path.close() -# Given a compound path, it converts it to a compound polygon -# by flattening curves. -# All coordinates can be scaled for convenience (see pyclipper) -def compound_path_to_compound_polygon(compound_path, tolerance, scale=1): - polygons = [ - [item.flattened(tolerance).scaled(scale) for item in path] - for path in compound_path - ] - return [unfold_polygon(folded_polygon) for folded_polygon in polygons] - -# Given a compound polygon, it converts it to a compound path. -# All coordinates can be scaled for convenience (see pyclipper) -def compound_polygon_to_compound_path(compound_polygon, scale = 1): - return [[SegmentList.from_polygon(polygon).scaled(scale)] for polygon in compound_polygon] \ No newline at end of file + return compound_path diff --git a/vectorizing/geometry/segment_list.py b/vectorizing/geometry/segment_list.py deleted file mode 100644 index 61192da..0000000 --- a/vectorizing/geometry/segment_list.py +++ /dev/null @@ -1,34 +0,0 @@ -import numpy as np -from vectorizing.geometry.bounds import Bounds - -# Class to represent a list of segments -# IMPORTANT: points should be an ndarray, and not a python list -class SegmentList: - def __init__(self, points): - self.start = points[0] - self.points = points - - def scaled(self, s): - return SegmentList(self.points * s) - - def flattened(self, _): - return self - - def to_list(self): - return list(self.points) - - def bounds(self, _): - t = self.points.T - x = t[0] - y = t[1] - - return Bounds( - np.min(x), - np.min(y), - np.max(x), - np.max(y) - ) - - @staticmethod - def from_polygon(polygon): - return SegmentList(np.array(polygon)) \ No newline at end of file diff --git a/vectorizing/solvers/color/ColorSolver.py b/vectorizing/solvers/color/ColorSolver.py index 9fdb854..fa19490 100644 --- a/vectorizing/solvers/color/ColorSolver.py +++ b/vectorizing/solvers/color/ColorSolver.py @@ -6,45 +6,41 @@ from vectorizing.solvers.color.clip import remove_layering from vectorizing.solvers.color.bitmaps import create_bitmaps + class ColorSolver: - def __init__(self, img, color_count, tolerance, timer): + def __init__(self, img, color_count, timer): color_count = color_count or ColorSolver.DEFAULT_COLOR_COUNT color_count = max(color_count, ColorSolver.MIN_COLOR_COUNT) color_count = min(color_count, ColorSolver.MAX_COLOR_COUNT) self.color_count = color_count - self.tolerance = tolerance self.img = limit_size(img) - + # Init image array self.img_arr = np.asarray(self.img).astype(np.uint8) self.timer = timer def solve(self): - self.timer.start_timer('Quantization') + self.timer.start_timer("Quantization") labels, colors, has_background = quantize(self.img_arr, self.color_count) self.timer.end_timer() - self.timer.start_timer('Bitmap Creation') + self.timer.start_timer("Bitmap Creation") bitmaps, colors = create_bitmaps(labels, colors, has_background) self.timer.end_timer() - self.timer.start_timer('Bitmap Tracing') + self.timer.start_timer("Bitmap Tracing") traced_bitmaps = [potrace.Bitmap(bitmap).trace() for bitmap in bitmaps] self.timer.end_timer() - self.timer.start_timer('Polygon Clipping') - compound_paths = remove_layering(traced_bitmaps, self.tolerance) + self.timer.start_timer("Polygon Clipping") + compound_paths = remove_layering(traced_bitmaps) self.timer.end_timer() - - return [ - compound_paths, - colors, - self.img.size[0], - self.img.size[1] - ] - + + return [compound_paths, colors, self.img.size[0], self.img.size[1]] + + ColorSolver.MIN_COLOR_COUNT = 2 ColorSolver.DEFAULT_COLOR_COUNT = 6 -ColorSolver.MAX_COLOR_COUNT = 64 \ No newline at end of file +ColorSolver.MAX_COLOR_COUNT = 64 diff --git a/vectorizing/solvers/color/clip.py b/vectorizing/solvers/color/clip.py index a409566..9d1563f 100644 --- a/vectorizing/solvers/color/clip.py +++ b/vectorizing/solvers/color/clip.py @@ -1,45 +1,16 @@ -import pyclipper +from pathops import op, PathOp +from vectorizing.geometry.potrace import potrace_path_to_compound_path -from vectorizing.geometry.potrace import ( - potrace_path_to_compound_path, - compound_path_to_compound_polygon, - compound_polygon_to_compound_path -) - -SCALE = 1_000_000 # Ensures paths are disjoint after tracing. # See bitmaps.py for why this is important. - -# NOTE: The clipper library uses integer coordinates only for numerical robustness. -# That's why coordinates are scaled by a big factor, to preserve precision. -def remove_layering(traced_bitmaps, tolerance): +def remove_layering(traced_bitmaps): compound_paths = [ - potrace_path_to_compound_path(traced) - for traced in traced_bitmaps + potrace_path_to_compound_path(traced) for traced in traced_bitmaps ] - compound_polygons = [ - compound_path_to_compound_polygon(compound_path, tolerance, SCALE) - for compound_path in compound_paths - ] - - for x in range(len(compound_polygons) - 1): - next = compound_polygons[x + 1] - - pc = pyclipper.Pyclipper() - pc.AddPaths(next, pyclipper.PT_CLIP, True) - pc.AddPaths(compound_polygons[x], pyclipper.PT_SUBJECT, True) - - compound_polygons[x] = pc.Execute( - pyclipper.CT_DIFFERENCE, - pyclipper.PFT_EVENODD, - pyclipper.PFT_EVENODD - ) - - compound_paths = [ - compound_polygon_to_compound_path(compound_polygon, 1 / SCALE) - for compound_polygon in compound_polygons - ] + for x in range(len(compound_paths) - 1): + next = compound_paths[x + 1] + compound_paths[x] = op(compound_paths[x], next, PathOp.DIFFERENCE) - return compound_paths \ No newline at end of file + return compound_paths diff --git a/vectorizing/svg/draw.py b/vectorizing/svg/draw.py deleted file mode 100644 index c89bfe9..0000000 --- a/vectorizing/svg/draw.py +++ /dev/null @@ -1,43 +0,0 @@ -from vectorizing.geometry.cubic_bezier import CubicBezier -from vectorizing.geometry.segment_list import SegmentList - -# Truncates a number to 2 decimal places -def truncate(number): - return "{:.2f}".format(number) - -# Draws a segment list's path data -def draw_segment_list(segment_list, first_in_path = False): - l = ' '.join([ - f'L {truncate(point[0])},{truncate(point[1])}' - for point in segment_list.points[1:] - ]) + ' ' - - if first_in_path: - m = f'M {truncate(segment_list.start[0])},{truncate(segment_list.start[1])} ' - return m + l - - return l - -# Draws a cubic bezier path data -def draw_cubic_bezier(cubic_bezier, first_in_path = False): - c = 'C' \ - f'{truncate(cubic_bezier.p1[0])},{truncate(cubic_bezier.p1[1])} ' \ - f'{truncate(cubic_bezier.p2[0])},{truncate(cubic_bezier.p2[1])} ' \ - f'{truncate(cubic_bezier.p3[0])},{truncate(cubic_bezier.p3[1])} ' - - if first_in_path: - m = f'M {truncate(cubic_bezier.p0[0])},{truncate(cubic_bezier.p0[1])} ' - return m + c - - return c - -class UnknownGeometricEntity (Exception): - def __init__ (self): - super().__init__(self, 'draw_geo: Unknown geometric entity.') - -def draw_geo(geo, first_in_path = False): - if isinstance(geo, CubicBezier): - return draw_cubic_bezier(geo, first_in_path) - if isinstance(geo, SegmentList): - return draw_segment_list(geo, first_in_path) - raise UnknownGeometricEntity() \ No newline at end of file diff --git a/vectorizing/svg/markup.py b/vectorizing/svg/markup.py index ffdf195..e44766a 100644 --- a/vectorizing/svg/markup.py +++ b/vectorizing/svg/markup.py @@ -1,26 +1,95 @@ -from vectorizing.svg.draw import draw_geo +def truncate(number): + """ + Truncates a number to two decimal places. + We use this to limit the amount of decimal places used + in the final generated SVG markup (reduces file sizes). -# Converts a color to a valid svg color value -def to_SVG_color(color): - func = 'rgb' if len(color) == 3 else 'rgba' - tuple = ''.join([f'{item},' if i != len(color) - 1 else f'{item}' for i, item in enumerate(color)]) - return f'{func}({tuple})' + Parameters: + number: The number to truncate. -# Creates markup of traced paths -def create_markup(compound_paths, colors, width, height): + Returns: + The truncated number. + """ + return "{:.2f}".format(number) + + +def to_SVG_color_string(color): + """ + Converts a color (represented as a tuple) to a valid + SVG color string. + + Parameters: + color: The input color. + + Returns: + The SVG color string. + + Example: + to_SVG_color_string([20, 30, 40]) => rgb(20, 30, 40) + to_SVG_color_string([20, 30, 40, 0.5]) => rgba(20, 30, 40, 0.5) + + """ + func = "rgb" if len(color) == 3 else "rgba" + tuple = "".join( + [ + f"{item}," if i != len(color) - 1 else f"{item}" + for i, item in enumerate(color) + ] + ) + return f"{func}({tuple})" + + +def generate_SVG_markup(compound_paths, colors, width, height): + """ + Generates SVG markup from a list of compound paths, + their respective colors, and the document dimensions. + + Parameters: + compound_paths: The compound path list (SKPath). + colors: Each compound path's color. + width: The width of the document. + height: The height of the document. + + Returns: + The SVG markup. + + Note: + compound paths are assumed to be made out of + only lines and cubic beziers. + """ paths_markup = [] for compound_path, color in zip(compound_paths, colors): - if not len(compound_path): + segments = list(compound_path.segments) + if not len(segments): continue - d = ''.join([draw_geo(geo, i == 0) for path in compound_path for i, geo in enumerate(path)]) + 'Z' - path_markup = f'' + d = "" + for segment in segments: + command = segment[0] + if command == "moveTo": + x = segment[1][0][0] + y = segment[1][0][1] + d += f"M {truncate(x)} {truncate(y)} " + if command == "lineTo": + x = segment[1][0][0] + y = segment[1][0][1] + d += f"L {truncate(x)} {truncate(y)} " + if command == "curveTo": + d += ( + "C" + f"{truncate(segment[1][0][0])},{truncate(segment[1][0][1])} " + f"{truncate(segment[1][1][0])},{truncate(segment[1][1][1])} " + f"{truncate(segment[1][2][0])},{truncate(segment[1][2][1])} " + ) + if command == "closePath": + d += "Z" + + path_markup = f'' paths_markup.append(path_markup) - paths_markup = '\n'.join(paths_markup) + paths_markup = "\n".join(paths_markup) return ( f'\n' - f'\n{paths_markup}\n\n' - f'' - ) \ No newline at end of file + f"\n{paths_markup}\n\n" + f"" + ) From a7cc6557cc5c7efd4090e3e9c13d3bf82bf1dead Mon Sep 17 00:00:00 2001 From: big-nacho Date: Fri, 30 May 2025 14:04:02 +0000 Subject: [PATCH 2/2] Add docstring CU-86c3mbjx8 --- vectorizing/solvers/color/clip.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/vectorizing/solvers/color/clip.py b/vectorizing/solvers/color/clip.py index 9d1563f..e47ce7b 100644 --- a/vectorizing/solvers/color/clip.py +++ b/vectorizing/solvers/color/clip.py @@ -1,10 +1,17 @@ from pathops import op, PathOp from vectorizing.geometry.potrace import potrace_path_to_compound_path - -# Ensures paths are disjoint after tracing. -# See bitmaps.py for why this is important. def remove_layering(traced_bitmaps): + """ + Performs boolean operations on a list of traced bitmaps + to ensure that they are all disjoint. + + Parameters: + traced_bitmaps: The list of traced bitmaps (potrace paths). + + Returns: + The processed list of compound paths. + """ compound_paths = [ potrace_path_to_compound_path(traced) for traced in traced_bitmaps ]