Last active
March 27, 2024 12:43
-
-
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 )
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 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