diff --git a/.gitignore b/.gitignore index b53f36acf..dca4d788c 100644 --- a/.gitignore +++ b/.gitignore @@ -31,4 +31,25 @@ docs/site/ Manifest.toml # output data -data/ +examples/data/ +data/*.csv +data/*.pdf +data/all-non-faces/ +data/alt/ +data/classifiers_* +data/faceness-scores-* +data/haarcascades +data/lfw-all +data/lizzie-testset +data/main +data/scores +data/wider +data/ffhq/thumbnails128x128/ +data/ffhq/LICENSE.txt +data/ffhq/*.py +data/ffhq/*.json +data/things/object_images/ +data/things/object_images_all/ +data/things/password.txt +data/things/all_categories.txt +data/things/all_categories_filtered.txt \ No newline at end of file diff --git a/Project.toml b/Project.toml index d9e9203e4..b24598f21 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "FaceDetection" uuid = "00808967-75e2-4046-a522-2ca211e35506" authors = ["Jake W. Ireland and contributors"] -version = "1.0.2" +version = "1.1.0" [deps] ColorTypes = "3da002f7-5984-5a60-b8a6-cbb66c0b333f" @@ -11,6 +11,7 @@ ImageIO = "82e4d734-157c-48bb-816b-45c225c6df19" ImageMagick = "6218d12a-5da1-5696-b52f-db25d2ecc6d1" ImageView = "86fae568-95e7-573e-a6b2-d8a6b900c9ef" Images = "916415d5-f1e6-5110-898d-aaa5f9f070e0" +IntegralArrays = "1d092043-8f09-5a30-832f-7509e371ab51" Netpbm = "f09324ee-3d7c-5217-9330-fc30815ba969" ProgressMeter = "92933f4c-e287-5a05-a399-4b506db050ca" QuartzImageIO = "dca85d43-d64c-5e67-8c65-017450d5d020" diff --git a/data/ffhq/readme.md b/data/ffhq/readme.md new file mode 100644 index 000000000..856809f04 --- /dev/null +++ b/data/ffhq/readme.md @@ -0,0 +1,6 @@ +The [FFHQ database](https://github.com/NVlabs/ffhq-dataset/) is a great dataset for positive training images, as it has some 70,001 images of faces, mostly alone in the image. + +To download this dataset, please run +```shell +$ bash setup.sh +``` \ No newline at end of file diff --git a/data/ffhq/setup.sh b/data/ffhq/setup.sh new file mode 100644 index 000000000..eb544e719 --- /dev/null +++ b/data/ffhq/setup.sh @@ -0,0 +1,22 @@ +echo "Please ensure you have followed the Google Drive API instructions listed here: https://docs.iterative.ai/PyDrive2/quickstart/" +sleep 5 + +pip3 install pydrive2 +curl 'https://gist.githubusercontent.com/jakewilliami/6e361ca59df521c874a9021bde1d2c81/raw/2f277c36bcd725df71d30174e13f920d7bee7b97/download_ffhq_pydrive.py' > download_ffhq_pydrive.py +echo "Downloading image thumbnails" +python3 download_ffhq.py -t --pydrive --cmd_auth + +echo "Moving the images into one directory and deleting subdirectories." +# move images out of their subdirectories +for d in thumbnails128x128/*; do + [ -d "$d" ] || continue + for f in "$d"/*; do + mv "$f" "thumbnails128x128/$(basename "$f")" + done +done +# clean up the subdirectories +for d in thumbnails128x128/*; do + if [ -d "$d" ]; then + rm -d "$d" + done +done diff --git a/data/things/misc_filter_categories.txt b/data/things/misc_filter_categories.txt new file mode 100644 index 000000000..fbc5f4eb9 --- /dev/null +++ b/data/things/misc_filter_categories.txt @@ -0,0 +1,83 @@ +baby +bandanna +beanie +beard +blindfold +bobsled +bowler hat +braid +breathalyzer +chick +chicken2 +chihuahua +cockatoo +costume +dalmatian +denture +doll +duckling +ear +earplug +eye +eye patch +eyeliner +face +figurine +football helmet +gargoyle +gas mask +gingerbread man +girl +glasses +goggles +gondola +groundhog +hair +hairnet +hat +headband +headdress +headlamp +headscarf +hearing aid +helmet +hood +jetski +kitten +lamb +man +mannequin +mascara +mask +mouth +mouthpiece +mustache +piggy bank +piglet +playpen +pogo stick +poodle +poster +pug +puppet +puppy +racehorse +ram +rickshaw +robot +sarcophagus +scarecrow +scarf +seagull +seal +skeleton +skull +snorkel +snowman +statue +tadpole +teddy bear +totem pole +toy +warthog +woman diff --git a/data/things/object_categories.jl b/data/things/object_categories.jl new file mode 100644 index 000000000..b257bef78 --- /dev/null +++ b/data/things/object_categories.jl @@ -0,0 +1,79 @@ +get_category_from_image_name(s::String) = join(split(basename(s), '_')[1:(end - 1)], ' ') + +# Return a list of object categories from the images +function get_object_categories(object_images::Vector{String}) + object_categories = String[] + for object_image in object_images + object_image = basename(object_image) + object_category = get_category_from_image_name(object_image) + if object_category ∉ object_categories + push!(object_categories, object_category) + end + end + return object_categories +end +get_object_categories(object_image_dir::String) = + get_object_categories(readdir(object_image_dir)) + +# Filter out animals from the categories +function filter_out_animals(object_image_categories::Vector{String}) + animals = readlines(download("https://gist.githubusercontent.com/atduskgreg/3cf8ef48cb0d29cf151bedad81553a54/raw/82f142562cf50b0f6fb8010f890b2f934093553e/animals.txt")) + animals = String[string(lowercase(animal)) for animal in animals] + filtered_categories = String[] + for image_category in object_image_categories + category_is_animal = image_category ∈ animals + # category_starts_with_animal = any(startswith(image_category, animal) for animal in animals) + if !category_is_animal # || !category_starts_with_animal + push!(filtered_categories, image_category) + end + end + return filtered_categories +end +filter_out_animals(object_image_dir::String) = + filter_out_animals(get_object_categories(object_image_dir)) + +# Get the category lists and write them to file +function main(all_object_image_dir::String) + outfile_all_categories_list = "all_categories.txt" + outfile_all_categories_filtered_list = "all_categories_filtered.txt" + misc_filter_categories_list = "misc_filter_categories.txt" + + all_object_images = readdir(all_object_image_dir, sort = true, join = true) + + all_categories = get_object_categories(all_object_images) + all_categories_filtered = filter_out_animals(all_categories) + misc_filter_categories = readlines(misc_filter_categories_list) + filter!(category -> category ∉ misc_filter_categories, all_categories_filtered) + + open(outfile_all_categories_list, "w") do io + for category in all_categories + write(io, category, '\n') + end + end + + open(outfile_all_categories_filtered_list, "w") do io + for category in all_categories_filtered + write(io, category, '\n') + end + end + + @info "There are currently $(length(all_object_images)) images in your object directory" + categories_warned = String[] + removed = 0 + for object_image in all_object_images + object_category = get_category_from_image_name(object_image) + if object_category ∉ all_categories_filtered + if object_category ∉ categories_warned + @warn("Removing images of the category \"$object_category\"") + push!(categories_warned, object_category) + end + rm(object_image) + removed += 1 + end + end + @info "We have removed all of the images that needed removing, and are left with $(length(all_object_images) - removed) images in your object directory" + + return nothing +end + +main("object_images/") diff --git a/data/things/readme.md b/data/things/readme.md new file mode 100644 index 000000000..3ecb9732f --- /dev/null +++ b/data/things/readme.md @@ -0,0 +1,22 @@ +The [THINGS dataset](https://osf.io/3fu6z/) is a great dataset for object images, containing 26,107 object images. However, there are some categories of images that may interfere with our face detection results, if we are to use these images as negative training images. Of these images, there are 1854 unique categories. After filtering out [animals](https://gist.github.com/atduskgreg/3cf8ef48cb0d29cf151bedad81553a54) from this dataset, there are 1702 unique categories. Further removing some categories (manually selected) that contained humans or facial features (see below), there are 1619 unique categories. + +To download the THINGS dataset in its entirety, run +```shell +$ bash setup.sh +``` + +Now that you have the dataset, please run +```shell +$ julia object_categories.jl +``` + +This will create two text files; one will have all unique categories of images (`all_categories.txt`); the other will contain that list (`all_categories_filtered.txt`), removing categories that are: + - Animals; + - Hat or hair related objects; + - Human-like objects; + - Specific parts of faces; + - Activities requiring humans. + +The Julia script will filter these categories out of the downloaded images, as they contain too many faces/facial features. Beyond animals, this filter process uses a list of categories manually selected from `misc_filter_categories.txt`. + +After filtering all the potentially interfering images out of the THINGS dataset, we are left with 22,558 images. diff --git a/data/things/setup.sh b/data/things/setup.sh new file mode 100755 index 000000000..45bb392d0 --- /dev/null +++ b/data/things/setup.sh @@ -0,0 +1,27 @@ +#!/bin/bash +wget -q 'https://files.osf.io/v1/resources/jum2f/providers/osfstorage/5d4d7ec80f488d0017907d30?action=download&direct&version=2' -O 'password.txt' +echo "Downloading object_images_A-C.zip" +wget 'https://files.osf.io/v1/resources/jum2f/providers/osfstorage/5f89eef1d85b700286657a33?action=download&direct&version=1' -O 'object_images_A-C.zip' +echo "Downloading object_images_D-K.zip" +wget 'https://files.osf.io/v1/resources/jum2f/providers/osfstorage/5f89f02b37b6bb0248309053?action=download&direct&version=1' -O 'object_images_D-K.zip' +echo "Downloading object_images_L-Q.zip" +wget 'https://files.osf.io/v1/resources/jum2f/providers/osfstorage/5f89f10e37b6bb02483092bb?action=download&direct&version=2' -O 'object_images_L-Q.zip' +echo "Downloading object_images_R-S.zip" +wget 'https://files.osf.io/v1/resources/jum2f/providers/osfstorage/5f89f218d85b700291656821?action=download&direct&version=1' -O 'object_images_R-S.zip' +echo "Downloading object_images_T-Z.zip" +wget 'https://files.osf.io/v1/resources/jum2f/providers/osfstorage/5f89f30a37b6bb02483098c8?action=download&direct&version=1' -O 'object_images_T-Z.zip' + +mkdir object_images +for z in ./*.zip; do + unzip -P 'things4all' "$z" +end + +for d in ./object_images_*; do + [ -d "$d" ] || continue + for d2 in "$d"/*; do + for f in "$d2"/*; do + mv "$f" ./object_images/"$(basename "$f")" + done + done + rm -d "$d" +done diff --git a/examples/Project.toml b/examples/Project.toml new file mode 100644 index 000000000..594ae9ac5 --- /dev/null +++ b/examples/Project.toml @@ -0,0 +1,260 @@ +[deps] +ATK_jll = "7b86fcea-f67b-53e1-809c-8f1719c154e8" +AbstractFFTs = "621f4979-c628-5d54-868e-fcf4e3e8185c" +Adapt = "79e6a3ab-5dfb-504d-930d-738a2a938a0e" +Arpack = "7d9fca2a-8960-54d3-9f78-7d1dccf2cb97" +Arpack_jll = "68821587-b530-5797-8361-c406ea357684" +ArrayInterface = "4fba245c-0d91-5ea0-9b3e-6abc04ee57a9" +Artifacts = "56f22d72-fd6d-98f1-02f0-08ddc0907c33" +AxisAlgorithms = "13072b0f-2c55-5437-9ae7-d433b7a33950" +AxisArrays = "39de3d68-74b9-583c-8d2d-e117c070f3a9" +Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" +Bzip2_jll = "6e34b625-4abd-537c-b88f-471c36dfa7a0" +CEnum = "fa961155-64e5-5f13-b03f-caf6b980ea82" +CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" +Cairo = "159f3aea-2a34-519c-b102-8c37f9878175" +Cairo_jll = "83423d85-b0ee-5818-9007-b63ccbeb887a" +Calculus = "49dc2e85-a5d0-5ad3-a950-438e2897f1b9" +CatIndices = "aafaddc9-749c-510e-ac4f-586e18779b91" +CategoricalArrays = "324d7699-5711-5eae-9e2f-1d82baa6b597" +Clustering = "aaaa29a8-35af-508c-8bc3-b662a17a0fe5" +ColorSchemes = "35d6a980-a343-548e-a6ea-1d62b119f2f4" +ColorTypes = "3da002f7-5984-5a60-b8a6-cbb66c0b333f" +ColorVectorSpace = "c3611d14-8923-5661-9e6a-0046d554d3a4" +Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" +Combinatorics = "861a8166-3701-5b0c-9a16-15d98fcdc6aa" +CommonSubexpressions = "bbf7d656-a473-5ed7-a52c-81e309532950" +Compat = "34da2185-b29b-5c13-b0c7-acf172513d20" +CompilerSupportLibraries_jll = "e66e0078-7015-5450-92f7-15fbd957f2ae" +Compose = "a81c6b42-2e10-5240-aca2-a61377ecd94b" +ComputationalResources = "ed09eef8-17a6-5b46-8889-db040fac31e3" +Contour = "d38c429a-6771-53c6-b99e-75d170b6e991" +CoordinateTransformations = "150eb455-5306-5404-9cee-2592286d6298" +CoupledFields = "7ad07ef1-bdf2-5661-9d2b-286fd4296dac" +CustomUnitRanges = "dc8bdbbb-1ca9-579f-8c36-e416f6a65cce" +DataAPI = "9a962f9c-6df0-11e9-0e5d-c546b8b5ee8a" +DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" +DataFramesMeta = "1313f7d8-7da2-5740-9ea0-a2ca25f37964" +DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" +DataValueInterfaces = "e2d170a0-9d28-54be-80f0-106bbe20a464" +DataValues = "e7dc6d0d-1eca-5fa6-8ad6-5aecde8b7ea5" +Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" +Dbus_jll = "ee1fde0b-3d02-5ea6-8484-8dfef6360eab" +DelimitedFiles = "8bb1440f-4735-579b-a4ab-409b98df4dab" +DiffEqDiffTools = "01453d9d-ee7c-5054-8395-0335cb756afa" +DiffResults = "163ba53b-c6d8-5494-b064-1a9d43ac40c5" +DiffRules = "b552c78f-8df3-52c6-915a-8e097449b14b" +Distances = "b4f34e82-e78d-54a5-968a-f98e89d6e8f7" +Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b" +Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" +DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" +EarCut_jll = "5ae413db-bbd1-5e63-b57d-d24a61df00f5" +EllipsisNotation = "da5c29d0-fa7d-589e-88eb-ea29b0a81949" +Expat_jll = "2e619515-83b5-522b-bb60-26c02a35a201" +FFMPEG = "c87230d0-a227-11e9-1b43-d7ebe4e7570a" +FFMPEG_jll = "b22a6f82-2f65-5046-a5b2-351ab43fb4e5" +FFTViews = "4f61f5a4-77b1-5117-aa51-3ab5ef4ef0cd" +FFTW = "7a1cc6ca-52ef-59f5-83cd-3a7055c09341" +FFTW_jll = "f5851436-0d7a-5f13-b9de-f02708fd171a" +FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" +FillArrays = "1a297f60-69ca-5386-bcde-b61e274b549b" +FixedPointNumbers = "53c48c17-4a7d-5ca2-90c5-79b7896eea93" +Fontconfig_jll = "a3f928ae-7b40-5064-980b-68af3947d34b" +ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" +FreeType2_jll = "d7e528f0-a631-5988-bf34-fe36492bcfd7" +FriBidi_jll = "559328eb-81f9-559d-9380-de523a88c83c" +Future = "9fa8497b-333b-5362-9e8d-4d0656e87820" +GR = "28b8d3ca-fb5f-59d9-8090-bfdbd6d07a71" +GTK3_jll = "77ec8976-b24b-556a-a1bf-49a033a670a6" +Gadfly = "c91e804a-d5a3-530f-b6f0-dfbca275c004" +GeometryBasics = "5c1252a2-5f33-56bf-86c9-59e7332b4326" +GeometryTypes = "4d00f742-c7ba-57c2-abde-4428a4b178cb" +Gettext_jll = "78b55507-aeef-58d4-861c-77aaff3498b1" +Glib_jll = "7746bdde-850d-59dc-9ae8-88ece973131d" +Graphene_jll = "75302f13-0b7e-5bab-a6d1-23fa92e4c2ea" +Graphics = "a2bd30eb-e257-5431-a919-1863eab51364" +Graphite2_jll = "3b182d85-2403-5c21-9c21-1e1f0cc25472" +Grisu = "42e2da0e-8278-4e71-bc24-59509adca0fe" +Gtk = "4c0ca9eb-093a-5379-98c5-f87ac0bbbf44" +GtkReactive = "27996c0f-39cd-5cc1-a27a-05f136f946b6" +HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3" +HarfBuzz_jll = "2e76f6c2-a576-52d4-95c1-20adfe4de566" +Hexagons = "a1b4810d-1bce-5fbd-ac56-80944d57a21f" +HypothesisTests = "09f84164-cd44-5f33-b23f-e6b0d136a0d5" +ICU_jll = "a51ab1cf-af8e-5615-a023-bc2c838bba6b" +IdentityRanges = "bbac6d45-d8f3-5730-bfe4-7a449cd117ca" +ImageAxes = "2803e5a7-5153-5ecf-9a86-9b4c37f5f5ac" +ImageContrastAdjustment = "f332f351-ec65-5f6a-b3d1-319c6670881a" +ImageCore = "a09fc81d-aa75-5fe9-8630-4744c3626534" +ImageDistances = "51556ac3-7006-55f5-8cb3-34580c88182d" +ImageDraw = "4381153b-2b60-58ae-a1ba-fd683676385f" +ImageFiltering = "6a3955dd-da59-5b1f-98d4-e7296123deb5" +ImageIO = "82e4d734-157c-48bb-816b-45c225c6df19" +ImageMagick = "6218d12a-5da1-5696-b52f-db25d2ecc6d1" +ImageMagick_jll = "c73af94c-d91f-53ed-93a7-00f77d67a9d7" +ImageMetadata = "bc367c6b-8a6b-528e-b4bd-a4b897500b49" +ImageMorphology = "787d08f9-d448-5407-9aad-5290dd7ab264" +ImageQualityIndexes = "2996bd0c-7a13-11e9-2da2-2f5ce47296a9" +ImageShow = "4e3cecfd-b093-5904-9786-8bbb286a6a31" +ImageTransformations = "02fcd773-0e25-5acc-982a-7f6622650795" +ImageView = "86fae568-95e7-573e-a6b2-d8a6b900c9ef" +Images = "916415d5-f1e6-5110-898d-aaa5f9f070e0" +IndirectArrays = "9b13fd28-a010-5f03-acff-a1bbcff69959" +IniFile = "83e8ac13-25f8-5344-8a64-a9f2b223428f" +IntegralArrays = "1d092043-8f09-5a30-832f-7509e371ab51" +IntelOpenMP_jll = "1d5cc7b8-4909-519e-a0f8-d0f5ad9712d0" +InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240" +Interpolations = "a98d9a8b-a2ab-59e6-89dd-64a1c18fca59" +IntervalSets = "8197267c-284f-5f27-9208-e0e47529a953" +InvertedIndices = "41ab1584-1d38-5bbf-9106-f11c6c58b48f" +IterTools = "c8e1da08-722c-5040-9ed9-7db0dc04731e" +IteratorInterfaceExtensions = "82899510-4779-5014-852e-03e436cf321d" +JLLWrappers = "692b3bcd-3c85-4b1f-b108-f13ce0eb3210" +JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" +JpegTurbo_jll = "aacddb02-875f-59d6-b918-886e6ef4fbf8" +Juno = "e5e0dc1b-0480-54bc-9374-aad01c23163d" +KernelDensity = "5ab0869b-81aa-558d-bb23-cbf5423bbe9b" +LAME_jll = "c1c5ebd0-6772-5130-a774-d5fcae4a789d" +LZO_jll = "dd4b983a-f0e5-5f8d-a1b7-129d4a5fb1ac" +LibGit2 = "76f85450-5226-5b5a-8eaa-529ad045b433" +LibVPX_jll = "dd192d2f-8180-539f-9fb4-cc70b1dcf69a" +Libdl = "8f399da3-3557-5675-b5ff-fb832c97cbdb" +Libepoxy_jll = "42c93a91-0102-5b3f-8f9d-e41de60ac950" +Libffi_jll = "e9f186c6-92d2-5b65-8a66-fee21dc1b490" +Libgcrypt_jll = "d4300ac3-e22c-5743-9152-c294e39db1e4" +Libglvnd_jll = "7e76a0d4-f3c7-5321-8279-8d96eeed0f29" +Libgpg_error_jll = "7add5ba3-2f88-524e-9cd5-f83b8a55f7b8" +Libiconv_jll = "94ce4f54-9a6c-5748-9c1c-f9c7231a4531" +Libmount_jll = "4b2f31a3-9ecc-558c-b454-b3730dcb73e9" +Libtiff_jll = "89763e89-9b03-5906-acba-b20f662cd828" +Libuuid_jll = "38a345b3-de98-5d2b-a5d3-14cd9215e700" +LightXML = "9c8b4983-aa76-5018-a973-4c85ecc9e179" +LineSearches = "d3d80556-e9d4-5f37-9878-2ab0fcc64255" +LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +Loess = "4345ca2d-374a-55d4-8d30-97f9976e7612" +Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" +MKL_jll = "856f044c-d86e-5d09-b602-aeab76dc8ba7" +MacroTools = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" +MappedArrays = "dbb5928d-eab1-5f90-85c2-b9b0edb7c900" +Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a" +MbedTLS = "739be429-bea8-5141-9913-cc70e7f3736d" +MbedTLS_jll = "c8ffd9c3-330d-5841-b78e-0817d7145fa1" +Measures = "442fdcdd-2543-5da2-b0f3-8c86c306513e" +Media = "e89f7d12-3494-54d1-8411-f7d8b9ae1f27" +Missings = "e1d29d7a-bbdc-5cf2-9ac0-f12de2c33e28" +Mmap = "a63ad114-7e13-5084-954f-fe012c677804" +MosaicViews = "e94cdb99-869f-56ef-bcf0-1ae2bcbe0389" +MultivariateStats = "6f286f6a-111f-5878-ab1e-185364afe411" +NLSolversBase = "d41bc354-129a-5804-8e4c-c37616107c6c" +NaNMath = "77ba4419-2d1f-58cd-9bb1-8ffee604a2e3" +NearestNeighbors = "b8a86587-4115-5ab1-83bc-aa920d37bbce" +Netpbm = "f09324ee-3d7c-5217-9330-fc30815ba969" +Observables = "510215fc-4207-5dde-b226-833fc4488ee2" +OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881" +Ogg_jll = "e7412a2a-1a6e-54c0-be00-318e2571c051" +OpenBLAS_jll = "4536629a-c528-5b80-bd46-f80d51c5b363" +OpenSSL_jll = "458c3c95-2e84-50aa-8efc-19380b2a3a95" +OpenSpecFun_jll = "efe28fd5-8261-553b-a9e1-b2916fc3738e" +Optim = "429524aa-4258-5aef-a3af-852621145aeb" +Opus_jll = "91d4177d-7536-5919-b921-800302f37372" +OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" +PCRE_jll = "2f80f16e-611a-54ab-bc61-aa92de5b98fc" +PDMats = "90014a1f-27ba-587c-ab20-58faa44d9150" +PNGFiles = "f57f5aa1-a3ce-4bc8-8ab9-96f992907883" +PaddedViews = "5432bcbf-9aad-5242-b902-cca2824c8663" +Pango_jll = "36c8627f-9965-5494-a995-c6b170f724f3" +Parameters = "d96e819e-fc66-5662-9728-84c9c7592b0a" +Parsers = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" +Pixman_jll = "30392449-352a-5448-841d-b1acce4e97dc" +Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" +PlotThemes = "ccf2f8ad-2431-5c83-bf29-c5338b663b6a" +PlotUtils = "995b91a9-d308-5afd-9ec6-746e21dbc043" +Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" +PooledArrays = "2dfb63ee-cc39-5dd5-95bd-886bf059d720" +PositiveFactorizations = "85a6dd25-e78a-55b7-8502-1745935b8125" +Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" +Profile = "9abbd945-dff8-562f-b5e8-e1ebf5ef1b79" +ProgressMeter = "92933f4c-e287-5a05-a399-4b506db050ca" +QuadGK = "1fd47b50-473d-5c70-9696-f719f8f3bcdc" +QuartzImageIO = "dca85d43-d64c-5e67-8c65-017450d5d020" +REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +RangeArrays = "b3c3ace0-ae52-54e7-9d0b-2c1406fd6b9d" +Ratios = "c84ed2f1-dad5-54f0-aa8e-dbefe2724439" +Reactive = "a223df75-4e93-5b7c-acf9-bdd599c0f4de" +RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01" +RecipesPipeline = "01d81517-befc-4cb6-b9ec-a95719d0359c" +Reexport = "189a3867-3050-52da-a836-e630ba90ab69" +Requires = "ae029012-a4dd-5104-9daa-d747884805df" +Rmath = "79098fc4-a85e-5d69-aa6a-4863f24498fa" +Rmath_jll = "f50d1b31-88e8-58de-be2c-1cc44531875f" +Roots = "f2b01f46-fcfa-551c-844a-d8ac1e96c665" +Rotations = "6038ab10-8711-5258-84ad-4b1120ba62dc" +RoundingIntegers = "d5f540fe-1c90-5db3-b776-2e2f362d9394" +SHA = "ea8e919c-243c-51af-8825-aaa63cd721ce" +SentinelArrays = "91c51154-3ec4-41a3-a24f-3f23e20d615c" +Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b" +SharedArrays = "1a1011a3-84de-559e-8e89-a11a2f7dc383" +Showoff = "992d4aef-0814-514b-bc4d-f2e9a6c4116f" +SimpleTraits = "699a6c99-e7fa-54fc-8d76-47d257e15c1d" +Sockets = "6462fe0b-24de-5631-8697-dd941f90decc" +SortingAlgorithms = "a2af1166-a08f-5f64-846c-94a0d3cef48c" +SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" +SpecialFunctions = "276daf66-3868-5448-9aa4-cd146d93841b" +StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" +Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" +StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91" +StatsFuns = "4c63d2b9-4356-54db-8cca-17b64c39e42c" +StatsPlots = "f3b207a7-027a-5e70-b257-86293d7955fd" +StructArrays = "09ab397b-f2b6-538f-b94a-2f83cf4a842a" +StructTypes = "856f2bd8-1eba-4b0a-8007-ebc267875bd4" +SuiteSparse = "4607b0f0-06f3-5cda-b6b1-a6196a1729e9" +Suppressor = "fd094767-a336-5f1f-9728-57cf17d0bbfb" +TableOperations = "ab02a1b2-a7df-11e8-156e-fb1833f50b87" +TableTraits = "3783bdb8-4a98-5b6b-af9a-565f29a5fe9c" +Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +TiledIteration = "06e1c1a7-607b-532d-9fad-de7d9aa2abac" +UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" +UnPack = "3a884ed6-31ef-47d7-9d2a-63182c4928ed" +Unicode = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" +Wayland_jll = "a2964d1f-97da-50d4-b82a-358c7fce9d89" +Wayland_protocols_jll = "2381bf8a-dfd0-557d-9999-79630e7b1b91" +Widgets = "cc8bc4a8-27d6-5769-a93b-9d913e69aa62" +WoodburyMatrices = "efce3f68-66dc-5838-9240-27a6d6f5f9b6" +XML2_jll = "02c8fc9c-b97f-50b9-bbe4-9be30ff0a78a" +XSLT_jll = "aed1982a-8fda-507f-9586-7b0439959a61" +Xorg_libX11_jll = "4f6342f7-b3d2-589e-9d20-edeb45f2b2bc" +Xorg_libXau_jll = "0c0b7dd1-d40b-584c-a123-a41640f87eec" +Xorg_libXcomposite_jll = "3c9796d7-64a0-5134-86ad-79f8eb684845" +Xorg_libXcursor_jll = "935fb764-8cf2-53bf-bb30-45bb1f8bf724" +Xorg_libXdamage_jll = "0aeada51-83db-5f97-b67e-184615cfc6f6" +Xorg_libXdmcp_jll = "a3789734-cfe1-5b06-b2d0-1dd0d9d62d05" +Xorg_libXext_jll = "1082639a-0dae-5f34-9b06-72781eeb8cb3" +Xorg_libXfixes_jll = "d091e8ba-531a-589c-9de9-94069b037ed8" +Xorg_libXi_jll = "a51aa0fd-4e3c-5386-b890-e753decda492" +Xorg_libXinerama_jll = "d1454406-59df-5ea1-beac-c340f2130bc3" +Xorg_libXrandr_jll = "ec84b674-ba8e-5d96-8ba1-2a689ba10484" +Xorg_libXrender_jll = "ea2f1a96-1ddc-540d-b46f-429655e07cfa" +Xorg_libXtst_jll = "b6f176f1-7aea-5357-ad67-1d3e565ea1c6" +Xorg_libpthread_stubs_jll = "14d82f49-176c-5ed1-bb49-ad3f5cbd8c74" +Xorg_libxcb_jll = "c7cfdc94-dc32-55de-ac96-5a1b8d977c5b" +Xorg_libxkbfile_jll = "cc61e674-0454-545c-8b26-ed2c68acab7a" +Xorg_xkbcomp_jll = "35661453-b289-5fab-8a00-3d9160c6a3a4" +Xorg_xkeyboard_config_jll = "33bec58e-1273-512f-9401-5d533626f822" +Xorg_xtrans_jll = "c5fb5394-a638-5e4d-96e5-b29de1b5cf10" +Zlib_jll = "83775a58-1f1d-513f-b197-d71354ab007a" +Zstd_jll = "3161d3a3-bdf6-5164-811a-617609db77b4" +adwaita_icon_theme_jll = "b437f822-2cd6-5e08-a15c-8bac984d38ee" +at_spi2_atk_jll = "de012916-1e3f-58c2-8f29-df3ef51d412d" +at_spi2_core_jll = "0fc3237b-ac94-5853-b45c-d43d59a06200" +gdk_pixbuf_jll = "da03df04-f53b-5353-a52f-6a8b0620ced0" +hicolor_icon_theme_jll = "059c91fe-1bad-52ad-bddd-f7b78713c282" +iso_codes_jll = "bf975903-5238-5d20-8243-bc370bc1e7e5" +libass_jll = "0ac62f75-1d6f-5e53-bd7c-93b484bb37c0" +libfdk_aac_jll = "f638f0a6-7fb0-5443-88ba-1cc74229b280" +libpng_jll = "b53b4c65-9356-5827-b1ea-8c7a1a84506f" +libvorbis_jll = "f27f6e37-5d2b-51aa-960f-b287f2bc3b7a" +x264_jll = "1270edf5-f2f9-52d2-97e9-ab00b5d0237a" +x265_jll = "dfaa095f-4041-5dcd-9319-2fabd8486b76" +xkbcommon_jll = "d8fb68d0-12a3-5cfd-a85a-d49703b185fd" diff --git a/examples/basic.jl b/examples/basic.jl index c52f73d9d..e903e4167 100755 --- a/examples/basic.jl +++ b/examples/basic.jl @@ -69,4 +69,4 @@ function main(; @printf("%10.9s %10.15s %15s\n\n", "Non-faces:", non_faces_frac, non_faces_percent) end -@time main(smart_choose_feats=true, scale=true, scale_to=(20, 20)) +@time main(smart_choose_feats=false, scale=true, scale_to=(20, 20)) diff --git a/examples/basic_ffhq_things.jl b/examples/ffhq_things/basic.jl similarity index 88% rename from examples/basic_ffhq_things.jl rename to examples/ffhq_things/basic.jl index 64c254a7f..20d41eba4 100755 --- a/examples/basic_ffhq_things.jl +++ b/examples/ffhq_things/basic.jl @@ -11,20 +11,23 @@ using Images: imresize @info("...done") +function takerand!(list::Vector{T}) where {T} + j = rand(1:length(list)) + rand_elem = list[j] + deleteat!(list, j) + return rand_elem +end + +rand_subset!(list::Vector{T}, n::Int) where {T} = + String[takerand!(list) for _ in 1:n] + "Return a random subset of the contents of directory `path` of size `n`." function rand_subset_ls(path::String, n::Int) dir_contents = readdir(path, join=true, sort=false) filter!(f -> !occursin(r".*\.DS_Store", f), dir_contents) @assert(length(dir_contents) >= n, "Not enough files in given directory to select `n` random.") - subset_ls = Vector{String}(undef, n) - for i in 1:n - j = rand(1:length(dir_contents)) - subset_ls[i] = dir_contents[j] - deleteat!(dir_contents, j) - end - - return subset_ls + return rand_subset!(dir_contents, n) end function main( @@ -34,7 +37,7 @@ function main( scale::Bool=true, scale_to::Tuple=(128, 128) ) - data_path = joinpath(dirname(@__DIR__), "data") + data_path = joinpath(dirname(dirname(@__DIR__)), "data") pos_training_path = joinpath(data_path, "ffhq", "thumbnails128x128") neg_training_path = joinpath(data_path, "things", "object_images") @@ -57,7 +60,9 @@ function main( @info("...done. Maximum feature width selected is $max_feature_width pixels; minimum feature width is $min_feature_width; maximum feature height is $max_feature_height pixels; minimum feature height is $min_feature_height.\n") else - max_feature_width, max_feature_height, min_feature_height, min_feature_width = (67, 67, 65, 65) + # max_feature_width, max_feature_height, min_feature_height, min_feature_width = (67, 67, 65, 65) + # max_feature_width, max_feature_height, min_feature_height, min_feature_width = (100, 100, 30, 30) + max_feature_width, max_feature_height, min_feature_height, min_feature_width = (70, 70, 50, 50) min_size_img = (128, 128) end @@ -70,7 +75,7 @@ function main( pos_testing_images = all_pos_images[(num_pos + 1):2num_pos] neg_testing_images = all_neg_images[(num_neg + 1):2num_neg] num_faces = length(pos_testing_images) - num_non_faces = length(neg_testing_images) + num_non_faces = length(neg_testing_images) correct_faces = sum(ensemble_vote_all(pos_testing_images, classifiers, scale=scale, scale_to=scale_to)) correct_non_faces = num_non_faces - sum(ensemble_vote_all(neg_testing_images, classifiers, scale=scale, scale_to=scale_to)) @@ -89,7 +94,7 @@ function main( @printf("%10.9s %10.15s %15s\n\n", "Non-faces:", non_faces_frac, non_faces_percent) end -@time main(2000, 2000; smart_choose_feats = false, scale = true, scale_to = (128, 128)) +@time main(200, 200; smart_choose_feats = false, scale = true, scale_to = (128, 128)) #= [ Info: Loading required libraries (it will take a moment to precompile if it is your first time doing this)... diff --git a/examples/ffhq_things/multiple_categories_read_and_score.jl b/examples/ffhq_things/multiple_categories_read_and_score.jl new file mode 100755 index 000000000..735403a35 --- /dev/null +++ b/examples/ffhq_things/multiple_categories_read_and_score.jl @@ -0,0 +1,203 @@ +Threads.nthreads() > 1 || @warn("You are currently only using one thread, when the programme supports multithreading") +@info "Loading required libraries (it will take a moment to precompile if it is your first time doing this)..." + +using Serialization: deserialize + +include(joinpath(dirname(dirname(@__DIR__)), "src", "FaceDetection.jl")) + +using .FaceDetection +using Images: imresize +using StatsPlots # StatsPlots required for box plots # plot boxplot @layout :origin savefig +using CSV: write +using DataFrames: DataFrame +using HypothesisTests: UnequalVarianceTTest +using Serialization: deserialize + +@enum ImageType begin + FACE = 1 + PAREIDOLIA = 2 + FLOWER = 3 + OBJECT = 4 +end + +@info("...done") +function takerand!(list::Vector{T}) where {T} + j = rand(1:length(list)) + rand_elem = list[j] + deleteat!(list, j) + return rand_elem +end + +rand_subset!(list::Vector{T}, n::Int) where {T} = + String[takerand!(list) for _ in 1:n] + +"Return a random subset of the contents of directory `path` of size `n`." +function rand_subset_ls(path::String, n::Int) + dir_contents = readdir(path, join=true, sort=false) + filter!(f -> !occursin(r".*\.DS_Store", f), dir_contents) + @assert(length(dir_contents) >= n, "Not enough files in given directory to select `n` random.") + + return rand_subset!(dir_contents, n) +end + +function main( + classifiers_file::String, + num_pos::Int, + num_neg::Int; + scale::Bool=true, + scale_to::Tuple=(128, 128) +) + classifiers = deserialize(classifiers_file) + sort!(classifiers, by = c -> c.weight, rev = true) + println(classifiers) + + @info("Calculating test face scores and constructing dataset...") + sleep(0.5) + + data_path = joinpath(dirname(dirname(@__DIR__)), "data") + + pos_training_path = joinpath(data_path, "ffhq", "thumbnails128x128") + neg_training_path = joinpath(data_path, "things", "object_images") + testing_path = joinpath(data_path, "lizzie-testset", "2021") + + #= + correct_faces = sum(ensemble_vote_all(pos_testing_images, classifiers, scale=scale, scale_to=scale_to)) + correct_non_faces = num_non_faces - sum(ensemble_vote_all(neg_testing_images, classifiers, scale=scale, scale_to=scale_to)) + correct_faces_percent = (correct_faces / num_faces) * 100 + correct_non_faces_percent = (correct_non_faces / num_non_faces) * 100 + + faces_frac = string(correct_faces, "/", num_faces) + faces_percent = string("(", correct_faces_percent, "% of faces were recognised as faces)") + non_faces_frac = string(correct_non_faces, "/", num_non_faces) + non_faces_percent = string("(", correct_non_faces_percent, "% of non-faces were identified as non-faces)") + + @info("...done.\n") + @info("Result:\n") + + @printf("%10.9s %10.15s %15s\n", "Faces:", faces_frac, faces_percent) + @printf("%10.9s %10.15s %15s\n\n", "Non-faces:", non_faces_frac, non_faces_percent) + =# + face_testing_image_paths = readdir(joinpath(testing_path, "Faces"), join=true, sort=false) + pareidolia_testing_image_paths = readdir(joinpath(testing_path, "Pareidolia"), join=true, sort=false) + flower_testing_image_paths = readdir(joinpath(testing_path, "Flowers"), join=true, sort=false) + object_testing_image_paths = readdir(joinpath(testing_path, "Objects"), join=true, sort=false) + + face_testing_images = load_image.(face_testing_image_paths, scale=scale, scale_to=scale_to) + pareidolia_testing_images = load_image.(pareidolia_testing_image_paths, scale=scale, scale_to=scale_to) + flower_testing_images = load_image.(flower_testing_image_paths, scale=scale, scale_to=scale_to) + object_testing_images = load_image.(object_testing_image_paths, scale=scale, scale_to=scale_to) + + num_faces = length(face_testing_images) + num_pareidolia = length(pareidolia_testing_images) + num_flowers = length(flower_testing_images) + num_objects = length(object_testing_images) + + face_scores = Float64[ensemble_vote(img, classifiers) for img in face_testing_images] + pareidolia_scores = Float64[ensemble_vote(img, classifiers) for img in pareidolia_testing_images] + flower_scores = Float64[ensemble_vote(img, classifiers) for img in flower_testing_images] + object_scores = Float64[ensemble_vote(img, classifiers) for img in object_testing_images] + + face_faceness_scores = Float64[get_faceness(classifiers, img) for img in face_testing_images] + pareidolia_faceness_scores = Float64[get_faceness(classifiers, img) for img in pareidolia_testing_images] + flower_faceness_scores = Float64[get_faceness(classifiers, img) for img in flower_testing_images] + object_faceness_scores = Float64[get_faceness(classifiers, img) for img in object_testing_images] + + face_names = String[basename(img) for img in face_testing_image_paths] + pareidolia_names = String[basename(img) for img in pareidolia_testing_image_paths] + flower_names = String[basename(img) for img in flower_testing_image_paths] + object_names = String[basename(img) for img in object_testing_image_paths] + + scores_df = DataFrame(image_name = String[], image_type = Int[], image_score_binary = Int8[], faceness_score = Float64[]) + anova_df = DataFrame(image_type = Int[], faceness_score = Float64[]) + for (image_type, image_names, image_scores, faceness_scores) in ((FACE, face_names, face_scores, face_faceness_scores), (PAREIDOLIA, pareidolia_names, pareidolia_scores, pareidolia_faceness_scores), (FLOWER, flower_names, flower_scores, flower_faceness_scores), (OBJECT, object_names, object_scores, object_faceness_scores)) + for (n, s, f) in zip(image_names, image_scores, faceness_scores) + push!(scores_df, (n, Int(image_type), s, f)) + push!(anova_df, (Int(image_type), f)) + end + end + + # write score data + data_file = joinpath(dirname(dirname(@__DIR__)), "data", "faceness-scores.csv") + write(data_file, scores_df, writeheader=true) + @info("...done. Dataset written to $(data_file).\n") + + ### FACES VS OBJECTS + + @info("Computing differences in scores between faces and objects...") + welch_t = UnequalVarianceTTest(face_faceness_scores, object_faceness_scores) + @info("...done. $welch_t\n") + @info("Constructing box plot with said dataset...") + + gr() # set plot backend + theme(:solarized) + plot = StatsPlots.plot( + StatsPlots.boxplot(face_faceness_scores, xaxis=false, label = false), + StatsPlots.boxplot(object_faceness_scores, xaxis=false, label = false), + title = ["Facenesses of Faces" "Facenesses of Objects"], + fontfamily = font("Times"), + layout = @layout([a b]), + link = :y, + ) + StatsPlots.savefig(plot, joinpath(dirname(dirname(@__DIR__)), "figs", "faceness_of_faces_versus_objects.pdf")) + @info("...done. Plot created at $(joinpath(dirname(dirname(@__DIR__)), "figs", "faceness_of_faces_versus_objects.pdf"))") + + ### PAREIDOLIA VS OBJECTS + + @info("Computing differences in scores between pareidolia and objects...") + welch_t = UnequalVarianceTTest(pareidolia_faceness_scores, object_faceness_scores) + @info("...done. $welch_t\n") + @info("Constructing box plot with said dataset...") + + plot = StatsPlots.plot( + StatsPlots.boxplot(pareidolia_faceness_scores, xaxis=false, label = false), + StatsPlots.boxplot(object_faceness_scores, xaxis=false, label = false), + title = ["Facenesses of Pareidolia" "Facenesses of Objects"], + fontfamily = font("Times"), + layout = @layout([a b]), + link = :y, + ) + StatsPlots.savefig(plot, joinpath(dirname(dirname(@__DIR__)), "figs", "faceness_of_pareidolia_versus_objects.pdf")) + @info("...done. Plot created at $(joinpath(dirname(dirname(@__DIR__)), "figs", "faceness_of_pareidolia_versus_objects.pdf"))") + + ### FACES VS FLOWERS + + @info("Computing differences in scores between faces and flowers...") + welch_t = UnequalVarianceTTest(face_faceness_scores, flower_faceness_scores) + @info("...done. $welch_t\n") + @info("Constructing box plot with said dataset...") + + plot = StatsPlots.plot( + StatsPlots.boxplot(face_faceness_scores, xaxis=false, label = false), + StatsPlots.boxplot(flower_faceness_scores, xaxis=false, label = false), + title = ["Facenesses of Faces" "Facenesses of Flowers"], + fontfamily = font("Times"), + layout = @layout([a b]), + link = :y, + ) + StatsPlots.savefig(plot, joinpath(dirname(dirname(@__DIR__)), "figs", "faceness_of_faces_versus_flowers.pdf")) + @info("...done. Plot created at $(joinpath(dirname(dirname(@__DIR__)), "figs", "faceness_of_faces_versus_flowers.pdf"))") + + ### PAREIDOLIA VS FLOWERS + + @info("Computing differences in scores between pareidolia and flowers...") + welch_t = UnequalVarianceTTest(pareidolia_faceness_scores, flower_faceness_scores) + @info("...done. $welch_t\n") + @info("Constructing box plot with said dataset...") + + plot = StatsPlots.plot( + StatsPlots.boxplot(pareidolia_faceness_scores, xaxis=false, label = false), + StatsPlots.boxplot(flower_faceness_scores, xaxis=false, label = false), + title = ["Facenesses of Pareidolia" "Facenesses of Flowers"], + fontfamily = font("Times"), + layout = @layout([a b]), + link = :y, + ) + StatsPlots.savefig(plot, joinpath(dirname(dirname(@__DIR__)), "figs", "faceness_of_pareidolia_versus_flowers.pdf")) + @info("...done. Plot created at $(joinpath(dirname(dirname(@__DIR__)), "figs", "faceness_of_pareidolia_versus_flowers.pdf"))") +end + +# data_file = joinpath(dirname(@__DIR__), "data", "classifiers_10_from_2000_pos_2000_neg_(128,128)_(100,100,30,30)") +# data_file = joinpath(dirname(@__DIR__), "data", "classifiers_10_from_5000_pos_5000_neg_(128,128)_(100,100,30,30)") +data_file = joinpath(dirname(@__DIR__), "data", "classifiers_10_from_1500_pos_1500_neg_(128,128)_(128,128,1,1)") +# data_file = joinpath(dirname(@__DIR__), "data", "classifiers_10_from_4000_pos_4000_neg_(24,24)_(-1,-1,1,1)") +@time main(data_file, 500, 500, scale=true, scale_to=(128, 128)) diff --git a/examples/ffhq_things/multiple_categories_train_and_score.jl b/examples/ffhq_things/multiple_categories_train_and_score.jl new file mode 100755 index 000000000..b6f2b1fc5 --- /dev/null +++ b/examples/ffhq_things/multiple_categories_train_and_score.jl @@ -0,0 +1,191 @@ +Threads.nthreads() > 1 || @warn("You are currently only using one thread, when the programme supports multithreading") +@info "Loading required libraries (it will take a moment to precompile if it is your first time doing this)..." + +include(joinpath(dirname(@__DIR__), "src", "FaceDetection.jl")) + +using .FaceDetection +using Images: imresize +using StatsPlots # StatsPlots required for box plots # plot boxplot @layout :origin savefig +using CSV: write +using DataFrames: DataFrame +using HypothesisTests: UnequalVarianceTTest +using Serialization: deserialize + +@enum ImageType begin + FACE = 1 + PAREIDOLIA = 2 + FLOWER = 3 + OBJECT = 4 +end + +@info("...done") +function takerand!(list::Vector{T}) where {T} + j = rand(1:length(list)) + rand_elem = list[j] + deleteat!(list, j) + return rand_elem +end + +rand_subset!(list::Vector{T}, n::Int) where {T} = + String[takerand!(list) for _ in 1:n] + +"Return a random subset of the contents of directory `path` of size `n`." +function rand_subset_ls(path::String, n::Int) + dir_contents = readdir(path, join=true, sort=false) + filter!(f -> !occursin(r".*\.DS_Store", f), dir_contents) + @assert(length(dir_contents) >= n, "Not enough files in given directory to select `n` random.") + + return rand_subset!(dir_contents, n) +end + +function main( + num_pos::Int, + num_neg::Int; + smart_choose_feats::Bool=false, + scale::Bool=true, + scale_to::Tuple=(128, 128) +) + data_path = joinpath(dirname(dirname(@__DIR__)), "data") + + pos_training_path = joinpath(data_path, "ffhq", "thumbnails128x128") + neg_training_path = joinpath(data_path, "things", "object_images") + testing_path = joinpath(data_path, "lizzie-testset", "2021") + + pos_training_images = rand_subset_ls(pos_training_path, num_pos) + neg_training_images = rand_subset_ls(neg_training_path, num_neg) + + num_classifiers = 10 + local min_size_img::Tuple{Int, Int} + + if smart_choose_feats + # For performance reasons restricting feature size + @info("Selecting best feature width and height...") + + max_feature_width, max_feature_height, min_feature_height, min_feature_width, min_size_img = + determine_feature_size(vcat(pos_training_images, neg_training_images); scale = scale, scale_to = scale_to, show_progress = true) + + @info("...done. Maximum feature width selected is $max_feature_width pixels; minimum feature width is $min_feature_width; maximum feature height is $max_feature_height pixels; minimum feature height is $min_feature_height.\n") + else + # max_feature_width, max_feature_height, min_feature_height, min_feature_width = (67, 67, 65, 65) + max_feature_width, max_feature_height, min_feature_height, min_feature_width = (100, 100, 30, 30) + # max_feature_width, max_feature_height, min_feature_height, min_feature_width = (70, 70, 50, 50) + min_size_img = (128, 128) + end + + # classifiers are haar like features + classifiers = learn(pos_training_images, neg_training_images, num_classifiers, min_feature_height, max_feature_height, min_feature_width, max_feature_width; scale = scale, scale_to = scale_to) + println(classifiers) + @info("Calculating test face scores and constructing dataset...") + sleep(0.5) + + + face_testing_images = readdir(joinpath(testing_path, "Faces"), join=true, sort=false) + pareidolia_testing_images = readdir(joinpath(testing_path, "Pareidolia"), join=true, sort=false) + flower_testing_images = readdir(joinpath(testing_path, "Flowers"), join=true, sort=false) + object_testing_images = readdir(joinpath(testing_path, "Objects"), join=true, sort=false) + + num_faces = length(face_testing_images) + num_pareidolia = length(pareidolia_testing_images) + num_flowers = length(flower_testing_images) + num_objects = length(object_testing_images) + + face_scores = Float64[get_faceness(classifiers, load_image(img, scale=scale, scale_to=scale_to)) for img in face_testing_images] + pareidolia_scores = Float64[get_faceness(classifiers, load_image(img, scale=scale, scale_to=scale_to)) for img in pareidolia_testing_images] + flower_scores = Float64[get_faceness(classifiers, load_image(img, scale=scale, scale_to=scale_to)) for img in flower_testing_images] + object_scores = Float64[get_faceness(classifiers, load_image(img, scale=scale, scale_to=scale_to)) for img in object_testing_images] + + face_names = String[basename(img) for img in face_testing_images] + pareidolia_names = String[basename(img) for img in pareidolia_testing_images] + flower_names = String[basename(img) for img in flower_testing_images] + object_names = String[basename(img) for img in object_testing_images] + + scores_df = DataFrame(image_name = String[], image_score = Float64[]) + anova_df = DataFrame(image_type = Int[], image_score = Float64[]) + for (image_type, image_names, image_scores) in ((FACE, face_names, face_scores), (PAREIDOLIA, pareidolia_names, pareidolia_scores), (FLOWER, flower_names, flower_scores), (OBJECT, object_names, object_scores)) + for (n, s) in zip(image_names, image_scores) + push!(scores_df, (n, s)) + push!(anova_df, (Int(image_type), s)) + end + end + + # write score data + data_file = joinpath(dirname(dirname(@__DIR__)), "data", "faceness-scores.csv") + write(data_file, scores_df, writeheader=false) + @info("...done. Dataset written to $(data_file).\n") + + ### FACES VS OBJECTS + + @info("Computing differences in scores between faces and objects...") + welch_t = UnequalVarianceTTest(face_scores, object_scores) + @info("...done. $welch_t\n") + @info("Constructing box plot with said dataset...") + + gr() # set plot backend + theme(:solarized) + plot = StatsPlots.plot( + StatsPlots.boxplot(face_scores, xaxis=false, label = false), + StatsPlots.boxplot(object_scores, xaxis=false, label = false), + title = ["Facenesses of Faces" "Facenesses of Objects"], + fontfamily = font("Times"), + layout = @layout([a b]), + link = :y, + ) + StatsPlots.savefig(plot, joinpath(dirname(dirname(@__DIR__)), "figs", "faceness_of_faces_versus_objects.pdf")) + @info("...done. Plot created at $(joinpath(dirname(dirname(@__DIR__)), "figs", "faceness_of_faces_versus_objects.pdf"))") + + ### PAREIDOLIA VS OBJECTS + + @info("Computing differences in scores between pareidolia and objects...") + welch_t = UnequalVarianceTTest(pareidolia_scores, object_scores) + @info("...done. $welch_t\n") + @info("Constructing box plot with said dataset...") + + plot = StatsPlots.plot( + StatsPlots.boxplot(pareidolia_scores, xaxis=false, label = false), + StatsPlots.boxplot(object_scores, xaxis=false, label = false), + title = ["Facenesses of Pareidolia" "Facenesses of Objects"], + fontfamily = font("Times"), + layout = @layout([a b]), + link = :y, + ) + StatsPlots.savefig(plot, joinpath(dirname(dirname(@__DIR__)), "figs", "faceness_of_pareidolia_versus_objects.pdf")) + @info("...done. Plot created at $(joinpath(dirname(dirname(@__DIR__)), "figs", "faceness_of_pareidolia_versus_objects.pdf"))") + + ### FACES VS FLOWERS + + @info("Computing differences in scores between faces and flowers...") + welch_t = UnequalVarianceTTest(face_scores, flower_scores) + @info("...done. $welch_t\n") + @info("Constructing box plot with said dataset...") + + plot = StatsPlots.plot( + StatsPlots.boxplot(face_scores, xaxis=false, label = false), + StatsPlots.boxplot(flower_scores, xaxis=false, label = false), + title = ["Facenesses of Faces" "Facenesses of Flowers"], + fontfamily = font("Times"), + layout = @layout([a b]), + link = :y, + ) + StatsPlots.savefig(plot, joinpath(dirname(dirname(@__DIR__)), "figs", "faceness_of_faces_versus_flowers.pdf")) + @info("...done. Plot created at $(joinpath(dirname(dirname(@__DIR__)), "figs", "faceness_of_faces_versus_flowers.pdf"))") + + ### PAREIDOLIA VS FLOWERS + + @info("Computing differences in scores between pareidolia and flowers...") + welch_t = UnequalVarianceTTest(pareidolia_scores, flower_scores) + @info("...done. $welch_t\n") + @info("Constructing box plot with said dataset...") + + plot = StatsPlots.plot( + StatsPlots.boxplot(pareidolia_scores, xaxis=false, label = false), + StatsPlots.boxplot(flower_scores, xaxis=false, label = false), + title = ["Facenesses of Pareidolia" "Facenesses of Flowers"], + fontfamily = font("Times"), + layout = @layout([a b]), + link = :y, + ) + StatsPlots.savefig(plot, joinpath(dirname(dirname(@__DIR__)), "figs", "faceness_of_pareidolia_versus_flowers.pdf")) + @info("...done. Plot created at $(joinpath(dirname(dirname(@__DIR__)), "figs", "faceness_of_pareidolia_versus_flowers.pdf"))") +end + +@time main(500, 500, smart_choose_feats=false, scale=true, scale_to=(128, 128)) diff --git a/examples/ffhq_things/read.jl b/examples/ffhq_things/read.jl new file mode 100755 index 000000000..c0b81e6a6 --- /dev/null +++ b/examples/ffhq_things/read.jl @@ -0,0 +1,83 @@ +# Adapted from https://github.com/Simon-Hohberg/Viola-Jones/ + +# Faces dataset: [FFHQ](https://github.com/NVlabs/ffhq-dataset/) 70_001 images of faces +# Non-faces dataset: [THINGS](https://osf.io/3fu6z/); 26_107 object images + +@info "Loading required libraries (it will take a moment to precompile if it is your first time doing this)..." + +using FaceDetection +using Printf: @printf +using Images: imresize +using Serialization: deserialize + +@info("...done") + +function takerand!(list::Vector{T}) where {T} + j = rand(1:length(list)) + rand_elem = list[j] + deleteat!(list, j) + return rand_elem +end + +rand_subset!(list::Vector{T}, n::Int) where {T} = + String[takerand!(list) for _ in 1:n] + +"Return a random subset of the contents of directory `path` of size `n`." +function rand_subset_ls(path::String, n::Int) + dir_contents = readdir(path, join=true, sort=false) + filter!(f -> !occursin(r".*\.DS_Store", f), dir_contents) + @assert(length(dir_contents) >= n, "Not enough files in given directory to select `n` random.") + + return rand_subset!(dir_contents, n) +end + +function main( + classifiers_file::String, + num_pos::Int, + num_neg::Int; + smart_choose_feats::Bool=false, + scale::Bool=true, + scale_to::Tuple=(128, 128) +) + data_path = joinpath(dirname(dirname(@__DIR__)), "data") + + pos_training_path = joinpath(data_path, "ffhq", "thumbnails128x128") + neg_training_path = joinpath(data_path, "things", "object_images") + + all_pos_images = rand_subset_ls(pos_training_path, 2num_pos) + all_neg_images = rand_subset_ls(neg_training_path, 2num_neg) + + pos_training_images = all_pos_images[1:num_pos] + neg_training_images = all_neg_images[1:num_neg] + + classifiers = deserialize(classifiers_file) + + @info("Testing selected classifiers...") + sleep(3) # sleep here because sometimes the threads from `learn` are still catching up and then `ensemble_vote_all` errors + + pos_testing_images = all_pos_images[(num_pos + 1):2num_pos] + neg_testing_images = all_neg_images[(num_neg + 1):2num_neg] + num_faces = length(pos_testing_images) + num_non_faces = length(neg_testing_images) + + correct_faces = sum(ensemble_vote_all(pos_testing_images, classifiers, scale=scale, scale_to=scale_to)) + correct_non_faces = num_non_faces - sum(ensemble_vote_all(neg_testing_images, classifiers, scale=scale, scale_to=scale_to)) + correct_faces_percent = (correct_faces / num_faces) * 100 + correct_non_faces_percent = (correct_non_faces / num_non_faces) * 100 + + faces_frac = string(correct_faces, "/", num_faces) + faces_percent = string("(", correct_faces_percent, "% of faces were recognised as faces)") + non_faces_frac = string(correct_non_faces, "/", num_non_faces) + non_faces_percent = string("(", correct_non_faces_percent, "% of non-faces were identified as non-faces)") + + @info("...done.\n") + @info("Result:\n") + + @printf("%10.9s %10.15s %15s\n", "Faces:", faces_frac, faces_percent) + @printf("%10.9s %10.15s %15s\n\n", "Non-faces:", non_faces_frac, non_faces_percent) +end + +data_file = joinpath(dirname(@__DIR__), "data", "classifiers_10_from_2000_pos_2000_neg_(128,128)_(100,100,30,30)") +# data_file = joinpath(dirname(@__DIR__), "data", "classifiers_10_from_5000_pos_5000_neg_(128,128)_(100,100,30,30)") +# data_file = joinpath(dirname(@__DIR__), "data", "classifiers_10_from_1500_pos_1500_neg_(128,128)_(128,128,1,1)") +@time main(data_file, 2000, 2000; smart_choose_feats = false, scale = true, scale_to = (128, 128)) diff --git a/examples/ffhq_things/read_determine_experimental.jl b/examples/ffhq_things/read_determine_experimental.jl new file mode 100755 index 000000000..44607b7f1 --- /dev/null +++ b/examples/ffhq_things/read_determine_experimental.jl @@ -0,0 +1,82 @@ +# Adapted from https://github.com/Simon-Hohberg/Viola-Jones/ + +# Faces dataset: [FFHQ](https://github.com/NVlabs/ffhq-dataset/) 70_001 images of faces +# Non-faces dataset: [THINGS](https://osf.io/3fu6z/); 26_107 object images + +@info "Loading required libraries (it will take a moment to precompile if it is your first time doing this)..." + +using FaceDetection +using Printf: @printf +using Images: imresize +using Serialization: deserialize + +@info("...done") + +function main( + classifiers_file::String; + smart_choose_feats::Bool=false, + scale::Bool=true, + scale_to::Tuple=(128, 128) +) + + @info("Calculating test face scores and constructing dataset...") + sleep(0.5) + + data_path = joinpath(dirname(dirname(@__DIR__)), "data") + + pos_training_path = joinpath(data_path, "ffhq", "thumbnails128x128") + neg_training_path = joinpath(data_path, "things", "object_images") + testing_path = joinpath(data_path, "lizzie-testset", "2021") + data_path = joinpath(dirname(dirname(@__DIR__)), "data") + + classifiers = deserialize(classifiers_file) + sort!(classifiers, by = c -> c.weight, rev = true) + + @info("Testing selected classifiers...") + sleep(3) # sleep here because sometimes the threads from `learn` are still catching up and then `ensemble_vote_all` errors + + face_testing_images = readdir(joinpath(testing_path, "Faces"), join=true, sort=false) + # face_testing_images = readdir(joinpath(data_path, "lizzie-testset", "2022-unshined", "Selected faces"), join=true, sort=false) + # face_testing_images = readdir(joinpath(data_path, "lizzie-testset", "2022-unshined", "All "), join=true, sort=false) + pareidolia_testing_images = readdir(joinpath(testing_path, "Pareidolia"), join=true, sort=false) + flower_testing_images = readdir(joinpath(testing_path, "Flowers"), join=true, sort=false) + object_testing_images = readdir(joinpath(testing_path, "Objects"), join=true, sort=false) + + num_faces = length(face_testing_images) + num_pareidolia = length(pareidolia_testing_images) + num_flowers = length(flower_testing_images) + num_objects = length(object_testing_images) + + # aliases + pos_testing_images = face_testing_images + neg_testing_images = object_testing_images + num_faces = num_faces + num_non_faces = num_objects + + # determining how many were correctly classified + correct_faces = sum(ensemble_vote_all(pos_testing_images, classifiers, scale=scale, scale_to=scale_to)) + correct_non_faces = num_non_faces - sum(ensemble_vote_all(neg_testing_images, classifiers, scale=scale, scale_to=scale_to)) + correct_faces_percent = (correct_faces / num_faces) * 100 + correct_non_faces_percent = (correct_non_faces / num_non_faces) * 100 + + faces_frac = string(correct_faces, "/", num_faces) + faces_percent = string("(", correct_faces_percent, "% of faces were recognised as faces)") + non_faces_frac = string(correct_non_faces, "/", num_non_faces) + non_faces_percent = string("(", correct_non_faces_percent, "% of non-faces were identified as non-faces)") + + @info("...done.\n") + @info("Result:\n") + + @printf("%10.9s %10.15s %15s\n", "Faces:", faces_frac, faces_percent) + @printf("%10.9s %10.15s %15s\n\n", "Non-faces:", non_faces_frac, non_faces_percent) +end + +data_file = joinpath(dirname(@__DIR__), "data", "classifiers_10_from_2000_pos_2000_neg_(128,128)_(100,100,30,30)") +# data_file = joinpath(dirname(@__DIR__), "data", "classifiers_10_from_5000_pos_5000_neg_(128,128)_(100,100,30,30)") +# data_file = joinpath(dirname(@__DIR__), "data", "classifiers_10_from_1500_pos_1500_neg_(128,128)_(128,128,1,1)") +# data_file = joinpath(dirname(@__DIR__), "data", "classifiers_10_from_200_pos_200_neg_(128,128)_(70,70,50,50)") +# data_file = joinpath(dirname(@__DIR__), "data", "classifiers_10_from_200_pos_200_neg_(24,24)_(-1,-1,1,1)") +# data_file = joinpath(dirname(@__DIR__), "data", "classifiers_10_from_1000_pos_1000_neg_(24,24)_(-1,-1,1,1)") +# data_file = joinpath(dirname(@__DIR__), "data", "classifiers_10_from_2000_pos_2000_neg_(24,24)_(-1,-1,1,1)") +# data_file = joinpath(dirname(@__DIR__), "data", "classifiers_10_from_4000_pos_4000_neg_(24,24)_(-1,-1,1,1)") +@time main(data_file; smart_choose_feats = false, scale = true, scale_to = (128, 128)) diff --git a/examples/ffhq_things/write.jl b/examples/ffhq_things/write.jl new file mode 100755 index 000000000..b08440596 --- /dev/null +++ b/examples/ffhq_things/write.jl @@ -0,0 +1,77 @@ +# Adapted from https://github.com/Simon-Hohberg/Viola-Jones/ + +# Faces dataset: [FFHQ](https://github.com/NVlabs/ffhq-dataset/) 70_001 images of faces +# Non-faces dataset: [THINGS](https://osf.io/3fu6z/); 26_107 object images + +@info "Loading required libraries (it will take a moment to precompile if it is your first time doing this)..." + +include(joinpath(dirname(dirname(@__DIR__)), "src", "FaceDetection.jl")) + +using .FaceDetection +using Serialization: serialize +using Random: shuffle + +@info("...done") + +function takerand!(list::Vector{T}) where {T} + j = rand(1:length(list)) + rand_elem = list[j] + deleteat!(list, j) + return rand_elem +end + +rand_subset!(list::Vector{T}, n::Int) where {T} = + String[takerand!(list) for _ in 1:n] + +"Return a random subset of the contents of directory `path` of size `n`." +function rand_subset_ls(path::String, n::Int) + dir_contents = readdir(path, join=true, sort=false) + filter!(f -> !occursin(r".*\.DS_Store", f), dir_contents) + + if n == -1 + return shuffle(dir_contents) + end + + @assert(length(dir_contents) >= n, "Not enough files in given directory to select `n` random.") + + return rand_subset!(dir_contents, n) +end + +function main( + num_pos::Int, + num_neg::Int; + scale::Bool=true, + scale_to::Tuple=(128, 128) +) + data_path = joinpath(dirname(dirname(@__DIR__)), "data") + + pos_training_path = joinpath(data_path, "ffhq", "thumbnails128x128") + # pos_training_path = joinpath(data_path, "main", "trainset", "faces") + # pos_training_path = joinpath(data_path, "alt", "pos") + + neg_training_path = joinpath(data_path, "things", "object_images") + # neg_training_path = joinpath(data_path, "all-non-faces") + # neg_training_path = joinpath(data_path, "main", "trainset", "non-faces") + # neg_training_path = joinpath(data_path, "alt", "neg") + + pos_training_images = rand_subset_ls(pos_training_path, num_pos) + neg_training_images = rand_subset_ls(neg_training_path, num_neg) + println([basename(p) for p in pos_training_images]) + + num_classifiers = 10 + + # max_feature_width, max_feature_height, min_feature_height, min_feature_width = (67, 67, 65, 65) + # max_feature_width, max_feature_height, min_feature_height, min_feature_width = (70, 70, 50, 50) + # max_feature_width, max_feature_height, min_feature_height, min_feature_width = (100, 100, 30, 30) + max_feature_width, max_feature_height, min_feature_height, min_feature_width = (-1, -1, 1, 1) + + sleep(3) + + # classifiers are haar like features + classifiers = learn(pos_training_images, neg_training_images, num_classifiers, min_feature_height, max_feature_height, min_feature_width, max_feature_width; scale = scale, scale_to = scale_to) + + data_file = joinpath(dirname(@__DIR__), "data", "classifiers_$(num_classifiers)_from_$(num_pos)_pos_$(num_neg)_neg_($(join(scale_to, ',')))_($max_feature_width,$max_feature_height,$min_feature_height,$min_feature_width)") + serialize(data_file, classifiers) +end + +@time main(8000, 8000; scale = true, scale_to = (24, 24)) diff --git a/examples/scores.jl b/examples/scores.jl index 09215f179..c8173da77 100755 --- a/examples/scores.jl +++ b/examples/scores.jl @@ -10,7 +10,6 @@ include(joinpath(dirname(dirname(@__FILE__)), "src", "FaceDetection.jl")) using .FaceDetection -const FD = FaceDetection using Images: imresize using StatsPlots # StatsPlots required for box plots # plot boxplot @layout :origin savefig using CSV: write @@ -32,6 +31,7 @@ function main(; # read classifiers from file classifiers = deserialize("/Users/jakeireland/Desktop/classifiers_100_577_577_fixed_1idx") + num_classifiers = length(classifiers) @info("Calculating test face scores and constructing dataset...") @@ -39,8 +39,8 @@ function main(; faces_scores = Vector{Real}(undef, length(filtered_ls(pos_testing_path))) non_faces_scores = Vector{Real}(undef, length(filtered_ls(neg_testing_path))) - faces_scores[:] .= [sum([FD.get_faceness(c, load_image(face, scale=scale, scale_to=scale_to)) for c in classifiers]) / num_classifiers for face in filtered_ls(pos_testing_path)] - non_faces_scores[:] .= [sum([FD.get_faceness(c, load_image(non_face, scale=scale, scale_to=scale_to)) for c in classifiers]) / num_classifiers for non_face in filtered_ls(neg_testing_path)] + faces_scores[:] .= [sum([get_faceness(c, load_image(face, scale=scale, scale_to=scale_to)) for c in classifiers]) / num_classifiers for face in filtered_ls(pos_testing_path)] + non_faces_scores[:] .= [sum([get_faceness(c, load_image(non_face, scale=scale, scale_to=scale_to)) for c in classifiers]) / num_classifiers for non_face in filtered_ls(neg_testing_path)] face_names = basename.(filtered_ls(pos_testing_path)) non_face_names = basename.(filtered_ls(neg_testing_path)) @@ -86,7 +86,7 @@ function main(; # save plot StatsPlots.savefig(plot, joinpath(dirname(dirname(@__FILE__)), "figs", "scores.pdf")) - @ingo("...done. Plot created at ", joinpath(dirname(dirname(@__FILE__)), "figs", "scores.pdf"), "\n") + @info("...done. Plot created at $(joinpath(dirname(dirname(@__FILE__)), "figs", "scores.pdf"))") end @time main(smart_choose_feats=true, scale=true, scale_to=(577, 577)) diff --git a/examples/train_and_scores.jl b/examples/train_and_scores.jl new file mode 100755 index 000000000..5e979aa1e --- /dev/null +++ b/examples/train_and_scores.jl @@ -0,0 +1,138 @@ +Threads.nthreads() > 1 || @warn("You are currently only using one thread, when the programme supports multithreading") +@info "Loading required libraries (it will take a moment to precompile if it is your first time doing this)..." + +include(joinpath(dirname(dirname(@__FILE__)), "src", "FaceDetection.jl")) + +using .FaceDetection +using Images: imresize +using StatsPlots # StatsPlots required for box plots # plot boxplot @layout :origin savefig +using CSV: write +using DataFrames: DataFrame +using HypothesisTests: UnequalVarianceTTest +using Serialization: deserialize + +@info("...done") +function takerand!(list::Vector{T}) where {T} + j = rand(1:length(list)) + rand_elem = list[j] + deleteat!(list, j) + return rand_elem +end + +rand_subset!(list::Vector{T}, n::Int) where {T} = + String[takerand!(list) for _ in 1:n] + +"Return a random subset of the contents of directory `path` of size `n`." +function rand_subset_ls(path::String, n::Int) + dir_contents = readdir(path, join=true, sort=false) + filter!(f -> !occursin(r".*\.DS_Store", f), dir_contents) + @assert(length(dir_contents) >= n, "Not enough files in given directory to select `n` random.") + + return rand_subset!(dir_contents, n) +end + +function main( + num_pos::Int, + num_neg::Int; + smart_choose_feats::Bool=false, + scale::Bool=true, + scale_to::Tuple=(128, 128) +) + data_path = joinpath(dirname(@__DIR__), "data") + + pos_training_path = joinpath(data_path, "ffhq", "thumbnails128x128") + neg_training_path = joinpath(data_path, "things", "object_images") + + all_pos_images = rand_subset_ls(pos_training_path, 2num_pos) + all_neg_images = rand_subset_ls(neg_training_path, 2num_neg) + + pos_training_images = all_pos_images[1:num_pos] + neg_training_images = all_neg_images[1:num_neg] + + num_classifiers = 10 + local min_size_img::Tuple{Int, Int} + + if smart_choose_feats + # For performance reasons restricting feature size + @info("Selecting best feature width and height...") + + max_feature_width, max_feature_height, min_feature_height, min_feature_width, min_size_img = + determine_feature_size(vcat(pos_training_images, neg_training_images); scale = scale, scale_to = scale_to, show_progress = true) + + @info("...done. Maximum feature width selected is $max_feature_width pixels; minimum feature width is $min_feature_width; maximum feature height is $max_feature_height pixels; minimum feature height is $min_feature_height.\n") + else + # max_feature_width, max_feature_height, min_feature_height, min_feature_width = (67, 67, 65, 65) + # max_feature_width, max_feature_height, min_feature_height, min_feature_width = (100, 100, 30, 30) + max_feature_width, max_feature_height, min_feature_height, min_feature_width = (70, 70, 50, 50) + min_size_img = (128, 128) + end + + # classifiers are haar like features + classifiers = learn(pos_training_images, neg_training_images, num_classifiers, min_feature_height, max_feature_height, min_feature_width, max_feature_width; scale = scale, scale_to = scale_to) + + @info("Calculating test face scores and constructing dataset...") + sleep(0.5) + + pos_testing_images = all_pos_images[(num_pos + 1):2num_pos] + neg_testing_images = all_neg_images[(num_neg + 1):2num_neg] + num_faces = length(pos_testing_images) + num_non_faces = length(neg_testing_images) + + faces_scores = Vector{Real}(undef, num_faces) + non_faces_scores = Vector{Real}(undef, num_non_faces) + + # faces_scores[:] .= (sum(get_faceness(c, load_image(face, scale=scale, scale_to=scale_to)) for c in classifiers) / num_classifiers for face in pos_testing_images) + # non_faces_scores[:] .= (sum(get_faceness(c, load_image(non_face, scale=scale, scale_to=scale_to)) for c in classifiers) / num_classifiers for non_face in neg_testing_images) + faces_scores[:] .= (get_faceness(classifiers, load_image(face, scale=scale, scale_to=scale_to)) for face in pos_testing_images) + non_faces_scores[:] .= (get_faceness(classifiers, load_image(non_face, scale=scale, scale_to=scale_to)) for non_face in neg_testing_images) + + face_names = String[basename(i) for i in pos_testing_images] + non_face_names = String[basename(i) for i in neg_testing_images] + + # filling in the dataset with missing to easily write to csv + df_faces = faces_scores + df_non_faces = non_faces_scores + if length(faces_scores) < length(non_faces_scores) + to_add = num_non_faces - num_faces + df_faces = vcat(df_faces, Matrix{Union{Float64, Missing}}(undef, to_add, 1)) + face_names = vcat(face_names, Matrix{Union{Float64, Missing}}(undef, to_add, 1)) + elseif length(faces_scores) >= length(non_faces_scores) + to_add = num_faces - num_non_faces + df_non_faces = vcat(df_non_faces, Matrix{Union{Float64, Missing}}(undef, to_add, 1)) + non_face_names = vcat(non_face_names, Matrix{Union{Float64, Missing}}(undef, to_add, 1)) + else + error("unreachable") + end + + # write score data + data_file = joinpath(dirname(@__DIR__), "data", "faceness-scores.csv") + write(data_file, DataFrame(hcat(face_names, df_faces, non_face_names, df_non_faces), :auto), writeheader=false) + + @info("...done. Dataset written to $(data_file).\n") + @info("Computing differences in scores between faces and non-faces...") + + welch_t = UnequalVarianceTTest(faces_scores, non_faces_scores) + + @info("...done. $welch_t\n") + @info("Constructing box plot with said dataset...") + + gr() # set plot backend + theme(:solarized) + plot = StatsPlots.plot( + StatsPlots.boxplot(faces_scores, xaxis=false, label = false), + StatsPlots.boxplot(non_faces_scores, xaxis=false, label = false), + title = ["Scores of Faces" "Scores of Non-Faces"], + # label = ["faces" "non-faces"], + fontfamily = font("Times"), + layout = @layout([a b]), + # fillcolor = [:blue, :orange], + link = :y, + # framestyle = [:origin :origin] + ) + + # save plot + StatsPlots.savefig(plot, joinpath(dirname(@__DIR__), "figs", "scores.pdf")) + @info("...done. Plot created at $(joinpath(dirname(@__DIR__), "figs", "scores.pdf"))") +end + +@time main(200, 200, smart_choose_feats=false, scale=true, scale_to=(128, 128)) diff --git a/examples/write.jl b/examples/write.jl index fae1cd906..d05d552c2 100755 --- a/examples/write.jl +++ b/examples/write.jl @@ -1,10 +1,3 @@ -#!/usr/bin/env bash - #= - exec julia --project="$(realpath $(dirname $0))/" "${BASH_SOURCE[0]}" "$@" -e "include(popfirst!(ARGS))" \ - "${BASH_SOURCE[0]}" "$@" - =# - - #= Adapted from https://github.com/Simon-Hohberg/Viola-Jones/ =# diff --git a/src/AdaBoost.jl b/src/AdaBoost.jl index a3adc8578..5d98dfa1f 100755 --- a/src/AdaBoost.jl +++ b/src/AdaBoost.jl @@ -15,7 +15,7 @@ function get_feature_votes( max_feature_height::Integer=-one(Int32); scale::Bool = false, scale_to::Tuple = (Int32(200), Int32(200)), - show_progress::Bool = true + show_progress::Bool = get(ENV, "FACE_DETECTION_DISPLAY_LOGGING", "true") != "false" ) #this transforms everything to maintain type stability s₁, s₂ = scale_to @@ -50,7 +50,7 @@ function get_feature_votes( max_feature_width = max_feature_width == -_1 ? img_height : max_feature_width # Create features for all sizes and locations - features = create_features(img_height, img_width, min_feature_width, max_feature_width, min_feature_height, max_feature_height) + features = create_features(img_height, img_width, min_feature_width, max_feature_width, min_feature_height, max_feature_height, display_logging = show_progress) num_features = length(features) num_classifiers = num_classifiers == -_1 ? num_features : num_classifiers @@ -84,7 +84,7 @@ function get_feature_votes( max_feature_height::Integer=-one(Int32); scale::Bool = false, scale_to::Tuple = (Int32(200), Int32(200)), - show_progress::Bool = true + show_progress::Bool = get(ENV, "FACE_DETECTION_DISPLAY_LOGGING", "true") != "false" ) positive_files = filtered_ls(positive_path) negative_files = filtered_ls(negative_path) @@ -104,7 +104,7 @@ function learn( features::Array{HaarLikeObject, 1}, votes::Matrix{Int8}, num_classifiers::Integer=-one(Int32); - show_progress::Bool = true + show_progress::Bool = get(ENV, "FACE_DETECTION_DISPLAY_LOGGING", "true") != "false" ) # get number of positive and negative images (and create a global variable of the total number of images——global for the @everywhere scope) @@ -192,7 +192,7 @@ function learn( max_feature_height::Int=-1; scale::Bool = false, scale_to::Tuple = (200, 200), - show_progress::Bool = true + show_progress::Bool = get(ENV, "FACE_DETECTION_DISPLAY_LOGGING", "true") != "false" ) votes, features = get_feature_votes( @@ -219,7 +219,7 @@ function learn( max_feature_height::Int=-1; scale::Bool = false, scale_to::Tuple = (200, 200), - show_progress::Bool = true + show_progress::Bool = get(ENV, "FACE_DETECTION_DISPLAY_LOGGING", "true") != "false" ) return learn( @@ -262,15 +262,22 @@ function create_features( min_feature_width::Int, max_feature_width::Int, min_feature_height::Int, - max_feature_height::Int + max_feature_height::Int; + display_logging::Bool = get(ENV, "FACE_DETECTION_DISPLAY_LOGGING", "true") != "false", + display_warn::Bool = get(ENV, "FACE_DETECTION_DISPLAY_WARN", "true") != "false" ) - if img_width < max_feature_width || img_height < max_feature_height - error(""" - Cannot possibly find classifiers whose size is greater than the image itself [(width,height) = ($img_width,$img_height)]. + width_capacity_reached = img_width < max_feature_width + height_capacity_reached = img_height < max_feature_height + if width_capacity_reached || height_capacity_reached + width_capacity_reached && (max_feature_width = img_width) + height_capacity_reached && (max_feature_height = img_height) + display_warn && @warn(""" + Cannot possibly find classifiers whose size is greater than the image itself ((width, height) = ($img_width, $img_height)). + Limiting the maximum feature score by image size; (width, height) = ($max_feature_width, $max_feature_height) """) end - @info("Creating Haar-like features...") + display_logging && @info("Creating Haar-like features...") features = HaarLikeObject[] for (feature_first, feature_last) in values(FEATURE_TYPES) # (feature_types are just tuples) @@ -280,15 +287,16 @@ function create_features( for feature_height in feature_start_height:feature_last:max_feature_height for x in 1:(img_width - feature_width) for y in 1:(img_height - feature_height) - push!(features, HaarLikeObject((feature_first, feature_last), (x, y), feature_width, feature_height, 0, 1)) - push!(features, HaarLikeObject((feature_first, feature_last), (x, y), feature_width, feature_height, 0, -1)) + # HaarLikeObject( feature_type, position, width, height, threshold, polarity) + push!(features, HaarLikeObject((feature_first, feature_last), (x, y), feature_width, feature_height, 0, 1 )) + push!(features, HaarLikeObject((feature_first, feature_last), (x, y), feature_width, feature_height, 0, -1)) end # end for y end # end for x end # end for feature height end # end for feature width end # end for feature in feature types - @info("...finished processing; $(length(features)) features created.") + display_logging && @info("...finished processing; $(length(features)) features created.") return features end diff --git a/src/FaceDetection.jl b/src/FaceDetection.jl index 1c0a79f73..d4a9b1612 100755 --- a/src/FaceDetection.jl +++ b/src/FaceDetection.jl @@ -5,13 +5,22 @@ using Images: Images, coords_spatial using ProgressMeter: Progress, next! using Base.Threads: @threads using Base.Iterators: partition +using IntegralArrays # , IntervalSets -export to_integral_image, sum_region +# export IntegralArray, to_integral_image, sum_region +export IntegralArray, sum_region export learn, get_feature_votes export FEATURE_TYPES, HaarLikeObject, get_score, get_vote -export displaymatrix, filtered_ls, load_image, ensemble_vote_all, - reconstruct, get_random_image, generate_validation_image, - get_faceness, determine_feature_size +export displaymatrix, filtered_ls, load_image, ensemble_vote, + ensemble_vote_all, reconstruct, get_random_image, + generate_validation_image, get_faceness, determine_feature_size + +# Setting these environment variables here doesn't do anything to the environment for some reason. +# Various functions in Adaboost will pull from the environment, defaulting to `"true"`. If you +# need to turn off warnings and logging in the training phase, you can set these to `"false"` in +# your scripts. +# ENV["FACE_DETECTION_DISPLAY_LOGGING"] = "true" +# ENV["FACE_DETECTION_DISPLAY_WARN"] = "true" include("IntegralImage.jl") include("HaarLikeFeature.jl") diff --git a/src/HaarLikeFeature.jl b/src/HaarLikeFeature.jl index c5624b12a..9ef8d8eaa 100755 --- a/src/HaarLikeFeature.jl +++ b/src/HaarLikeFeature.jl @@ -86,7 +86,6 @@ Get score for given integral image array. This is the feature cascade. """ function get_score(feature::HaarLikeObject{I, F}, int_img::IntegralArray{T, N}) where {I, F, T, N} score = zero(I) - faceness = zero(I) _2f = F(2) _3f = F(3) _half = F(0.5) @@ -96,24 +95,20 @@ function get_score(feature::HaarLikeObject{I, F}, int_img::IntegralArray{T, N}) _first = sum_region(int_img, feature.top_left, (first(feature.top_left) + feature.width, round(I, last(feature.top_left) + feature.height / 2))) second = sum_region(int_img, (first(feature.top_left), round(I, last(feature.top_left) + feature.height / 2)), feature.bottom_right) score = _first - second - faceness = I(1) elseif feature.feature_type == FEATURE_TYPES.two_horizontal _first = sum_region(int_img, feature.top_left, (round(I, first(feature.top_left) + feature.width / 2), last(feature.top_left) + feature.height)) second = sum_region(int_img, (round(I, first(feature.top_left) + feature.width / 2), last(feature.top_left)), feature.bottom_right) score = _first - second - faceness = I(2) elseif feature.feature_type == FEATURE_TYPES.three_horizontal _first = sum_region(int_img, feature.top_left, (round(I, first(feature.top_left) + feature.width / 3), last(feature.top_left) + feature.height)) second = sum_region(int_img, (round(I, first(feature.top_left) + feature.width / 3), last(feature.top_left)), (round(I, first(feature.top_left) + 2 * feature.width / 3), last(feature.top_left) + feature.height)) third = sum_region(int_img, (round(I, first(feature.top_left) + 2 * feature.width / 3), last(feature.top_left)), feature.bottom_right) score = _first - second + third - faceness = I(3) elseif feature.feature_type == FEATURE_TYPES.three_vertical _first = sum_region(int_img, feature.top_left, (first(feature.bottom_right), round(I, last(feature.top_left) + feature.height / 3))) second = sum_region(int_img, (first(feature.top_left), round(I, last(feature.top_left) + feature.height / 3)), (first(feature.bottom_right), round(I, last(feature.top_left) + 2 * feature.height / 3))) third = sum_region(int_img, (first(feature.top_left), round(I, last(feature.top_left) + 2 * feature.height / 3)), feature.bottom_right) score = _first - second + third - faceness = I(4) elseif feature.feature_type == FEATURE_TYPES.four # top left area _first = sum_region(int_img, feature.top_left, (round(I, first(feature.top_left) + feature.width / 2), round(I, last(feature.top_left) + feature.height / 2))) @@ -124,10 +119,9 @@ function get_score(feature::HaarLikeObject{I, F}, int_img::IntegralArray{T, N}) # bottom right area fourth = sum_region(int_img, (round(I, first(feature.top_left) + feature.width / 2), round(I, last(feature.top_left) + feature.height / 2)), feature.bottom_right) score = _first - second - third + fourth - faceness = I(5) end - return score, faceness + return score end """ @@ -137,7 +131,7 @@ Get vote of this feature for given integral image. # Arguments -- `feature::HaarLikeObject`: given Haar-like feature (parameterised replacement of Python's `self`) +- `feature::HaarLikeObject`: given Haar-like feature - `int_img::IntegralArray`: Integral image array # Returns @@ -147,9 +141,6 @@ Get vote of this feature for given integral image. -1 otherwise """ function get_vote(feature::HaarLikeObject{I, F}, int_img::IntegralArray{T, N}) where {I, F, T, N} - score, _ = get_score(feature, int_img) # we only care about score here, not faceness - # return (feature.weight * score) < (feature.polarity * feature.threshold) ? one(Int8) : -one(Int8) - # return feature.weight * (score < feature.polarity * feature.threshold ? one(Int8) : -one(Int8)) + score = get_score(feature, int_img) return score < feature.polarity * feature.threshold ? feature.weight : -feature.weight - # self.weight * (1 if score < self.polarity * self.threshold else -1) end diff --git a/src/IntegralImage.jl b/src/IntegralImage.jl index 229a504c0..827a8ebf2 100755 --- a/src/IntegralImage.jl +++ b/src/IntegralImage.jl @@ -1,3 +1,38 @@ +# Uses IntegralArrays and IntervalSets.:(..) (exported by IntegralArrays) + +""" + sum_region( + integral_image_arr::AbstractArray, + top_left::Tuple{Int,Int}, + bottom_right::Tuple{Int,Int} + ) -> Number + +# Arguments + +- `iA::IntegralArray{T, N}`: The intermediate Integral Image +- `top_left::NTuple{N, Int}`: coordinates of the rectangle's top left corner +- `bottom_right::NTuple{N, Int}`: coordinates of the rectangle's bottom right corner + +# Returns + +- `sum::T` The sum of all pixels in the given rectangle defined by the parameters `top_left` and `bottom_right` +""" +sum_region(iA::IntegralArray{T, N}, top_left::CartesianIndex{N}, bottom_right::CartesianIndex{N}) where {T, N} = + iA[top_left..bottom_right] +sum_region(iA::IntegralArray{T, N}, top_left::NTuple{N, Int}, bottom_right::NTuple{N, Int}) where {T, N} = + iA[CartesianIndex(top_left)..CartesianIndex(bottom_right)] + +#= +sum_region(iA::IntegralArray{T, N}, top_left::NTuple{N, Int}, bottom_right::NTuple{N, Int}) where {T, N} = + iA[ntuple(i -> top_left[i]..bottom_right[i], N)...] +=# + +####################################### + +### The below implementation of IntegralArray is deprecated in favaour of +### IntegralArrays.jl. Functions from Images.jl that I were using here +### have been moved ([d8e5d3d](https://github.com/JuliaImages/Images.jl/commit/d8e5d3d)). +#= """ IntegralArray{T, N, A} <: AbstractArray{T, N} @@ -39,11 +74,13 @@ function to_integral_image(img_arr::AbstractArray{T, N}) where {T, N} return IntegralArray{T, N}(integral_image_arr) end -LinearIndices(A::IntegralArray) = Base.LinearFast() +# LinearIndices(A::IntegralArray) = Base.LinearFast() # TODO: fix this line # use IndexLinear? @inline size(A::IntegralArray) = size(A.data) @inline getindex(A::IntegralArray, i::Int...) = A.data[i...] @inline getindex(A::IntegralArray, ids::Tuple...) = getindex(A, first(ids)...) +=# +#= """ sum_region( integral_image_arr::AbstractArray, @@ -67,20 +104,121 @@ function sum_region( bottom_right::Tuple{T, T} ) where T <: Integer _1 = one(T) - _0 = zero(0) - _sum = integral_image_arr[last(bottom_right), first(bottom_right)] + _0 = zero(0) + _sum = integral_image_arr[last(bottom_right), first(bottom_right)] # _sum -= first(top_left) > _1 ? integral_image_arr[last(bottom_right), first(top_left) - _1] : _0 - if first(top_left) > _1 - _sum -= integral_image_arr[last(bottom_right), first(top_left) - _1] - end + if first(top_left) > _1 + _sum -= integral_image_arr[last(bottom_right), first(top_left) - _1] + end # _sum -= last(top_left) > _1 ? integral_image_arr[last(top_left) - _1, first(bottom_right)] : _0 - # _sum += last(top_left) > _1 && first(top_left) > _1 ? integral_image_arr[last(top_left) - _1, first(top_left) - _1] : _0 - if last(top_left) > _1 - _sum -= integral_image_arr[last(top_left) - _1, first(bottom_right)] - if first(top_left) > _1 - _sum += integral_image_arr[last(top_left) - _1, first(top_left) - _1] - end - end - + # _sum += last(top_left) > _1 && first(top_left) > _1 ? integral_image_arr[last(top_left) - _1, first(top_left) - _1] : _0 + if last(top_left) > _1 + _sum -= integral_image_arr[last(top_left) - _1, first(bottom_right)] + if first(top_left) > _1 + _sum += integral_image_arr[last(top_left) - _1, first(top_left) - _1] + end + end + return _sum end + +function sum_region(iX::IntegralArray, top_left::CartesianIndex, bottom_right::CartesianIndex) + total = iX[last(bottom_right.I), first(bottom_right.I)] + if first(top_left.I) > 1 + total -= iX[last(bottom_right.I), first(top_left.I) - 0] + end + if last(top_left.I) > 1 + total -= iX[last(top_left.I) - 1, first(bottom_right.I)] + if first(top_left.I) > 1 + total += iX[last(top_left.I) - 1, first(top_left.I) - 1] + end + end + return total +end + +function sum_region(iX::IntegralArray, top_left::CartesianIndex, bottom_right::CartesianIndex) + # @assert(length(top_left.I) == length(bottom_right.I), "Opposing Cartesian coordinates must have the same dimension") + # each_vertex_itr = foldr((itr1, itr2) -> ((v, w...) for w in itr2 for v in itr1), ((t1[i], t2[i]) for i in eachindex(t1))) + + + top_right = CartesianIndex(first(top_left.I), last()) + total = iX[last(bottom_right.I), first(bottom_right.I)] + if first(top_left.I) > 1 + total -= iX[last(bottom_right.I), first(top_left.I) - 0] + end + if last(top_left.I) > 1 + total -= iX[last(top_left.I) - 1, first(bottom_right.I)] + if first(top_left.I) > 1 + total += iX[last(top_left.I) - 1, first(top_left.I) - 1] + end + end + return total +end +sum_region(iX::IntegralArray, top_left::CartesianIndex, dim_sizes::Int...) +2^N vertices + +# top left and bottom right are inclusive! +function sum_region(iX::IntegralArray, top_left::CartesianIndex{N}, bottom_right::CartesianIndex{N}) where {N} + top_left = CartesianIndex(top_left.I .- 1) + each_vertex = foldr((itr1, itr2) -> (CartesianIndex(v, w...) for w in itr2 for v in itr1), ((top_left.I[k], bottom_right.I[k]) for k in Base.OneTo(N))) + vertices_coords = collect(each_vertex) + mask = Int[1, -1, -1, 1] + return sum((iX[i] for i in vertices_coords) .* mask) + # return sum((iX[i] for i in vertices_coords) .* mask) + # return iX[vertices_coords[4]] - iX[vertices_coords[2]] - iX[vertices_coords[3]] + iX[vertices_coords[1]] +end + +function sum_region(iX::IntegralArray, top_left::CartesianIndex{2}, bottom_right::CartesianIndex{2}) + top_left_outer = CartesianIndex(max(first(top_left.I))) + top_right = CartesianIndex(first(top_left_outer.I), last(bottom_right.I)) + bottom_left = CartesianIndex(last(top_left_outer.I), first(bottom_right.I)) + return iX[bottom_right] - iX[bottom_left] - iX[top_right] + iX[top_left_outer] +end +sum_region(iX::IntegralArray, top_left::CartesianIndex, window_width::Int, window_height::Int) = + sum_region(iX, top_left, CartesianIndex(top_left.I .+ (window_height, window_width))) +=# + +#= +_snap_to_bounds(sz::Int, i::Int) = i < 1 ? (1, false) : i > sz ? (sz, false) : (i, true) # Returns a tuple of (index::Int, in_bounds::Bool) +function _safe_bounds_get(A::AbstractArray{T, N}, i::NTuple{N, Int}) where {T, N} + @assert(N == 2, "We currently only support 2-dimensional arrays") + n, m = size(A) + x, x_in_bounds = _snap_to_bounds(n, first(i)) + y, y_in_bounds = _snap_to_bounds(m, last(i)) + return (x_in_bounds && y_in_bounds) ? A[CartesianIndex(x, y)] : zero(T) +end +function sum_region(iX::IntegralArray, top_left::CartesianIndex{2}, bottom_right::CartesianIndex{2}) + # top_left_outer = CartesianIndex(top_left.I .- 1) + x₀, y₀ = top_left.I + x₁, y₁ = bottom_right.I + + a = _safe_bounds_get(iX, (x₀ - 1, y₀ - 1)) + b = _safe_bounds_get(iX, (x₀ - 1, y₁)) + c = _safe_bounds_get(iX, (x₁, y₀ - 1)) + d = _safe_bounds_get(iX, (x₁, y₁)) + # top_left_outer = _safe_bounds_get(iX, top_left.I .- 1) + # top_right = CartesianIndex(first(top_left_outer.I), last(bottom_right.I)) + # bottom_left = CartesianIndex(last(top_left_outer.I), first(bottom_right.I)) + mask = Int[1, -1, -1, 1] + # return sum((checkbounds(Bool, iX, i) ? iX[i] : 0 for i in (bottom_right, bottom_left, top_right, top_left_outer)) .* mask) + # return iX[bottom_right] - iX[bottom_left] - iX[top_right] + iX[top_left_outer] + return sum((d, b, c, a) .* mask) + # return d - b - c + a +end +sum_region(iX::IntegralArray, top_left::NTuple{N, Int}, bottom_right::NTuple{N, Int}) where {N} = + sum_region(iX, CartesianIndex(top_left), CartesianIndex(bottom_right)) +=# + +#= +function coordinates_to_matrix() +end +M = [1 2; 3 4] +M_permutations = Matrix{CharFrequency}(undef, nrows^ncols, ncols) # similar(M, nrows^ncols, ncols) +# for i in axes(M, 2) # 1:nrows +for i in axes(M, 1) # 1:ncols +# for i in eachindex(t1) + M_permutations[:, i] .= repeat(view(M, :, i), inner = nrows^(ncols - i), outer = nrows^(i - 1)) +end + +foldr((itr1, itr2) -> ((v, w...) for w in itr2 for v in itr1), ((t1[i], t2[i]) for i in eachindex(t1))) +=# diff --git a/src/Utils.jl b/src/Utils.jl index 892514bc1..1e80c6031 100755 --- a/src/Utils.jl +++ b/src/Utils.jl @@ -27,7 +27,7 @@ Loads an image as gray_scale # Returns - - `Array{Float64, N}`: An array of floating point values representing the image + - `IntegralArray{Float64, N}`: An array of floating point values representing the image """ function load_image( image_path::String; @@ -39,9 +39,10 @@ function load_image( if scale img = imresize(img, scale_to) end - img = convert(Array{Float64}, Gray.(img)) + # img = convert(Array{Float64}, Gray.(img)) - return to_integral_image(img) + # return to_integral_image(img) + return IntegralArray(Gray.(img)) end """ @@ -123,6 +124,54 @@ function determine_feature_size( end +function _ensemble_vote(int_img::IntegralArray{T, N}, classifiers::Vector{HaarLikeObject}) where {T, N} + @debug("This function (`_ensemble_vote`) needs review to verify its correctness! See FaceDetection.jl#56.") + #= + # Algorithm b + F = typeof(first(classifiers).weight) + all_votes = F[get_vote(c, int_img) for c in classifiers] + faceness = 0 + for vote in all_votes + if vote < 0 + # then no face is found using this classifier + # we reject this face + break + end + faceness += 1 + end + summed_vote = sum(all_votes) ≥ zero(Int8) ? one(Int8) : zero(Int8) + return summed_vote, faceness + =# + + # Algorithm c + F = typeof(first(classifiers).weight) + all_votes = F[get_vote(c, int_img) for c in classifiers] + faceness = 0 + for vote in all_votes + # faceness += vote < 0 ? -1 : 1 + if vote >= 0 + faceness += 1 + end + end + summed_vote = sum(all_votes) ≥ zero(Int8) ? one(Int8) : zero(Int8) + return summed_vote, faceness + + #= + # Algorithm a + # TODO: check if the original vote algorithm works okay + F = typeof(first(classifiers).weight) + all_votes = F[get_vote(c, int_img) for c in classifiers] + faceness = 0 + for vote in all_votes + if vote < 0 + return zero(Int8), faceness + end + faceness += 1 + end + return one(Int8), faceness + =# +end + @doc raw""" ensemble_vote(int_img::IntegralArray, classifiers::AbstractArray) -> Integer @@ -149,8 +198,8 @@ h(x) = \begin{cases} 1 ⟺ sum of classifier votes > 0 0 otherwise """ -ensemble_vote(int_img::IntegralArray{T, N}, classifiers::Vector{HaarLikeObject}) where {T, N} = - sum(get_vote(c, int_img) for c in classifiers) ≥ zero(Int8) ? one(Int8) : zero(Int8) +ensemble_vote(int_img::IntegralArray{T, N}, classifiers::Vector{HaarLikeObject}) where {T, N} = + first(_ensemble_vote(int_img, classifiers)) """ ensemble_vote_all(images::Vector{String}, classifiers::Vector{HaarLikeObject}) -> Vector{Int8} @@ -200,9 +249,13 @@ Get facelikeness for a given feature. - `score::Number`: Score for given feature """ function get_faceness(feature::HaarLikeObject{I, F}, int_img::IntegralArray{T, N}) where {I, F, T, N} - score, faceness = get_score(feature, int_img) + error("Not implemented: as `get_score` no longer returns `faceness` (error in calculation; see 3a17220), it does not make sense to calculate the faceness of an image using a single feature. You should use the other method of `get_faceness`, which calculates the faceness given potentially many classifiers.") + # _, faceness = _ensemble_vote(int_img, [feature]) + score = get_score(feature, int_img) return (feature.weight * score) < (feature.polarity * feature.threshold) ? faceness : zero(T) end +get_faceness(classifiers::Vector{HaarLikeObject}, int_img::IntegralArray{T, N}) where {T, N} = + last(_ensemble_vote(int_img, classifiers)) #= reconstruct(classifiers::Vector, img_size::Tuple) -> AbstractArray diff --git a/test/runtests.jl b/test/runtests.jl index 860a47633..2f744da67 100755 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -7,7 +7,7 @@ using Logging Logging.disable_logging(Logging.Info) @time @testset "FaceDetection.jl" begin - # constants and variables + # Test initialisation: constants and variables main_data_path = joinpath(@__DIR__, "images") pos_training_path = joinpath(main_data_path, "pos") neg_training_path = joinpath(main_data_path, "neg") @@ -43,44 +43,51 @@ Logging.disable_logging(Logging.Info) features = [] p, n = 0, 0 random_img = load_image(rand(vcat(filtered_ls.([pos_training_path, neg_training_path, pos_testing_path, neg_testing_path])...))) - - # IntegralImage.jl - @test isequal(to_integral_image([17 24 1 8 15; 23 5 7 14 16; 4 6 13 20 22; 10 12 19 21 3; 11 18 25 2 9]), [17 41 42 50 65; 40 69 77 99 130; 44 79 100 142 195; 54 101 141 204 260; 65 130 195 260 325]) - @test isequal(sum_region(to_integral_image([1 7 4 2 9; 7 2 3 8 2; 1 8 7 9 1; 3 2 3 1 5; 2 9 5 6 6]), (4,4), (5,5)), 18) - @test typeof(sum_region(to_integral_image([1 7 4 2 9.9; 7 2 3 8 2; 1 8 7 9 1; 3 2 3 1 5; 2 9 5 6 6]), (4,4), (5,5))) <: AbstractFloat - @test sum_region(to_integral_image([1 7 4 2 9.9; 7 2 3 8 2; 1 8 7 9 1; 3 2 3 1 5; 2 9 5 6 6]), (4,4), (5,5)) isa AbstractFloat - - # HaarLikeFeature.jl - @test HaarLikeObject(a, b, c, d, e, f) isa HaarLikeObject - @test HaarLikeObject((1,3), (1,3), 10, 8, 0, 1).feature_type isa Tuple{Integer, Integer} - @test HaarLikeObject((1,3), (1,3), 10, 8, 0, 1).position isa Tuple{Integer, Integer} - @test HaarLikeObject((1,3), (1,3), 10, 8, 0, 1).top_left isa Tuple{Integer, Integer} - @test HaarLikeObject((1,3), (1,3), 10, 8, 0, 1).bottom_right isa Tuple{Integer, Integer} - @test HaarLikeObject((1,3), (1,3), 10, 8, 0, 1).width isa Integer - @test HaarLikeObject((1,3), (1,3), 10, 8, 0, 1).height isa Integer - @test HaarLikeObject((1,3), (1,3), 10, 8, 0, 1).threshold ∈ [0, 1] - @test HaarLikeObject((1,3), (1,3), 10, 8, 0, 1).polarity ∈ [0, 1] - @test HaarLikeObject((1,3), (1,3), 10, 8, 0, 1).weight ∈ [0, 1] - @test get_vote(HaarLikeObject(a, b, c, d, e, f), arr) ∈ [-1, 1] - @test get_vote(feature_2v, int_img) == expected_2v - @test get_vote(feature_2v, int_img) != expected_2v_fail - @test get_vote(feature_2h, int_img) == expected_2h - @test get_vote(feature_3h, int_img) == expected_3h - @test get_vote(feature_3v, int_img) == expected_3v - @test get_vote(feature_4, int_img) == expected_4 + + @testset "IntegralImage.jl" begin + A = [1 7 4 2 9; 7 2 3 8 2; 1 8 7 9 1; 3 2 3 1 5; 2 9 5 6 6] + iA = IntegralArray(A) + @test isequal(IntegralArray([17 24 1 8 15; 23 5 7 14 16; 4 6 13 20 22; 10 12 19 21 3; 11 18 25 2 9]), [17 41 42 50 65; 40 69 77 99 130; 44 79 100 142 195; 54 101 141 204 260; 65 130 195 260 325]) + @test isequal(sum_region(iA, (4,4), (5,5)), 18) + @test typeof(sum_region(iA, (4,4), (5,5))) <: Integer + @test sum_region(iA, (4,4), (5,5)) isa Integer + @test isequal(sum_region(iA, CartesianIndex(1, 2), CartesianIndex(3, 4)), 50) + end - # AdaBoost.jl - classifiers = learn(pos_training_path, neg_training_path, 10, 8, 10, 8, 10; show_progress = false) - features = FaceDetection.create_features(19, 19, 8, 10, 8, 10) - @test length(features) == 4520 + @testset "HaarLikeFeature.jl" begin + @test HaarLikeObject(a, b, c, d, e, f) isa HaarLikeObject + @test HaarLikeObject((1,3), (1,3), 10, 8, 0, 1).feature_type isa Tuple{Integer, Integer} + @test HaarLikeObject((1,3), (1,3), 10, 8, 0, 1).position isa Tuple{Integer, Integer} + @test HaarLikeObject((1,3), (1,3), 10, 8, 0, 1).top_left isa Tuple{Integer, Integer} + @test HaarLikeObject((1,3), (1,3), 10, 8, 0, 1).bottom_right isa Tuple{Integer, Integer} + @test HaarLikeObject((1,3), (1,3), 10, 8, 0, 1).width isa Integer + @test HaarLikeObject((1,3), (1,3), 10, 8, 0, 1).height isa Integer + @test HaarLikeObject((1,3), (1,3), 10, 8, 0, 1).threshold ∈ [0, 1] + @test HaarLikeObject((1,3), (1,3), 10, 8, 0, 1).polarity ∈ [0, 1] + @test HaarLikeObject((1,3), (1,3), 10, 8, 0, 1).weight ∈ [0, 1] + @test get_vote(HaarLikeObject(a, b, c, d, e, f), arr) ∈ [-1, 1] + @test get_vote(feature_2v, int_img) == expected_2v + @test get_vote(feature_2v, int_img) != expected_2v_fail + @test get_vote(feature_2h, int_img) == expected_2h + @test get_vote(feature_3h, int_img) == expected_3h + @test get_vote(feature_3v, int_img) == expected_3v + @test get_vote(feature_4, int_img) == expected_4 + end + + @testset "AdaBoost.jl" begin + classifiers = learn(pos_training_path, neg_training_path, 10, 8, 10, 8, 10; show_progress = false) + features = FaceDetection.create_features(19, 19, 8, 10, 8, 10) + @test length(features) == 4520 + end - # Utils.jl - @test determine_feature_size(pos_training_path, neg_training_path) == (10, 10, 8, 8, (19, 19)) - @test get_faceness(classifiers[rand(1:length(classifiers))], random_img) isa Real - num_faces = length(filtered_ls(pos_testing_path)) - num_non_faces = length(filtered_ls(neg_testing_path)) - p = sum(ensemble_vote_all(pos_testing_path, classifiers)) / num_faces - n = (num_non_faces - sum(ensemble_vote_all(neg_testing_path, classifiers))) / num_non_faces - @test isapprox(p, 0.496, atol=1e-1) # these tests implicitly test the whole algorithm - @test isapprox(n, 0.536, atol=1e-1) + @testset "Utils.jl" begin + @test determine_feature_size(pos_training_path, neg_training_path) == (10, 10, 8, 8, (19, 19)) + @test get_faceness(classifiers, random_img) isa Real + num_faces = length(filtered_ls(pos_testing_path)) + num_non_faces = length(filtered_ls(neg_testing_path)) + p = sum(ensemble_vote_all(pos_testing_path, classifiers)) / num_faces + n = (num_non_faces - sum(ensemble_vote_all(neg_testing_path, classifiers))) / num_non_faces + @test isapprox(p, 0.496, atol=1e-1) # these tests implicitly test the whole algorithm + @test isapprox(n, 0.536, atol=1e-1) # ibid. + end end # end tests