Skip to content

Instantly share code, notes, and snippets.

@doraTeX
Last active March 27, 2024 12:43
Show Gist options
  • Save doraTeX/4552b8552d73f39dcc19557888dfac5d to your computer and use it in GitHub Desktop.
Save doraTeX/4552b8552d73f39dcc19557888dfac5d to your computer and use it in GitHub Desktop.
A macOS script that masks faces of individuals in photos using emojis ( https://doratex.hatenablog.jp/entry/20240324/1711246435 )
#!/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 maskFaces () {
/usr/bin/osascript <<EOF
use framework "Vision"
global CA
set CA to current application
on max(x, y)
if x >= y then
return x
else
return y
end if
end max
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 maskFaces(thePath, theCharacter)
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 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 x to x * entireWidth
set y to y * entireHeight
set width to width * entireWidth
set height to height * entireHeight
set faceRect to {{x, y}, {width, height}}
set theFont to (CA's NSFont's systemFontOfSize:(max(width, height) * 1.2))
set theAttributes to CA's NSMutableDictionary's dictionary()
(theAttributes's setObject:(theFont) forKey:(CA's NSFontAttributeName))
set emojiString to (CA's NSAttributedString's alloc()'s initWithString:theCharacter attributes:theAttributes)
set emojiStringSize to emojiString's |size|()
set thePoint to {x + (width - (emojiStringSize's width)) / 2, y}
(emojiString's drawAtPoint:(thePoint))
end repeat
newImage's addRepresentation:newBitmap
CA's NSGraphicsContext's restoreGraphicsState()
return newImage
end maskFaces
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 theEmoji to "$3"
set newImage to my maskFaces(thePath, theEmoji)
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 " --emoji <EMOJI> Set emoji character for masking (default: 😂)"
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=()
EMOJI="😂"
COMPRESSION=0.8
DPI=72
I=0
while [ $I -lt ${#args[@]} ]; do
OPT="${args[$I]}"
case $OPT in
-h | --help )
usage
exit 0
;;
--emoji )
if [[ -z "${args[$(($I+1))]}" ]]; then
echo "$SCRIPTNAME: option requires an argument -- $OPT" 1>&2
exit 1
fi
EMOJI="${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=$(maskFaces "$(realpath ${params[0]})" "$(realpath ${params[1]})" $EMOJI $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