Compare commits

...

30 Commits

Author SHA1 Message Date
6e66cba64c bump version to 0.8 2024-07-29 02:08:07 -07:00
afe4b74eda update reqs in readme 2024-07-29 02:07:32 -07:00
7766a7c43c bump minimum versions 2024-07-29 02:06:47 -07:00
379abb5e82 add configs for ruff any mypy 2024-07-29 00:42:49 -07:00
1799faeddd add custom exception type 2024-07-29 00:42:31 -07:00
9749cecef8 add some more keywords 2024-07-29 00:38:25 -07:00
a12ef190fa float_ -> floating 2024-07-29 00:37:29 -07:00
89d0611cfc repeat names for re-export 2024-07-29 00:37:20 -07:00
6e32eda1c7 update type hints 2024-07-18 00:30:18 -07:00
0a81c97af1 bump version to v0.7 2022-08-18 23:13:19 -07:00
56989009e0 move to hatch-based build 2022-08-18 23:13:15 -07:00
jan
6ee12d8db9 update pypi tags 2021-10-24 17:56:54 -07:00
38dc51aa7e update email 2021-07-11 17:23:41 -07:00
81e34520aa strip whitespace from version string 2021-07-11 17:23:36 -07:00
6d861d8a9c update req. python version 2020-11-03 01:23:31 -08:00
f8d3a6600e bump version to v0.6 2020-11-03 01:21:26 -08:00
e5e30a9414 Use VERSION.py instead of plain text VERSION 2020-11-03 01:20:53 -08:00
522b610209 add .mypy_cache to .gitignore 2020-11-03 01:17:54 -08:00
ecefdff781 typing and comment updates 2020-11-03 01:17:43 -08:00
085bb79ed7 add py.typed to enable downstream type checking 2020-11-03 01:16:43 -08:00
94ebf9fa18 bump version to 0.5 2019-09-30 23:21:59 -07:00
99b35a1561 gitignore build artifacts 2019-09-30 23:18:41 -07:00
50e3822eac Create folder-based module and use VERSION file
use VERSION to avoid importing the module before it's installed
2019-09-30 23:14:13 -07:00
f68ab6bedb Update README 2019-04-07 18:04:46 -07:00
1ab4b17247 add classifiers 2019-04-07 17:12:31 -07:00
76f6c68472 add MANIFEST.in 2019-04-07 17:03:00 -07:00
jan
c10fd556e2 Update source url 2019-02-09 19:38:15 -08:00
219e4b9926 Use readme as long_description 2018-09-16 20:06:57 -07:00
756c015b87 Move version info inside module 2018-09-16 20:06:42 -07:00
3ca8afb3ef Use python3 for setup.py 2018-09-16 20:04:46 -07:00
9 changed files with 184 additions and 72 deletions

7
.gitignore vendored
View File

@ -1,3 +1,10 @@
*.pyc
__pycache__
*.idea
build/
dist/
*.egg-info/
.mypy_cache

View File

@ -6,13 +6,22 @@ float_raster calculates pixel values with float64 precision and is capable of dr
with variable pixel widths and heights.
- [Source repository](https://mpxd.net/code/jan/float_raster)
- [PyPi](https://pypi.org/project/float_raster)
## Installation
Requirements:
* python 3 (written and tested with 3.5)
* python >=3.11
* numpy
Install with pip, via git:
Install with pip:
```bash
pip install git+https://mpxd.net/gogs/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
View File

@ -0,0 +1 @@
../LICENSE.md

1
float_raster/README.md Symbolic link
View File

@ -0,0 +1 @@
../README.md

18
float_raster/__init__.py Normal file
View 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'

View File

@ -1,22 +1,19 @@
"""
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
from numpy.typing import ArrayLike, NDArray
import numpy
from numpy import logical_and, diff, floor, ceil, ones, zeros, hstack, full_like, newaxis
from scipy import sparse
__author__ = 'Jan Petykiewicz'
class FloatRasterError(Exception):
""" Custom exception for float_raster """
pass
def raster(vertices: numpy.ndarray,
grid_x: numpy.ndarray,
grid_y: numpy.ndarray
) -> numpy.ndarray:
def raster(
vertices: ArrayLike,
grid_x: ArrayLike,
grid_y: ArrayLike,
) -> NDArray[numpy.float64]:
"""
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
@ -27,11 +24,14 @@ def raster(vertices: numpy.ndarray,
Polygons are assumed to have clockwise vertex order; reversing the vertex order is equivalent
to multiplying the result by -1.
:param vertices: 2xN ndarray containing x,y coordinates for each vertex of the polygon
:param grid_x: x-coordinates for the edges of each pixel (ie, the leftmost two columns span
x=grid_x[0] to x=grid_x[1] and x=grid_x[1] to x=grid_x[2])
:param grid_y: y-coordinates for the edges of each pixel (see grid_x)
:return: 2D ndarray with pixel values in the range [0, 1] containing the anti-aliased polygon
Args:
vertices: 2xN ndarray containing `x,y` coordinates for each vertex of the polygon
grid_x: x-coordinates for the edges of each pixel (ie, the leftmost two columns span
`x=grid_x[0]` to `x=grid_x[1]` and `x=grid_x[1]` to `x=grid_x[2]`)
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)
grid_x = numpy.array(grid_x)
@ -55,15 +55,15 @@ def raster(vertices: numpy.ndarray,
def find_intersections(
vertices: numpy.ndarray,
grid_x: numpy.ndarray,
grid_y: numpy.ndarray
) -> Tuple[numpy.ndarray]:
vertices: NDArray[numpy.floating],
grid_x: NDArray[numpy.floating],
grid_y: NDArray[numpy.floating],
) -> tuple[NDArray[numpy.float64], NDArray[numpy.float64], NDArray[numpy.float64]]:
"""
Find intersections between a polygon and grid lines
"""
if vertices.shape[0] != 2:
raise Exception('vertices must be 2xN')
raise FloatRasterError('vertices must be 2xN')
min_bounds = floor(vertices.min(axis=1))
max_bounds = ceil(vertices.max(axis=1))
@ -132,18 +132,18 @@ def find_intersections(
def create_vertices(
vertices: numpy.ndarray,
grid_x: numpy.ndarray,
grid_y: numpy.ndarray,
new_vertex_data: Tuple[numpy.ndarray] = None
vertices: NDArray[numpy.floating],
grid_x: NDArray[numpy.floating],
grid_y: NDArray[numpy.floating],
new_vertex_data: tuple[NDArray[numpy.float64], NDArray[numpy.float64], NDArray[numpy.float64]] | None = None
) -> sparse.coo_matrix:
"""
Create additional vertices where a polygon crosses gridlines
"""
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:
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]
@ -180,13 +180,14 @@ def create_vertices(
return vertices
def clip_vertices_to_window(
vertices: numpy.ndarray,
vertices: NDArray[numpy.float64],
min_x: float = -numpy.inf,
max_x: float = numpy.inf,
min_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)
@ -205,40 +206,49 @@ def clip_vertices_to_window(
def get_raster_parts(
vertices: numpy.ndarray,
grid_x: numpy.ndarray,
grid_y: numpy.ndarray
vertices: ArrayLike,
grid_x: ArrayLike,
grid_y: ArrayLike,
) -> 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
(-area + 1j * cover)
`(-area + 1j * cover)`
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,
multiplied by the sign of (y_final - y_initial)
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
onto the pixel's left (i.e., lowest-x) edge, again multiplied by
the sign of (y_final - y_initial)
`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)`
`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
onto the pixel's left (i.e., lowest-x) edge, again multiplied by
the sign of `(y_final - y_initial)`
Note that polygons are assumed to be wound clockwise.
The result from raster(...) can be obtained with
raster_result = numpy.real(lines_result) + numpy.imag(lines_result).cumsum(axis=0)
The result from `raster(...)` can be obtained with
`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
:param grid_x: x-coordinates for the edges of each pixel (ie, the leftmost two columns span
x=grid_x[0] to x=grid_x[1] and x=grid_x[1] to x=grid_x[2])
:param grid_y: y-coordinates for the edges of each pixel (see grid_x)
:return: Complex sparse COO matrix containing area and cover information
Args:
vertices: 2xN ndarray containing `x, y` coordinates for each point in the polygon
grid_x: x-coordinates for the edges of each pixel (ie, the leftmost two columns span
`x=grid_x[0]` to `x=grid_x[1]` and `x=grid_x[1]` to `x=grid_x[2]`)
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:
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
vertices = clip_vertices_to_window(vertices,
grid_x[0], grid_x[-1],
grid_y[0], grid_y[-1])
vertices = clip_vertices_to_window(
vertices,
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 vertices.size == 0:

0
float_raster/py.typed Normal file
View File

82
pyproject.toml Normal file
View 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

View File

@ -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/gogs/jan/float_raster',
py_modules=['float_raster'],
install_requires=[
'numpy',
'scipy',
],
)