├── .dockerignore ├── heroku.yml ├── .gitignore ├── fixtures ├── filled.png ├── forest.jpg └── big_boobs.cropped.png ├── .gitmodules ├── hooks └── post_checkout ├── app.json ├── go.mod ├── docker ├── test │ └── docker-compose.yml └── dev │ └── docker-compose.yml ├── Dockerfile.heroku ├── main.go ├── helpers.go ├── router.go ├── scoring.go ├── file.go ├── Readme.md ├── go.sum ├── Dockerfile ├── handlers_test.go ├── handlers.go └── anAlgorithm.go /.dockerignore: -------------------------------------------------------------------------------- 1 | /.git -------------------------------------------------------------------------------- /heroku.yml: -------------------------------------------------------------------------------- 1 | build: 2 | docker: 3 | web: Dockerfile.heroku -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /tmp 2 | /.idea 3 | adult-image-detector 4 | rest-api.http 5 | vendor/*/ 6 | uploads/* -------------------------------------------------------------------------------- /fixtures/filled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-dating/adult-image-detector/HEAD/fixtures/filled.png -------------------------------------------------------------------------------- /fixtures/forest.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-dating/adult-image-detector/HEAD/fixtures/forest.jpg -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "models/open_nsfw"] 2 | path = models/open_nsfw 3 | url = https://github.com/yahoo/open_nsfw 4 | -------------------------------------------------------------------------------- /fixtures/big_boobs.cropped.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-dating/adult-image-detector/HEAD/fixtures/big_boobs.cropped.png -------------------------------------------------------------------------------- /hooks/post_checkout: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Docker hub does a recursive clone, then checks the branch out, 3 | # so when a PR adds a submodule (or updates it), it fails. 4 | git submodule update --init -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adult-image-detector", 3 | "description": "Use deep neural networks and other algos for detect nude images", 4 | "repository": "https://github.com/open-dating/adult-image-detector", 5 | "stack": "container", 6 | "success_url": "/" 7 | } -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module go.zerodha.tech/common/nsfw-image-detector 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/caarlos0/env v3.5.1-0.20181024121956-515df9e9ee27+incompatible 7 | github.com/google/uuid v1.1.2-0.20190416172445-c2e93f3ae59f 8 | github.com/pdfcpu/pdfcpu v0.3.9 9 | github.com/stretchr/testify v1.7.0 // indirect 10 | gocv.io/x/gocv v0.26.0 11 | ) 12 | -------------------------------------------------------------------------------- /docker/test/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | go.adult-image-detector-test: 4 | container_name: adult-image-detector-go-test 5 | build: 6 | context: ../../ 7 | dockerfile: Dockerfile 8 | args: 9 | tests: "skip_on_build" 10 | volumes: 11 | - ../../:/go/src/adult-image-detector 12 | command: "sh -c 'go test'" -------------------------------------------------------------------------------- /docker/dev/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | go.adult-image-detector-dev: 4 | container_name: adult-image-detector-go-dev 5 | ports: 6 | - "9191:9191" 7 | build: 8 | context: ../../ 9 | dockerfile: Dockerfile 10 | args: 11 | tests: "skip_on_build" 12 | volumes: 13 | - ../../:/go/src/adult-image-detector 14 | command: "sh -c 'fresh'" -------------------------------------------------------------------------------- /Dockerfile.heroku: -------------------------------------------------------------------------------- 1 | # using precompiled version due to heroku has 2 | # docker compile timeout=15min 3 | FROM opendating/adult-image-detector:0.4.0 4 | 5 | RUN go install github.com/pilu/fresh@0fa698148017fa2234856bdc881d9cc62517f62b 6 | 7 | WORKDIR $GOPATH/src/adult-image-detector 8 | 9 | COPY ./ ./ 10 | 11 | ARG tests 12 | RUN if test $tests = 'skip_on_build'; then \ 13 | echo "run tests skipped!"; \ 14 | else \ 15 | echo "run tests!"; \ 16 | go test; \ 17 | fi 18 | 19 | RUN go mod tidy && go build 20 | 21 | EXPOSE 9191 22 | 23 | CMD ["/go/src/adult-image-detector/nsfw-image-detector"] 24 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | 7 | "github.com/caarlos0/env" 8 | ) 9 | 10 | // VERSION of the service. 11 | const VERSION = "0.4.0" 12 | 13 | type params struct { 14 | Port string `env:"PORT" envDefault:"9191"` 15 | CorsOrigin string `env:"CORS_ORIGIN" envDefault:"*"` 16 | } 17 | 18 | func main() { 19 | var cfg params 20 | 21 | log.Println("App version", VERSION) 22 | 23 | if err := env.Parse(&cfg); err != nil { 24 | log.Printf("%+v\n", err) 25 | } 26 | 27 | log.Printf("Server run at http://localhost:%s", cfg.Port) 28 | 29 | if err := http.ListenAndServe(":"+cfg.Port, &router{cfg: cfg}); err != nil { 30 | panic(err) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /helpers.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | 7 | "gocv.io/x/gocv" 8 | ) 9 | 10 | // convert rgb to ycbr 11 | func rgbaToYCbCr(pixCol color.Color) (y int, cb int, cr int) { 12 | rInt, gInt, bInt, _ := pixCol.RGBA() 13 | 14 | // y8, cb8, cr8 := color.RGBToYCbCr(uint8(rInt), uint8(gInt), uint8(bInt)) 15 | // return int(y8), int(cb8), int(cr8) 16 | 17 | r := float32(rInt / 256) 18 | g := float32(gInt / 256) 19 | b := float32(bInt / 256) 20 | 21 | return int(16.0 + 0.256788*r + 0.504129*g + 0.097905*b), 22 | int(128.0 - 0.148223*r - 0.290992*g + 0.439215*b), 23 | int(128.0 + 0.439215*r - 0.367788*g - 0.071427*b) 24 | } 25 | 26 | // Convert image to gocv.Mat 27 | // https://github.com/hybridgroup/gocv/issues/228 28 | func imageToRGB8Mat(img image.Image) (gocv.Mat, error) { 29 | bounds := img.Bounds() 30 | x := bounds.Dx() 31 | y := bounds.Dy() 32 | bytes := make([]byte, 0, x*y*3) 33 | 34 | //don't get surprised of reversed order everywhere below 35 | for j := bounds.Min.Y; j < bounds.Max.Y; j++ { 36 | for i := bounds.Min.X; i < bounds.Max.X; i++ { 37 | r, g, b, _ := img.At(i, j).RGBA() 38 | bytes = append(bytes, byte(b>>8), byte(g>>8), byte(r>>8)) 39 | } 40 | } 41 | return gocv.NewMatFromBytes(y, x, gocv.MatTypeCV8UC3, bytes) 42 | } 43 | -------------------------------------------------------------------------------- /router.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | ) 7 | 8 | // router routes requests 9 | type router struct { 10 | cfg params 11 | } 12 | 13 | // handler http responses 14 | func (rtr *router) ServeHTTP(w http.ResponseWriter, r *http.Request) { 15 | switch r.URL.Path { 16 | case "/api/v1/detect": 17 | switch r.Method { 18 | case http.MethodPost: 19 | // allow ajax reponses from browser 20 | w.Header().Set("Access-Control-Allow-Origin", rtr.cfg.CorsOrigin) 21 | 22 | proceedImage(w, r) 23 | default: 24 | ShowImageForm(w) 25 | } 26 | 27 | case "/api/v1/pdf_detect": 28 | switch r.Method { 29 | case http.MethodPost: 30 | w.Header().Set("Access-Control-Allow-Origin", rtr.cfg.CorsOrigin) 31 | proceedPDF(w, r) 32 | case http.MethodGet: 33 | ShowPDFForm(w) 34 | default: 35 | HandleError(w, fmt.Errorf("bad request. make http POST request instead")) 36 | return 37 | } 38 | 39 | default: 40 | HandleError(w, fmt.Errorf(` 41 | 42 | 43 | Bad request. Invalid endpoint. Avalaibled endpoints: 44 |
/api/v1/detect - for detect in image
45 |
/api/v1/pdf_detect - for detect in pdf
46 | 47 | 48 | `)) 49 | return 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /scoring.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "image" 6 | _ "image/gif" 7 | _ "image/jpeg" 8 | _ "image/png" 9 | "os" 10 | 11 | "gocv.io/x/gocv" 12 | ) 13 | 14 | // get score from yahoo open nsfw https://github.com/yahoo/open_nsfw 15 | func getOpenNsfwScore(filePath string, net gocv.Net) (score float32, err error) { 16 | img := gocv.IMRead(filePath, gocv.IMReadColor) 17 | if img.Empty() { 18 | return 0, errors.New("Invalid image") 19 | } 20 | defer img.Close() 21 | 22 | blob := gocv.BlobFromImage( 23 | img, 24 | 1.0, 25 | image.Pt(224, 224), 26 | gocv.NewScalar(104, 117, 123, 0), 27 | true, 28 | false, 29 | ) 30 | if blob.Empty() { 31 | return 0, errors.New("Invalid blob") 32 | } 33 | defer blob.Close() 34 | 35 | net.SetInput(blob, "") 36 | 37 | detBlob := net.Forward("") 38 | defer detBlob.Close() 39 | 40 | return detBlob.GetFloatAt(0, 1), nil 41 | } 42 | 43 | // get result from An Algorithm for Nudity Detection by Rigan Ap-apid 44 | func getAnAlgorithmForNudityDetectionResult(filePath string, debug bool) (result bool, err error) { 45 | existingImageFile, err := os.Open(filePath) 46 | if err != nil { 47 | return true, errors.New("Cant open file") 48 | } 49 | defer existingImageFile.Close() 50 | 51 | imageData, _, err := image.Decode(existingImageFile) 52 | if err != nil { 53 | return true, errors.New("Decode err") 54 | } 55 | 56 | anAlg := anAlgorithm{ 57 | img: imageData, 58 | debug: debug, 59 | } 60 | return anAlg.IsNude() 61 | } 62 | -------------------------------------------------------------------------------- /file.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "io/ioutil" 6 | "net/http" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | "time" 11 | 12 | "github.com/google/uuid" 13 | "github.com/pdfcpu/pdfcpu/pkg/api" 14 | "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" 15 | ) 16 | 17 | type uploadedFileInfo struct { 18 | FilePath string 19 | Filename string 20 | FileExt string 21 | SaveAsFilename string 22 | disableOpenNsfw bool 23 | disableAnAlgorithm bool 24 | debug bool 25 | password string 26 | } 27 | 28 | func getImagesFromPDF(fp, dir, password string) ([]string, string, error) { 29 | var extractedImages []string 30 | 31 | if err := api.ExtractImagesFile(fp, dir, nil, &pdfcpu.Configuration{ 32 | Reader15: true, 33 | DecodeAllStreams: true, 34 | UserPW: password, 35 | }); err != nil { 36 | return nil, "", err 37 | } 38 | 39 | files, err := ioutil.ReadDir(dir) 40 | if err != nil { 41 | return nil, "", err 42 | } 43 | 44 | for _, v := range files { 45 | if isImage(v.Name()) { 46 | extractedImages = append(extractedImages, filepath.Join(dir, v.Name())) 47 | } 48 | } 49 | 50 | return extractedImages, dir, nil 51 | } 52 | 53 | func isImage(name string) bool { 54 | parts := strings.Split(name, ".") 55 | fileExt := strings.ToLower(parts[len(parts)-1]) 56 | 57 | return fileExt == "jpg" || fileExt == "jpeg" || fileExt == "png" || fileExt == "gif" 58 | } 59 | 60 | // procced multipart form and save file 61 | func uploadFileFormHandler(r *http.Request, dir, formFile string) (parsedForm uploadedFileInfo, err error) { 62 | r.ParseMultipartForm(32 << 20) 63 | file, handler, err := r.FormFile(formFile) 64 | if err != nil { 65 | return parsedForm, err 66 | } 67 | defer file.Close() 68 | 69 | parts := strings.Split(handler.Filename, ".") 70 | fileExt := parts[len(parts)-1] 71 | 72 | parsedForm.SaveAsFilename = time.Now().Format(time.RFC3339) + "_" + uuid.New().String() + "." + fileExt 73 | 74 | filePath, err := filepath.Abs(filepath.Join(dir, parsedForm.SaveAsFilename)) 75 | if err != nil { 76 | return parsedForm, err 77 | } 78 | 79 | f, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0777) 80 | if err != nil { 81 | return parsedForm, err 82 | } 83 | 84 | defer f.Close() 85 | io.Copy(f, file) 86 | 87 | parsedForm.Filename = handler.Filename 88 | parsedForm.FilePath = filePath 89 | parsedForm.FileExt = fileExt 90 | 91 | parsedForm.disableAnAlgorithm = r.FormValue("disableAnAlgorithm") != "" 92 | parsedForm.disableOpenNsfw = r.FormValue("disableOpenNsfw") != "" 93 | parsedForm.debug = r.FormValue("debug") != "" 94 | parsedForm.password = r.FormValue("password") 95 | return parsedForm, nil 96 | } 97 | 98 | // remove file 99 | func removeFile(filePath string) { 100 | err := os.RemoveAll(filePath) 101 | if err != nil { 102 | panic(err) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | ### adult-image-detector 2 | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/open-dating/adult-image-detector) 3 | [![Docker Cloud Build Status](https://img.shields.io/docker/cloud/build/opendating/adult-image-detector)](https://hub.docker.com/repository/docker/opendating/adult-image-detector) 4 | 5 | 6 | Use deep neural networks and other algos for detect nude images in images and pdf files. 7 | 8 | [Try detection](https://adult-image-detector.herokuapp.com/) 9 | 10 | ### Usage 11 | For detect nudity in images exec: 12 | ``` 13 | curl -i -X POST -F "image=@Daddy_Lets_Me_Ride_His_Cock_preview_720p.mp4.jpg" http://localhost:9191/api/v1/detect 14 | ``` 15 | Result: 16 | ```json 17 | { 18 | "app_version":"0.2.0", 19 | "open_nsfw_score":0.81577397, 20 | "an_algorithm_for_nudity_detection": true, 21 | "image_name":"Daddy_Lets_Me_Ride_His_Cock_preview_720p.mp4.jpg" 22 | } 23 | ``` 24 | 25 | For detect nudity in pdf exec: 26 | ``` 27 | curl -i -X POST -F "pdf=@Daddy_Lets_Me_Ride_His_Cock_preview_720p.mp4.pdf" http://localhost:9191/api/v1/pdf_detect 28 | ``` 29 | Result: 30 | ```json 31 | { 32 | "app_version": "0.4.0", 33 | "result": { 34 | "2021-06-05T09:01:38Z_ac749c19-8bd1-48fa-88d5-0a448c0d948c_1_Im4.png": { 35 | "open_nsfw_score": 0.0015096113, 36 | "an_algorithm_for_nudity_detection": false 37 | }, 38 | "2021-06-05T09:01:38Z_ac749c19-8bd1-48fa-88d5-0a448c0d948c_1_Im5.png": { 39 | "open_nsfw_score": 0.00092005456, 40 | "an_algorithm_for_nudity_detection": false 41 | } 42 | }, 43 | "nudity_detection_disabled": false, 44 | "image_scoring_disabled": false, 45 | "image_name": "Daddy_Lets_Me_Ride_His_Cock_preview_720p.mp4.pdf", 46 | "an_algorithm_for_nudity_detection": false, 47 | "open_nsfw_score": 0.0015096113 48 | } 49 | ``` 50 | 51 | ### Docker 52 | #### Run 53 | ``` 54 | docker run -p 9191:9191 opendating/adult-image-detector 55 | ``` 56 | 57 | #### Build 58 | ``` 59 | git clone https://github.com/open-dating/adult-image-detector --recursive 60 | docker build -t adult-image-detector . 61 | ``` 62 | 63 | #### Development 64 | ``` 65 | git clone https://github.com/open-dating/adult-image-detector --recursive 66 | cd docker/dev 67 | docker-compose up 68 | ``` 69 | 70 | #### Test 71 | ``` 72 | cd docker/test 73 | docker-compose up 74 | ``` 75 | 76 | ### Install to heroku 77 | Use deploy button or: 78 | 79 | fork, create app and change stack to container 80 | ``` 81 | heroku stack:set container 82 | ``` 83 | 84 | ### Requirements 85 | Go 1.17 86 | 87 | opencv 4.5.1 88 | 89 | ### Development without docker 90 | Recursive clone that repo: 91 | ``` 92 | git clone https://github.com/open-dating/adult-image-detector --recursive 93 | ``` 94 | or manually install submodules: 95 | ``` 96 | git submodule init 97 | git submodule update 98 | ``` 99 | 100 | Install opencv 4.5.1 101 | 102 | Run hot reload with fresh: 103 | ``` 104 | go install github.com/pilu/fresh@0fa698148017fa2234856bdc881d9cc62517f62b 105 | fresh 106 | ``` 107 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/caarlos0/env v3.5.1-0.20181024121956-515df9e9ee27+incompatible h1:Rxha6nBJiwIacRKWZPPYRsVCz6HrEMqCLNXuwxoruAM= 2 | github.com/caarlos0/env v3.5.1-0.20181024121956-515df9e9ee27+incompatible/go.mod h1:tdCsowwCzMLdkqRYDlHpZCp2UooDD3MspDBjZ2AD02Y= 3 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/google/uuid v1.1.2-0.20190416172445-c2e93f3ae59f h1:XXzyYlFbxK3kWfcmu3Wc+Tv8/QQl/VqwsWuSYF1Rj0s= 6 | github.com/google/uuid v1.1.2-0.20190416172445-c2e93f3ae59f/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 7 | github.com/hhrutter/lzw v0.0.0-20190827003112-58b82c5a41cc/go.mod h1:yJBvOcu1wLQ9q9XZmfiPfur+3dQJuIhYQsMGLYcItZk= 8 | github.com/hhrutter/lzw v0.0.0-20190829144645-6f07a24e8650 h1:1yY/RQWNSBjJe2GDCIYoLmpWVidrooriUr4QS/zaATQ= 9 | github.com/hhrutter/lzw v0.0.0-20190829144645-6f07a24e8650/go.mod h1:yJBvOcu1wLQ9q9XZmfiPfur+3dQJuIhYQsMGLYcItZk= 10 | github.com/hhrutter/tiff v0.0.0-20190829141212-736cae8d0bc7 h1:o1wMw7uTNyA58IlEdDpxIrtFHTgnvYzA8sCQz8luv94= 11 | github.com/hhrutter/tiff v0.0.0-20190829141212-736cae8d0bc7/go.mod h1:WkUxfS2JUu3qPo6tRld7ISb8HiC0gVSU91kooBMDVok= 12 | github.com/pdfcpu/pdfcpu v0.3.9 h1:gHPreswsOGwe1zViJxufbvNZf0xhK4mxj/r1CwLp958= 13 | github.com/pdfcpu/pdfcpu v0.3.9/go.mod h1:EfJ1EIo3n5+YlGF53DGe1yF1wQLiqK1eqGDN5LuKALs= 14 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 15 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 16 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 17 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 18 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 19 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 20 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 21 | gocv.io/x/gocv v0.26.0 h1:1azNvYEM245YN1bdw/WdX5YJzLg3Sr4STX0MqdWBIXM= 22 | gocv.io/x/gocv v0.26.0/go.mod h1:7Ju5KbPo+R85evmlhhKPVMwXtgDRNX/PtfVfbToSrLU= 23 | golang.org/x/image v0.0.0-20190823064033-3a9bac650e44/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 24 | golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 h1:QelT11PB4FXiDEXucrfNckHoFxwt8USGY1ajP1ZF5lM= 25 | golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 26 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 27 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 28 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 29 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= 30 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 31 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 32 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 33 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Dockerfile based on https://github.com/hybridgroup/gocv/blob/2c7172c36b2b14c26e1f14b9eb0d29e9a733ce0b/Dockerfile 2 | FROM ubuntu:18.04 AS opencv 3 | 4 | RUN apt-get update && apt-get install -y --no-install-recommends \ 5 | git build-essential cmake pkg-config unzip libgtk2.0-dev \ 6 | curl ca-certificates libcurl4-openssl-dev libssl-dev \ 7 | libavcodec-dev libavformat-dev libswscale-dev libtbb2 libtbb-dev \ 8 | libjpeg-dev libpng-dev libtiff-dev libdc1394-22-dev && \ 9 | rm -rf /var/lib/apt/lists/* 10 | 11 | ARG OPENCV_VERSION="4.5.1" 12 | ENV OPENCV_VERSION $OPENCV_VERSION 13 | 14 | RUN curl -Lo opencv.zip https://github.com/opencv/opencv/archive/${OPENCV_VERSION}.zip && \ 15 | unzip -q opencv.zip && \ 16 | curl -Lo opencv_contrib.zip https://github.com/opencv/opencv_contrib/archive/${OPENCV_VERSION}.zip && \ 17 | unzip -q opencv_contrib.zip && \ 18 | rm opencv.zip opencv_contrib.zip && \ 19 | cd opencv-${OPENCV_VERSION} && \ 20 | mkdir build && cd build && \ 21 | cmake -D CMAKE_BUILD_TYPE=RELEASE \ 22 | -D CMAKE_INSTALL_PREFIX=/usr/local \ 23 | -D OPENCV_EXTRA_MODULES_PATH=../../opencv_contrib-${OPENCV_VERSION}/modules \ 24 | -D WITH_JASPER=OFF \ 25 | -D BUILD_DOCS=OFF \ 26 | -D BUILD_EXAMPLES=OFF \ 27 | -D BUILD_TESTS=OFF \ 28 | -D BUILD_PERF_TESTS=OFF \ 29 | -D BUILD_opencv_java=NO \ 30 | -D BUILD_opencv_python=NO \ 31 | -D BUILD_opencv_python2=NO \ 32 | -D BUILD_opencv_python3=NO \ 33 | -D OPENCV_GENERATE_PKGCONFIG=ON .. && \ 34 | make -j $(nproc --all) && \ 35 | make preinstall && make install && ldconfig && \ 36 | cd / && rm -rf opencv* 37 | 38 | ################# 39 | # Go + OpenCV # 40 | ################# 41 | FROM opencv AS gocv 42 | 43 | ARG GOVERSION="1.17" 44 | ENV GOVERSION $GOVERSION 45 | 46 | RUN apt-get update && apt-get install -y --no-install-recommends \ 47 | git software-properties-common && \ 48 | curl -Lo go${GOVERSION}.linux-amd64.tar.gz https://dl.google.com/go/go${GOVERSION}.linux-amd64.tar.gz && \ 49 | tar -C /usr/local -xzf go${GOVERSION}.linux-amd64.tar.gz && \ 50 | rm go${GOVERSION}.linux-amd64.tar.gz && \ 51 | rm -rf /var/lib/apt/lists/* 52 | 53 | ENV GOPATH /go 54 | ENV PATH $GOPATH/bin:/usr/local/go/bin:$PATH 55 | 56 | RUN mkdir -p "$GOPATH/src" "$GOPATH/bin" && chmod -R 777 "$GOPATH" 57 | WORKDIR $GOPATH 58 | 59 | ######################### 60 | # adult-image-detector # 61 | ######################### 62 | FROM gocv AS adult-image-detector 63 | 64 | RUN go install github.com/pilu/fresh@0fa698148017fa2234856bdc881d9cc62517f62b 65 | 66 | WORKDIR $GOPATH/src/adult-image-detector 67 | 68 | COPY ./ ./ 69 | 70 | ARG tests 71 | RUN if test $tests = 'skip_on_build'; then \ 72 | echo "run tests skipped!"; \ 73 | else \ 74 | echo "run tests!"; \ 75 | go test; \ 76 | fi 77 | 78 | RUN go mod tidy && go build 79 | 80 | EXPOSE 9191 81 | 82 | CMD ["/go/src/adult-image-detector/nsfw-image-detector"] 83 | -------------------------------------------------------------------------------- /handlers_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io" 7 | "io/ioutil" 8 | "mime/multipart" 9 | "net/http/httptest" 10 | "os" 11 | "strconv" 12 | "strings" 13 | "testing" 14 | ) 15 | 16 | func TestShowForm(t *testing.T) { 17 | w := httptest.NewRecorder() 18 | 19 | ShowImageForm(w) 20 | 21 | resp := w.Result() 22 | body, _ := ioutil.ReadAll(resp.Body) 23 | 24 | textBody := string(body) 25 | 26 | if strings.Contains(textBody, " 0.5 == false { 63 | t.Errorf("Expected OpenNsfwScore > 0.5 for big_boobs.cropped.png got OpenNsfwScore %f", result.OpenNsfwScore) 64 | } 65 | } 66 | 67 | // support method for tests image recognition result 68 | func uploadFixtureAndGetResult(filePath string) (body []byte, err error) { 69 | r, _ := os.Open(filePath) 70 | values := map[string]io.Reader{ 71 | "image": r, 72 | } 73 | form, contentType, err := createForm(values) 74 | if err != nil { 75 | return body, err 76 | } 77 | 78 | req := httptest.NewRequest("POST", "/api/v1/detect", &form) 79 | req.Header.Set("Content-Type", contentType) 80 | 81 | w := httptest.NewRecorder() 82 | proceedImage(w, req) 83 | 84 | resp := w.Result() 85 | return ioutil.ReadAll(resp.Body) 86 | } 87 | 88 | // support method for tests 89 | func createForm(values map[string]io.Reader) (b bytes.Buffer, contentType string, err error) { 90 | form := multipart.NewWriter(&b) 91 | for key, r := range values { 92 | var fw io.Writer 93 | if x, ok := r.(io.Closer); ok { 94 | defer x.Close() 95 | } 96 | // Add an file 97 | if x, ok := r.(*os.File); ok { 98 | if fw, err = form.CreateFormFile(key, x.Name()); err != nil { 99 | return b, "", err 100 | } 101 | } else { 102 | // Add other fields 103 | if fw, err = form.CreateFormField(key); err != nil { 104 | return b, "", err 105 | } 106 | } 107 | if _, err = io.Copy(fw, r); err != nil { 108 | return b, "", err 109 | } 110 | 111 | } 112 | form.Close() 113 | return b, form.FormDataContentType(), nil 114 | } 115 | -------------------------------------------------------------------------------- /handlers.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "log" 8 | "net/http" 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | 13 | "gocv.io/x/gocv" 14 | ) 15 | 16 | type imageScoringResult struct { 17 | AppVersion string `json:"app_version"` 18 | OpenNsfwScore float32 `json:"open_nsfw_score"` 19 | AnAlgorithmForNudityDetection bool `json:"an_algorithm_for_nudity_detection"` 20 | ImageName string `json:"image_name"` 21 | } 22 | 23 | type testResult struct { 24 | OpenNsfwScore float32 `json:"open_nsfw_score,omitempty"` 25 | Nudity bool `json:"an_algorithm_for_nudity_detection"` 26 | } 27 | 28 | type pdfResponse struct { 29 | AppVersion string `json:"app_version"` 30 | Result map[string]testResult `json:"result"` 31 | NudityDetectionDisabled bool `json:"nudity_detection_disabled"` 32 | ImageScoringDisabled bool `json:"image_scoring_disabled"` 33 | ImageName string `json:"image_name"` 34 | AlgorithmForNudityDetection bool `json:"an_algorithm_for_nudity_detection"` 35 | OpenNsfwScore float32 `json:"open_nsfw_score,omitempty"` 36 | } 37 | 38 | func proceedPDF(w http.ResponseWriter, r *http.Request) { 39 | dir, err := ioutil.TempDir(os.TempDir(), "adult-image-detector-*-pdf") 40 | if err != nil { 41 | HandleError(w, err) 42 | return 43 | } 44 | parsedForm, err := uploadFileFormHandler(r, dir, "pdf") 45 | if err != nil { 46 | HandleError(w, err) 47 | return 48 | } 49 | 50 | if parsedForm.FileExt != strings.ToLower("pdf") { 51 | HandleError(w, fmt.Errorf("bad request. Invalid file type")) 52 | return 53 | } 54 | 55 | images, dir, err := getImagesFromPDF(parsedForm.FilePath, dir, parsedForm.password) 56 | if err != nil { 57 | HandleError(w, err) 58 | return 59 | } 60 | 61 | body := pdfResponse{ 62 | AppVersion: VERSION, 63 | ImageScoringDisabled: parsedForm.disableOpenNsfw, 64 | NudityDetectionDisabled: parsedForm.disableAnAlgorithm, 65 | ImageName: parsedForm.Filename, 66 | } 67 | 68 | var res = make(map[string]testResult) 69 | 70 | protoPath, _ := filepath.Abs("./models/open_nsfw/nsfw_model/deploy.prototxt") 71 | modelPath, _ := filepath.Abs("./models/open_nsfw/nsfw_model/resnet_50_1by2_nsfw.caffemodel") 72 | 73 | net := gocv.ReadNetFromCaffe( 74 | protoPath, 75 | modelPath, 76 | ) 77 | if net.Empty() { 78 | HandleError(w, err) 79 | return 80 | } 81 | 82 | defer net.Close() 83 | 84 | for _, v := range images { 85 | var r testResult 86 | // r.ImageName = v 87 | if !parsedForm.disableOpenNsfw { 88 | openNsfwScore, err := getOpenNsfwScore(v, net) 89 | if err != nil { 90 | continue 91 | } 92 | 93 | if body.OpenNsfwScore < openNsfwScore { 94 | body.OpenNsfwScore = openNsfwScore 95 | } 96 | 97 | r.OpenNsfwScore = openNsfwScore 98 | } 99 | 100 | if !parsedForm.disableAnAlgorithm { 101 | nudity, err := getAnAlgorithmForNudityDetectionResult(v, parsedForm.debug) 102 | if err != nil { 103 | continue 104 | } 105 | 106 | if nudity { 107 | body.AlgorithmForNudityDetection = nudity 108 | } 109 | 110 | r.Nudity = nudity 111 | } 112 | 113 | res[filepath.Base(v)] = r 114 | } 115 | 116 | body.Result = res 117 | 118 | // remove uploaded file 119 | removeFile(parsedForm.FilePath) 120 | removeFile(dir) 121 | 122 | // serialize answer 123 | out, err := json.Marshal(body) 124 | if err != nil { 125 | HandleError(w, err) 126 | return 127 | } 128 | 129 | w.Header().Set("Content-Type", "application/json") 130 | w.Write(out) 131 | } 132 | 133 | // save uploaded image and get scoring 134 | func proceedImage(w http.ResponseWriter, r *http.Request) { 135 | dir, err := ioutil.TempDir(os.TempDir(), "adult-image-detector-*-image") 136 | if err != nil { 137 | HandleError(w, err) 138 | return 139 | } 140 | 141 | defer removeFile(dir) 142 | 143 | // save image and get options 144 | parsedForm, err := uploadFileFormHandler(r, dir, "image") 145 | if err != nil { 146 | HandleError(w, err) 147 | return 148 | } 149 | 150 | log.Printf("Uploaded file %s, saved as %s", parsedForm.Filename, parsedForm.SaveAsFilename) 151 | 152 | res := imageScoringResult{ 153 | ImageName: parsedForm.Filename, 154 | AppVersion: VERSION, 155 | } 156 | 157 | protoPath, _ := filepath.Abs("./models/open_nsfw/nsfw_model/deploy.prototxt") 158 | modelPath, _ := filepath.Abs("./models/open_nsfw/nsfw_model/resnet_50_1by2_nsfw.caffemodel") 159 | 160 | net := gocv.ReadNetFromCaffe( 161 | protoPath, 162 | modelPath, 163 | ) 164 | if net.Empty() { 165 | HandleError(w, err) 166 | return 167 | } 168 | 169 | defer net.Close() 170 | 171 | if parsedForm.disableOpenNsfw == false { 172 | // get yahoo open nfsw score 173 | openNsfwScore, err := getOpenNsfwScore(parsedForm.FilePath, net) 174 | if err != nil { 175 | HandleError(w, err) 176 | return 177 | } 178 | res.OpenNsfwScore = openNsfwScore 179 | 180 | log.Printf("For file %s, openNsfwScore=%f", parsedForm.SaveAsFilename, openNsfwScore) 181 | } 182 | 183 | if parsedForm.disableAnAlgorithm == false { 184 | // get An Algorithm for Nudity Detection 185 | anAlgorithmForNudityDetection, err := getAnAlgorithmForNudityDetectionResult(parsedForm.FilePath, parsedForm.debug) 186 | if err != nil { 187 | HandleError(w, err) 188 | return 189 | } 190 | res.AnAlgorithmForNudityDetection = anAlgorithmForNudityDetection 191 | 192 | log.Printf("For file %s, anAlgorithmForNudityDetection=%t", parsedForm.SaveAsFilename, anAlgorithmForNudityDetection) 193 | } 194 | 195 | // remove uploaded file 196 | removeFile(parsedForm.FilePath) 197 | 198 | // serialize answer 199 | out, err := json.Marshal(res) 200 | if err != nil { 201 | HandleError(w, err) 202 | return 203 | } 204 | 205 | w.Header().Set("Content-Type", "application/json") 206 | w.Write(out) 207 | } 208 | 209 | // HandleError handles http error. 210 | func HandleError(w http.ResponseWriter, err error) { 211 | w.Header().Set("Content-Type", "text/html; charset=utf-8") 212 | w.Header().Set("X-Content-Type-Options", "nosniff") 213 | w.WriteHeader(500) 214 | fmt.Fprintln(w, err.Error()) 215 | } 216 | 217 | // ShowImageForm to upload jpg image. 218 | func ShowImageForm(w http.ResponseWriter) { 219 | form := ` 220 | 221 | 222 |
223 | 224 |
225 | 226 |
229 |
232 | 233 | 234 |
235 |
236 | curl -i -X POST -F "image=@Daddy_Lets_Me_Ride_His_Cock_preview_720p.mp4.jpg" http://localhost:9191/api/v1/detect
237 | 		
238 | 239 | 240 | ` 241 | w.Write([]byte(form)) 242 | } 243 | 244 | // ShowPDFForm to upload pdf file. 245 | func ShowPDFForm(w http.ResponseWriter) { 246 | form := ` 247 | 248 | 249 |
250 | 251 |
252 | 253 |
256 |
259 |
262 |
265 | 266 | 267 |
268 |
269 | curl -i -X POST -F "image=@Daddy_Lets_Me_Ride_His_Cock_preview_720p.mp4.pdf" http://localhost:9191/api/v1/pdf_detect
270 | 		
271 | 272 | 273 | ` 274 | w.Write([]byte(form)) 275 | } 276 | -------------------------------------------------------------------------------- /anAlgorithm.go: -------------------------------------------------------------------------------- 1 | // An Algorithm for Nudity Detection 2 | // by Rigan Ap-apid 3 | // http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.96.9872&rep=rep1&type=pdf 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "image" 9 | "image/color" 10 | "image/draw" 11 | "sort" 12 | 13 | "gocv.io/x/gocv" 14 | ) 15 | 16 | const ( 17 | skinCbMin = 80 18 | skinCbMax = 120 19 | skinCrMin = 133 20 | skinCrMax = 173 21 | ) 22 | 23 | type anAlgorithm struct { 24 | img image.Image 25 | height int 26 | width int 27 | skinMap image.Image 28 | backgroundPixelCount int 29 | skinPixelCount int 30 | regions []anAlgorithmRegion 31 | regionsContours [][]image.Point 32 | boundsPoly anAlgorithmBoundsPolygon 33 | debug bool 34 | } 35 | 36 | type anAlgorithmRegion struct { 37 | area float64 38 | contour []image.Point 39 | } 40 | 41 | type anAlgorithmBoundsPolygon struct { 42 | area float64 43 | contour []image.Point 44 | hue float64 45 | skinPixels []color.Color 46 | skinPixelCount int 47 | avgSkinsIntensity float64 48 | image image.Image 49 | height int 50 | width int 51 | } 52 | 53 | // find skins on image and create mask with white/black regions 54 | // do manually, becouse have some trouble with gocv.InRange https://github.com/hybridgroup/gocv/issues/159 55 | func (a *anAlgorithm) maskSkinAndCountSkinPixels() { 56 | upLeft := image.Point{0, 0} 57 | lowRight := image.Point{a.width, a.height} 58 | 59 | a.skinMap = image.NewRGBA(image.Rectangle{upLeft, lowRight}) 60 | 61 | black := color.RGBA{0, 0, 0, 0xff} 62 | white := color.RGBA{255, 255, 255, 0xff} 63 | 64 | a.backgroundPixelCount = 0 65 | a.skinPixelCount = 0 66 | 67 | var drawPixCol color.RGBA 68 | for x := 0; x < a.width; x++ { 69 | for y := 0; y < a.height; y++ { 70 | pixCol := a.img.At(x, y) 71 | 72 | drawPixCol = white 73 | if a.yCbCrSkinDetector(pixCol) == true { 74 | a.skinPixelCount++ 75 | drawPixCol = black 76 | } else { 77 | a.backgroundPixelCount++ 78 | } 79 | 80 | a.skinMap.(draw.Image).Set(x, y, drawPixCol) 81 | } 82 | } 83 | 84 | // out, _ := os.Create("uploads/0.maskSkinAndCountSkinPixels.jpg") 85 | // jpeg.Encode(out, a.skinMap, nil) 86 | } 87 | 88 | func (a *anAlgorithm) findRegions() { 89 | img, _ := imageToRGB8Mat(a.skinMap) 90 | 91 | mask := gocv.NewMat() 92 | gocv.InRangeWithScalar(img, gocv.NewScalar(0.0, 0.0, 0.0, 0.0), gocv.NewScalar(1.0, 1.0, 1.0, 0.0), &mask) 93 | 94 | contours := gocv.FindContours(mask, gocv.RetrievalList, gocv.ChainApproxNone) 95 | a.regionsContours = contours 96 | 97 | for i := range contours { 98 | a.regions = append(a.regions, anAlgorithmRegion{ 99 | area: gocv.ContourArea(contours[i]), 100 | contour: contours[i], 101 | }) 102 | } 103 | 104 | sort.Slice(a.regions, func(i, j int) bool { 105 | return a.regions[i].area > a.regions[j].area 106 | }) 107 | 108 | // if we havent 2rd region 109 | if len(a.regions) == 1 { 110 | a.regions = append(a.regions, a.regions[0]) 111 | } 112 | 113 | // if we havent 3rd region 114 | if len(a.regions) == 2 { 115 | a.regions = append(a.regions, a.regions[1]) 116 | } 117 | } 118 | 119 | // Identify the leftmost, the uppermost, the rightmost, 120 | // and the lowermost skin pixels of the three largest 121 | // skin regions. Use these points as the corner points 122 | func (a *anAlgorithm) findBoundsPolyCorners() { 123 | leftmost := a.width 124 | uppermost := a.height 125 | rightmost := 0 126 | lowermost := 0 127 | 128 | for i, region := range a.regions { 129 | if i > 2 { 130 | break 131 | } 132 | 133 | // find corners 134 | for _, p := range region.contour { 135 | if p.X < leftmost { 136 | leftmost = p.X 137 | } 138 | if p.X > rightmost { 139 | rightmost = p.X 140 | } 141 | if p.Y < uppermost { 142 | uppermost = p.Y 143 | } 144 | if p.Y > lowermost { 145 | lowermost = p.Y 146 | } 147 | } 148 | } 149 | 150 | width := rightmost - leftmost 151 | height := lowermost - uppermost 152 | 153 | a.boundsPoly = anAlgorithmBoundsPolygon{ 154 | area: float64(width * height), 155 | contour: []image.Point{ 156 | {X: leftmost, Y: uppermost}, 157 | {X: rightmost, Y: lowermost}, 158 | }, 159 | skinPixelCount: 0, 160 | } 161 | 162 | upLeft := image.Point{0, 0} 163 | lowRight := image.Point{width, height} 164 | a.boundsPoly.image = image.NewRGBA(image.Rectangle{upLeft, lowRight}) 165 | } 166 | 167 | // create poly from images, cacl pixel count 168 | // and save pixels fro find avg in we need it 169 | func (a *anAlgorithm) createBoundsPolyAndCalcSkins() { 170 | 171 | xBig := 0 172 | yBig := 0 173 | 174 | for x := a.boundsPoly.contour[0].X; x < a.boundsPoly.contour[1].X; x++ { 175 | for y := a.boundsPoly.contour[0].Y; y < a.boundsPoly.contour[1].Y; y++ { 176 | pixCol := a.img.At(x, y) 177 | 178 | if a.yCbCrSkinDetector(pixCol) == true { 179 | a.boundsPoly.skinPixelCount++ 180 | a.boundsPoly.skinPixels = append(a.boundsPoly.skinPixels, pixCol) 181 | } 182 | 183 | a.boundsPoly.image.(draw.Image).Set(xBig, yBig, pixCol) 184 | 185 | yBig++ 186 | } 187 | yBig = 0 188 | xBig++ 189 | } 190 | 191 | // out, _ := os.Create("uploads/5.createBoundsPolyAndCalcSkins.jpg") 192 | // jpeg.Encode(out, a.boundsPoly.image, nil) 193 | } 194 | 195 | // detect is skin 196 | func (a *anAlgorithm) yCbCrSkinDetector(pixCol color.Color) bool { 197 | _, cb, cr := rgbaToYCbCr(pixCol) 198 | 199 | return cb >= skinCbMin && cb <= skinCbMax && cr >= skinCrMin && cr <= skinCrMax 200 | } 201 | 202 | // find avg intensity in boundsPoly skins region 203 | func (a *anAlgorithm) findAverageSkinsIntensityInBoundsPoly() { 204 | 205 | a.boundsPoly.avgSkinsIntensity = 0 206 | 207 | skinsLen := len(a.boundsPoly.skinPixels) 208 | if skinsLen == 0 { 209 | return 210 | } 211 | 212 | var ( 213 | cbSum int 214 | crSum int 215 | ) 216 | 217 | for i := 0; i < skinsLen; i++ { 218 | _, cb, cr := rgbaToYCbCr(a.boundsPoly.skinPixels[i]) 219 | 220 | cbSum += cb 221 | crSum += cr 222 | } 223 | 224 | avgColorVal := float64((cbSum + crSum) / skinsLen) 225 | if avgColorVal == 0 { 226 | return 227 | } 228 | 229 | a.boundsPoly.avgSkinsIntensity = float64(skinCbMax-skinCbMin+skinCrMax-skinCrMin) / avgColorVal 230 | } 231 | 232 | // return is nude image or not 233 | func (a *anAlgorithm) IsNude() (bool, error) { 234 | a.width = a.img.Bounds().Max.X 235 | a.height = a.img.Bounds().Max.Y 236 | 237 | a.maskSkinAndCountSkinPixels() 238 | 239 | totalPixelCount := a.skinPixelCount + a.backgroundPixelCount 240 | 241 | totalSkinPortion := float32(a.skinPixelCount) / float32(totalPixelCount) 242 | 243 | if totalPixelCount == 0 { 244 | if a.debug { 245 | fmt.Println("No pixels found") 246 | } 247 | return false, nil 248 | } 249 | 250 | // Criteria (a) 251 | if a.debug { 252 | fmt.Println("a: totalSkinPortion=", totalSkinPortion, " < 0.15") 253 | } 254 | if totalSkinPortion < 0.15 { 255 | return false, nil 256 | } 257 | 258 | a.findRegions() 259 | largestRegionPortion := 0.0 260 | nextRegionPortion := 0.0 261 | thirdRegionPortion := 0.0 262 | 263 | if len(a.regions) > 0 { 264 | largestRegionPortion = a.regions[0].area / float64(a.skinPixelCount) 265 | nextRegionPortion = a.regions[1].area / float64(a.skinPixelCount) 266 | thirdRegionPortion = a.regions[2].area / float64(a.skinPixelCount) 267 | } 268 | 269 | // Criteria (b) 270 | if a.debug { 271 | fmt.Println("b: largestRegionPortion=", largestRegionPortion, " < 0.35 && nextRegionPortion=", nextRegionPortion, " < 0.30 && thirdRegionPortion=", thirdRegionPortion, " < 0.30") 272 | } 273 | if largestRegionPortion < 0.35 && nextRegionPortion < 0.30 && thirdRegionPortion < 0.30 { 274 | return false, nil 275 | } 276 | 277 | // Criteria (c) 278 | if a.debug { 279 | fmt.Println("c: largestRegionPortion=", largestRegionPortion, " < 0.45") 280 | } 281 | if largestRegionPortion < 0.45 { 282 | return false, nil 283 | } 284 | 285 | // Criteria (d) 286 | a.findBoundsPolyCorners() 287 | a.createBoundsPolyAndCalcSkins() 288 | 289 | if a.debug { 290 | fmt.Println("d: totalSkinPortion=", totalSkinPortion, " < 0.30") 291 | } 292 | if totalSkinPortion < 0.30 { 293 | boundsPolySkinPortion := float64(a.boundsPoly.skinPixelCount) / a.boundsPoly.area 294 | 295 | if a.debug { 296 | fmt.Println("d: boundsPolySkinPortion=", boundsPolySkinPortion, " < 0.55") 297 | } 298 | if boundsPolySkinPortion < 0.55 { 299 | return false, nil 300 | } 301 | } 302 | 303 | // Criteria (e) 304 | if a.debug { 305 | fmt.Println("e: len(a.regions)=", len(a.regions), " > 60") 306 | } 307 | if len(a.regions) > 60 { 308 | a.findAverageSkinsIntensityInBoundsPoly() 309 | 310 | if a.debug { 311 | fmt.Println("e: boundsPoly.avgSkinsIntensity=", a.boundsPoly.avgSkinsIntensity, " < 0.25") 312 | } 313 | if a.boundsPoly.avgSkinsIntensity < 0.25 { 314 | return false, nil 315 | } 316 | } 317 | 318 | return true, nil 319 | } 320 | --------------------------------------------------------------------------------