Initial commit
- find receipt - find lines
203
.gitignore
vendored
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
# custom
|
||||||
|
result/*
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
pip-wheel-metadata/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
.python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don’t work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# celery beat schedule file
|
||||||
|
celerybeat-schedule
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
|
||||||
|
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||||
|
|
||||||
|
# User-specific stuff
|
||||||
|
.idea/**/workspace.xml
|
||||||
|
.idea/**/tasks.xml
|
||||||
|
.idea/**/usage.statistics.xml
|
||||||
|
.idea/**/dictionaries
|
||||||
|
.idea/**/shelf
|
||||||
|
|
||||||
|
# Generated files
|
||||||
|
.idea/**/contentModel.xml
|
||||||
|
|
||||||
|
# Sensitive or high-churn files
|
||||||
|
.idea/**/dataSources/
|
||||||
|
.idea/**/dataSources.ids
|
||||||
|
.idea/**/dataSources.local.xml
|
||||||
|
.idea/**/sqlDataSources.xml
|
||||||
|
.idea/**/dynamic.xml
|
||||||
|
.idea/**/uiDesigner.xml
|
||||||
|
.idea/**/dbnavigator.xml
|
||||||
|
|
||||||
|
# Gradle
|
||||||
|
.idea/**/gradle.xml
|
||||||
|
.idea/**/libraries
|
||||||
|
|
||||||
|
# Gradle and Maven with auto-import
|
||||||
|
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||||
|
# since they will be recreated, and may cause churn. Uncomment if using
|
||||||
|
# auto-import.
|
||||||
|
# .idea/modules.xml
|
||||||
|
# .idea/*.iml
|
||||||
|
# .idea/modules
|
||||||
|
|
||||||
|
# CMake
|
||||||
|
cmake-build-*/
|
||||||
|
|
||||||
|
# Mongo Explorer plugin
|
||||||
|
.idea/**/mongoSettings.xml
|
||||||
|
|
||||||
|
# File-based project format
|
||||||
|
*.iws
|
||||||
|
|
||||||
|
# IntelliJ
|
||||||
|
out/
|
||||||
|
|
||||||
|
# mpeltonen/sbt-idea plugin
|
||||||
|
.idea_modules/
|
||||||
|
|
||||||
|
# JIRA plugin
|
||||||
|
atlassian-ide-plugin.xml
|
||||||
|
|
||||||
|
# Cursive Clojure plugin
|
||||||
|
.idea/replstate.xml
|
||||||
|
|
||||||
|
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||||
|
com_crashlytics_export_strings.xml
|
||||||
|
crashlytics.properties
|
||||||
|
crashlytics-build.properties
|
||||||
|
fabric.properties
|
||||||
|
|
||||||
|
# Editor-based Rest Client
|
||||||
|
.idea/httpRequests
|
||||||
|
|
||||||
|
# Android studio 3.1+ serialized cache file
|
||||||
|
.idea/caches/build_file_checksums.ser
|
||||||
4
.idea/encodings.xml
generated
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Encoding" addBOMForNewFiles="with NO BOM" />
|
||||||
|
</project>
|
||||||
28
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoredPackages">
|
||||||
|
<value>
|
||||||
|
<list size="1">
|
||||||
|
<item index="0" class="java.lang.String" itemvalue="PIL" />
|
||||||
|
</list>
|
||||||
|
</value>
|
||||||
|
</option>
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PyPep8Inspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoredErrors">
|
||||||
|
<list>
|
||||||
|
<option value="E501" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PyUnresolvedReferencesInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoredIdentifiers">
|
||||||
|
<list>
|
||||||
|
<option value="int.dot" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</inspection_tool>
|
||||||
|
</profile>
|
||||||
|
</component>
|
||||||
7
.idea/misc.xml
generated
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="JavaScriptSettings">
|
||||||
|
<option name="languageLevel" value="ES6" />
|
||||||
|
</component>
|
||||||
|
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.5.2 (/usr/bin/python3.5)" project-jdk-type="Python SDK" />
|
||||||
|
</project>
|
||||||
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/receipt-recognition.iml" filepath="$PROJECT_DIR$/.idea/receipt-recognition.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/other.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="PySciProjectComponent">
|
||||||
|
<option name="PY_SCI_VIEW" value="true" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
13
.idea/receipt-recognition.iml
generated
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="PYTHON_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/data" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="jdk" jdkName="Python 3.5.2 (/usr/bin/python3.5)" jdkType="Python SDK" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
<component name="TestRunnerService">
|
||||||
|
<option name="PROJECT_TEST_RUNNER" value="Unittests" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
BIN
data/receipt-01.png
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
data/receipt-02.jpg
Normal file
|
After Width: | Height: | Size: 78 KiB |
BIN
data/receipt-03.png
Normal file
|
After Width: | Height: | Size: 754 KiB |
BIN
data/receipt-04.jpg
Normal file
|
After Width: | Height: | Size: 331 KiB |
BIN
data/receipt-05.jpg
Normal file
|
After Width: | Height: | Size: 156 KiB |
BIN
data/receipt-06.png
Normal file
|
After Width: | Height: | Size: 868 KiB |
BIN
data/receipt-07.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
data/receipt-08.jpg
Normal file
|
After Width: | Height: | Size: 3.3 MiB |
BIN
data/receipt-09.jpg
Normal file
|
After Width: | Height: | Size: 940 KiB |
BIN
data/receipt-10.jpg
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
data/receipt-11.jpg
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
data/receipt-12.jpg
Normal file
|
After Width: | Height: | Size: 23 KiB |
20
main.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import glob
|
||||||
|
import os
|
||||||
|
|
||||||
|
from src.processing.loader import load_image, save_image
|
||||||
|
from src.processing.linefinder import find_lines, preparation
|
||||||
|
from matplotlib import pyplot as plt
|
||||||
|
|
||||||
|
from src.processing.receiptcutter import cut_receipt
|
||||||
|
|
||||||
|
for root, dirs, files in os.walk("data"):
|
||||||
|
for file in files:
|
||||||
|
if file.startswith("receipt-08"):
|
||||||
|
image = load_image("data/"+file)
|
||||||
|
receipt = cut_receipt(image, draw_steps=True)
|
||||||
|
plt.imshow(receipt)
|
||||||
|
plt.show()
|
||||||
|
lines = find_lines(receipt)
|
||||||
|
for line in lines:
|
||||||
|
plt.imshow(line, cmap="gray")
|
||||||
|
plt.show()
|
||||||
BIN
paper/A_Fast_Decision_Technique_for_Hierarchical_Hough_T.pdf
Normal file
6
requirements.txt
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
imageio
|
||||||
|
PIL
|
||||||
|
scikit-image
|
||||||
|
scipy
|
||||||
|
numpy
|
||||||
|
matplotlib
|
||||||
0
src/__init__.py
Normal file
0
src/processing/__init__.py
Normal file
15
src/processing/imageprocessing.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
def rgb2gray(image):
|
||||||
|
if image.shape[2] == 4:
|
||||||
|
return image.dot(np.array([0.2627, 0.6780, 0.0593, 0]) / 255)
|
||||||
|
else:
|
||||||
|
return image.dot(np.array([0.2627, 0.6780, 0.0593]) / 255)
|
||||||
|
|
||||||
|
def rgb2gray_value(image):
|
||||||
|
image = image[:, :, :3]
|
||||||
|
maxc = np.maximum(np.maximum(image[:, :, 0], image[:, :, 1]), image[:, :, 2])/255
|
||||||
|
minc = np.minimum(np.minimum(image[:, :, 0], image[:, :, 1]), image[:, :, 2])/255
|
||||||
|
|
||||||
|
return maxc - minc
|
||||||
166
src/processing/linefinder.py
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
import numpy as np
|
||||||
|
from scipy.ndimage import measurements
|
||||||
|
|
||||||
|
from src.processing.imageprocessing import rgb2gray
|
||||||
|
from src.processing.loader import load_numpy, save_numpy, save_image, load_image
|
||||||
|
from src.utils.cmap_generator import rand_cmap, list_cmap
|
||||||
|
from matplotlib import pyplot as plt
|
||||||
|
|
||||||
|
|
||||||
|
def find_lines(image):
|
||||||
|
gray, binary, magnitude = preparation(image)
|
||||||
|
plt.imshow(binary, cmap="gray")
|
||||||
|
plt.show()
|
||||||
|
backtrack = load_numpy("result/backtrack.npz")
|
||||||
|
if backtrack is None:
|
||||||
|
energy, backtrack = minimum_seam(binary, magnitude)
|
||||||
|
save_numpy("result/backtrack.npz", backtrack)
|
||||||
|
save_image("result/gray.png", gray)
|
||||||
|
seams = calculate_seams(backtrack)
|
||||||
|
labeled, ncomponents = group_empty_boxes(seams)
|
||||||
|
return generate_lines(labeled, ncomponents, gray)
|
||||||
|
|
||||||
|
|
||||||
|
def preparation(image):
|
||||||
|
gray = rgb2gray(image)
|
||||||
|
cnt, vals = np.histogram(gray, 256)
|
||||||
|
threshold = get_threshold(cnt)/256*0.96
|
||||||
|
binary = (gray > threshold).astype(np.int_)
|
||||||
|
magnitude = np.ones_like(binary)-binary
|
||||||
|
imin = 0
|
||||||
|
while np.sum(binary[:, imin]) / binary.shape[0] < 0.6:
|
||||||
|
imin += 1
|
||||||
|
imax = binary.shape[1]
|
||||||
|
while np.sum(binary[:, imax-1]) / binary.shape[0] < 0.6:
|
||||||
|
imax -= 1
|
||||||
|
jmin = 0
|
||||||
|
while np.sum(binary[jmin]) / binary.shape[1] < 0.6:
|
||||||
|
jmin += 1
|
||||||
|
jmax = binary.shape[0]
|
||||||
|
while np.sum(binary[jmax-1]) / binary.shape[1] < 0.6:
|
||||||
|
jmax -= 1
|
||||||
|
return gray[jmin:jmax, imin:imax], binary[jmin:jmax, imin:imax], magnitude[jmin:jmax, imin:imax]
|
||||||
|
|
||||||
|
|
||||||
|
def get_threshold(hist, thresh=None):
|
||||||
|
# ISO data algorithm
|
||||||
|
# https://felixniklas.com/imageprocessing/binarization
|
||||||
|
if thresh is None:
|
||||||
|
thresh = hist.shape[0] // 2
|
||||||
|
m1 = median(hist, 0, thresh)
|
||||||
|
m2 = median(hist, thresh, hist.shape[0])
|
||||||
|
tk = int((m1 + m2) / 2)
|
||||||
|
if thresh == tk:
|
||||||
|
return np.round(tk)
|
||||||
|
else:
|
||||||
|
return get_threshold(hist, tk)
|
||||||
|
|
||||||
|
|
||||||
|
def median(values, start, stop):
|
||||||
|
p, x = 0, 0
|
||||||
|
for idx, val in enumerate(values[start:stop], start=start):
|
||||||
|
p += val
|
||||||
|
x += idx*val
|
||||||
|
if p == 0:
|
||||||
|
return start if start != 0 else stop+1
|
||||||
|
return x/p
|
||||||
|
|
||||||
|
|
||||||
|
def minimum_seam(img, energy_map):
|
||||||
|
r, c = img.shape
|
||||||
|
M = energy_map.copy()
|
||||||
|
backtrack = np.zeros_like(M, dtype=np.int)
|
||||||
|
for j in range(1, c):
|
||||||
|
for i in range(0, r):
|
||||||
|
# Handle the top edge of the image, to ensure we don't index -1
|
||||||
|
if i == 0:
|
||||||
|
idx = np.argmin(M[i:i+2, j-1])
|
||||||
|
backtrack[i, j] = idx + i
|
||||||
|
min_energy = M[idx+i, j-1]
|
||||||
|
if idx > 0:
|
||||||
|
min_energy += 1
|
||||||
|
else:
|
||||||
|
m = M[i-1:i+2, j-1]
|
||||||
|
idx = (np.argmin(np.roll(m, 2)) - 2) % len(m)
|
||||||
|
backtrack[i, j] = idx + i - 1
|
||||||
|
min_energy = M[idx+i-1, j-1]
|
||||||
|
if idx != 1:
|
||||||
|
min_energy += 1
|
||||||
|
M[i, j] += min_energy
|
||||||
|
return M, backtrack
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_seams(links):
|
||||||
|
h, w = links.shape
|
||||||
|
seams = np.zeros_like(links)
|
||||||
|
seams[:, w-1] = 1
|
||||||
|
for x in range(w-1, 0, -1):
|
||||||
|
for y in range(h):
|
||||||
|
seams[links[y, x], x-1] += seams[y, x]
|
||||||
|
return seams
|
||||||
|
|
||||||
|
|
||||||
|
def group_empty_boxes(seams):
|
||||||
|
clouds = 1 - np.minimum(seams, 1)
|
||||||
|
structure = np.array([[0, 1, 0], [1, 1, 1], [0, 1, 0]], dtype=np.int)
|
||||||
|
labeled, ncomponents = measurements.label(clouds, structure)
|
||||||
|
return labeled, ncomponents
|
||||||
|
|
||||||
|
|
||||||
|
def generate_lines(labeled, ncomponents, gray):
|
||||||
|
plt.imshow(labeled, cmap=rand_cmap(ncomponents, type='hard', first_color_black=True, last_color_black=False, verbose=False))
|
||||||
|
plt.show()
|
||||||
|
plt.imsave("result/groups.png", labeled, cmap=rand_cmap(ncomponents, type='hard', first_color_black=True, last_color_black=False, verbose=False))
|
||||||
|
groups = np.copy(labeled)
|
||||||
|
group_id = 1
|
||||||
|
|
||||||
|
entries = []
|
||||||
|
pixelgroup = None
|
||||||
|
in_top_mode = True
|
||||||
|
indices = np.indices(labeled.shape).T[:, :, [1, 0]]
|
||||||
|
indices = np.swapaxes(indices, 0, 1)
|
||||||
|
colors = [[0, 0, 0]]
|
||||||
|
for label in range(1, ncomponents+1):
|
||||||
|
pixel = indices[labeled == label]
|
||||||
|
minp = np.min(pixel, axis=0)
|
||||||
|
maxp = np.max(pixel, axis=0)
|
||||||
|
right_pixel = pixel[pixel[:, 0] == maxp[0]][:, 1]
|
||||||
|
second_pixel = pixel[pixel[:, 0] == maxp[0]-1][:, 1]
|
||||||
|
color = [0, 0, 1]
|
||||||
|
if second_pixel.size > 0 and min(right_pixel) > min(second_pixel):
|
||||||
|
# up
|
||||||
|
color[0] = 1
|
||||||
|
if not in_top_mode:
|
||||||
|
minps = np.min(pixelgroup, axis=0)
|
||||||
|
maxps = np.max(pixelgroup, axis=0)
|
||||||
|
if pixel.shape[0] < 20 or maxps[1]-minps[1] < 5 or maxps[0]-minps[0] < 5:
|
||||||
|
continue
|
||||||
|
entry = gray[minps[1]:maxps[1]+1, minps[0]:maxps[0]+1]
|
||||||
|
pixelgroup = np.subtract(pixelgroup, minps)
|
||||||
|
white = np.ones_like(entry) * np.max(entry)
|
||||||
|
white[pixelgroup[:, 1], pixelgroup[:, 0]] = entry[pixelgroup[:, 1], pixelgroup[:, 0]]
|
||||||
|
entries.append(white)
|
||||||
|
pixelgroup = None
|
||||||
|
group_id += 1
|
||||||
|
in_top_mode = True
|
||||||
|
if second_pixel.size > 0 and max(right_pixel) < max(second_pixel):
|
||||||
|
# down
|
||||||
|
color[1] = 1
|
||||||
|
in_top_mode = False
|
||||||
|
groups[labeled == label] = group_id
|
||||||
|
if pixelgroup is None:
|
||||||
|
pixelgroup = pixel[:, :]
|
||||||
|
else:
|
||||||
|
pixelgroup = np.concatenate((pixelgroup, pixel))
|
||||||
|
colors.append(color)
|
||||||
|
#plt.imsave("result/groups_types.png", labeled,
|
||||||
|
# cmap=list_cmap(np.array(colors)))
|
||||||
|
#g = np.array(load_image("result/groups_types.png")[:, :, :3], dtype="float")
|
||||||
|
#b = load_image("result/gray.png")
|
||||||
|
#t = (g[:, :, :3] + np.tile(b[:, :], (3, 1, 1)).swapaxes(2, 0).swapaxes(1, 0) * 1) / 2
|
||||||
|
#t = (g[:, :, :3] + np.tile(b[:, :], (3, 1, 1)).swapaxes(2, 0).swapaxes(1, 0) * 1) / 2
|
||||||
|
#save_image("result/combined_types.png", t/255)
|
||||||
|
#plt.imsave("result/combined_new.png", groups,
|
||||||
|
# cmap=rand_cmap(500, type='hard', first_color_black=True, last_color_black=False, verbose=False))
|
||||||
|
|
||||||
|
return entries
|
||||||
24
src/processing/loader.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import os
|
||||||
|
import imageio
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
def load_image(path):
|
||||||
|
image = imageio.imread(path)
|
||||||
|
return image
|
||||||
|
|
||||||
|
|
||||||
|
def save_image(path, image):
|
||||||
|
x = np.array(image)
|
||||||
|
imageio.imsave(path, x)
|
||||||
|
|
||||||
|
|
||||||
|
def save_numpy(path, array):
|
||||||
|
np.savez(path, array=array)
|
||||||
|
|
||||||
|
|
||||||
|
def load_numpy(path):
|
||||||
|
if os.path.isfile(path):
|
||||||
|
data = np.load(path)
|
||||||
|
return data['array']
|
||||||
|
return None
|
||||||
358
src/processing/receiptcutter.py
Normal file
@@ -0,0 +1,358 @@
|
|||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
from skimage.transform import resize
|
||||||
|
from scipy.ndimage import gaussian_filter
|
||||||
|
from matplotlib import pyplot as plt
|
||||||
|
|
||||||
|
from src.processing.imageprocessing import rgb2gray_value
|
||||||
|
from scipy import signal
|
||||||
|
import numpy as np
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
|
||||||
|
class Line:
|
||||||
|
def __init__(self, intercept=0, slope=0, points=None):
|
||||||
|
self.intercept = intercept
|
||||||
|
self.slope = slope
|
||||||
|
self.points = [] if points is None else points
|
||||||
|
self.splits = []
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "m="+str(self.slope)+";n="+str(self.intercept)+";split="+str(self.splits)
|
||||||
|
|
||||||
|
|
||||||
|
def cut_receipt(image, draw_steps=False):
|
||||||
|
# Hough params
|
||||||
|
THETA_RES = 5
|
||||||
|
WIDTH_RES = 5
|
||||||
|
image = image[:, :, :3]
|
||||||
|
gray, scale = prepare_image(image)
|
||||||
|
grad_strength, grad_angle = sobel_edges(gray)
|
||||||
|
edges = canny(grad_strength, grad_angle)
|
||||||
|
hough, references = hough_lines(edges, theta_res=THETA_RES, width_res=WIDTH_RES)
|
||||||
|
if draw_steps:
|
||||||
|
draw_hough_lines(image, scale, hough, references, edges.shape, theta_res=THETA_RES, width_res=WIDTH_RES)
|
||||||
|
lines = convert_to_lines(scale, hough, references, edges.shape, theta_res=THETA_RES, width_res=WIDTH_RES)
|
||||||
|
lines = split_segments(lines, image.shape)
|
||||||
|
lines = find_important_segments(lines, scale)
|
||||||
|
max_score, corners = find_largest_rectangle(lines, image.shape)
|
||||||
|
if corners is not None:
|
||||||
|
if draw_steps:
|
||||||
|
draw_rectangle(image, corners)
|
||||||
|
return crop_image(image, corners)
|
||||||
|
return image
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_image(image):
|
||||||
|
gray = rgb2gray_value(image)
|
||||||
|
gray = resize(gray, (500, int(gray.shape[1] / gray.shape[0] * 500)), mode="reflect")
|
||||||
|
scale = image.shape[0] / gray.shape[0]
|
||||||
|
gray = gaussian_filter(gray, sigma=max(3, scale * 2 - 8))
|
||||||
|
return gray, scale
|
||||||
|
|
||||||
|
|
||||||
|
def sobel_edges(gray):
|
||||||
|
sobely = np.array([[0, 2, 0], [0, 0, 0], [0, -2, 0]], dtype='float')
|
||||||
|
gray_y = signal.convolve2d(gray, sobely, boundary='symm', mode='same')
|
||||||
|
sobelx = np.array([[0, 0, 0], [-2, 0, 2], [0, 0, 0]], dtype='float')
|
||||||
|
gray_x = signal.convolve2d(gray, sobelx, boundary='symm', mode='same')
|
||||||
|
grad_strength = np.sqrt(np.square(gray_y)+np.square(gray_x))
|
||||||
|
grad_angle = np.arctan(np.true_divide(gray_y, gray_x, where=gray_x != 0))
|
||||||
|
return grad_strength, grad_angle
|
||||||
|
|
||||||
|
|
||||||
|
def canny(grad_strength, grad_angle):
|
||||||
|
# Angle preparation
|
||||||
|
grad_angle = np.round(grad_angle / np.pi * 4) + 2
|
||||||
|
grad_angle = np.array(grad_angle, dtype="uint8")
|
||||||
|
grad_angle[grad_angle == 4] = 0
|
||||||
|
h, w = grad_strength.shape
|
||||||
|
|
||||||
|
# Canny
|
||||||
|
CANNY_MIN = 0.01
|
||||||
|
CANNY_MAX = 0.02
|
||||||
|
strenghts = np.zeros_like(grad_strength)
|
||||||
|
for y in range(1, h-1):
|
||||||
|
for x in range(1, w-1):
|
||||||
|
if grad_strength[y, x] < CANNY_MIN:
|
||||||
|
continue
|
||||||
|
if grad_angle[y, x] == 0:
|
||||||
|
if grad_strength[y-1, x] < grad_strength[y, x] and grad_strength[y+1, x] < grad_strength[y, x]:
|
||||||
|
strenghts[y, x] = grad_strength[y, x]
|
||||||
|
elif grad_angle[y, x] == 1:
|
||||||
|
if grad_strength[y-1, x-1] < grad_strength[y, x] and grad_strength[y+1, x+1] < grad_strength[y, x]:
|
||||||
|
strenghts[y, x] = grad_strength[y, x]
|
||||||
|
elif grad_angle[y, x] == 2:
|
||||||
|
if grad_strength[y, x-1] < grad_strength[y, x] and grad_strength[y, x+1] < grad_strength[y, x]:
|
||||||
|
strenghts[y, x] = grad_strength[y, x]
|
||||||
|
elif grad_angle[y, x] == 3:
|
||||||
|
if grad_strength[y-1, x+1] < grad_strength[y, x] and grad_strength[y+1, x-1] < grad_strength[y, x]:
|
||||||
|
strenghts[y, x] = grad_strength[y, x]
|
||||||
|
return strenghts
|
||||||
|
|
||||||
|
|
||||||
|
def hough_lines(canny, theta_res=5, width_res=5):
|
||||||
|
h, w = canny.shape
|
||||||
|
theta_angle = 180 // theta_res
|
||||||
|
mid = int(np.round(np.sqrt((h//width_res)**2+(w//width_res)**2)))
|
||||||
|
|
||||||
|
hough = np.zeros((theta_angle, mid*2+2))
|
||||||
|
references = defaultdict(list)
|
||||||
|
for y in range(1, h-1):
|
||||||
|
for x in range(1, w-1):
|
||||||
|
if canny[y, x] > 0:
|
||||||
|
for theta in range(theta_angle):
|
||||||
|
t = theta * np.pi / theta_angle
|
||||||
|
q = (x * np.cos(t) + y * np.sin(t))/width_res + mid
|
||||||
|
hough[theta, int(q)] += 1
|
||||||
|
references[int(q) * theta_angle + theta].append([y, x])
|
||||||
|
lines = np.unravel_index(np.argsort(hough.ravel())[-100:][::-1], hough.shape)
|
||||||
|
lines = np.array(lines).T
|
||||||
|
results = []
|
||||||
|
COVER_Y = 10
|
||||||
|
COVER_X = 15
|
||||||
|
for line in lines:
|
||||||
|
if hough[line[0], line[1]] > 0:
|
||||||
|
results.append([line[0]*theta_res, (line[1]-mid)*width_res])
|
||||||
|
hough[max(0, line[0]-COVER_Y):line[0]+COVER_Y, max(0, line[1]-COVER_X):line[1]+COVER_X] = 0
|
||||||
|
y1, x1, y2, x2 = None, None, None, None
|
||||||
|
if line[0]-COVER_Y < 0:
|
||||||
|
y1 = hough.shape[0]+line[0]-COVER_Y
|
||||||
|
if line[1]-COVER_X < 0:
|
||||||
|
x1 = hough.shape[1]+line[1]-COVER_X
|
||||||
|
if line[0]+COVER_Y > hough.shape[0]:
|
||||||
|
y2 = line[0]+COVER_Y-hough.shape[0]
|
||||||
|
if line[1]+COVER_X > hough.shape[1]:
|
||||||
|
x2 = line[1]+COVER_X-hough.shape[1]
|
||||||
|
if any(x is not None for x in [y1, x1, y2, x2]):
|
||||||
|
ty1 = y1 if y1 is not None else (0 if y2 is not None else line[0]-COVER_Y)
|
||||||
|
tx1 = x1 if x1 is not None else (0 if x2 is not None else line[1]-COVER_X)
|
||||||
|
ty2 = y2 if y2 is not None else (hough.shape[0] if y1 is not None else line[0]+COVER_Y)
|
||||||
|
tx2 = x2 if x2 is not None else (hough.shape[1] if x1 is not None else line[1]+COVER_X)
|
||||||
|
hough[ty1:ty2, tx1:tx2] = 0
|
||||||
|
if len(results) > 5:
|
||||||
|
break
|
||||||
|
results = np.array(results)
|
||||||
|
return results, references
|
||||||
|
|
||||||
|
|
||||||
|
def draw_hough_lines(image, scale, results, references, shape, theta_res=5, width_res=5):
|
||||||
|
h, w = shape
|
||||||
|
theta_angle = 180 // theta_res
|
||||||
|
mid = int(np.round(np.sqrt((h//width_res)**2+(w//width_res)**2)))
|
||||||
|
|
||||||
|
draw_image = np.copy(image)
|
||||||
|
RED_WIDTH = 10
|
||||||
|
GREEN_WIDTH = 5
|
||||||
|
for result in results:
|
||||||
|
refs = references[(result[1]/width_res+mid) * theta_angle + result[0]//theta_res]
|
||||||
|
for ref in refs:
|
||||||
|
xa = int(scale * ref[1])
|
||||||
|
ya = int(scale * ref[0])
|
||||||
|
draw_image[max(0, ya - RED_WIDTH):ya + RED_WIDTH, max(0, xa - RED_WIDTH):xa + RED_WIDTH] = np.array([0, 255, 0])
|
||||||
|
if result[0] == 0:
|
||||||
|
x = int(result[1] * scale)
|
||||||
|
for y in range(image.shape[0]):
|
||||||
|
draw_image[max(0, y-GREEN_WIDTH):y+GREEN_WIDTH, max(0, x-GREEN_WIDTH):x+GREEN_WIDTH] = np.array([255, 0, 0])
|
||||||
|
else:
|
||||||
|
angle = (90 - result[0]) / 180 * np.pi
|
||||||
|
m = np.sin(angle) / np.cos(angle)
|
||||||
|
n = result[1] / np.cos(angle) * scale
|
||||||
|
for x in range(image.shape[1]):
|
||||||
|
y = int(n - x * m)
|
||||||
|
if 0 < y < image.shape[0]:
|
||||||
|
draw_image[max(0, y - GREEN_WIDTH):y + GREEN_WIDTH, max(0, x - GREEN_WIDTH):x + GREEN_WIDTH] = np.array([255, 0, 0])
|
||||||
|
plt.imshow(draw_image)
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
|
||||||
|
def convert_to_lines(scale, results, references, shape, theta_res=5, width_res=5):
|
||||||
|
h, w = shape
|
||||||
|
theta_angle = 180 // theta_res
|
||||||
|
mid = int(np.round(np.sqrt((h//width_res)**2+(w//width_res)**2)))
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
for result in results:
|
||||||
|
points = []
|
||||||
|
refs = references[(result[1]/width_res+mid) * theta_angle + result[0]//theta_res]
|
||||||
|
for ref in refs:
|
||||||
|
xa = int(scale * ref[1])
|
||||||
|
ya = int(scale * ref[0])
|
||||||
|
points.append([ya, xa])
|
||||||
|
if result[0] == 0:
|
||||||
|
x = int(result[1] * scale)
|
||||||
|
lines.append(Line(intercept=x, slope=None, points=points))
|
||||||
|
else:
|
||||||
|
angle = (90 - result[0]) / 180 * np.pi
|
||||||
|
m = np.sin(angle) / -np.cos(angle)
|
||||||
|
n = result[1] / np.cos(angle) * scale
|
||||||
|
lines.append(Line(intercept=n, slope=m, points=points))
|
||||||
|
return lines
|
||||||
|
|
||||||
|
|
||||||
|
def split_segments(lines, image_shape):
|
||||||
|
for idx1, line1 in enumerate(lines):
|
||||||
|
for idx2, line2 in enumerate(lines[idx1+1:], idx1+1):
|
||||||
|
if line1.slope == line2.slope:
|
||||||
|
continue
|
||||||
|
elif line1.slope is None:
|
||||||
|
x = line1.intercept
|
||||||
|
y = line2.intercept + line2.slope * line1.intercept
|
||||||
|
if 0 < y < image_shape[0]:
|
||||||
|
line1.splits.append((int(y), int(x), idx2))
|
||||||
|
line2.splits.append((int(y), int(x), idx1))
|
||||||
|
elif line2.slope is None:
|
||||||
|
x = line2.intercept
|
||||||
|
y = line1.intercept + line1.slope * line2.intercept
|
||||||
|
if 0 < y < image_shape[0]:
|
||||||
|
line1.splits.append((int(y), int(x), idx2))
|
||||||
|
line2.splits.append((int(y), int(x), idx1))
|
||||||
|
else:
|
||||||
|
x = (line2.intercept - line1.intercept) / (line1.slope - line2.slope)
|
||||||
|
y = line1.intercept + line1.slope * x
|
||||||
|
if 0 < x < image_shape[1] and 0 < y < image_shape[0]:
|
||||||
|
line1.splits.append((int(y), int(x), idx2))
|
||||||
|
line2.splits.append((int(y), int(x), idx1))
|
||||||
|
if line1.slope is None:
|
||||||
|
line1.splits.append((0, int(line1.intercept), None))
|
||||||
|
line1.splits.append((image_shape[0], int(line1.intercept), None))
|
||||||
|
elif line1.slope == 0:
|
||||||
|
line1.splits.append((int(line1.intercept), 0, None))
|
||||||
|
line1.splits.append((int(line1.intercept), image_shape[1], None))
|
||||||
|
else:
|
||||||
|
y = min(max(0, line1.intercept), image_shape[0])
|
||||||
|
x = (y - line1.intercept) / line1.slope
|
||||||
|
line1.splits.append((int(y), int(x), None))
|
||||||
|
y = min(max(0, line1.intercept+line1.slope*image_shape[1]), image_shape[0])
|
||||||
|
x = (y - line1.intercept) / line1.slope
|
||||||
|
line1.splits.append((int(y), int(x), None))
|
||||||
|
if line1.slope is not None:
|
||||||
|
line1.splits = sorted(line1.splits, key=lambda x: (x[1], x[0]))
|
||||||
|
else:
|
||||||
|
line1.splits = sorted(line1.splits, key=lambda x: (x[0], x[1]))
|
||||||
|
return lines
|
||||||
|
|
||||||
|
|
||||||
|
def find_important_segments(lines, scale):
|
||||||
|
NOT_RELEVANT_THRESHOLD = 0.10
|
||||||
|
ADD_TINY_FRAGMENTS = 30
|
||||||
|
for line in lines:
|
||||||
|
counts = np.zeros((len(line.splits)-1, ))
|
||||||
|
for point in line.points:
|
||||||
|
if line.slope is None:
|
||||||
|
x = point[0]
|
||||||
|
else:
|
||||||
|
a, b = point[1], point[0]
|
||||||
|
m, n = line.slope, line.intercept
|
||||||
|
x = (a+b*m-m*n) / (m**2 + 1)
|
||||||
|
for i in range(len(line.splits)-1):
|
||||||
|
if line.slope is not None:
|
||||||
|
lower = line.splits[i][1]
|
||||||
|
upper = line.splits[i+1][1]
|
||||||
|
else:
|
||||||
|
lower = line.splits[i][0]
|
||||||
|
upper = line.splits[i+1][0]
|
||||||
|
if lower <= x < upper:
|
||||||
|
counts[i] += 1
|
||||||
|
break
|
||||||
|
counts = counts / np.sum(counts)
|
||||||
|
start = None
|
||||||
|
end = None
|
||||||
|
for idx, count in enumerate(counts):
|
||||||
|
if count > NOT_RELEVANT_THRESHOLD:
|
||||||
|
if start is None:
|
||||||
|
start = idx
|
||||||
|
end = idx + 1
|
||||||
|
if start is None:
|
||||||
|
line.splits = []
|
||||||
|
else:
|
||||||
|
while start > 0 and np.sqrt((line.splits[start][0]-line.splits[start-1][0])**2 + (line.splits[start][1]-line.splits[start-1][1])**2) / scale < ADD_TINY_FRAGMENTS:
|
||||||
|
start -= 1
|
||||||
|
while end < len(line.splits)-1 and np.sqrt((line.splits[end][0]-line.splits[end+1][0])**2 + (line.splits[end][1]-line.splits[end+1][1])**2) / scale < ADD_TINY_FRAGMENTS:
|
||||||
|
end += 1
|
||||||
|
line.splits = line.splits[start:end+1]
|
||||||
|
|
||||||
|
# check if reverse reference exits
|
||||||
|
for idx, line in enumerate(lines):
|
||||||
|
new_splits = []
|
||||||
|
for split in line.splits:
|
||||||
|
if split[2] is None:
|
||||||
|
new_splits.append(split)
|
||||||
|
continue
|
||||||
|
for split2 in lines[split[2]].splits:
|
||||||
|
if split2[2] == idx:
|
||||||
|
new_splits.append(split)
|
||||||
|
break
|
||||||
|
line.splits = new_splits
|
||||||
|
return lines
|
||||||
|
|
||||||
|
|
||||||
|
def find_largest_rectangle(lines, image_shape):
|
||||||
|
def find_polygon(number, used, next, target, edges):
|
||||||
|
max_score, corners = 0, None
|
||||||
|
if number < 2:
|
||||||
|
return max_score, corners
|
||||||
|
if next is None:
|
||||||
|
return max_score, corners
|
||||||
|
current = lines[next]
|
||||||
|
for neighbor in current.splits:
|
||||||
|
if next == target and number == 2:
|
||||||
|
if neighbor[2] == used[0]:
|
||||||
|
e = np.array(edges[:] + [tuple(neighbor[:2])])
|
||||||
|
a = abs((e[0, 1] - e[2, 1])*(e[3, 0] - e[1, 0]) + (e[1, 1] - e[3, 1])*(e[0, 0] - e[2, 0]))/2
|
||||||
|
# a := "fraction of rectangle to overall image"
|
||||||
|
a = a/image_shape[0]/image_shape[1]
|
||||||
|
return a, edges[:] + [tuple(neighbor[:2])]
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
if neighbor[2] not in used:
|
||||||
|
res_score, res_corners = find_polygon(number - 1, used[:] + [next], neighbor[2], target, edges[:] + [tuple(neighbor[:2])])
|
||||||
|
if res_score > max_score:
|
||||||
|
max_score, corners = res_score, res_corners
|
||||||
|
return max_score, corners
|
||||||
|
|
||||||
|
max_score, corners = 0, None
|
||||||
|
for lidx, line in enumerate(lines):
|
||||||
|
for idx, s in enumerate(line.splits[:-1]):
|
||||||
|
for e in line.splits[idx + 1:]:
|
||||||
|
res_score, res_corners = find_polygon(4, [lidx], e[2], s[2], [tuple(e[:2])])
|
||||||
|
if res_score > max_score and res_score > 0.15:
|
||||||
|
max_score, corners = res_score, res_corners
|
||||||
|
|
||||||
|
if corners is not None:
|
||||||
|
corners = np.array(corners)
|
||||||
|
# check rectangle validity
|
||||||
|
l = corners.shape[0]
|
||||||
|
for i in range(corners.shape[0]):
|
||||||
|
a, b, c = i%l, (i+1)%l, (i+2)%l
|
||||||
|
v1 = corners[b, :] - corners[a, :]
|
||||||
|
v2 = corners[b, :] - corners[c, :]
|
||||||
|
phi = np.arccos(v1.dot(v2) / np.linalg.norm(v1) / np.linalg.norm(v2))
|
||||||
|
if phi < 0.8:
|
||||||
|
corners = None
|
||||||
|
break
|
||||||
|
return max_score, corners
|
||||||
|
|
||||||
|
|
||||||
|
def draw_rectangle(image, corners):
|
||||||
|
l = corners.shape[0]
|
||||||
|
draw_image = np.copy(image)
|
||||||
|
for i in range(corners.shape[0]):
|
||||||
|
a, b = corners[i%l], corners[(i+1)%l]
|
||||||
|
for i in range(5000):
|
||||||
|
x = int(a[1] + (b[1] - a[1]) * i / 5000)
|
||||||
|
y = int(a[0] + (b[0] - a[0]) * i / 5000)
|
||||||
|
draw_image[max(0, y - 15):y + 10, max(0, x - 10):x + 10] = np.array([255, 0, 0])
|
||||||
|
plt.imshow(draw_image)
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
|
||||||
|
def crop_image(image, corners):
|
||||||
|
topleft = np.argmin(np.linalg.norm(corners, axis=1))
|
||||||
|
corners = np.roll(corners, -topleft-1, axis=0)
|
||||||
|
h = int((corners[0, 0] + corners[1, 0] - corners[2, 0] - corners[3, 0]) / 2)
|
||||||
|
w = int((corners[1, 1] + corners[2, 1] - corners[0, 1] - corners[3, 1]) / 2)
|
||||||
|
pb = np.copy(corners)[:, ::-1].reshape((8,))
|
||||||
|
img = Image.fromarray(image)
|
||||||
|
convert = np.rot90(np.asarray(img.transform((h, w), Image.QUAD, pb, Image.BICUBIC)))
|
||||||
|
return convert
|
||||||
338
src/processing/receiptcutter_old.py
Normal file
@@ -0,0 +1,338 @@
|
|||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
from skimage.transform import resize
|
||||||
|
from scipy.ndimage import gaussian_filter
|
||||||
|
from matplotlib import pyplot as plt
|
||||||
|
|
||||||
|
from src.processing.imageprocessing import rgb2gray_value
|
||||||
|
from scipy import signal
|
||||||
|
import numpy as np
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
class Line:
|
||||||
|
def __init__(self, intercept=0, slope=0, points=None):
|
||||||
|
self.intercept = intercept
|
||||||
|
self.slope = slope
|
||||||
|
self.points = [] if points is None else points
|
||||||
|
self.splits = []
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "m="+str(self.slope)+";n="+str(self.intercept)+";split="+str(self.splits)
|
||||||
|
|
||||||
|
def cut_receipt(image):
|
||||||
|
image = image[:, :, :3]
|
||||||
|
gray = rgb2gray_value(image)
|
||||||
|
gray = resize(gray, (500, int(gray.shape[1] / gray.shape[0] * 500)), mode="reflect")
|
||||||
|
scale = image.shape[0] / gray.shape[0]
|
||||||
|
print(scale)
|
||||||
|
gray = gaussian_filter(gray, sigma=max(3, scale*2-8))
|
||||||
|
#gauss = np.array([[1, 2, 1], [2, 4, 2], [1, 2, 1]], dtype='float')/16
|
||||||
|
#gray = signal.convolve2d(gray, gauss, boundary='symm', mode='same')
|
||||||
|
sobely = np.array([[1, 2, 1], [0, 0, 0], [-1, -2, -1]], dtype='float')
|
||||||
|
sobely = np.array([[0, 2, 0], [0, 0, 0], [0, -2, 0]], dtype='float')
|
||||||
|
gray_y = signal.convolve2d(gray, sobely, boundary='symm', mode='same')
|
||||||
|
sobelx = np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]], dtype='float')
|
||||||
|
sobelx = np.array([[0, 0, 0], [-2, 0, 2], [0, 0, 0]], dtype='float')
|
||||||
|
gray_x = signal.convolve2d(gray, sobelx, boundary='symm', mode='same')
|
||||||
|
grad_strength = np.sqrt(np.square(gray_y)+np.square(gray_x))
|
||||||
|
grad_angle = np.arctan(np.true_divide(gray_y, gray_x, where=gray_x != 0))
|
||||||
|
angles = np.copy(grad_angle)
|
||||||
|
grad_angle = np.round(grad_angle / np.pi * 4) + 2
|
||||||
|
grad_angle = np.array(grad_angle, dtype="uint8")
|
||||||
|
grad_angle[grad_angle == 4] = 0
|
||||||
|
h, w = grad_strength.shape
|
||||||
|
#plt.imshow(grad_strength, cmap="gray")
|
||||||
|
#plt.show()
|
||||||
|
|
||||||
|
# Canny
|
||||||
|
CANNY_MIN = 0.01
|
||||||
|
CANNY_MAX = 0.002
|
||||||
|
strenghts = np.zeros_like(grad_strength)
|
||||||
|
for y in range(1, h-1):
|
||||||
|
for x in range(1, w-1):
|
||||||
|
if grad_strength[y, x] < CANNY_MIN:
|
||||||
|
continue
|
||||||
|
if grad_angle[y, x] == 0:
|
||||||
|
if grad_strength[y-1, x] < grad_strength[y, x] and grad_strength[y+1, x] < grad_strength[y, x]:
|
||||||
|
strenghts[y, x] = grad_strength[y, x]
|
||||||
|
elif grad_angle[y, x] == 1:
|
||||||
|
if grad_strength[y-1, x-1] < grad_strength[y, x] and grad_strength[y+1, x+1] < grad_strength[y, x]:
|
||||||
|
strenghts[y, x] = grad_strength[y, x]
|
||||||
|
elif grad_angle[y, x] == 2:
|
||||||
|
if grad_strength[y, x-1] < grad_strength[y, x] and grad_strength[y, x+1] < grad_strength[y, x]:
|
||||||
|
strenghts[y, x] = grad_strength[y, x]
|
||||||
|
elif grad_angle[y, x] == 3:
|
||||||
|
if grad_strength[y-1, x+1] < grad_strength[y, x] and grad_strength[y+1, x-1] < grad_strength[y, x]:
|
||||||
|
strenghts[y, x] = grad_strength[y, x]
|
||||||
|
|
||||||
|
# Hough-Lines
|
||||||
|
theta_res = 5
|
||||||
|
width_res = 5
|
||||||
|
theta_angle = 180 // theta_res
|
||||||
|
mid = int(np.round(np.sqrt((h//width_res)**2+(w//width_res)**2)))
|
||||||
|
hough = np.zeros((theta_angle, mid*2+2))
|
||||||
|
references = defaultdict(list)
|
||||||
|
for y in range(1, h-1):
|
||||||
|
for x in range(1, w-1):
|
||||||
|
if strenghts[y, x] > 0:
|
||||||
|
for theta in range(theta_angle):
|
||||||
|
t = theta * np.pi / theta_angle
|
||||||
|
q = (x * np.cos(t) + y * np.sin(t))/width_res + mid
|
||||||
|
#print(theta*theta_res, y//width_res, x//width_res, q - mid, t)
|
||||||
|
hough[theta, int(q)] += 1#strenghts[y, x]
|
||||||
|
#print(theta, int(q), 175//theta_res, -120//width_res + mid + 1)
|
||||||
|
references[int(q) * theta_angle + theta].append([y, x])
|
||||||
|
lines = np.unravel_index(np.argsort(hough.ravel())[-100:][::-1], hough.shape)
|
||||||
|
lines = np.array(lines).T
|
||||||
|
results = []
|
||||||
|
COVER_Y = 10
|
||||||
|
COVER_X = 15
|
||||||
|
for line in lines:
|
||||||
|
if hough[line[0], line[1]] > 0:
|
||||||
|
results.append([line[0]*theta_res, (line[1]-mid)*width_res])
|
||||||
|
hough[max(0, line[0]-COVER_Y):line[0]+COVER_Y, max(0, line[1]-COVER_X):line[1]+COVER_X] = 0
|
||||||
|
y1, x1, y2, x2 = None, None, None, None
|
||||||
|
if line[0]-COVER_Y < 0:
|
||||||
|
y1 = hough.shape[0]+line[0]-COVER_Y
|
||||||
|
if line[1]-COVER_X < 0:
|
||||||
|
x1 = hough.shape[1]+line[1]-COVER_X
|
||||||
|
if line[0]+COVER_Y > hough.shape[0]:
|
||||||
|
y2 = line[0]+COVER_Y-hough.shape[0]
|
||||||
|
if line[1]+COVER_X > hough.shape[1]:
|
||||||
|
x2 = line[1]+COVER_X-hough.shape[1]
|
||||||
|
if any(x is not None for x in [y1, x1, y2, x2]):
|
||||||
|
ty1 = y1 if y1 is not None else (0 if y2 is not None else line[0]-COVER_Y)
|
||||||
|
tx1 = x1 if x1 is not None else (0 if x2 is not None else line[1]-COVER_X)
|
||||||
|
ty2 = y2 if y2 is not None else (hough.shape[0] if y1 is not None else line[0]+COVER_Y)
|
||||||
|
tx2 = x2 if x2 is not None else (hough.shape[1] if x1 is not None else line[1]+COVER_X)
|
||||||
|
hough[ty1:ty2, tx1:tx2] = 0
|
||||||
|
if len(results) > 5:
|
||||||
|
break
|
||||||
|
results = np.array(results)
|
||||||
|
#print(results)
|
||||||
|
|
||||||
|
# draw image (removable)
|
||||||
|
draw_image = np.copy(image)
|
||||||
|
RED_WIDTH = 10
|
||||||
|
GREEN_WIDTH = 5
|
||||||
|
for result in results:
|
||||||
|
refs = references[(result[1]/width_res+mid) * theta_angle + result[0]//theta_res]
|
||||||
|
for ref in refs:
|
||||||
|
xa = int(scale * ref[1])
|
||||||
|
ya = int(scale * ref[0])
|
||||||
|
draw_image[max(0, ya - RED_WIDTH):ya + RED_WIDTH, max(0, xa - RED_WIDTH):xa + RED_WIDTH] = np.array([0, 255, 0])
|
||||||
|
if result[0] == 0:
|
||||||
|
x = int(result[1] * scale)
|
||||||
|
for y in range(image.shape[0]):
|
||||||
|
draw_image[max(0, y-GREEN_WIDTH):y+GREEN_WIDTH, max(0, x-GREEN_WIDTH):x+GREEN_WIDTH] = np.array([255, 0, 0])
|
||||||
|
else:
|
||||||
|
angle = (90 - result[0]) / 180 * np.pi
|
||||||
|
m = np.sin(angle) / np.cos(angle)
|
||||||
|
n = result[1] / np.cos(angle) * scale
|
||||||
|
for x in range(image.shape[1]):
|
||||||
|
y = int(n - x * m)
|
||||||
|
if 0 < y < image.shape[0]:
|
||||||
|
draw_image[max(0, y - GREEN_WIDTH):y + GREEN_WIDTH, max(0, x - GREEN_WIDTH):x + GREEN_WIDTH] = np.array([255, 0, 0])
|
||||||
|
#plt.imshow(draw_image)
|
||||||
|
#plt.show()
|
||||||
|
|
||||||
|
# convert to original image pixel
|
||||||
|
scale = image.shape[0] / gray.shape[0]
|
||||||
|
lines = []
|
||||||
|
for result in results:
|
||||||
|
points = []
|
||||||
|
refs = references[(result[1]/width_res+mid) * theta_angle + result[0]//theta_res]
|
||||||
|
for ref in refs:
|
||||||
|
xa = int(scale * ref[1])
|
||||||
|
ya = int(scale * ref[0])
|
||||||
|
points.append([ya, xa])
|
||||||
|
if result[0] == 0:
|
||||||
|
x = int(result[1] * scale)
|
||||||
|
lines.append(Line(intercept=x, slope=None, points=points))
|
||||||
|
else:
|
||||||
|
angle = (90 - result[0]) / 180 * np.pi
|
||||||
|
m = np.sin(angle) / -np.cos(angle)
|
||||||
|
n = result[1] / np.cos(angle) * scale
|
||||||
|
lines.append(Line(intercept=n, slope=m, points=points))
|
||||||
|
|
||||||
|
# split segments
|
||||||
|
for idx1, line1 in enumerate(lines):
|
||||||
|
for idx2, line2 in enumerate(lines[idx1+1:], idx1+1):
|
||||||
|
if line1.slope == line2.slope:
|
||||||
|
continue
|
||||||
|
elif line1.slope is None:
|
||||||
|
x = line1.intercept
|
||||||
|
y = line2.intercept + line2.slope * line1.intercept
|
||||||
|
if 0 < y < image.shape[0]:
|
||||||
|
line1.splits.append((int(y), int(x), idx2))
|
||||||
|
line2.splits.append((int(y), int(x), idx1))
|
||||||
|
elif line2.slope is None:
|
||||||
|
x = line2.intercept
|
||||||
|
y = line1.intercept + line1.slope * line2.intercept
|
||||||
|
if 0 < y < image.shape[0]:
|
||||||
|
line1.splits.append((int(y), int(x), idx2))
|
||||||
|
line2.splits.append((int(y), int(x), idx1))
|
||||||
|
else:
|
||||||
|
x = (line2.intercept - line1.intercept) / (line1.slope - line2.slope)
|
||||||
|
y = line1.intercept + line1.slope * x
|
||||||
|
#print(x, y)
|
||||||
|
if 0 < x < image.shape[1] and 0 < y < image.shape[0]:
|
||||||
|
line1.splits.append((int(y), int(x), idx2))
|
||||||
|
line2.splits.append((int(y), int(x), idx1))
|
||||||
|
if line1.slope is None:
|
||||||
|
line1.splits.append((0, int(line1.intercept), None))
|
||||||
|
line1.splits.append((image.shape[0], int(line1.intercept), None))
|
||||||
|
elif line1.slope == 0:
|
||||||
|
line1.splits.append((int(line1.intercept), 0, None))
|
||||||
|
line1.splits.append((int(line1.intercept), image.shape[1], None))
|
||||||
|
else:
|
||||||
|
y = min(max(0, line1.intercept), image.shape[0])
|
||||||
|
x = (y - line1.intercept) / line1.slope
|
||||||
|
line1.splits.append((int(y), int(x), None))
|
||||||
|
y = min(max(0, line1.intercept+line1.slope*image.shape[1]), image.shape[0])
|
||||||
|
x = (y - line1.intercept) / line1.slope
|
||||||
|
line1.splits.append((int(y), int(x), None))
|
||||||
|
if line1.slope is not None:
|
||||||
|
line1.splits = sorted(line1.splits, key=lambda x: (x[1], x[0]))
|
||||||
|
else:
|
||||||
|
line1.splits = sorted(line1.splits, key=lambda x: (x[0], x[1]))
|
||||||
|
#print(line1)
|
||||||
|
|
||||||
|
# find important segments
|
||||||
|
for line in lines:
|
||||||
|
counts = np.zeros((len(line.splits)-1, ))
|
||||||
|
for point in line.points:
|
||||||
|
if line.slope is None:
|
||||||
|
x = point[0]
|
||||||
|
else:
|
||||||
|
a, b = point[1], point[0]
|
||||||
|
m, n = line.slope, line.intercept
|
||||||
|
x = (a+b*m-m*n) / (m**2 + 1)
|
||||||
|
for i in range(len(line.splits)-1):
|
||||||
|
if line.slope is not None:
|
||||||
|
lower = line.splits[i][1]
|
||||||
|
upper = line.splits[i+1][1]
|
||||||
|
else:
|
||||||
|
lower = line.splits[i][0]
|
||||||
|
upper = line.splits[i+1][0]
|
||||||
|
if lower <= x < upper:
|
||||||
|
counts[i] += 1
|
||||||
|
break
|
||||||
|
#print(counts)
|
||||||
|
#print("before", line)
|
||||||
|
counts = counts / np.sum(counts)
|
||||||
|
start = None
|
||||||
|
end = None
|
||||||
|
for idx, count in enumerate(counts):
|
||||||
|
if count > 0.10:
|
||||||
|
if start is None:
|
||||||
|
start = idx
|
||||||
|
end = idx + 1
|
||||||
|
if start is None:
|
||||||
|
line.splits = []
|
||||||
|
else:
|
||||||
|
while start > 0 and np.sqrt((line.splits[start][0]-line.splits[start-1][0])**2 + (line.splits[start][1]-line.splits[start-1][1])**2) / scale < 30:
|
||||||
|
#print("start", np.sqrt((line.splits[start][0]-line.splits[start-1][0])**2 + (line.splits[start][1]-line.splits[start-1][1])**2) / scale)
|
||||||
|
start -= 1
|
||||||
|
while end < len(line.splits)-1 and np.sqrt((line.splits[end][0]-line.splits[end+1][0])**2 + (line.splits[end][1]-line.splits[end+1][1])**2) / scale < 30:
|
||||||
|
#print("end", np.sqrt((line.splits[end][0]-line.splits[end+1][0])**2 + (line.splits[end][1]-line.splits[end+1][1])**2) / scale)
|
||||||
|
end += 1
|
||||||
|
line.splits = line.splits[start:end+1]
|
||||||
|
#print("after", line)
|
||||||
|
|
||||||
|
for idx, line in enumerate(lines):
|
||||||
|
new_splits = []
|
||||||
|
for split in line.splits:
|
||||||
|
if split[2] is None:
|
||||||
|
new_splits.append(split)
|
||||||
|
continue
|
||||||
|
for split2 in lines[split[2]].splits:
|
||||||
|
if split2[2] == idx:
|
||||||
|
new_splits.append(split)
|
||||||
|
break
|
||||||
|
line.splits = new_splits
|
||||||
|
#print("after2", line)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# find largest rectangle
|
||||||
|
def find_polygon(number, used, next, target, edges):
|
||||||
|
#print(number, used, next, target, edges)
|
||||||
|
|
||||||
|
max_score, corners = 0, None
|
||||||
|
if number < 2:
|
||||||
|
return max_score, corners
|
||||||
|
if next is None:
|
||||||
|
return max_score, corners
|
||||||
|
current = lines[next]
|
||||||
|
for neighbor in current.splits:
|
||||||
|
#print(number, "--", neighbor)
|
||||||
|
if next == target and number == 2:
|
||||||
|
if neighbor[2] == used[0]:
|
||||||
|
e = np.array(edges[:] + [tuple(neighbor[:2])])
|
||||||
|
#print(used[:] + [next], target, edges[:] + [tuple(neighbor[:2])])
|
||||||
|
a = abs((e[0, 1] - e[2, 1])*(e[3, 0] - e[1, 0]) + (e[1, 1] - e[3, 1])*(e[0, 0] - e[2, 0]))/2
|
||||||
|
a = a/image.shape[0]/image.shape[1]
|
||||||
|
#print(a)
|
||||||
|
return a, edges[:] + [tuple(neighbor[:2])]
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
if neighbor[2] not in used:
|
||||||
|
res_score, res_corners = find_polygon(number - 1, used[:] + [next], neighbor[2], target, edges[:] + [tuple(neighbor[:2])])
|
||||||
|
if res_score > max_score:
|
||||||
|
max_score, corners = res_score, res_corners
|
||||||
|
return max_score, corners
|
||||||
|
|
||||||
|
max_score, corners = 0, None
|
||||||
|
for lidx, line in enumerate(lines):
|
||||||
|
for idx, s in enumerate(line.splits[:-1]):
|
||||||
|
for e in line.splits[idx + 1:]:
|
||||||
|
# print(line.name, (s[0], s[1], None if s[2] is None else lines[s[2]].name), (e[0], e[1], None if e[2] is None else lines[e[2]].name))
|
||||||
|
res_score, res_corners = find_polygon(4, [lidx], e[2], s[2], [tuple(e[:2])])
|
||||||
|
if res_score > max_score and res_score > 0.15:
|
||||||
|
max_score, corners = res_score, res_corners
|
||||||
|
#print(max_score, corners)
|
||||||
|
#print(image.shape)
|
||||||
|
|
||||||
|
|
||||||
|
if corners is not None:
|
||||||
|
corners = np.array(corners)
|
||||||
|
# check rectangle validity
|
||||||
|
l = corners.shape[0]
|
||||||
|
for i in range(corners.shape[0]):
|
||||||
|
a, b, c = i%l, (i+1)%l, (i+2)%l
|
||||||
|
v1 = corners[b, :] - corners[a, :]
|
||||||
|
v2 = corners[b, :] - corners[c, :]
|
||||||
|
#print(v1, v2, np.linalg.norm(v1))
|
||||||
|
phi = np.arccos(v1.dot(v2) / np.linalg.norm(v1) / np.linalg.norm(v2))
|
||||||
|
#print(phi, corners[b])
|
||||||
|
if phi < 0.8:
|
||||||
|
corners = None
|
||||||
|
break
|
||||||
|
|
||||||
|
if corners is not None:
|
||||||
|
# draw image (removable)
|
||||||
|
draw_image = np.copy(image)
|
||||||
|
for i in range(corners.shape[0]):
|
||||||
|
a, b = corners[i%l], corners[(i+1)%l]
|
||||||
|
for i in range(5000):
|
||||||
|
x = int(a[1] + (b[1] - a[1]) * i / 5000)
|
||||||
|
y = int(a[0] + (b[0] - a[0]) * i / 5000)
|
||||||
|
draw_image[max(0, y - 15):y + 10, max(0, x - 10):x + 10] = np.array([255, 0, 0])
|
||||||
|
#plt.imshow(draw_image)
|
||||||
|
#plt.show()
|
||||||
|
|
||||||
|
# crop image
|
||||||
|
topleft = np.argmin(np.linalg.norm(corners, axis=1))
|
||||||
|
corners = np.roll(corners, -topleft-1, axis=0)
|
||||||
|
print(corners)
|
||||||
|
h = int((corners[0, 0] + corners[1, 0] - corners[2, 0] - corners[3, 0]) / 2)
|
||||||
|
w = int((corners[1, 1] + corners[2, 1] - corners[0, 1] - corners[3, 1]) / 2)
|
||||||
|
pb = np.copy(corners)[:, ::-1].reshape((8,))
|
||||||
|
img = Image.fromarray(image)
|
||||||
|
convert = np.rot90(np.asarray(img.transform((h, w), Image.QUAD, pb, Image.BICUBIC)))
|
||||||
|
plt.imshow(convert)
|
||||||
|
plt.show()
|
||||||
|
return convert
|
||||||
|
else:
|
||||||
|
return image
|
||||||
0
src/utils/__init__.py
Normal file
93
src/utils/cmap_generator.py
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
from matplotlib.colors import LinearSegmentedColormap
|
||||||
|
import colorsys
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
def rand_cmap(nlabels, type='bright', first_color_black=True, last_color_black=False, verbose=True):
|
||||||
|
"""
|
||||||
|
Creates a random colormap to be used together with matplotlib. Useful for segmentation tasks
|
||||||
|
:param nlabels: Number of labels (size of colormap)
|
||||||
|
:param type: 'bright' for strong colors, 'soft' for pastel colors
|
||||||
|
:param first_color_black: Option to use first color as black, True or False
|
||||||
|
:param last_color_black: Option to use last color as black, True or False
|
||||||
|
:param verbose: Prints the number of labels and shows the colormap. True or False
|
||||||
|
:return: colormap for matplotlib
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if type not in ('bright', 'soft', 'hard'):
|
||||||
|
print ('Please choose "hard", "bright" or "soft" for type')
|
||||||
|
return
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
print('Number of labels: ' + str(nlabels))
|
||||||
|
|
||||||
|
# Generate color map for bright colors, based on hsv
|
||||||
|
if type == 'bright':
|
||||||
|
randHSVcolors = [(np.random.uniform(low=0.0, high=1),
|
||||||
|
np.random.uniform(low=0.2, high=1),
|
||||||
|
np.random.uniform(low=0.9, high=1)) for i in range(nlabels)]
|
||||||
|
|
||||||
|
# Convert HSV list to RGB
|
||||||
|
randRGBcolors = []
|
||||||
|
for HSVcolor in randHSVcolors:
|
||||||
|
randRGBcolors.append(colorsys.hsv_to_rgb(HSVcolor[0], HSVcolor[1], HSVcolor[2]))
|
||||||
|
|
||||||
|
if first_color_black:
|
||||||
|
randRGBcolors[0] = [0, 0, 0]
|
||||||
|
|
||||||
|
if last_color_black:
|
||||||
|
randRGBcolors[-1] = [0, 0, 0]
|
||||||
|
|
||||||
|
random_colormap = LinearSegmentedColormap.from_list('new_map', randRGBcolors, N=nlabels)
|
||||||
|
|
||||||
|
# Generate color map for bright colors, based on hsv
|
||||||
|
if type == 'hard':
|
||||||
|
randHSVcolors = [(np.random.uniform(low=0.0, high=1),
|
||||||
|
np.random.uniform(low=0.7, high=1),
|
||||||
|
np.random.uniform(low=0.9, high=1)) for i in range(nlabels)]
|
||||||
|
|
||||||
|
# Convert HSV list to RGB
|
||||||
|
randRGBcolors = []
|
||||||
|
for HSVcolor in randHSVcolors:
|
||||||
|
randRGBcolors.append(colorsys.hsv_to_rgb(HSVcolor[0], HSVcolor[1], HSVcolor[2]))
|
||||||
|
|
||||||
|
if first_color_black:
|
||||||
|
randRGBcolors[0] = [0, 0, 0]
|
||||||
|
|
||||||
|
if last_color_black:
|
||||||
|
randRGBcolors[-1] = [0, 0, 0]
|
||||||
|
|
||||||
|
random_colormap = LinearSegmentedColormap.from_list('new_map', randRGBcolors, N=nlabels)
|
||||||
|
|
||||||
|
# Generate soft pastel colors, by limiting the RGB spectrum
|
||||||
|
if type == 'soft':
|
||||||
|
low = 0.6
|
||||||
|
high = 0.95
|
||||||
|
randRGBcolors = [(np.random.uniform(low=low, high=high),
|
||||||
|
np.random.uniform(low=low, high=high),
|
||||||
|
np.random.uniform(low=low, high=high)) for i in range(nlabels)]
|
||||||
|
|
||||||
|
if first_color_black:
|
||||||
|
randRGBcolors[0] = [0, 0, 0]
|
||||||
|
|
||||||
|
if last_color_black:
|
||||||
|
randRGBcolors[-1] = [0, 0, 0]
|
||||||
|
random_colormap = LinearSegmentedColormap.from_list('new_map', randRGBcolors, N=nlabels)
|
||||||
|
|
||||||
|
# Display colorbar
|
||||||
|
if verbose:
|
||||||
|
from matplotlib import colors, colorbar
|
||||||
|
from matplotlib import pyplot as plt
|
||||||
|
fig, ax = plt.subplots(1, 1, figsize=(15, 0.5))
|
||||||
|
|
||||||
|
bounds = np.linspace(0, nlabels, nlabels + 1)
|
||||||
|
norm = colors.BoundaryNorm(bounds, nlabels)
|
||||||
|
|
||||||
|
cb = colorbar.ColorbarBase(ax, cmap=random_colormap, norm=norm, spacing='proportional', ticks=None,
|
||||||
|
boundaries=bounds, format='%1i', orientation=u'horizontal')
|
||||||
|
|
||||||
|
return random_colormap
|
||||||
|
|
||||||
|
def list_cmap(array):
|
||||||
|
return LinearSegmentedColormap.from_list('new_map', array, N=array.shape[0])
|
||||||