Compare commits
30 Commits
Author | SHA1 | Date | |
---|---|---|---|
6e66cba64c | |||
afe4b74eda | |||
7766a7c43c | |||
379abb5e82 | |||
1799faeddd | |||
9749cecef8 | |||
a12ef190fa | |||
89d0611cfc | |||
6e32eda1c7 | |||
0a81c97af1 | |||
56989009e0 | |||
6ee12d8db9 | |||
38dc51aa7e | |||
81e34520aa | |||
6d861d8a9c | |||
f8d3a6600e | |||
e5e30a9414 | |||
522b610209 | |||
ecefdff781 | |||
085bb79ed7 | |||
94ebf9fa18 | |||
99b35a1561 | |||
50e3822eac | |||
f68ab6bedb | |||
1ab4b17247 | |||
76f6c68472 | |||
c10fd556e2 | |||
219e4b9926 | |||
756c015b87 | |||
3ca8afb3ef |
7
.gitignore
vendored
7
.gitignore
vendored
@ -1,3 +1,10 @@
|
|||||||
*.pyc
|
*.pyc
|
||||||
__pycache__
|
__pycache__
|
||||||
|
|
||||||
*.idea
|
*.idea
|
||||||
|
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
*.egg-info/
|
||||||
|
|
||||||
|
.mypy_cache
|
||||||
|
15
README.md
15
README.md
@ -6,13 +6,22 @@ float_raster calculates pixel values with float64 precision and is capable of dr
|
|||||||
with variable pixel widths and heights.
|
with variable pixel widths and heights.
|
||||||
|
|
||||||
|
|
||||||
|
- [Source repository](https://mpxd.net/code/jan/float_raster)
|
||||||
|
- [PyPi](https://pypi.org/project/float_raster)
|
||||||
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Requirements:
|
Requirements:
|
||||||
* python 3 (written and tested with 3.5)
|
* python >=3.11
|
||||||
* numpy
|
* numpy
|
||||||
|
|
||||||
Install with pip, via git:
|
Install with pip:
|
||||||
```bash
|
```bash
|
||||||
pip install git+https://mpxd.net/code/jan/float_raster.git@release
|
pip3 install float_raster
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively, install via git
|
||||||
|
```bash
|
||||||
|
pip3 install git+https://mpxd.net/code/jan/float_raster.git@release
|
||||||
```
|
```
|
||||||
|
1
float_raster/LICENSE.md
Symbolic link
1
float_raster/LICENSE.md
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../LICENSE.md
|
1
float_raster/README.md
Symbolic link
1
float_raster/README.md
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../README.md
|
18
float_raster/__init__.py
Normal file
18
float_raster/__init__.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
"""
|
||||||
|
Module for rasterizing polygons, with float-precision anti-aliasing on
|
||||||
|
a non-uniform rectangular grid.
|
||||||
|
|
||||||
|
See the documentation for float_raster.raster(...) for details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .float_raster import (
|
||||||
|
raster as raster,
|
||||||
|
find_intersections as find_intersections,
|
||||||
|
create_vertices as create_vertices,
|
||||||
|
clip_vertices_to_window as clip_vertices_to_window,
|
||||||
|
get_raster_parts as get_raster_parts,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
__author__ = 'Jan Petykiewicz'
|
||||||
|
__version__ = '0.8'
|
@ -1,22 +1,19 @@
|
|||||||
"""
|
from numpy.typing import ArrayLike, NDArray
|
||||||
Module for rasterizing polygons, with float-precision anti-aliasing on
|
|
||||||
a non-uniform rectangular grid.
|
|
||||||
|
|
||||||
See the documentation for raster(...) for details.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from typing import Tuple
|
|
||||||
import numpy
|
import numpy
|
||||||
from numpy import logical_and, diff, floor, ceil, ones, zeros, hstack, full_like, newaxis
|
from numpy import logical_and, diff, floor, ceil, ones, zeros, hstack, full_like, newaxis
|
||||||
from scipy import sparse
|
from scipy import sparse
|
||||||
|
|
||||||
__author__ = 'Jan Petykiewicz'
|
|
||||||
|
class FloatRasterError(Exception):
|
||||||
|
""" Custom exception for float_raster """
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def raster(vertices: numpy.ndarray,
|
def raster(
|
||||||
grid_x: numpy.ndarray,
|
vertices: ArrayLike,
|
||||||
grid_y: numpy.ndarray
|
grid_x: ArrayLike,
|
||||||
) -> numpy.ndarray:
|
grid_y: ArrayLike,
|
||||||
|
) -> NDArray[numpy.float64]:
|
||||||
"""
|
"""
|
||||||
Draws a polygon onto a 2D grid of pixels, setting pixel values equal to the fraction of the
|
Draws a polygon onto a 2D grid of pixels, setting pixel values equal to the fraction of the
|
||||||
pixel area covered by the polygon. This implementation is written for accuracy and works with
|
pixel area covered by the polygon. This implementation is written for accuracy and works with
|
||||||
@ -27,11 +24,14 @@ def raster(vertices: numpy.ndarray,
|
|||||||
Polygons are assumed to have clockwise vertex order; reversing the vertex order is equivalent
|
Polygons are assumed to have clockwise vertex order; reversing the vertex order is equivalent
|
||||||
to multiplying the result by -1.
|
to multiplying the result by -1.
|
||||||
|
|
||||||
:param vertices: 2xN ndarray containing x,y coordinates for each vertex of the polygon
|
Args:
|
||||||
:param grid_x: x-coordinates for the edges of each pixel (ie, the leftmost two columns span
|
vertices: 2xN ndarray containing `x,y` coordinates for each vertex of the polygon
|
||||||
x=grid_x[0] to x=grid_x[1] and x=grid_x[1] to x=grid_x[2])
|
grid_x: x-coordinates for the edges of each pixel (ie, the leftmost two columns span
|
||||||
:param grid_y: y-coordinates for the edges of each pixel (see grid_x)
|
`x=grid_x[0]` to `x=grid_x[1]` and `x=grid_x[1]` to `x=grid_x[2]`)
|
||||||
:return: 2D ndarray with pixel values in the range [0, 1] containing the anti-aliased polygon
|
grid_y: y-coordinates for the edges of each pixel (see `grid_x`)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
2D ndarray with pixel values in the range [0, 1] containing the anti-aliased polygon
|
||||||
"""
|
"""
|
||||||
vertices = numpy.array(vertices)
|
vertices = numpy.array(vertices)
|
||||||
grid_x = numpy.array(grid_x)
|
grid_x = numpy.array(grid_x)
|
||||||
@ -55,15 +55,15 @@ def raster(vertices: numpy.ndarray,
|
|||||||
|
|
||||||
|
|
||||||
def find_intersections(
|
def find_intersections(
|
||||||
vertices: numpy.ndarray,
|
vertices: NDArray[numpy.floating],
|
||||||
grid_x: numpy.ndarray,
|
grid_x: NDArray[numpy.floating],
|
||||||
grid_y: numpy.ndarray
|
grid_y: NDArray[numpy.floating],
|
||||||
) -> Tuple[numpy.ndarray]:
|
) -> tuple[NDArray[numpy.float64], NDArray[numpy.float64], NDArray[numpy.float64]]:
|
||||||
"""
|
"""
|
||||||
Find intersections between a polygon and grid lines
|
Find intersections between a polygon and grid lines
|
||||||
"""
|
"""
|
||||||
if vertices.shape[0] != 2:
|
if vertices.shape[0] != 2:
|
||||||
raise Exception('vertices must be 2xN')
|
raise FloatRasterError('vertices must be 2xN')
|
||||||
|
|
||||||
min_bounds = floor(vertices.min(axis=1))
|
min_bounds = floor(vertices.min(axis=1))
|
||||||
max_bounds = ceil(vertices.max(axis=1))
|
max_bounds = ceil(vertices.max(axis=1))
|
||||||
@ -132,18 +132,18 @@ def find_intersections(
|
|||||||
|
|
||||||
|
|
||||||
def create_vertices(
|
def create_vertices(
|
||||||
vertices: numpy.ndarray,
|
vertices: NDArray[numpy.floating],
|
||||||
grid_x: numpy.ndarray,
|
grid_x: NDArray[numpy.floating],
|
||||||
grid_y: numpy.ndarray,
|
grid_y: NDArray[numpy.floating],
|
||||||
new_vertex_data: Tuple[numpy.ndarray] = None
|
new_vertex_data: tuple[NDArray[numpy.float64], NDArray[numpy.float64], NDArray[numpy.float64]] | None = None
|
||||||
) -> sparse.coo_matrix:
|
) -> sparse.coo_matrix:
|
||||||
"""
|
"""
|
||||||
Create additional vertices where a polygon crosses gridlines
|
Create additional vertices where a polygon crosses gridlines
|
||||||
"""
|
"""
|
||||||
if vertices.shape[0] != 2:
|
if vertices.shape[0] != 2:
|
||||||
raise Exception('vertices must be 2xN')
|
raise FloatRasterError('vertices must be 2xN')
|
||||||
if grid_x.size < 1 or grid_y.size < 1:
|
if grid_x.size < 1 or grid_y.size < 1:
|
||||||
raise Exception('Grid must contain at least one line in each direction?')
|
raise FloatRasterError('Grid must contain at least one line in each direction?')
|
||||||
|
|
||||||
num_poly_vertices = vertices.shape[1]
|
num_poly_vertices = vertices.shape[1]
|
||||||
|
|
||||||
@ -180,13 +180,14 @@ def create_vertices(
|
|||||||
|
|
||||||
return vertices
|
return vertices
|
||||||
|
|
||||||
|
|
||||||
def clip_vertices_to_window(
|
def clip_vertices_to_window(
|
||||||
vertices: numpy.ndarray,
|
vertices: NDArray[numpy.float64],
|
||||||
min_x: float = -numpy.inf,
|
min_x: float = -numpy.inf,
|
||||||
max_x: float = numpy.inf,
|
max_x: float = numpy.inf,
|
||||||
min_y: float = -numpy.inf,
|
min_y: float = -numpy.inf,
|
||||||
max_y: float = numpy.inf
|
max_y: float = numpy.inf
|
||||||
) -> numpy.ndarray:
|
) -> NDArray[numpy.float64]:
|
||||||
"""
|
"""
|
||||||
"""
|
"""
|
||||||
# Remove points outside the window (these will only be original points)
|
# Remove points outside the window (these will only be original points)
|
||||||
@ -205,40 +206,49 @@ def clip_vertices_to_window(
|
|||||||
|
|
||||||
|
|
||||||
def get_raster_parts(
|
def get_raster_parts(
|
||||||
vertices: numpy.ndarray,
|
vertices: ArrayLike,
|
||||||
grid_x: numpy.ndarray,
|
grid_x: ArrayLike,
|
||||||
grid_y: numpy.ndarray
|
grid_y: ArrayLike,
|
||||||
) -> sparse.coo_matrix:
|
) -> sparse.coo_matrix:
|
||||||
"""
|
"""
|
||||||
This function performs the same task as raster(...), but instead of returning a dense array
|
This function performs the same task as `raster(...)`, but instead of returning a dense array
|
||||||
of pixel values, it returns a sparse array containing the value
|
of pixel values, it returns a sparse array containing the value
|
||||||
(-area + 1j * cover)
|
`(-area + 1j * cover)`
|
||||||
for each pixel which contains a line segment, where
|
for each pixel which contains a line segment, where
|
||||||
cover is the fraction of the pixel's y-length that is traversed by the segment,
|
`cover` is the fraction of the pixel's y-length that is traversed by the segment,
|
||||||
multiplied by the sign of (y_final - y_initial)
|
multiplied by the sign of `(y_final - y_initial)`
|
||||||
area is the fraction of the pixel's area covered by the trapezoid formed by
|
`area` is the fraction of the pixel's area covered by the trapezoid formed by
|
||||||
the line segment's endpoints (clipped to the cell edges) and their projections
|
the line segment's endpoints (clipped to the cell edges) and their projections
|
||||||
onto the pixel's left (i.e., lowest-x) edge, again multiplied by
|
onto the pixel's left (i.e., lowest-x) edge, again multiplied by
|
||||||
the sign of (y_final - y_initial)
|
the sign of `(y_final - y_initial)`
|
||||||
Note that polygons are assumed to be wound clockwise.
|
Note that polygons are assumed to be wound clockwise.
|
||||||
|
|
||||||
The result from raster(...) can be obtained with
|
The result from `raster(...)` can be obtained with
|
||||||
raster_result = numpy.real(lines_result) + numpy.imag(lines_result).cumsum(axis=0)
|
`raster_result = numpy.real(lines_result) + numpy.imag(lines_result).cumsum(axis=0)`
|
||||||
|
|
||||||
:param vertices: 2xN ndarray containing x,y coordinates for each point in the polygon
|
Args:
|
||||||
:param grid_x: x-coordinates for the edges of each pixel (ie, the leftmost two columns span
|
vertices: 2xN ndarray containing `x, y` coordinates for each point in the polygon
|
||||||
x=grid_x[0] to x=grid_x[1] and x=grid_x[1] to x=grid_x[2])
|
grid_x: x-coordinates for the edges of each pixel (ie, the leftmost two columns span
|
||||||
:param grid_y: y-coordinates for the edges of each pixel (see grid_x)
|
`x=grid_x[0]` to `x=grid_x[1]` and `x=grid_x[1]` to `x=grid_x[2]`)
|
||||||
:return: Complex sparse COO matrix containing area and cover information
|
grid_y: y-coordinates for the edges of each pixel (see `grid_x`)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Complex sparse COO matrix containing area and cover information
|
||||||
"""
|
"""
|
||||||
|
vertices = numpy.array(vertices)
|
||||||
|
grid_x = numpy.array(grid_x)
|
||||||
|
grid_y = numpy.array(grid_y)
|
||||||
|
|
||||||
if grid_x.size < 2 or grid_y.size < 2:
|
if grid_x.size < 2 or grid_y.size < 2:
|
||||||
raise Exception('Grid must contain at least one full pixel')
|
raise FloatRasterError('Grid must contain at least one full pixel')
|
||||||
|
|
||||||
num_xy_px = numpy.array([grid_x.size, grid_y.size]) - 1
|
num_xy_px = numpy.array([grid_x.size, grid_y.size]) - 1
|
||||||
|
|
||||||
vertices = clip_vertices_to_window(vertices,
|
vertices = clip_vertices_to_window(
|
||||||
grid_x[0], grid_x[-1],
|
vertices,
|
||||||
grid_y[0], grid_y[-1])
|
grid_x[0], grid_x[-1],
|
||||||
|
grid_y[0], grid_y[-1],
|
||||||
|
)
|
||||||
|
|
||||||
# If the shape fell completely outside our area, just return a blank grid
|
# If the shape fell completely outside our area, just return a blank grid
|
||||||
if vertices.size == 0:
|
if vertices.size == 0:
|
0
float_raster/py.typed
Normal file
0
float_raster/py.typed
Normal file
82
pyproject.toml
Normal file
82
pyproject.toml
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "float_raster"
|
||||||
|
description = "High-precision anti-aliasing polygon rasterizer"
|
||||||
|
readme = "README.md"
|
||||||
|
license = { file = "LICENSE.md" }
|
||||||
|
authors = [
|
||||||
|
{ name="Jan Petykiewicz", email="jan@mpxd.net" },
|
||||||
|
]
|
||||||
|
homepage = "https://mpxd.net/code/jan/float_raster"
|
||||||
|
repository = "https://mpxd.net/code/jan/float_raster"
|
||||||
|
keywords = [
|
||||||
|
"coverage",
|
||||||
|
"raster",
|
||||||
|
"anti-alias",
|
||||||
|
"polygon",
|
||||||
|
]
|
||||||
|
classifiers = [
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"Development Status :: 4 - Beta",
|
||||||
|
"Intended Audience :: Developers",
|
||||||
|
"Intended Audience :: Information Technology",
|
||||||
|
"Intended Audience :: Manufacturing",
|
||||||
|
"Intended Audience :: Science/Research",
|
||||||
|
"License :: OSI Approved :: GNU Affero General Public License v3",
|
||||||
|
"Topic :: Scientific/Engineering",
|
||||||
|
"Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)",
|
||||||
|
"Topic :: Multimedia :: Graphics :: Graphics Conversion",
|
||||||
|
]
|
||||||
|
requires-python = ">=3.11"
|
||||||
|
dynamic = ["version"]
|
||||||
|
dependencies = [
|
||||||
|
"numpy>=1.26",
|
||||||
|
"scipy~=1.14",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.hatch.version]
|
||||||
|
path = "float_raster/__init__.py"
|
||||||
|
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
exclude = [
|
||||||
|
".git",
|
||||||
|
"dist",
|
||||||
|
]
|
||||||
|
line-length = 145
|
||||||
|
indent-width = 4
|
||||||
|
lint.dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
||||||
|
lint.select = [
|
||||||
|
"NPY", "E", "F", "W", "B", "ANN", "UP", "SLOT", "SIM", "LOG",
|
||||||
|
"C4", "ISC", "PIE", "PT", "RET", "TCH", "PTH", "INT",
|
||||||
|
"ARG", "PL", "R", "TRY",
|
||||||
|
"G010", "G101", "G201", "G202",
|
||||||
|
"Q002", "Q003", "Q004",
|
||||||
|
]
|
||||||
|
lint.ignore = [
|
||||||
|
#"ANN001", # No annotation
|
||||||
|
"ANN002", # *args
|
||||||
|
"ANN003", # **kwargs
|
||||||
|
"ANN401", # Any
|
||||||
|
"ANN101", # self: Self
|
||||||
|
"SIM108", # single-line if / else assignment
|
||||||
|
"RET504", # x=y+z; return x
|
||||||
|
"PIE790", # unnecessary pass
|
||||||
|
"ISC003", # non-implicit string concatenation
|
||||||
|
"C408", # dict(x=y) instead of {'x': y}
|
||||||
|
"PLR09", # Too many xxx
|
||||||
|
"PLR2004", # magic number
|
||||||
|
"PLC0414", # import x as x
|
||||||
|
"TRY003", # Long exception message
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
[[tool.mypy.overrides]]
|
||||||
|
module = [
|
||||||
|
"scipy",
|
||||||
|
"scipy.sparse",
|
||||||
|
]
|
||||||
|
ignore_missing_imports = true
|
16
setup.py
16
setup.py
@ -1,16 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
|
|
||||||
from setuptools import setup
|
|
||||||
|
|
||||||
setup(name='float_raster',
|
|
||||||
version='0.4',
|
|
||||||
description='High-precision anti-aliasing polygon rasterizer',
|
|
||||||
author='Jan Petykiewicz',
|
|
||||||
author_email='anewusername@gmail.com',
|
|
||||||
url='https://mpxd.net/code/jan/float_raster',
|
|
||||||
py_modules=['float_raster'],
|
|
||||||
install_requires=[
|
|
||||||
'numpy',
|
|
||||||
'scipy',
|
|
||||||
],
|
|
||||||
)
|
|
Loading…
x
Reference in New Issue
Block a user