Skip to content

Instantly share code, notes, and snippets.

@doraTeX
Last active March 27, 2024 13:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save doraTeX/b2b5bc869a68964989be9394a5c247fb to your computer and use it in GitHub Desktop.
Save doraTeX/b2b5bc869a68964989be9394a5c247fb to your computer and use it in GitHub Desktop.
A macOS script that applies mosaic effects to faces of individuals in photos ( https://doratex.hatenablog.jp/entry/20240327/1711546952 )
#!/bin/bash
SCRIPTNAME=$(basename "$0")
function realpath () {
f=$@
if [ -d "$f" ]; then
base=""
dir="$f"
else
base="/$(basename "$f")"
dir=$(dirname "$f")
fi
dir=$(cd "$dir" && /bin/pwd)
echo "$dir$base"
}
function mosaicFaces () {
/usr/bin/osascript <<EOF
use framework "Vision"
global CA
set CA to current application
on createNewBitmap(theWidth, theHeight)
set theSize to {theWidth, theHeight}
set newImage to CA's NSImage's alloc()'s initWithSize:theSize
set newBitmap to CA's NSBitmapImageRep's alloc()'s initWithBitmapDataPlanes:(missing value) pixelsWide:theWidth pixelsHigh:theHeight bitsPerSample:8 samplesPerPixel:4 hasAlpha:true isPlanar:false colorSpaceName:(CA's NSDeviceRGBColorSpace) bytesPerRow:0 bitsPerPixel:0
newBitmap's setSize:theSize
return newBitmap
end createNewBitmap
on mosaicImage(theNsImage, theScale)
set filter to CA's CIFilter's filterWithName:"CIPixellate"
set rep to CA's NSBitmapImageRep's imageRepWithData:(theNsImage's TIFFRepresentation())
set theCiImage to CA's CIImage's alloc()'s initWithBitmapImageRep:rep
set theSize to theNsImage's |size|()
filter's setValue:theCiImage forKey:"inputImage"
filter's setValue:(theSize's width()) * theScale / 1000 forKey:"inputScale"
set outputImage to filter's outputImage()
set ciImageRep to CA's NSCIImageRep's imageRepWithCIImage:outputImage
set newNsImage to CA's NSImage's alloc()'s initWithSize:theSize
newNsImage's addRepresentation:ciImageRep
return newNsImage
end mosaicImage
on mosaicFaces(thePath, theScale)
set image to CA's NSImage's alloc()'s initWithContentsOfFile:thePath
set rep to CA's NSBitmapImageRep's imageRepWithData:(image's TIFFRepresentation())
set entireWidth to rep's pixelsWide
set entireHeight to rep's pixelsHigh
set newImage to CA's NSImage's alloc()'s initWithSize:{entireWidth, entireHeight}
set newBitmap to my createNewBitmap(entireWidth, entireHeight)
CA's NSGraphicsContext's saveGraphicsState()
set currentContext to CA's NSGraphicsContext's graphicsContextWithBitmapImageRep:newBitmap
CA's NSGraphicsContext's setCurrentContext:currentContext
image's drawInRect:{{0, 0}, {entireWidth, entireHeight}} fromRect:(CA's NSZeroRect) operation:(CA's NSCompositingOperationSourceOver) fraction:1.0
set mosaickedImage to my mosaicImage(image, theScale)
set mosaickedEntireWidth to mosaickedImage's |size|()'s width
set mosaickedEntireHeight to mosaickedImage's |size|()'s height
set faceDetectionRequest to CA's VNDetectFaceRectanglesRequest's alloc()'s init()
set requestHandler to (CA's VNImageRequestHandler's alloc's initWithData:(image's TIFFRepresentation) options:(missing value))
(requestHandler's performRequests:[faceDetectionRequest] |error|:(missing value))
set results to faceDetectionRequest's results()
repeat with requestResult in results
set faceBox to CA's NSRectFromCGRect(requestResult's boundingBox)
set x to item 1 of item 1 of faceBox
set y to item 2 of item 1 of faceBox
set width to item 1 of item 2 of faceBox
set height to item 2 of item 2 of faceBox
set x1 to x * entireWidth
set y1 to y * entireHeight
set width1 to width * entireWidth
set height1 to height * entireHeight
set faceRect1 to {{x1, y1}, {width1, height1}}
set x2 to x * mosaickedEntireWidth
set y2 to y * mosaickedEntireHeight
set width2 to width * mosaickedEntireWidth
set height2 to height * mosaickedEntireHeight
set faceRect2 to {{x2, y2}, {width2, height2}}
(mosaickedImage's drawInRect:faceRect1 fromRect:faceRect2 operation:(CA's NSCompositingOperationSourceOver) fraction:1.0)
end repeat
newImage's addRepresentation:newBitmap
CA's NSGraphicsContext's restoreGraphicsState()
return newImage
end mosaicFaces
on saveImageAsJPEGFile(image, thePath, compressionFactor, dpi)
set bitmapRep to CA's NSBitmapImageRep's alloc()'s initWithData:(image's TIFFRepresentation())
set pixelWidth to bitmapRep's pixelsWide
set pixelHeight to bitmapRep's pixelsHigh
set pointWidth to bitmapRep's |size|()'s width
set currentDPI to 72.0 * pixelWidth / pointWidth
set factor to (dpi / currentDPI)
bitmapRep's setPixelsWide:(factor * pixelWidth)
bitmapRep's setPixelsHigh:(factor * pixelHeight)
set jpegProperties to CA's NSMutableDictionary's dictionary()
jpegProperties's setObject:(compressionFactor) forKey:(CA's NSImageCompressionFactor)
set jpegData to bitmapRep's representationUsingType:(CA's NSJPEGFileType) |properties|:(jpegProperties)
set success to jpegData's writeToFile:(thePath) atomically:(true)
return success
end saveImageAsJPEGFile
set thePath to "$1"
set newImage to my mosaicFaces(thePath, $3)
saveImageAsJPEGFile(newImage, "$2", $4, $5)
EOF
}
function usage() {
echo "Usage: $SCRIPTNAME [--emoji <EMOJI>] [--compression <VALUE>] [--dpi <VALUE>] <INPUT_IMAGE_PATH> <OUTPUT_JPEG_PATH>"
echo
echo "Options:"
echo " -h, --help Show help"
echo " --scale <VALUE> Set mosaic tile scale (default: 30)"
echo " --compression <VALUE> Set compression factor [from 0.0 (maximum compression) to 1.0 (no compression)] of output image (default: 0.8)"
echo " --dpi <VALUE> Set DPI value of output image (default: 72)"
echo
}
# parse arguments
declare -a args=("$@")
declare -a params=()
SCALE=30
COMPRESSION=0.8
DPI=72
I=0
while [ $I -lt ${#args[@]} ]; do
OPT="${args[$I]}"
case $OPT in
-h | --help )
usage
exit 0
;;
--scale )
if [[ -z "${args[$(($I+1))]}" ]]; then
echo "$SCRIPTNAME: option requires an argument -- $OPT" 1>&2
exit 1
fi
SCALE="${args[$(($I+1))]}"
I=$(($I+1))
;;
--compression )
if [[ -z "${args[$(($I+1))]}" ]]; then
echo "$SCRIPTNAME: option requires an argument -- $OPT" 1>&2
exit 1
fi
COMPRESSION="${args[$(($I+1))]}"
I=$(($I+1))
;;
--dpi )
if [[ -z "${args[$(($I+1))]}" ]]; then
echo "$SCRIPTNAME: option requires an argument -- $OPT" 1>&2
exit 1
fi
DPI="${args[$(($I+1))]}"
I=$(($I+1))
;;
-- | -)
I=$(($I+1))
while [ $I -lt ${#args[@]} ]; do
params+=("${args[$I]}")
I=$(($I+1))
done
break
;;
-*)
echo "$SCRIPTNAME: illegal option -- '$(echo $OPT | sed 's/^-*//')'" 1>&2
exit 1
;;
*)
if [[ ! -z "$OPT" ]] && [[ ! "$OPT" =~ ^-+ ]]; then
params+=( "$OPT" )
fi
;;
esac
I=$(($I+1))
done
# handle invalid arguments
if [ ${#params[@]} -ne 2 ]; then
echo "$SCRIPTNAME: Specify input and output file names." 1>&2
echo "Try '$SCRIPTNAME --help' for more information." 1>&2
exit 1
fi
SUCCESS=$(mosaicFaces "$(realpath ${params[0]})" "$(realpath ${params[1]})" $SCALE $COMPRESSION $DPI)
if [ $SUCCESS = "true" ]; then
exit 0
else
exit 1
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment