Compare commits

..

1 Commits

Author SHA1 Message Date
e12e52cbcf move code to new location 2018-01-15 22:33:36 -08:00
9 changed files with 72 additions and 184 deletions

7
.gitignore vendored
View File

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

View File

@ -6,22 +6,13 @@ 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.11 * python 3 (written and tested with 3.5)
* numpy * numpy
Install with pip: Install with pip, via git:
```bash ```bash
pip3 install float_raster pip install git+https://mpxd.net/code/jan/float_raster.git@release
```
Alternatively, install via git
```bash
pip3 install git+https://mpxd.net/code/jan/float_raster.git@release
``` ```

View File

@ -1,19 +1,22 @@
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( def raster(vertices: numpy.ndarray,
vertices: ArrayLike, grid_x: numpy.ndarray,
grid_x: ArrayLike, grid_y: numpy.ndarray
grid_y: ArrayLike, ) -> numpy.ndarray:
) -> 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
@ -24,14 +27,11 @@ def raster(
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.
Args: :param vertices: 2xN ndarray containing x,y coordinates for each vertex of the polygon
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
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])
`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)
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
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(
def find_intersections( def find_intersections(
vertices: NDArray[numpy.floating], vertices: numpy.ndarray,
grid_x: NDArray[numpy.floating], grid_x: numpy.ndarray,
grid_y: NDArray[numpy.floating], grid_y: numpy.ndarray
) -> tuple[NDArray[numpy.float64], NDArray[numpy.float64], NDArray[numpy.float64]]: ) -> Tuple[numpy.ndarray]:
""" """
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 FloatRasterError('vertices must be 2xN') raise Exception('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: NDArray[numpy.floating], vertices: numpy.ndarray,
grid_x: NDArray[numpy.floating], grid_x: numpy.ndarray,
grid_y: NDArray[numpy.floating], grid_y: numpy.ndarray,
new_vertex_data: tuple[NDArray[numpy.float64], NDArray[numpy.float64], NDArray[numpy.float64]] | None = None new_vertex_data: Tuple[numpy.ndarray] = 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 FloatRasterError('vertices must be 2xN') raise Exception('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 FloatRasterError('Grid must contain at least one line in each direction?') raise Exception('Grid must contain at least one line in each direction?')
num_poly_vertices = vertices.shape[1] num_poly_vertices = vertices.shape[1]
@ -180,14 +180,13 @@ def create_vertices(
return vertices return vertices
def clip_vertices_to_window( def clip_vertices_to_window(
vertices: NDArray[numpy.float64], vertices: numpy.ndarray,
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
) -> NDArray[numpy.float64]: ) -> numpy.ndarray:
""" """
""" """
# Remove points outside the window (these will only be original points) # Remove points outside the window (these will only be original points)
@ -206,49 +205,40 @@ def clip_vertices_to_window(
def get_raster_parts( def get_raster_parts(
vertices: ArrayLike, vertices: numpy.ndarray,
grid_x: ArrayLike, grid_x: numpy.ndarray,
grid_y: ArrayLike, grid_y: numpy.ndarray
) -> 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)
Args: :param vertices: 2xN ndarray containing x,y coordinates for each point in the polygon
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
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])
`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)
grid_y: y-coordinates for the edges of each pixel (see `grid_x`) :return: Complex sparse COO matrix containing area and cover information
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 FloatRasterError('Grid must contain at least one full pixel') raise Exception('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 = clip_vertices_to_window(vertices,
vertices,
grid_x[0], grid_x[-1], grid_x[0], grid_x[-1],
grid_y[0], grid_y[-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:

View File

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

View File

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

View File

@ -1,18 +0,0 @@
"""
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

View File

@ -1,82 +0,0 @@
[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 Normal file
View File

@ -0,0 +1,16 @@
#!/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',
],
)