Skip to content
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
Binary file added example/assets/components/dandelion.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed example/assets/components/snow-globe.png
Binary file not shown.
Binary file added example/assets/dandelion/dandelion-atlas.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
112 changes: 112 additions & 0 deletions example/assets/dandelion/tools/build-atlas.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
#!/bin/sh
# Builds dandelion-atlas.jpg from the source photo. Needs ImageMagick 7 and Xcode
# (Vision, for the foreground mask). Run from this directory with the photo as $1:
#
# curl -A 'rn-effects-example/1.0' -o source.jpg "<thumb.wikimedia.org url from the Commons API, iiurlwidth=3840>"
# sh build-atlas.sh source.jpg
#
# The committed atlas was built from the 3840x5120 thumb; the output is
# byte-identical on a rebuild from that source.
#
# Photo: "Taraxacum seedhead Paslieres 2013-05-09 n01.jpg", Marie-Lan Nguyen,
# CC BY 2.5, https://commons.wikimedia.org/wiki/File:Taraxacum_seedhead_Paslieres_2013-05-09_n01.jpg
#
# Output is one image, three panels of PW x PH side by side:
# left the photo, scaled to PW wide and extended upward with smeared bokeh so
# the seeds have sky to fly into on a phone-shaped canvas
# middle the same with the seed head painted out (normalised-convolution fill of
# the surrounding bokeh), stem kept, a small receptacle knob at the centre
# right data: R = soft head mask, G = hair matte (white pappus pixels), B = 0
set -eu
SRC=${1:?source photo}
PW=1000; PH=2174
CX=503; CY=1321; R=395 # head centre + radius on the canvas, px (measured from the mask)
STEM_X0=455; STEM_X1=545 # stem column on the canvas
T=$(mktemp -d)

# 1. Scale, then extend upward: the photo is 3:4 and the canvas is phone-shaped.
# The extension is the photo's top rows stretched into vertical streaks (more
# out-of-focus grass), and the photo's first FADE rows cross-fade into it so
# the join has no line. The head starts ~110 rows in, so FADE stays under that.
FADE=90
magick "$SRC" -resize ${PW}x "$T/photo.png"
PHOTO_H=$(identify -format %h "$T/photo.png")
EXT=$((PH - PHOTO_H))
magick "$T/photo.png" -crop ${PW}x12+0+0 +repage -resize ${PW}x$((EXT + FADE))! -blur 0x20 "$T/ext.png"
magick -size ${PW}x${FADE} gradient:black-white "$T/fade.png"
magick "$T/photo.png" -crop ${PW}x${FADE}+0+0 +repage "$T/fade.png" -alpha off -compose CopyOpacity -composite "$T/photo_top_a.png"
magick "$T/ext.png" "$T/photo_top_a.png" -geometry +0+${EXT} -compose Over -composite "$T/ext.png"
magick "$T/photo.png" -crop ${PW}x$((PHOTO_H - FADE))+0+${FADE} +repage "$T/photo_rest.png"
magick "$T/ext.png" "$T/photo_rest.png" -append +repage "$T/canvas.png"

# 2. Foreground mask (Vision), on the canvas.
[ -x ./liftmask ] || swiftc -O liftmask.swift -o liftmask
./liftmask "$T/photo.png" "$T/mask_photo.png"
magick -size ${PW}x${EXT} xc:black \( "$T/mask_photo.png" -colorspace gray \) -append +repage -depth 8 "$T/vision.png"

# 3. Head disc, stem, hair matte.
magick -size ${PW}x${PH} xc:black -fill white -draw "circle $CX,$CY $((CX + R)),$CY" -blur 0x6 "$T/disc.png"
magick -size ${PW}x${PH} xc:black -fill white -draw "rectangle $STEM_X0,$((CY + R * 86 / 100)) $STEM_X1,$PH" "$T/stemcol.png"
magick "$T/vision.png" "$T/stemcol.png" -compose Multiply -composite "$T/stem.png"
magick "$T/disc.png" "$T/vision.png" -compose Multiply -composite \
\( "$T/stem.png" -negate \) -compose Multiply -composite "$T/head.png"
magick "$T/canvas.png" -colorspace HSL -channel G -separate +channel -negate -level 45%,85% "$T/satkey.png"
magick "$T/canvas.png" -colorspace gray -level 40%,80% "$T/lumkey.png"
magick "$T/satkey.png" "$T/lumkey.png" -compose Multiply -composite "$T/head.png" -compose Multiply -composite "$T/hair.png"

# 4. Bald panel: paint the head out. A push-pull inpaint — repeated normalised
# convolutions, blur(img * known) / blur(known), each pass filling the pixels
# the previous one reached — pulls the surrounding bokeh inward with its
# gradient instead of averaging it into one flat disc. Then the stem is
# extended up to the centre (it was hidden behind the seeds) and a small
# receptacle sits on top. A bare receptacle is a pale pitted dome, so it is
# the photo's own centre with the hairs blurred away and shrunk down: the
# achene bases become the pits. Pasting the centre in at full size left a
# dandelion in miniature on the stem.
# The fill reaches well past the disc and feathers out over ~50 px: the fringe
# hairs run ~40 px beyond R, and a fill that stopped short of them pulled
# their white into the estimate; a hard-edged fill, however far out, read as
# a smooth pale disc set into the mottled bokeh.
magick -size ${PW}x${PH} xc:black -fill white -draw "circle $CX,$CY $((CX + R + 70)),$CY" -blur 0x25 \
\( "$T/stem.png" -negate \) -compose Multiply -composite -alpha off "$T/fill.png"
# Masks must be plain grayscale: a stray alpha channel turns Multiply into a blend.
magick "$T/fill.png" -negate -threshold 50% -alpha off "$T/known.png"
cp "$T/canvas.png" "$T/work.png"
for SIGMA in 6 10 16 26 40 64 100; do
magick "$T/work.png" "$T/known.png" -compose Multiply -composite -blur 0x$SIGMA "$T/num.png"
magick "$T/known.png" -blur 0x$SIGMA "$T/den.png"
magick "$T/num.png" "$T/den.png" -compose Divide_Src -composite "$T/est.png"
# Pixels the blur reached with some confidence become known this pass.
magick "$T/den.png" -threshold 4% -alpha off "$T/reached.png"
magick "$T/reached.png" \( "$T/known.png" -negate \) -compose Multiply -composite -alpha off "$T/newly.png"
magick "$T/est.png" "$T/newly.png" -alpha off -compose CopyOpacity -composite "$T/est_a.png"
magick "$T/work.png" "$T/est_a.png" -compose Over -composite "$T/work.png"
magick "$T/known.png" "$T/newly.png" -compose Plus -composite -alpha off "$T/known.png"
done
magick "$T/work.png" -blur 0x3 "$T/fill.png" -alpha off -compose CopyOpacity -composite "$T/infill_a.png"
magick "$T/canvas.png" "$T/infill_a.png" -compose Over -composite "$T/bald.png"
# Stem up to the receptacle: the visible stem's top rows stretched upward,
# feathered at the sides so the strip blends into the fill.
STEM_TOP=$((CY + R * 86 / 100))
SEG_H=$((STEM_TOP - CY + 12))
magick "$T/canvas.png" -crop $((STEM_X1 - STEM_X0))x40+${STEM_X0}+${STEM_TOP} +repage -resize $((STEM_X1 - STEM_X0))x${SEG_H}! "$T/stemseg.png"
magick "$T/stem.png" -crop $((STEM_X1 - STEM_X0))x1+${STEM_X0}+$((STEM_TOP + 20)) +repage -blur 0x3 -resize $((STEM_X1 - STEM_X0))x${SEG_H}! "$T/stemseg_m.png"
magick "$T/stemseg.png" "$T/stemseg_m.png" -alpha off -compose CopyOpacity -composite "$T/stemseg_a.png"
magick "$T/bald.png" "$T/stemseg_a.png" -geometry +${STEM_X0}+$((CY - 12)) -compose Over -composite "$T/bald.png"
KR=$((R / 7))
SRC_R=$((R / 3))
magick "$T/canvas.png" -crop $((2 * SRC_R))x$((2 * SRC_R))+$((CX - SRC_R))+$((CY - SRC_R)) +repage \
-blur 0x6 -resize $((2 * KR))x$((2 * KR))! -modulate 118,45,100 "$T/knob.png"
# Dome shading: bright at the crown, falling off toward the rim.
magick -size $((2 * KR))x$((2 * KR)) radial-gradient:white-gray55 "$T/dome.png"
magick "$T/knob.png" "$T/dome.png" -compose Multiply -composite "$T/knob.png"
magick -size $((2 * KR))x$((2 * KR)) xc:black -fill white -draw "circle $KR,$KR $((2 * KR - 2)),$KR" -blur 0x2 "$T/knobmask.png"
magick "$T/knob.png" "$T/knobmask.png" -alpha off -compose CopyOpacity -composite "$T/knob_a.png"
magick "$T/bald.png" "$T/knob_a.png" -geometry +$((CX - KR))+$((CY - KR)) -compose Over -composite "$T/bald.png"

# 5. Data panel and atlas.
magick "$T/head.png" "$T/hair.png" -size ${PW}x${PH} xc:black -channel RGB -combine "$T/data.png"
magick "$T/canvas.png" "$T/bald.png" "$T/data.png" +append -sampling-factor 1x1 -quality 90 ../dandelion-atlas.jpg
cp "$T/canvas.png" "$T/bald.png" "$T/data.png" "${ATLAS_DEBUG_DIR:-$T}/" 2>/dev/null || true
identify ../dandelion-atlas.jpg
ls -la ../dandelion-atlas.jpg
23 changes: 23 additions & 0 deletions example/assets/dandelion/tools/liftmask.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// swift liftmask.swift in.jpg mask.png
// Foreground-instance mask (the same model iOS "lift subject" uses), soft, full-res.
import Foundation
import Vision
import CoreImage
import AppKit

let args = CommandLine.arguments
guard args.count == 3 else { fputs("usage: liftmask in out.png\n", stderr); exit(1) }
let url = URL(fileURLWithPath: args[1])
guard let ci = CIImage(contentsOf: url) else { fputs("cannot read input\n", stderr); exit(1) }
let handler = VNImageRequestHandler(ciImage: ci, options: [:])
let req = VNGenerateForegroundInstanceMaskRequest()
try handler.perform([req])
guard let obs = req.results?.first else { fputs("no foreground found\n", stderr); exit(2) }
let pb = try obs.generateScaledMaskForImage(forInstances: obs.allInstances, from: handler)
let maskCI = CIImage(cvPixelBuffer: pb)
let ctx = CIContext()
guard let cg = ctx.createCGImage(maskCI, from: maskCI.extent) else { exit(3) }
let rep = NSBitmapImageRep(cgImage: cg)
guard let png = rep.representation(using: .png, properties: [:]) else { exit(4) }
try png.write(to: URL(fileURLWithPath: args[2]))
print("mask \(cg.width)x\(cg.height)")
4 changes: 2 additions & 2 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ import BirthdayCandleScreen from './screens/BirthdayCandleScreen';
import ParticleDispersionScreen from './screens/ParticleDispersionScreen';
import FoamLabScreen from './screens/FoamLabScreen';
import VoiceDeleteScreen from './screens/VoiceDeleteScreen';
import SnowGlobeScreen from './screens/SnowGlobeScreen';
import Magic8BallScreen from './screens/Magic8BallScreen';
import CigaretteScreen from './screens/CigaretteScreen';
import DandelionScreen from './screens/DandelionScreen';
import type { RootStackParamList } from './types';

const Stack = createNativeStackNavigator<RootStackParamList>();
Expand Down Expand Up @@ -137,9 +137,9 @@ export default function App() {
name="VoiceDeleteExample"
component={VoiceDeleteScreen}
/>
<Stack.Screen name="SnowGlobeExample" component={SnowGlobeScreen} />
<Stack.Screen name="Magic8BallExample" component={Magic8BallScreen} />
<Stack.Screen name="CigaretteExample" component={CigaretteScreen} />
<Stack.Screen name="DandelionExample" component={DandelionScreen} />
</Stack.Navigator>
</NavigationContainer>
</GestureHandlerRootView>
Expand Down
Loading
Loading