Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented SkiaPImage class and image APIs for skia #385

Merged
merged 7 commits into from
Sep 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/reference/image.rst
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ no_tint()
.. autofunction:: no_tint
:noindex:

save_canvas()
-------------

.. autofunction:: save_canvas
:noindex:

Pixels
======

Expand Down
6 changes: 3 additions & 3 deletions p5/core/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
)
from .color import color_mode
from .attribs import no_fill, no_tint, no_stroke, stroke_cap, stroke_join, stroke_weight
from .image import load_image, load_pixels, save_frame, image_mode
from .image import load_image, load_pixels, save_canvas, image_mode
from .font import (
create_font,
load_font,
Expand Down Expand Up @@ -363,8 +363,8 @@ def loadPixels():
load_pixels()


def saveFrame(filename=None):
save_frame(filename)
def saveCanvas(filename=None):
save_canvas(filename)


def createFont(name, size=10):
Expand Down
12 changes: 12 additions & 0 deletions p5/core/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,3 +247,15 @@ class SType(Enum):
# color parse modes
RGB = "RGB"
HSB = "HSB"

# Image filters
THRESHOLD = "THRESHOLD"
GRAY = "GRAY"
OPAQUE = "OPAQUE"
INVERT = "INVERT"
POSTERIZE = "POSTERIZE"
BLUR = "BLUR"
ERODE = "ERODE"
DILATE = "DILATE"

RGBA_CHANNELS = 4
51 changes: 40 additions & 11 deletions p5/core/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
"image_mode",
"load_pixels",
"update_pixels",
"create_image",
"save_canvas",
]


Expand Down Expand Up @@ -90,6 +92,11 @@ def size(self):
@size.setter
@abstractmethod
def size(self, new_size):
"""
set or resize the PImage
:param size: size of the image
:type size: tuple
"""
pass

@property
Expand Down Expand Up @@ -170,30 +177,30 @@ def save(self, file_name):
pass


def image(*args, size=None):
def image(img, x, y, w=None, h=None):
"""Draw an image to the display window.

Images must be in the same folder as the sketch (or the image path
should be explicitly mentioned). The color of an image may be
modified with the :meth:`p5.tint` function.

:param img: the image to be displayed.
:type img: p5.Image
:type img: PImage

:param x: x-coordinate of the image by default
:type float:
:type x: float

:param y: y-coordinate of the image by default
:type float:
:type y: float

:param w: width to display the image by default
:type float:
:type w: float

:param h: height to display the image by default
:type float:
:type h: float

"""
p5.renderer.image(*args)
p5.renderer.image(img, x, y, w, h)


def image_mode(mode):
Expand Down Expand Up @@ -243,8 +250,8 @@ def load_image(filename):
file-extennsion is automatically inferred.
:type filename: str

:returns: An :class:`p5.PImage` instance with the given image data
:rtype: :class:`p5.PImage`
:returns: An :class:`PImage` instance with the given image data
:rtype: :class:`PImage`

"""
return p5.renderer.load_image(filename)
Expand All @@ -269,5 +276,27 @@ def update_pixels():
p5.renderer.update_pixels()


def save_frame(filename=None):
p5.renderer.save_frame(filename)
def save_canvas(filename=None, canvas=None):
"""
Saves the given Canvas as an image with filename
:param filename: filename/path for the image
:type filename: str

:param canvas: Canvas to be saved. If not specified default canvas is used
:type canvas: PGraphics
"""
p5.renderer.save_canvas(filename, canvas)


def create_image(width, height):
"""
Creates a new p5.Image (the datatype for storing images). This provides a fresh buffer of pixels to play with.
Set the size of the buffer with the width and height parameters.

:param width: Width in pixels
:type width: int

:param height: Height in pixels
:type height: int
"""
return p5.renderer.create_image(width, height)
119 changes: 119 additions & 0 deletions p5/sketch/Skia2DRenderer/image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
from p5.core import constants
from p5.core import PImage
import numpy as np
import skia
import builtins

class SkiaPImage(PImage):
def __init__(self, width, height, pixels=None):
self._width = width
self._height = height
self.pixels = (
pixels
if pixels is not None
else np.zeros((width, height, constants.RGBA_CHANNELS), dtype=np.uint8)
)

@property
def width(self):
return self._width

@width.setter
def width(self, width):
self._width = width

@property
def height(self):
return self._height

@height.setter
def height(self, height):
self._height = height

@property
def size(self):
return (self.width, self.height)

@size.setter
def size(self, size):
self.width, self.height = size
self.pixels.resize((*size, constants.RGBA_CHANNELS))

@property
def aspect_ratio(self):
return self.width / self.height

def load_pixels(self):
builtins.pixels = self.pixels

def update_pixels(self):
pass

def mask(self, image):
"""
:param image: Image to be used as make
:type image: SkiaPImage
"""
with skia.Surface(self.pixels) as canvas:
canvas.drawImage(image.get_skia_image())

def filter(self, kind, param=0.5):
# https://www.baeldung.com/cs/convert-rgb-to-grayscale#3-luminosity-method
if kind == constants.THRESHOLD:
threshold = param * 255
mask = np.dot(self.pixels[..., :3], [0.2989, 0.5870, 0.1140]) < threshold
ziyaointl marked this conversation as resolved.
Show resolved Hide resolved
self.pixels[:, :, :3][mask] = 1

if kind == constants.GRAY:
mask = np.dot(self.pixels[..., :3], [0.2989, 0.5870, 0.1140])
mask = np.array([[i] * 3 for i in mask.flatten()]).reshape(
(self.pixels.shape[0], self.pixels.shape[1], 3)
)
self.pixels[:, :, :3] = mask

if kind == constants.OPAQUE:
self.pixels[..., 3:] = 255

if kind == constants.INVERT:

def invert(x):
return 255 - x

self.pixels[..., :3] = invert(self.pixels[..., :3])

if kind == constants.POSTERIZE:
raise NotImplementedError("POSTERIZE is not yet implemented for skia")

if kind == constants.DILATE:
with skia.Surface(self.pixels) as canvas:
paint = skia.Paint(ImageFilter=skia.ImageFilters.Dilate(param, param))
image = canvas.getSurface().makeImageSnapshot()
canvas.clear(skia.ColorWHITE)
canvas.drawImage(image, 0, 0, paint)

if kind == constants.ERODE:
with skia.Surface(self.pixels) as canvas:
paint = skia.Paint(ImageFilter=skia.ImageFilters.Erode(param, param))
image = canvas.getSurface().makeImageSnapshot()
canvas.clear(skia.Color(0, 0, 0, 0))
canvas.drawImage(image, 0, 0, paint)

if kind == constants.BLUR:
with skia.Surface(self.pixels) as canvas:
paint = skia.Paint(ImageFilter=skia.ImageFilters.Blur(param, param))
image = canvas.getSurface().makeImageSnapshot()
canvas.clear(skia.Color(0, 0, 0, 0))
canvas.drawImage(image, 0, 0, paint)

def blend(self, other, mode):
# TODO: Implement blend
raise NotImplementedError("To be implemented")

def save(self, filename):
if filename.endswith(".png"):
skia.Image.fromarray(self.pixels).save(filename, skia.kPNG)
else:
skia.Image.fromarray(self.pixels).save(filename, skia.kJPEG)

def get_skia_image(self):
return skia.Image.fromarray(self.pixels)
43 changes: 41 additions & 2 deletions p5/sketch/Skia2DRenderer/renderer2d.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
from p5.core.primitives import point, line
from p5.pmath.utils import *

from .image import SkiaPImage


@dataclass
class Style2D:
Expand Down Expand Up @@ -68,6 +70,7 @@ def __init__(self):
self.style_stack = []
self.path = None
self.curve_tightness = 0
self.pimage = None

# Transforms functions
def push_matrix(self):
Expand Down Expand Up @@ -704,6 +707,42 @@ def end_shape(
self.path.lineTo(v[0], v[1])
self._do_fill_stroke_close(close_shape)

# Fonts functions

# Images functions
def create_image(self, width, height):
return SkiaPImage(width, height)

def image(self, pimage, x, y, w=None, h=None):
if self.style.image_mode == constants.CORNER:
if w and h:
pimage.size = (w, h)
elif self.style.image_mode == constants.CORNERS:
pimage.size = (w - x, h - y)
elif self.style.image_mode == constants.CENTER:
if w and h:
pimage.size = (w, h)
size = pimage.size
x += size[0] // 2
y += size[1] // 2
self.canvas.drawImage(pimage.get_skia_image(), x, y)

def load_pixels(self):
c_array = self.canvas.toarray()
self.pimage = SkiaPImage(c_array.shape[0], c_array.shape[1], c_array)
self.pimage.load_pixels()

def update_pixels(self):
self.pimage.update_pixels()
self.image(self.pimage, 0, 0)

def load_image(self, filename):
image = skia.Image.open(filename)
return SkiaPImage(image.width, image.height, pixels=image.toarray())

def save_canvas(self, filename, canvas):
if canvas:
# TODO: Get the surface of the PGraphics object yet to be implemented
pass
else:
canvas = self.canvas
image = canvas.getSurface().makeImageSnapshot()
image.save(filename)
24 changes: 11 additions & 13 deletions p5/sketch/Vispy2DRenderer/renderer2d.py
Original file line number Diff line number Diff line change
Expand Up @@ -537,22 +537,20 @@ def text_style(self):
def text_wrap(self, text_wrap_style):
raise NotImplementedError("Not Implemented in Vispy")

def image(self, *args, size=None):
if len(args) == 3:
img, location = args[0], args[1:]
elif len(args) == 5:
img, location, size = args[0], args[1:3], args[3:]
else:
raise ValueError("Unexpected number of arguments passed to image()")
def image(self, img, x, y, w, h):

location = (x, y)
if w is None:
w = img.size[0]
if h is None:
h = img.size[1]

if size is None:
size = img.size
size = (w, h)
# Add else statement below to resize the img._img first,
# or it will take much time to render large image,
# even when small size is specified to the image
else:
if size != img.size:
img.size = size
if size != img.size:
img.size = size

lx, ly = location
sx, sy = size
Expand Down Expand Up @@ -597,7 +595,7 @@ def update_pixels(self):

builtins.pixels = None

def save_frame(self, filename):
def save_canvas(self, filename, canvas):
if filename:
p5.sketch.screenshot(filename)
else:
Expand Down
2 changes: 1 addition & 1 deletion p5/sketch/userspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,7 @@ def save(filename="screen.png"):
"""
# TODO: images saved using ``save()`` should *not* be numbered.
# --abhikpal (2018-08-14)
p5.sketch.screenshot(filename)
p5.renderer.save_canvas(filename)


def save_frame(filename="screen.png"):
Expand Down