pax_global_header 0000666 0000000 0000000 00000000064 13447211137 0014515 g ustar 00root root 0000000 0000000 52 comment=16faed252a5e5950bd4fc5b87042014bbf924369
python3-escher-pattern-16faed252a5e5950bd4fc5b87042014bbf924369/ 0000775 0000000 0000000 00000000000 13447211137 0022761 5 ustar 00root root 0000000 0000000 python3-escher-pattern-16faed252a5e5950bd4fc5b87042014bbf924369/Dockerfile 0000664 0000000 0000000 00000000713 13447211137 0024754 0 ustar 00root root 0000000 0000000 FROM python:alpine3.9
EXPOSE 5000
COPY . /app
WORKDIR /app
RUN apk add --no-cache libgfortran build-base libstdc++ \
libpng libpng-dev \
freetype freetype-dev && \
# Install Python dependencies
pip3 install -v --no-cache-dir flask==1.0.2 matplotlib==3.0.3 && \
# Cleanup
apk del --purge build-base libgfortran libpng-dev freetype-dev && \
rm -vrf /var/cache/apk/*
CMD python ./index.py
python3-escher-pattern-16faed252a5e5950bd4fc5b87042014bbf924369/README.md 0000664 0000000 0000000 00000003266 13447211137 0024247 0 ustar 00root root 0000000 0000000 # python3-escher-pattern
[Online example](https://community.elphel.com/files/escher_pattern/?PAGE_WIDTH=1524&PAGE_HEIGHT=3048&LPM=2.705449885575893&ROTATE=14.036243467)
Generates:
* Escher pattern
* Checker board pattern
Used for camera systems optical calibration.
Outputs is a PDF file. No borders. Ready for printing.
## comparison
![](https://community.elphel.com/pictures/escher_vs_checker.png)
# Recommendations
* Overall pattern rotation angle 0 is bad for correct MTF computation - because sensor pixel grid
# Requirements
* matplotlib
# Usage
## Docker
```
docker build --tag ep .
docker run -d -p 3113:5000 ep
```
Then:
```
http://localhost:3113/?PAGE_WIDTH=1524&PAGE_HEIGHT=3048&LPM=2.705449885575893&ROTATE=14.036243467
```
Parameters:
* **PAGE_WIDTH** - page width in mm
* **PAGE_HEIGHT** - page width in mm
* **LPM** - cell pairs per meter
* **ROTATE** - pattern rotation angle
* **ESCHER** - cell border curvature - at 0 or inf becomes a normal checker board. Max curvature is at ~2.4. Optimal - 2.0
## Command line
escher.py:
```
from escher_pattern import Escher_Pattern
ep = Escher_Pattern(width= 1524, height= 3048, escher=2, lpm=2.705449885575893, rotate=14.036243467)
ep.generate()
ep.save()
```
checkerboard.py:
```
from escher_pattern import Escher_Pattern
ep = Escher_Pattern(width= 1524, height= 3048, escher=0, lpm=2.705449885575893, rotate=14.036243467)
ep.generate()
ep.save()
```
Parameters:
* **width** - page width in mm
* **height** - page width in mm
* **lpm** - cell pairs per meter
* **rotate** - pattern rotation angle
* **escher** - cell border curvature - at 0 or inf becomes a normal checker board. Max curvature is at ~2.4. Optimal - 2.0
python3-escher-pattern-16faed252a5e5950bd4fc5b87042014bbf924369/escher_pattern.py 0000664 0000000 0000000 00000021634 13447211137 0026347 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python3
'''
/**
* @file escher_pattern_class.py
* @brief escher pattern generator
* @par License:
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
'''
import matplotlib.pyplot as plt
from matplotlib.backends.backend_pdf import PdfPages
import numpy as np
from matplotlib.patches import Arc
from matplotlib.patches import Circle
from matplotlib.patches import Rectangle
from matplotlib.patches import Polygon
from matplotlib.patches import Wedge
from matplotlib.patches import PathPatch
import matplotlib as mpl
from matplotlib import collections
from matplotlib.path import Path
import copy
import math
import sys
'''
requirements:
- matplotlib, version 3.0.3
- BUG?: matplotlib, version 2.0.2 draws a little padding around which is not desired
install:
- sudo pip3 install matplotlib
- sudo apt install python3-tk
'''
class Escher_Pattern:
# units are mms but coordinates are in pt to compare with php script
MM2PT = 72.0/25.4
opts_k = {'facecolor':'black','edgecolor':'r','linewidth':0}
opts_w = {'facecolor':'white','edgecolor':'r','linewidth':0}
opts_b = {'facecolor':'blue', 'edgecolor':'r','linewidth':0}
opts_r = {'facecolor':'red', 'edgecolor':'r','linewidth':0}
# init
def __init__(self,
width = 270, # width in units
height = 210, # height in units
lpm = 50, # lines per meter
escher = 2.0, # curvature coefficient
rotate = 5, # degrees
units = 'mm',
):
self.width = int(width*self.MM2PT)
self.height = int(height*self.MM2PT)
self.lpm = lpm
self.escher = escher
self.angle = rotate
self.units = units
self.basename = 'escher-pattern'
self.basename += '-ESCHER'+str(self.escher)
self.basename += '-LPM'+str(self.lpm)
self.basename += '-ROT'+str(self.angle)
self.basename += '-PAGE_WIDTH'+str(width)
self.basename += '-PAGE_HEIGHT'+str(height)
self.pdf_name = self.basename+".pdf"
plt.autoscale(tight=True)
plt.axis('off')
plt.margins(0.0)
#plt.rcParams["figure.figsize"] = [60,120]
self.fig, self.ax = plt.subplots()
self.fig.set_size_inches(self.width/72, self.height/72)
self.ax.get_xaxis().set_visible(False)
self.ax.get_yaxis().set_visible(False)
self.ax.set_aspect('equal')
self.ax.spines['top'].set_visible(False)
self.ax.spines['right'].set_visible(False)
self.ax.spines['left'].set_visible(False)
self.ax.spines['bottom'].set_visible(False)
self.ax.set_ylim([0,self.height])
self.ax.set_xlim([0,self.width])
self.ax.set_facecolor('white')
self.rotation = mpl.transforms.Affine2D().rotate_deg(-self.angle) + self.ax.transData
#print("init done")
# generate and place a patch
def generate_cell(self,x,y,tpts,template,halfAngle,r,ba):
for k,v in enumerate(template):
# even-even black cell
vcp = copy.copy(v)
if (type(v)==Wedge):
vcp.set_center((tpts[k][0]+x,tpts[k][1]+y))
elif (type(v)==Rectangle):
vcp.set_xy((tpts[k][0]+x,tpts[k][1]+y))
self.ax.add_patch(vcp)
vcp.set_transform(self.rotation)
# do clipping
if (type(v)==Wedge):
sin0 = math.sin(math.radians(halfAngle))
cos0 = math.cos(math.radians(halfAngle))
if ba[k]==0:
w0 = r - r*cos0
h0 = 2*r*sin0
x0 = tpts[k][0] + r - w0
y0 = tpts[k][1] - h0/2
if ba[k]==180:
w0 = r - r*cos0
h0 = 2*r*sin0
x0 = tpts[k][0] - r
y0 = tpts[k][1] - h0/2
if ba[k]==270:
w0 = 2*r*sin0
h0 = r - r*cos0
x0 = tpts[k][0] - w0/2
y0 = tpts[k][1] - r
if ba[k]==90:
w0 = 2*r*sin0
h0 = r - r*cos0
x0 = tpts[k][0] - w0/2
y0 = tpts[k][1] + r - h0
# correction, so the shadow line from Rectange is not seen
if ba[k]==0:
x0 -= w0
w0 *= 2
if ba[k]==180:
w0 *= 2
if ba[k]==270:
h0 *= 2
if ba[k]==90:
y0 -= h0
h0 *= 2
cp = Rectangle((x0,y0),w0,h0,**self.opts_w, lw=0)
vcp2 = copy.copy(cp)
xy = vcp2.get_xy()
vcp2.set_xy((xy[0]+x,xy[1]+y))
path = vcp2.get_path()
transform = vcp2.get_transform()
path = transform.transform_path(path)
vcp2 = PathPatch(path, fc='none', ec='none', lw=0)
self.ax.add_patch(vcp2)
# rotate here
vcp2.set_transform(self.rotation)
vcp.set_clip_path(vcp2)
# generate the whole pattern
def generate(self):
side = 500/self.lpm*self.MM2PT
# escher pattern
if (self.escher>0):
# no rounding
Size = side
qSize = Size/4
hSize = Size/2
a = self.escher*(math.sqrt(2)-1.0)
r = (a*a+1)/(2*a)*qSize
r2 = r*r
h = math.sqrt(r2-qSize*qSize)
dc = 2*qSize-h
halfAngle = math.degrees(math.atan(qSize/h))
center = dc+r
ba = [
None,
0,
180,
270,
90,
180,
0,
90,
270
]
tpts = [
[center-hSize, center-hSize],
[center-Size+dc, center-qSize],
[center-dc, center+qSize],
[center-qSize, center+Size-dc],
[center+qSize, center+dc],
[center+Size-dc, center+qSize],
[center+dc, center-qSize],
[center+qSize, center-Size+dc],
[center-qSize, center-dc]
]
template = [
Rectangle( tpts[0], Size, Size, **self.opts_k),
Wedge( tpts[1], r, ba[1]-halfAngle, ba[1]+halfAngle, **self.opts_w),
Wedge( tpts[2], r, ba[2]-halfAngle, ba[2]+halfAngle, **self.opts_k),
Wedge( tpts[3], r, ba[3]-halfAngle, ba[3]+halfAngle, **self.opts_w),
Wedge( tpts[4], r, ba[4]-halfAngle, ba[4]+halfAngle, **self.opts_k),
Wedge( tpts[5], r, ba[5]-halfAngle, ba[5]+halfAngle, **self.opts_w),
Wedge( tpts[6], r, ba[6]-halfAngle, ba[6]+halfAngle, **self.opts_k),
Wedge( tpts[7], r, ba[7]-halfAngle, ba[7]+halfAngle, **self.opts_w),
Wedge( tpts[8], r, ba[8]-halfAngle, ba[8]+halfAngle, **self.opts_k)
]
# checker board
else:
halfAngle = 0
r = 0
ba = [
None
]
tpts = [
[0,0]
]
template = [
Rectangle(tpts[0],side,side, **self.opts_k)
]
# calc how much more is needed for the rotation
abs_angle_rad = math.radians(abs(self.angle))
extra_w = self.height*math.tan(abs_angle_rad) # + side
extra_h = self.width *math.tan(abs_angle_rad) # + side
# would like to pass (0,0)
extra_w = int(extra_w/(2*side)+1)*2*side
extra_h = int(extra_h/(2*side)+1)*2*side
a = np.arange(-extra_w, self.width +extra_w, 2*side)
b = np.arange(-extra_h, self.height+extra_h, 2*side)
for x, y in [(x,y) for x in a for y in b]:
self.generate_cell(x ,y ,tpts,template,halfAngle,r,ba)
self.generate_cell(x+side,y+side,tpts,template,halfAngle,r,ba)
# test
def test(self):
x = [0, 100, 0]
y = [0, 0, 100]
self.ax.fill(x, y)
circ = Circle((0,0),0.01,color="red")
#circ.set_transform(t_end)
self.ax.add_patch(circ)
circ = Circle((100,0),1,color="red")
#circ.set_transform(t_end)
self.ax.add_patch(circ)
circ = Circle((0,100),1,color="red")
#circ.set_transform(t_end)
self.ax.add_patch(circ)
circ = Circle((50,50),1,color="red")
#circ.set_transform(t_end)
self.ax.add_patch(circ)
# save function
def save(self):
pp = PdfPages(self.pdf_name)
self.fig.tight_layout(pad=0)
#plt.show()
self.fig.savefig(pp,format='pdf',bbox_inches='tight',pad_inches=0)
#self.fig.savefig(pp,format='pdf',pad_inches=0)
pp.close()
if __name__ == "__main__":
#ep = Escher_Pattern("test.pdf", escher=2.0, lpm=50, rotate=10)
#http://192.168.0.137/escher/escher_pattern.php?PAGE_WIDTH=1524&PAGE_HEIGHT=3048&LPM=2.705449885575893&ROTATE=14.036243467
#ep = Escher_Pattern(width= 1524, height= 3048, escher=0, lpm=2.705449885575893, rotate=10)
ep = Escher_Pattern(width= 1524, height= 3048, escher=2, lpm=2.705449885575893, rotate=14.036243467)
#ep = Escher_Pattern(width= 160, height= 320, escher=2, lpm=50, rotate=13)
ep.generate()
ep.save()
python3-escher-pattern-16faed252a5e5950bd4fc5b87042014bbf924369/index.py 0000664 0000000 0000000 00000003331 13447211137 0024442 0 ustar 00root root 0000000 0000000 from flask import Flask
from flask import request
import flask
import os
import time
from escher_pattern import Escher_Pattern
app = Flask(__name__)
@app.route("/")
def generate_pattern():
# defaults
params = {
'LPM': 50,
'PAGE_WIDTH': 270,
'PAGE_HEIGHT': 210,
'ROTATE': 5,
'ESCHER': 2
}
for k,v in params.items():
rq_param = request.args.get(k)
if rq_param!=None:
params[k] = float(rq_param)
basename = 'escher-pattern'
basename += '-ESCHER'+str(params['ESCHER'])
basename += '-LPM'+str(params['LPM'])
basename += '-ROT'+str(params['ROTATE'])
basename += '-PAGE_WIDTH'+str(params['PAGE_WIDTH'])
basename += '-PAGE_HEIGHT'+str(params['PAGE_HEIGHT'])
pdf_name = basename+".pdf"
ep = Escher_Pattern(
width = params['PAGE_WIDTH'],
height = params['PAGE_HEIGHT'],
escher = params['ESCHER'],
lpm = params['LPM'],
rotate = params['ROTATE'])
ep.generate()
ep.save()
with open(pdf_name,'rb') as f:
contents = f.read()
os.remove(pdf_name)
filesize = str(len(contents))
filesize = filesize.encode('utf-8')
resp = flask.Response(contents)
resp.headers['Content-Type'] = 'application/pdf'
resp.headers['Cache-Control'] = 'public, must-revalidate, max-age=0'
resp.headers['Pragma'] = 'public'
resp.headers['Expires'] = 'Sat, 26 Jul 1997 05:00:00 GMT'
resp.headers['Last-Modified'] = time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime())
resp.headers['Content-Length'] = str(len(contents))
resp.headers['Content-Disposition'] = 'inline; filename='+pdf_name+';'
return resp
if __name__ == "__main__":
app.run(host="0.0.0.0", port=int("5000"), debug=True)