Last active
March 27, 2024 13:42
-
-
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 )
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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