image masking

This commit is contained in:
Ryan Oldenburg 2021-02-08 14:31:20 -06:00
parent c233894283
commit bbe207baa0
8 changed files with 170 additions and 52 deletions

View file

@ -9,6 +9,10 @@ proc fractional*(v: float32): float32 {.inline.} =
result = abs(v)
result = result - floor(result)
proc lerp*(a, b: uint8, t: float32): uint8 {.inline.} =
let t = round(t * 255).uint32
((a * (255 - t) + b * t) div 255).uint8
proc lerp*(a, b: ColorRGBA, t: float32): ColorRGBA {.inline.} =
let x = round(t * 255).uint32
result.r = ((a.r.uint32 * (255 - x) + b.r.uint32 * x) div 255).uint8

View file

@ -196,17 +196,8 @@ proc draw(
rx = parseFloat(node.attr("rx"))
ry = parseFloat(node.attr("ry"))
let
magicX = (4.0 * (-1.0 + sqrt(2.0)) / 3) * rx
magicY = (4.0 * (-1.0 + sqrt(2.0)) / 3) * ry
var path: Path
path.moveTo(cx + rx, cy)
path.bezierCurveTo(cx + rx, cy + magicY, cx + magicX, cy + ry, cx, cy + ry)
path.bezierCurveTo(cx - magicX, cy + ry, cx - rx, cy + magicY, cx - rx, cy)
path.bezierCurveTo(cx - rx, cy - magicY, cx - magicX, cy - ry, cx, cy - ry)
path.bezierCurveTo(cx + magicX, cy - ry, cx + rx, cy - magicY, cx + rx, cy)
path.closePath()
path.ellipse(cx, cy, rx, ry)
if ctx.fill != ColorRGBA():
img.fillPath(path, ctx.fill, ctx.transform)

View file

@ -1,4 +1,4 @@
import chroma, blends, bumpy, vmath, common, system/memory
import chroma, blends, bumpy, vmath, common, system/memory, masks
when defined(amd64) and not defined(pixieNoSimd):
import nimsimd/sse2
@ -263,6 +263,40 @@ proc toStraightAlpha*(image: Image) =
c.g = ((c.g.uint32 * multiplier) div 255).uint8
c.b = ((c.b.uint32 * multiplier) div 255).uint8
proc maskCorrect*(image: Image, mask: Mask, mat = mat3()) =
var
matInv = mat.inverse()
mask = mask
block: # Shrink mask by 2 as needed
var
dx = matInv * vec2(1, 0)
dy = matInv * vec2(0, 1)
while max(dx.length, dy.length) > 2:
mask = mask.minifyBy2()
dx /= 2
dy /= 2
matInv = matInv * scale(vec2(0.5, 0.5))
for y in 0 ..< image.height:
for x in 0 ..< image.width:
let
maskPos = matInv * vec2(x.float32 + h, y.float32 + h)
xFloat = maskPos.x - h
yFloat = maskPos.y - h
value = mask.getValueSmooth(xFloat, yFloat).uint32
rgba = image.getRgbaUnsafe(x, y)
blended = rgba(
((rgba.r * value) div 255).uint8,
((rgba.g * value) div 255).uint8,
((rgba.b * value) div 255).uint8,
((rgba.a * value) div 255).uint8
)
image.setRgbaUnsafe(x, y, blended)
proc mask*(image: Image, mask: Mask, pos = vec2(0, 0)) {.inline.} =
image.maskCorrect(mask, translate(pos))
when defined(release):
{.pop.}
@ -286,15 +320,17 @@ proc invert*(image: Image) =
proc getRgbaSmooth*(image: Image, x, y: float32): ColorRGBA {.inline.} =
let
minX = x.floor.int
diffX = x - x.floor
minY = y.floor.int
diffY = y - y.floor
minX = floor(x)
minY = floor(y)
diffX = x - minX
diffY = y - minY
x = minX.int
y = minY.int
x0y0 = image[minX, minY].toPremultipliedAlpha()
x1y0 = image[minX + 1, minY].toPremultipliedAlpha()
x0y1 = image[minX, minY + 1].toPremultipliedAlpha()
x1y1 = image[minX + 1, minY + 1].toPremultipliedAlpha()
x0y0 = image[x + 0, y + 0].toPremultipliedAlpha()
x1y0 = image[x + 1, y + 0].toPremultipliedAlpha()
x0y1 = image[x + 0, y + 1].toPremultipliedAlpha()
x1y1 = image[x + 1, y + 1].toPremultipliedAlpha()
bottomMix = lerp(x0y0, x1y0, diffX)
topMix = lerp(x0y1, x1y1, diffX)
@ -407,24 +443,21 @@ proc sharpOpacity*(image: Image) =
else:
rgba = rgba(255, 255, 255, 255)
proc drawCorrect*(a, b: Image, mat: Mat3, blendMode: BlendMode) =
proc drawCorrect*(a, b: Image, mat = mat3(), blendMode = bmNormal) =
## Draws one image onto another using matrix with color blending.
var
matInv = mat.inverse()
# Compute movement vectors
p = matInv * vec2(0 + h, 0 + h)
dx = matInv * vec2(1 + h, 0 + h) - p
dy = matInv * vec2(0 + h, 1 + h) - p
minFilterBy2 = max(dx.length, dy.length)
b = b
while minFilterBy2 > 2.0:
b = b.minifyBy2()
p /= 2
dx /= 2
dy /= 2
minFilterBy2 /= 2
matInv = matInv * scale(vec2(0.5, 0.5))
block: # Shrink image by 2 as needed
var
dx = matInv * vec2(1, 0)
dy = matInv * vec2(0, 1)
while max(dx.length, dy.length) > 2:
b = b.minifyBy2()
dx /= 2
dy /= 2
matInv = matInv * scale(vec2(0.5, 0.5))
let blender = blendMode.blender()
for y in 0 ..< a.height:

View file

@ -1,4 +1,4 @@
import common, vmath
import common, vmath, system/memory
type
Mask* = ref object
@ -81,5 +81,33 @@ proc minifyBy2*(mask: Mask, power = 1): Mask =
mask.getValueUnsafe(x * 2 + 0, y * 2 + 1)
result.setValueUnsafe(x, y, (value div 4).uint8)
proc fillUnsafe(data: var seq[uint8], value: uint8, start, len: int) =
## Fills the mask data with the parameter value starting at index start and
## continuing for len indices.
nimSetMem(data[start].addr, value.cint, len)
proc fill*(mask: Mask, value: uint8) {.inline.} =
## Fills the mask with the parameter value.
fillUnsafe(mask.data, value, 0, mask.data.len)
proc getValueSmooth*(mask: Mask, x, y: float32): uint8 =
let
minX = floor(x)
minY = floor(y)
diffX = x - minX
diffY = y - minY
x = minX.int
y = minY.int
x0y0 = mask[x + 0, y + 0]
x1y0 = mask[x + 1, y + 0]
x0y1 = mask[x + 0, y + 1]
x1y1 = mask[x + 1, y + 1]
bottomMix = lerp(x0y0, x1y0, diffX)
topMix = lerp(x0y1, x1y1, diffX)
lerp(bottomMix, topMix, diffY)
when defined(release):
{.pop.}

View file

@ -363,6 +363,18 @@ proc rect*(path: var Path, x, y, w, h: float32) =
proc rect*(path: var Path, pos: Vec2, wh: Vec2) {.inline.} =
path.rect(pos.x, pos.y, wh.x, wh.y)
proc ellipse*(path: var Path, cx, cy, rx, ry: float32) =
let
magicX = (4.0 * (-1.0 + sqrt(2.0)) / 3) * rx
magicY = (4.0 * (-1.0 + sqrt(2.0)) / 3) * ry
path.moveTo(cx + rx, cy)
path.bezierCurveTo(cx + rx, cy + magicY, cx + magicX, cy + ry, cx, cy + ry)
path.bezierCurveTo(cx - magicX, cy + ry, cx - rx, cy + magicY, cx - rx, cy)
path.bezierCurveTo(cx - rx, cy - magicY, cx - magicX, cy - ry, cx, cy - ry)
path.bezierCurveTo(cx + magicX, cy - ry, cx + rx, cy - magicY, cx + rx, cy)
path.closePath()
proc polygon*(path: var Path, x, y, size: float32, sides: int) =
## Draws a n sided regular polygon at (x, y) with size.
path.moveTo(x + size * cos(0.0), y + size * sin(0.0))

View file

@ -1,48 +1,80 @@
import chroma, pixie, benchy
let a = newImage(2560, 1440)
let image = newImage(2560, 1440)
proc reset() =
image.fill(rgba(63, 127, 191, 191))
reset()
timeIt "fill":
a.fill(rgba(255, 255, 255, 255))
doAssert a[0, 0] == rgba(255, 255, 255, 255)
image.fill(rgba(255, 255, 255, 255))
doAssert image[0, 0] == rgba(255, 255, 255, 255)
reset()
timeIt "fill_rgba":
a.fill(rgba(63, 127, 191, 191))
doAssert a[0, 0] == rgba(63, 127, 191, 191)
image.fill(rgba(63, 127, 191, 191))
doAssert image[0, 0] == rgba(63, 127, 191, 191)
reset()
timeIt "subImage":
keep a.subImage(0, 0, 256, 256)
keep image.subImage(0, 0, 256, 256)
reset()
# timeIt "superImage":
# discard
reset()
timeIt "minifyBy2":
let minified = a.minifyBy2()
let minified = image.minifyBy2()
doAssert minified[0, 0] == rgba(63, 127, 191, 191)
reset()
timeIt "invert":
a.invert()
keep(a)
image.invert()
reset()
timeIt "applyOpacity":
a.applyOpacity(0.5)
keep(a)
image.applyOpacity(0.5)
reset()
timeIt "sharpOpacity":
a.sharpOpacity()
keep(a)
image.sharpOpacity()
a.fill(rgba(63, 127, 191, 191))
reset()
timeIt "toPremultipliedAlpha":
a.toPremultipliedAlpha()
image.toPremultipliedAlpha()
reset()
timeIt "toStraightAlpha":
a.toStraightAlpha()
image.toStraightAlpha()
reset()
block:
var path: Path
path.ellipse(image.width / 2, image.height / 2, 300, 300)
let mask = newMask(image.width, image.height)
mask.fillPath(path)
timeIt "mask":
image.mask(mask)
reset()
timeIt "lerp integers":
for i in 0 ..< 100000:
let c = a[0, 0]
let c = image[0, 0]
var z: int
for t in 0 .. 100:
z += lerp(c, c, t.float32 / 100).a.int
@ -50,7 +82,7 @@ timeIt "lerp integers":
timeIt "lerp floats":
for i in 0 ..< 100000:
let c = a[0, 0]
let c = image[0, 0]
var z: int
for t in 0 .. 100:
z += lerp(c.color, c.color, t.float32 / 100).rgba().a.int

View file

@ -91,3 +91,17 @@ block:
a = readImage("tests/images/flipped1.png")
b = a.minifyBy2(2)
b.writeFile("tests/images/minifiedBy4.png")
block:
let image = newImage(100, 100)
image.fill(rgba(255, 100, 100, 255))
var path: Path
path.ellipse(image.width / 2, image.height / 2, 25, 25)
let mask = newMask(image.width, image.height)
mask.fillPath(path)
image.mask(mask)
image.toStraightAlpha()
image.writeFile("tests/images/circleMask.png")

View file

@ -16,4 +16,8 @@ block:
path.arcTo(x, y, x + w, y, r)
mask.fillPath(path)
writeFile("tests/images/masks/maskMinified.png", mask.minifyBy2().encodePng())
let minified = mask.minifyBy2()
doAssert minified.width == 50 and minified.height == 50
writeFile("tests/images/masks/maskMinified.png", minified.encodePng())