├── .bluemix ├── Dockerfile ├── deploy.json ├── icon.svg ├── pipeline.yml ├── toolchain.svg └── toolchain.yml ├── .gitignore ├── .travis.yml ├── Dockerfile ├── README.md ├── app.js ├── bin └── www ├── detected_images └── output.jpg ├── error.html ├── kubernetes ├── kube-config.yml ├── local-volumes.yaml └── postgres.yaml ├── package-lock.json ├── package.json ├── public └── stylesheets │ └── style.css ├── routes ├── index.js └── users.js ├── sample_videos └── vid10.mp4 ├── scripts ├── bx_auth.sh ├── cv_object_detection.py ├── deploy.sh ├── install.sh ├── object_detection.cpp ├── quickstart.sh └── resources.sh └── views ├── error.jade ├── error.pug ├── index.html ├── index.jade ├── index.pug ├── layout.jade └── layout.pug /.bluemix/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM kkbankol/opencv_yolo 2 | ADD . /opt/cameras_app 3 | ADD bin/example_dnn_object_detection /nodejsAction/cv/example_dnn_object_detection 4 | RUN cd /opt/cameras_app && rm -rf node_modules && npm install 5 | RUN cp /nodejsAction/cv/object_detection_classes_yolov3.txt /tmp/object_detection_classes_yolov3.txt 6 | CMD cd /opt/cameras_app && npm start 7 | -------------------------------------------------------------------------------- /.bluemix/deploy.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "title": "Sample Deploy Stage", 4 | "description": "sample toolchain", 5 | "longDescription": "The Delivery Pipeline automates continuous deployment.", 6 | "type": "object", 7 | "properties": { 8 | "prod-region": { 9 | "description": "The bluemix region", 10 | "type": "string" 11 | }, 12 | "prod-organization": { 13 | "description": "The bluemix org", 14 | "type": "string" 15 | }, 16 | "prod-space": { 17 | "description": "The bluemix space", 18 | "type": "string" 19 | }, 20 | "prod-app-name": { 21 | "description": "The name of your Drupal app", 22 | "type": "string" 23 | }, 24 | "bluemix-user": { 25 | "description": "Your Bluemix user ID", 26 | "type": "string" 27 | }, 28 | "bluemix-password": { 29 | "description": "Your Bluemix Password", 30 | "type": "string" 31 | }, 32 | "bluemix-api-key": { 33 | "description": "Required for **Federated ID** since Federated ID can't login with Bluemix user and password via Bluemix CLI. You can obtain your API_KEY via https://console.ng.bluemix.net/iam/#/apikeys by clicking **Create API key** (Each API key only can be viewed once).", 34 | "type": "string" 35 | }, 36 | "bluemix-cluster-account": { 37 | "description": "The GUID of the Bluemix account where you created the cluster. Retrieve it with [bx iam accounts].", 38 | "type": "string" 39 | }, 40 | "bluemix-cluster-name": { 41 | "description": "Your cluster name. Retrieve it with [bx cs clusters].", 42 | "type": "string" 43 | } 44 | }, 45 | "required": ["prod-region", "prod-organization", "prod-space", "bluemix-cluster-name"], 46 | "anyOf": [ 47 | { 48 | "required": ["bluemix-user", "bluemix-password", "bluemix-cluster-account"] 49 | }, 50 | { 51 | "required": ["bluemix-api-key"] 52 | } 53 | ], 54 | "form": [ 55 | { 56 | "type": "validator", 57 | "url": "/devops/setup/bm-helper/helper.html" 58 | }, 59 | { 60 | "type": "text", 61 | "readonly": false, 62 | "title": "Bluemix User ID", 63 | "key": "bluemix-user" 64 | },{ 65 | "type": "password", 66 | "readonly": false, 67 | "title": "Bluemix Password", 68 | "key": "bluemix-password" 69 | },{ 70 | "type": "password", 71 | "readonly": false, 72 | "title": "Bluemix API Key (Optional)", 73 | "key": "bluemix-api-key" 74 | },{ 75 | "type": "password", 76 | "readonly": false, 77 | "title": "Bluemix Cluster Account ID", 78 | "key": "bluemix-cluster-account" 79 | },{ 80 | "type": "text", 81 | "readonly": false, 82 | "title": "Bluemix Cluster Name", 83 | "key": "bluemix-cluster-name" 84 | }, 85 | { 86 | "type": "table", 87 | "columnCount": 4, 88 | "widths": ["15%", "28%", "28%", "28%"], 89 | "items": [ 90 | { 91 | "type": "label", 92 | "title": "" 93 | }, 94 | { 95 | "type": "label", 96 | "title": "Region" 97 | }, 98 | { 99 | "type": "label", 100 | "title": "Organization" 101 | }, 102 | { 103 | "type": "label", 104 | "title": "Space" 105 | }, 106 | { 107 | "type": "label", 108 | "title": "Production stage" 109 | }, 110 | { 111 | "type": "select", 112 | "key": "prod-region" 113 | }, 114 | { 115 | "type": "select", 116 | "key": "prod-organization" 117 | }, 118 | { 119 | "type": "select", 120 | "key": "prod-space", 121 | "readonly": false 122 | } 123 | ] 124 | } 125 | ] 126 | } 127 | -------------------------------------------------------------------------------- /.bluemix/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 24 | 25 | 26 | 27 | 28 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /.bluemix/pipeline.yml: -------------------------------------------------------------------------------- 1 | --- 2 | stages: 3 | - name: Build 4 | inputs: 5 | - type: git 6 | branch: master 7 | # service: ${SAMPLE_REPO} 8 | triggers: 9 | - type: commit 10 | jobs: 11 | - name: Build 12 | type: builder 13 | artifact_dir: '' 14 | build_type: shell 15 | script: |- 16 | #!/bin/bash 17 | bash -n *.sh 18 | - name: Deploy 19 | inputs: 20 | - type: job 21 | stage: Build 22 | job: Build 23 | dir_name: null 24 | triggers: 25 | - type: stage 26 | properties: 27 | - name: BLUEMIX_USER 28 | type: text 29 | value: ${BLUEMIX_USER} 30 | - name: BLUEMIX_PASSWORD 31 | type: secure 32 | value: ${BLUEMIX_PASSWORD} 33 | - name: BLUEMIX_ACCOUNT 34 | type: secure 35 | value: ${BLUEMIX_ACCOUNT} 36 | - name: CLUSTER_NAME 37 | type: text 38 | value: ${CLUSTER_NAME} 39 | - name: API_KEY 40 | type: secure 41 | value: ${API_KEY} 42 | jobs: 43 | - name: Deploy 44 | type: deployer 45 | target: 46 | region_id: ${PROD_REGION_ID} 47 | api_key: ${API_KEY} 48 | kubernetes_cluster: ${CLUSTER_NAME} 49 | application: Pipeline 50 | script: | 51 | #!/bin/bash 52 | ./scripts/deploy.sh 53 | # hooks: 54 | # - enabled: true 55 | # label: null 56 | # ssl_enabled: false 57 | # url: https://devops-api-integration.stage1.ng.bluemix.net/v1/messaging/webhook/publish 58 | -------------------------------------------------------------------------------- /.bluemix/toolchain.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 35 | 36 | background 37 | 38 | 39 | 40 | Layer 1 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | ISSUE TRACKER 239 | GitHub 240 | THINK 241 | CODE 242 | DELIVER 243 | RUN 244 | REPOSITORY 245 | GitHub 246 | PIPELINE 247 | BLUEMIX 248 | 249 | 250 | -------------------------------------------------------------------------------- /.bluemix/toolchain.yml: -------------------------------------------------------------------------------- 1 | # --- 2 | # name: "Deploy YOLO / Opencv object detection in Bluemix" 3 | # description: "Toolchain to deploy Opencv on Kubernetes in Bluemix" 4 | # version: 0.1 5 | # image: data:image/svg+xml;base64,$file(toolchain.svg,base64) 6 | # icon: data:image/svg+xml;base64,$file(icon.svg,base64) 7 | # required: 8 | # - deploy 9 | # - sample-repo 10 | # 11 | # # Github repos 12 | # sample-repo: 13 | # service_id: githubpublic 14 | # parameters: 15 | # repo_name: dnn-object-detection 16 | # repo_url: https://github.com/kkbankol-ibm/dnn-object-detection 17 | # type: clone 18 | # has_issues: false 19 | # 20 | # # Pipelines 21 | # sample-build: 22 | # service_id: pipeline 23 | # parameters: 24 | # name: "{{name}}" 25 | # ui-pipeline: true 26 | # configuration: 27 | # content: $file(pipeline.yml) 28 | # env: 29 | # SAMPLE_REPO: "sample-repo" 30 | # CF_APP_NAME: "{{deploy.parameters.prod-app-name}}" 31 | # PROD_SPACE_NAME: "{{deploy.parameters.prod-space}}" 32 | # PROD_ORG_NAME: "{{deploy.parameters.prod-organization}}" 33 | # PROD_REGION_ID: "{{deploy.parameters.prod-region}}" 34 | # BLUEMIX_USER: "{{deploy.parameters.bluemix-user}}" 35 | # BLUEMIX_PASSWORD: "{{deploy.parameters.bluemix-password}}" 36 | # API_KEY: "{{deploy.parameters.bluemix-api-key}}" 37 | # BLUEMIX_ACCOUNT: "{{deploy.parameters.bluemix-cluster-account}}" 38 | # CLUSTER_NAME: "{{deploy.parameters.bluemix-cluster-name}}" 39 | # execute: true 40 | # services: ["sample-repo"] 41 | # hidden: [form, description] 42 | # 43 | # # Deployment 44 | # deploy: 45 | # schema: 46 | # $ref: deploy.json 47 | # service-category: pipeline 48 | # parameters: 49 | # prod-app-name: "{{sample-repo.parameters.repo_name}}" 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | uploads 3 | node_modules/ 4 | motion_images 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: python 3 | python: 2.7 4 | cache: pip 5 | sudo: required 6 | 7 | env: 8 | - TEST_RUN="./tests/test-minikube.sh" 9 | - TEST_RUN="./tests/test-kubernetes.sh" 10 | 11 | before_install: 12 | - sudo apt-get install shellcheck 13 | - pip install -U -r test-requirements.txt 14 | 15 | install: 16 | - "./scripts/install.sh" 17 | - "./scripts/bx_auth.sh" 18 | 19 | before_script: 20 | - "./tests/test-shellcheck.sh" 21 | - "./tests/test-yamllint.sh" 22 | 23 | script: "$TEST_RUN" 24 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM kkbankol/opencv_yolo_pod 2 | RUN rm -rf /opt/cameras_app/* 3 | COPY . /opt/cameras_app/ 4 | #git clone 5 | COPY bin/example_dnn_object_detection /nodejsAction/cv/example_dnn_object_detection 6 | RUN cd /opt/cameras_app && rm -rf node_modules && npm install 7 | RUN cp /nodejsAction/cv/object_detection_classes_yolov3.txt /tmp/object_detection_classes_yolov3.txt 8 | RUN bash -c 'curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.11/install.sh | bash' 9 | # RUN cat /root/.bash_profile 10 | COPY .bash_profile_nvm /root/.bash_profile 11 | COPY .bashrc /root/.bashrc 12 | 13 | #RUN echo '# Place next three lines in ~/.bash_profile \ 14 | #export NVM_DIR="$HOME/.nvm" \ 15 | #[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm \ 16 | #[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"' >> /root/.bash_profile 17 | RUN bash -c "source /root/.bash_profile && \ 18 | nvm install v8.9.0 && \ 19 | nvm use 8.9.0 && \ 20 | nvm alias default 8.9.0" 21 | RUN bash -c "source /root/.bash_profile && cd /opt/cameras_app && rm -rf node_modules && nvm use 8.9.0 && npm install" 22 | CMD bash -c "source /root/.bash_profile && nvm use 8.9.0 && npm start --prefix /opt/cameras_app" 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | # Analyze real time CCTV images with Convolutional Neural Networks 6 | In this Code Pattern, we will deploy an application that will leverage neural networking models to analyze RTSP (Real Time Streaming Protocol) video streams using with OpenCV / Darknet. 7 | 8 | There are many surveillance cameras that have been installed, but cannot be closely monitored throughout the day. Since events are more likely to occur while the operator is not watching, many significant events go undetected, even when they are recorded. Users can't be expected to trace through hours of video footage, especially if they're not sure what they're looking for. 9 | 10 | This project aims to alleviate this problem by using deep learning algorithms to detect movement, and identify objects in a video feed. These algorithms can be applied to both live streams and previously recorded video. After each video frame has been analyzed, the labeled screenshot and corresponding metadata are also uploaded to a Cloudant database. This allows for an operator to invoke complex queries and run analytics against the collected data. A few example queries might be 11 | - Select all screenshots in which a person was detected at camera 3 during the previous Monday 12 | - Get total count of cars detected last Saturday 13 | 14 | When the reader has completed this Code Pattern, they will understand how to: 15 | * Connect to a RTSP video stream via Python + Opencv 16 | * Use Opencv and Numpy to process video frames and determine when significant motion has occured 17 | * Identify objects in a photograph or video using a pre-built Deep Learning model 18 | 19 | 20 | 21 |

22 | 23 | 24 |

25 | 26 | ## Flow 27 | 1. Connect a motion detection script to a RTSP stream or video file 28 | 2. If motion is detected, capture screenshot and forward to Node.js server hosted locally or in IBM Cloud container service 29 | 3. Analyze screenshot using Darknet / YOLO object detection algorithm 30 | 4. Upload labeled screenshot and associated metadata (time, camera channel) to Cloudant database 31 | 32 | 33 | 34 | 36 | 37 | [![Deploy to IBM Cloud](https://bluemix.net/deploy/button.png)](https://bluemix.net/deploy?repository=https://github.com/kkbankol-ibm/dnn-object-detection.git&branch=master) 38 | 39 | ## Install Prerequisites: 40 | ### IBM Cloud CLI (Hosted) 41 | To interact with the hosted offerings, the IBM Cloud CLI will need to be installed beforehand. The latest CLI releases can be found at the link [here](https://console.bluemix.net/docs/cli/reference/bluemix_cli/download_cli.html#download_install). An install script is maintained at the mentioned link, which can be executed with one of the following commands 42 | 43 | ``` 44 | # Mac OSX 45 | curl -fsSL https://clis.ng.bluemix.net/install/osx | sh 46 | 47 | # Linux 48 | curl -fsSL https://clis.ng.bluemix.net/install/linux | sh 49 | 50 | # Powershell 51 | iex(New-Object Net.WebClient).DownloadString('https://clis.ng.bluemix.net/install/powershell') 52 | ``` 53 | After installation is complete, confirm the CLI is working by printing the version like so 54 | ``` 55 | bx -v 56 | ``` 57 | 58 | ### Kubernetes CLI (Hosted) 59 | 60 | *Linux* 61 | ``` 62 | sudo apt-get update && sudo apt-get install -y apt-transport-https 63 | curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add - 64 | echo "deb http://apt.kubernetes.io/ kubernetes-xenial main" | sudo tee -a /etc/apt/sources.list.d/kubernetes.list 65 | sudo apt-get update 66 | sudo apt-get install -y kubectl 67 | ``` 68 | 69 | *MacOS* 70 | ``` 71 | brew install kubernetes-cli 72 | ``` 73 | 74 | ### Node.js + NPM (Local) 75 | If expecting to run this application locally, please continue by installing [Node.js](https://nodejs.org/en/) runtime and NPM. We'd suggest using [nvm](https://github.com/creationix/nvm) to easily switch between node versions, which can be done with the following commands 76 | ``` 77 | curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.11/install.sh | bash 78 | # Place next three lines in ~/.bash_profile 79 | export NVM_DIR="$HOME/.nvm" 80 | [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm 81 | [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion 82 | nvm install v8.9.0 83 | nvm use 8.9.0 84 | ``` 85 | 86 | 88 | 89 | # Steps 90 | Use the ``Deploy to IBM Cloud`` instructions **OR** create the services and run locally. 91 | 92 | 93 | 94 | ## Included components 95 | * [Cloudant DB](https://console.bluemix.net/catalog/services/blockchain) 96 | * [Kubernetes](https://console.bluemix.net/containers-kubernetes/catalog/cluster) 97 | 98 | 99 | ## Featured technologies 100 | 101 | * [NPM](https://www.npmjs.com/) 102 | * [Node.js](https://nodejs.org/en/) 103 | * [Darknet / YOLO](https://pjreddie.com/darknet/yolo/) 104 | * [OpenCV](https://github.com/opencv/opencv) 105 | 106 | 107 | To begin setting up this project, the Node.js backend will need to be deployed first. After setting up the node.js server, continue by [setting up the Raspberry Pi / Linux client](#set-up-the-raspberry-pi-client). 108 | 109 | ## Deploy backend server to IBM Cloud 110 | 111 | 112 | 113 | 114 | 115 | 116 | 118 | 119 | 120 | 1. Use the IBM Cloud dashboard to create the following services (The free or "lite" version will suffice for both services) 121 | * [Cloudant-db](https://console.bluemix.net/catalog/services/cloudant) 122 | * [Kubernetes](https://console.bluemix.net/containers-kubernetes/catalog/cluster) 123 | 124 | 2. Install the IBM Cloud CLI on your development system using the following [instructions](https://console.bluemix.net/docs/containers/cs_cli_install.html) 125 | 126 | 3. Export the `KUBECONFIG` path. This should be presented just after creating the container cluster 127 | ``` 128 | export KUBECONFIG=/Users/$USER/.bluemix/plugins/container-service/clusters/mycluster/kube-config-hou02-mycluster.yml 129 | ``` 130 | 131 | 4. Store Cloudant Credentials 132 | > Store the **CLOUDANT_USERNAME**, **CLOUDANT_PASSWORD**, and **CLOUDANT_DB** into a `.env` file 133 | ``` 134 | CLOUDANT_USERNAME="username" 135 | CLOUDANT_PASSWORD="password" 136 | CLOUDANT_DB="imagedb" 137 | ``` 138 | 139 | Run the following kubectl command. This will generate a "secret", allowing for the Cloudant credentials to be accessible as a environment 140 | ``` 141 | source .env 142 | kubectl create secret generic cloudant-auth --from-literal=CLOUDANT_USERNAME=${CLOUDANT_USERNAME} --from-literal=CLOUDANT_PASSWORD=${CLOUDANT_PASSWORD} --from-literal=CLOUDANT_DB=${CLOUDANT_DB} 143 | ``` 144 | 145 | 146 | 147 | 148 | 5. Deploy the kubernetes application with the following command 149 | ``` 150 | kubectl apply -f kubernetes/kube-config.yml 151 | ``` 152 | 153 | 154 | To access the pod filesystem and confirm the credentials have been imported correctly, run the following commands (Optional) 155 | ``` 156 | kubectl get pods 157 | kubectl exec -it dnn-pod bash 158 | # print environment vars in pod/container, filter by cloudant 159 | env | grep -i cloudant 160 | ``` 161 | 162 | After confirming the Cloudant credentials are accessible as environment variables, tail the app logs with the following (Optional) 163 | ``` 164 | kubectl logs -f dnn-pod 165 | ``` 166 | 167 | 6. Find the public ip address of the Kubernetes cluster 168 | ``` 169 | # Get id of cluster 170 | ibmcloud ks clusters 171 | 172 | # Print workers associated with cluster, take note of public ip. Default name is "mycluster" 173 | ibmcloud ks workers 174 | ``` 175 | 176 | 7. Confirm that the Node.js backend is up and running 177 | ``` 178 | curl :30000/status 179 | ``` 180 | 181 | ## Deploy backend server locally 182 | 183 | If Docker is installed on your system, running the following command will start the backend service locally instead 184 | ``` 185 | docker run -d -p 30000:30000 -e CLOUDANT_USERNAME=${CLOUDANT_USERNAME} -e CLOUDANT_PASSWORD=${CLOUDANT_PASSWORD} -e CLOUDANT_DB=${CLOUDANT_DB} --name opencv_yolo kkbankol/opencv_yolo_pod 186 | ``` 187 | 188 | If Docker is not installed, continue with the following steps 189 | 1. [Clone the repo](#1-clone-the-repo) 190 | 2. [Create Watson services with IBM Cloud]() 191 | 3. [Configure credentials]() 192 | 4. [Install backend dependencies](#4-run-the-application) 193 | 5. [Start the backend node.js server](#4-run-the-application) 194 | 195 | ### 1. Clone the repo 196 | 197 | Clone the `dnn-object-detection` project locally. In a terminal, run: 198 | 199 | ``` 200 | $ git clone https://github.com/IBM/dnn-object-detection 201 | ``` 202 | 203 | ### 2. Create Watson services in IBM Cloud dashboard 204 | 205 | Create the following services: 206 | 207 | * [**Cloudant DB**](https://console.bluemix.net/catalog/services/cloudant) 208 | * [**Kubernetes**](https://console.bluemix.net/containers-kubernetes/catalog/cluster) (Hosted) 209 | 210 | ### 3. Configure credentials 211 | The credentials for IBM Cloud services (Cloudant DB), can be found in the ``Services`` menu in IBM Cloud by selecting the ``Service Credentials`` option for each service. 212 | > Store the **CLOUDANT_USERNAME**, **CLOUDANT_PASSWORD**, and **CLOUDANT_DB** into the `.env` file 213 | ``` 214 | CLOUDANT_USERNAME="username" 215 | CLOUDANT_PASSWORD="password" 216 | CLOUDANT_DB="imagedb" 217 | ``` 218 | 219 | ### 4. Install backend dependencies 220 | ``` 221 | npm install 222 | ``` 223 | 224 | ### 5. Start the backend Node.js server 225 | 1. Install [Node.js](https://nodejs.org/en/) runtime or NPM. 226 | 1. Start the app by running `npm install`, followed by `npm start`. 227 | > Note: server host can be changed as required in server.js and `PORT` can be set in `.env`. 228 | 229 | 230 | ``` 231 | Kalonjis-MacBook-Pro:dnn-object-detection kkbankol@us.ibm.com$ npm start 232 | 233 | > cameras-app@0.0.0 start /Users/kkbankol@us.ibm.com/projects/smart-city-cameras/dnn-object-detection 234 | > node ./bin/www 235 | ``` 236 | 237 | ## Set up the Raspberry Pi Client 238 | Now that we have a backend process up and running, we'll set up a device on the same local network as the CCTV cameras. The reasoning for this is that continuously pulling multiple video streams would be too demanding on the network bandwidth, and there would likely be latency issues. So as an alternative, we'll set up a Raspberry Pi on the same network as the CCTV system, and connect the two devices over the LAN instead. Any alternative Linux system should work. 239 | 240 | We'll start by installing a few dependencies for our "motion detection" script. Skip the packages that are already installed on your system 241 | ``` 242 | # Linux 243 | sudo apt-get update 244 | sudo apt-get install python -y 245 | sudo apt-get install ffmpeg -y 246 | 247 | # Mac OS 248 | brew install ffmpeg 249 | brew install python 250 | 251 | pip install numpy cv2 requests 252 | ``` 253 | 254 | Next, we can actually begin processing video that has either been pre-recorded or being live streamed. The script expects two arguments. The first argument is the video source (file or RTSP stream). The second argument is the ip address where images/metadata can be sent, this ip will either be 127.0.0.1 (if running node backend locally), or the public ip of the Kubernetes cluster. 255 | ``` 256 | # process pre-recorded video 257 | python scripts/cv_object_detection.py sample_videos/vid10.mp4 258 | 259 | # process live stream video 260 | python cv_object_detection.py rtsp://@192.168.1.2:8080/out.h264 261 | ``` 262 | 263 | Once this script begins, it'll iterate through each frame from the video source. As these iterations continue, the Opencv libary is used to calculate a "running average". Each frame gets compared to the running average, and if a significant difference is observed, the frame is then saved as an image and forwarded to the nodejs backend. 264 | 265 | As images are uploaded to the Node backend, they are then processed by the YOLO object detection algorithm, and labeled 266 |

267 | 268 |

269 | 270 | The labeled photo, identified classes, and metadata are then uploaded to Cloudant. Each Cloudant document is arranged like so 271 |

272 | 273 |

274 | 275 | 276 | 277 | # Troubleshooting 278 | 279 | 280 | 281 | # Sources 282 | - [YOLO](https://pjreddie.com/darknet/yolo/) 283 | - [OpenCV Motion Detection Script (Used for cv_object_detection.py)](https://github.com/iandees/speedtrack) 284 | 285 | 286 | # License 287 | [Apache 2.0](LICENSE) 288 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | var createError = require('http-errors'); 2 | var express = require('express'); 3 | var path = require('path'); 4 | var cookieParser = require('cookie-parser'); 5 | var logger = require('morgan'); 6 | 7 | var indexRouter = require('./routes/index'); 8 | var usersRouter = require('./routes/users'); 9 | 10 | // var multer = require('multer'); 11 | // var upload = multer({dest: './routes/uploads/'}); 12 | 13 | 14 | var app = express(); 15 | 16 | // view engine setup 17 | app.set('views', path.join(__dirname, 'views')); 18 | // app.set('view engine', 'jade'); 19 | app.engine('html', require('ejs').renderFile); 20 | // app.set('view engine', 'html'); 21 | app.set('view engine', 'html'); 22 | 23 | 24 | app.use(logger('dev')); 25 | app.use(express.json()); 26 | app.use(express.urlencoded({ extended: false })); 27 | app.use(cookieParser()); 28 | app.use(express.static(path.join(__dirname, './public'))); 29 | app.use(express.static(path.join(__dirname, '../images'))); 30 | // app.use(express.bodyParser()); 31 | 32 | app.use('/', indexRouter); 33 | app.use('/users', usersRouter); 34 | 35 | // catch 404 and forward to error handler 36 | app.use(function(req, res, next) { 37 | next(createError(404)); 38 | }); 39 | 40 | // error handler 41 | app.use(function(err, req, res, next) { 42 | // set locals, only providing error in development 43 | res.locals.message = err.message; 44 | res.locals.error = req.app.get('env') === 'development' ? err : {}; 45 | 46 | // render the error page 47 | res.status(err.status || 500); 48 | res.render('error'); 49 | }); 50 | 51 | 52 | // app.get('/post/:id', async (req, res, next) => { 53 | // app.get('/sql', async (req, res, next) => { 54 | // try { 55 | // const db = await dbPromise; 56 | // const [post, categories] = await Promise.all([ 57 | // db.get('SELECT * FROM Post WHERE id = ?', req.params.id), 58 | // db.all('SELECT * FROM Category') 59 | // ]); 60 | // // res.render('post', { post, categories }); 61 | // } catch (err) { 62 | // next(err); 63 | // } 64 | // }); 65 | 66 | 67 | // app.post('/test_image', upload.single('image'), function(request, respond) { 68 | // // console.dir(req.files) 69 | // // res.send(200) 70 | // // console.log(req.body) 71 | // if(request.file) console.log(request.file); 72 | // // console.log(req) 73 | // // console.log(Object.keys(req)) 74 | // // res.send(console.dir(req.files)); // DEBUG: display available fields 75 | // }) 76 | 77 | 78 | module.exports = app; 79 | -------------------------------------------------------------------------------- /bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var app = require('../app'); 8 | var debug = require('debug')('cameras-app:server'); 9 | var http = require('http'); 10 | 11 | /** 12 | * Get port from environment and store in Express. 13 | */ 14 | 15 | var port = normalizePort(process.env.PORT || '3000'); 16 | app.set('port', port); 17 | 18 | /** 19 | * Create HTTP server. 20 | */ 21 | 22 | var server = http.createServer(app); 23 | 24 | /** 25 | * Listen on provided port, on all network interfaces. 26 | */ 27 | 28 | server.listen(port); 29 | server.on('error', onError); 30 | server.on('listening', onListening); 31 | 32 | /** 33 | * Normalize a port into a number, string, or false. 34 | */ 35 | 36 | function normalizePort(val) { 37 | var port = parseInt(val, 10); 38 | 39 | if (isNaN(port)) { 40 | // named pipe 41 | return val; 42 | } 43 | 44 | if (port >= 0) { 45 | // port number 46 | return port; 47 | } 48 | 49 | return false; 50 | } 51 | 52 | /** 53 | * Event listener for HTTP server "error" event. 54 | */ 55 | 56 | function onError(error) { 57 | if (error.syscall !== 'listen') { 58 | throw error; 59 | } 60 | 61 | var bind = typeof port === 'string' 62 | ? 'Pipe ' + port 63 | : 'Port ' + port; 64 | 65 | // handle specific listen errors with friendly messages 66 | switch (error.code) { 67 | case 'EACCES': 68 | console.error(bind + ' requires elevated privileges'); 69 | process.exit(1); 70 | break; 71 | case 'EADDRINUSE': 72 | console.error(bind + ' is already in use'); 73 | process.exit(1); 74 | break; 75 | default: 76 | throw error; 77 | } 78 | } 79 | 80 | /** 81 | * Event listener for HTTP server "listening" event. 82 | */ 83 | 84 | function onListening() { 85 | var addr = server.address(); 86 | var bind = typeof addr === 'string' 87 | ? 'pipe ' + addr 88 | : 'port ' + addr.port; 89 | debug('Listening on ' + bind); 90 | } 91 | -------------------------------------------------------------------------------- /detected_images/output.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/dnn-object-detection/ef442ac569fff9d57923967dba6352cda3a1ca62/detected_images/output.jpg -------------------------------------------------------------------------------- /error.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/dnn-object-detection/ef442ac569fff9d57923967dba6352cda3a1ca62/error.html -------------------------------------------------------------------------------- /kubernetes/kube-config.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: api-service 5 | labels: 6 | srv: api 7 | spec: 8 | selector: 9 | srv: api 10 | type: NodePort 11 | ports: 12 | - port: 3000 13 | nodePort: 30000 14 | --- 15 | apiVersion: v1 16 | kind: Pod 17 | metadata: 18 | # srv: api 19 | labels: 20 | srv: api 21 | name: dnn-pod 22 | spec: 23 | # ports: 24 | # - port: 3000 25 | # protocol: TCP 26 | containers: 27 | - name: opencv-yolo 28 | image: kkbankol/opencv_yolo_pod 29 | imagePullPolicy: Always 30 | envFrom: 31 | - secretRef: 32 | name: cloudant-auth 33 | #env: 34 | # - name: CLOUDANT_USERNAME 35 | # value: ${CLOUDANT_USERNAME} 36 | # - name: CLOUDANT_PASSWORD 37 | # value: ${CLOUDANT_PASSWORD 38 | -------------------------------------------------------------------------------- /kubernetes/local-volumes.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: PersistentVolume 4 | metadata: 5 | name: local-volume-1 6 | labels: 7 | type: local 8 | spec: 9 | capacity: 10 | storage: 10Gi 11 | accessModes: 12 | - ReadWriteOnce 13 | hostPath: 14 | path: /tmp/data/pv-1 15 | persistentVolumeReclaimPolicy: Recycle 16 | --- 17 | apiVersion: v1 18 | kind: PersistentVolume 19 | metadata: 20 | name: local-volume-2 21 | labels: 22 | type: local 23 | spec: 24 | capacity: 25 | storage: 10Gi 26 | accessModes: 27 | - ReadWriteOnce 28 | hostPath: 29 | path: /tmp/data/pv-2 30 | persistentVolumeReclaimPolicy: Recycle 31 | --- 32 | apiVersion: v1 33 | kind: PersistentVolume 34 | metadata: 35 | name: local-volume-3 36 | labels: 37 | type: local 38 | spec: 39 | capacity: 40 | storage: 10Gi 41 | accessModes: 42 | - ReadWriteOnce 43 | hostPath: 44 | path: /tmp/data/pv-3 45 | persistentVolumeReclaimPolicy: Recycle 46 | -------------------------------------------------------------------------------- /kubernetes/postgres.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: postgresql 6 | labels: 7 | app: drupal 8 | spec: 9 | ports: 10 | - port: 5432 11 | selector: 12 | app: drupal 13 | tier: postgreSQL 14 | --- 15 | apiVersion: v1 16 | kind: PersistentVolumeClaim 17 | metadata: 18 | name: postgres-claim 19 | labels: 20 | app: drupal 21 | spec: 22 | accessModes: 23 | - ReadWriteOnce 24 | resources: 25 | requests: 26 | storage: 10Gi 27 | --- 28 | apiVersion: extensions/v1beta1 29 | kind: Deployment 30 | metadata: 31 | name: postgresql 32 | labels: 33 | app: drupal 34 | spec: 35 | strategy: 36 | type: Recreate 37 | template: 38 | metadata: 39 | labels: 40 | app: drupal 41 | tier: postgreSQL 42 | spec: 43 | containers: 44 | - image: postgres:latest 45 | name: postgresql 46 | env: 47 | - name: POSTGRES_USER 48 | value: drupal 49 | - name: POSTGRES_DB 50 | value: drupal_production 51 | - name: POSTGRES_PASSWORD 52 | value: drupal 53 | ports: 54 | - containerPort: 5432 55 | name: postgresql 56 | volumeMounts: 57 | - name: postgresql 58 | mountPath: /var/lib/postgresql/data 59 | volumes: 60 | - name: postgresql 61 | persistentVolumeClaim: 62 | claimName: postgres-claim 63 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cameras-app", 3 | "version": "0.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@cloudant/cloudant": { 8 | "version": "2.4.0", 9 | "resolved": "https://registry.npmjs.org/@cloudant/cloudant/-/cloudant-2.4.0.tgz", 10 | "integrity": "sha512-D7lzWd7Tu1/lwNguTEOK3nShg8E2//cW2ClqsS0bsCjVvndfXR/tNdnNmDpvmAnc3fRF5Nwnq1CH6Ain6ZmXGw==", 11 | "requires": { 12 | "@types/nano": "6.4.6", 13 | "@types/request": "2.47.1", 14 | "async": "2.1.2", 15 | "concat-stream": "1.6.2", 16 | "debug": "3.2.6", 17 | "lockfile": "1.0.3", 18 | "nano": "6.4.4", 19 | "request": "2.88.0", 20 | "tmp": "0.0.33" 21 | }, 22 | "dependencies": { 23 | "async": { 24 | "version": "2.1.2", 25 | "resolved": "http://registry.npmjs.org/async/-/async-2.1.2.tgz", 26 | "integrity": "sha1-YSpKtF70KnDN6Aa62G7m2wR+g4U=", 27 | "requires": { 28 | "lodash": "4.17.10" 29 | } 30 | }, 31 | "debug": { 32 | "version": "3.2.6", 33 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", 34 | "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", 35 | "requires": { 36 | "ms": "2.1.1" 37 | } 38 | }, 39 | "ms": { 40 | "version": "2.1.1", 41 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", 42 | "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" 43 | } 44 | } 45 | }, 46 | "@types/caseless": { 47 | "version": "0.12.1", 48 | "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.1.tgz", 49 | "integrity": "sha512-FhlMa34NHp9K5MY1Uz8yb+ZvuX0pnvn3jScRSNAb75KHGB8d3rEU6hqMs3Z2vjuytcMfRg6c5CHMc3wtYyD2/A==" 50 | }, 51 | "@types/events": { 52 | "version": "1.2.0", 53 | "resolved": "http://registry.npmjs.org/@types/events/-/events-1.2.0.tgz", 54 | "integrity": "sha512-KEIlhXnIutzKwRbQkGWb/I4HFqBuUykAdHgDED6xqwXJfONCjF5VoE0cXEiurh3XauygxzeDzgtXUqvLkxFzzA==" 55 | }, 56 | "@types/form-data": { 57 | "version": "2.2.1", 58 | "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-2.2.1.tgz", 59 | "integrity": "sha512-JAMFhOaHIciYVh8fb5/83nmuO/AHwmto+Hq7a9y8FzLDcC1KCU344XDOMEmahnrTFlHjgh4L0WJFczNIX2GxnQ==", 60 | "requires": { 61 | "@types/node": "10.12.0" 62 | } 63 | }, 64 | "@types/nano": { 65 | "version": "6.4.6", 66 | "resolved": "https://registry.npmjs.org/@types/nano/-/nano-6.4.6.tgz", 67 | "integrity": "sha512-4rhvDQ/0TuLUZXxehuwYfNCiYtuxx/U5Gi8/8dMJp32fr3RmsA2znOdNdMbj5X6ntaMdOmZo+7I5NcYtwmkBJQ==", 68 | "requires": { 69 | "@types/events": "1.2.0", 70 | "@types/node": "10.12.0", 71 | "@types/request": "2.47.1" 72 | } 73 | }, 74 | "@types/node": { 75 | "version": "10.12.0", 76 | "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.0.tgz", 77 | "integrity": "sha512-3TUHC3jsBAB7qVRGxT6lWyYo2v96BMmD2PTcl47H25Lu7UXtFH/2qqmKiVrnel6Ne//0TFYf6uvNX+HW2FRkLQ==" 78 | }, 79 | "@types/request": { 80 | "version": "2.47.1", 81 | "resolved": "https://registry.npmjs.org/@types/request/-/request-2.47.1.tgz", 82 | "integrity": "sha512-TV3XLvDjQbIeVxJ1Z3oCTDk/KuYwwcNKVwz2YaT0F5u86Prgc4syDAp6P96rkTQQ4bIdh+VswQIC9zS6NjY7/g==", 83 | "requires": { 84 | "@types/caseless": "0.12.1", 85 | "@types/form-data": "2.2.1", 86 | "@types/node": "10.12.0", 87 | "@types/tough-cookie": "2.3.3" 88 | } 89 | }, 90 | "@types/tough-cookie": { 91 | "version": "2.3.3", 92 | "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-2.3.3.tgz", 93 | "integrity": "sha512-MDQLxNFRLasqS4UlkWMSACMKeSm1x4Q3TxzUC7KQUsh6RK1ZrQ0VEyE3yzXcBu+K8ejVj4wuX32eUG02yNp+YQ==" 94 | }, 95 | "accepts": { 96 | "version": "1.3.5", 97 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", 98 | "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", 99 | "requires": { 100 | "mime-types": "2.1.19", 101 | "negotiator": "0.6.1" 102 | } 103 | }, 104 | "ajv": { 105 | "version": "5.5.2", 106 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", 107 | "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", 108 | "requires": { 109 | "co": "4.6.0", 110 | "fast-deep-equal": "1.1.0", 111 | "fast-json-stable-stringify": "2.0.0", 112 | "json-schema-traverse": "0.3.1" 113 | } 114 | }, 115 | "align-text": { 116 | "version": "0.1.4", 117 | "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", 118 | "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", 119 | "requires": { 120 | "kind-of": "3.2.2", 121 | "longest": "1.0.1", 122 | "repeat-string": "1.6.1" 123 | } 124 | }, 125 | "amdefine": { 126 | "version": "1.0.1", 127 | "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", 128 | "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=" 129 | }, 130 | "append-field": { 131 | "version": "1.0.0", 132 | "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", 133 | "integrity": "sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY=" 134 | }, 135 | "array-flatten": { 136 | "version": "1.1.1", 137 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 138 | "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" 139 | }, 140 | "asn1": { 141 | "version": "0.2.4", 142 | "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", 143 | "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", 144 | "requires": { 145 | "safer-buffer": "2.1.2" 146 | } 147 | }, 148 | "assert-plus": { 149 | "version": "1.0.0", 150 | "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", 151 | "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" 152 | }, 153 | "async": { 154 | "version": "2.6.1", 155 | "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz", 156 | "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==", 157 | "requires": { 158 | "lodash": "4.17.10" 159 | } 160 | }, 161 | "asynckit": { 162 | "version": "0.4.0", 163 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 164 | "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" 165 | }, 166 | "await": { 167 | "version": "0.2.6", 168 | "resolved": "https://registry.npmjs.org/await/-/await-0.2.6.tgz", 169 | "integrity": "sha1-yI5u5pPW7Tcv8PUuwV1WVZCIVc4=" 170 | }, 171 | "aws-sign2": { 172 | "version": "0.7.0", 173 | "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", 174 | "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" 175 | }, 176 | "aws4": { 177 | "version": "1.8.0", 178 | "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", 179 | "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" 180 | }, 181 | "basic-auth": { 182 | "version": "2.0.0", 183 | "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.0.tgz", 184 | "integrity": "sha1-AV2z81PgLlY3d1X5YnQuiYHnu7o=", 185 | "requires": { 186 | "safe-buffer": "5.1.1" 187 | } 188 | }, 189 | "bcrypt-pbkdf": { 190 | "version": "1.0.2", 191 | "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", 192 | "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", 193 | "requires": { 194 | "tweetnacl": "0.14.5" 195 | } 196 | }, 197 | "bluebird": { 198 | "version": "3.5.1", 199 | "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", 200 | "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==" 201 | }, 202 | "body-parser": { 203 | "version": "1.18.2", 204 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz", 205 | "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=", 206 | "requires": { 207 | "bytes": "3.0.0", 208 | "content-type": "1.0.4", 209 | "debug": "2.6.9", 210 | "depd": "1.1.2", 211 | "http-errors": "1.6.3", 212 | "iconv-lite": "0.4.19", 213 | "on-finished": "2.3.0", 214 | "qs": "6.5.1", 215 | "raw-body": "2.3.2", 216 | "type-is": "1.6.16" 217 | } 218 | }, 219 | "browser-request": { 220 | "version": "0.3.3", 221 | "resolved": "https://registry.npmjs.org/browser-request/-/browser-request-0.3.3.tgz", 222 | "integrity": "sha1-ns5bWsqJopkyJC4Yv5M975h2zBc=" 223 | }, 224 | "buffer-from": { 225 | "version": "1.1.1", 226 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", 227 | "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" 228 | }, 229 | "busboy": { 230 | "version": "0.2.14", 231 | "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz", 232 | "integrity": "sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=", 233 | "requires": { 234 | "dicer": "0.2.5", 235 | "readable-stream": "1.1.14" 236 | } 237 | }, 238 | "bytes": { 239 | "version": "3.0.0", 240 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", 241 | "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" 242 | }, 243 | "camelcase": { 244 | "version": "1.2.1", 245 | "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", 246 | "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=" 247 | }, 248 | "caseless": { 249 | "version": "0.12.0", 250 | "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", 251 | "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" 252 | }, 253 | "center-align": { 254 | "version": "0.1.3", 255 | "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", 256 | "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", 257 | "requires": { 258 | "align-text": "0.1.4", 259 | "lazy-cache": "1.0.4" 260 | } 261 | }, 262 | "charenc": { 263 | "version": "0.0.2", 264 | "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", 265 | "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=" 266 | }, 267 | "clean-css": { 268 | "version": "3.4.28", 269 | "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-3.4.28.tgz", 270 | "integrity": "sha1-vxlF6C/ICPVWlebd6uwBQA79A/8=", 271 | "requires": { 272 | "commander": "2.8.1", 273 | "source-map": "0.4.4" 274 | } 275 | }, 276 | "cliui": { 277 | "version": "2.1.0", 278 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", 279 | "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", 280 | "requires": { 281 | "center-align": "0.1.3", 282 | "right-align": "0.1.3", 283 | "wordwrap": "0.0.2" 284 | } 285 | }, 286 | "cloudant-follow": { 287 | "version": "0.17.0", 288 | "resolved": "http://registry.npmjs.org/cloudant-follow/-/cloudant-follow-0.17.0.tgz", 289 | "integrity": "sha512-JQ1xvKAHh8rsnSVBjATLCjz/vQw1sWBGadxr2H69yFMwD7hShUGDwwEefdypaxroUJ/w6t1cSwilp/hRUxEW8w==", 290 | "requires": { 291 | "browser-request": "0.3.3", 292 | "debug": "3.2.6", 293 | "request": "2.88.0" 294 | }, 295 | "dependencies": { 296 | "debug": { 297 | "version": "3.2.6", 298 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", 299 | "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", 300 | "requires": { 301 | "ms": "2.1.1" 302 | } 303 | }, 304 | "ms": { 305 | "version": "2.1.1", 306 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", 307 | "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" 308 | } 309 | } 310 | }, 311 | "co": { 312 | "version": "4.6.0", 313 | "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", 314 | "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" 315 | }, 316 | "combined-stream": { 317 | "version": "1.0.7", 318 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz", 319 | "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==", 320 | "requires": { 321 | "delayed-stream": "1.0.0" 322 | } 323 | }, 324 | "commander": { 325 | "version": "2.8.1", 326 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.8.1.tgz", 327 | "integrity": "sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ=", 328 | "requires": { 329 | "graceful-readlink": "1.0.1" 330 | } 331 | }, 332 | "concat-stream": { 333 | "version": "1.6.2", 334 | "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", 335 | "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", 336 | "requires": { 337 | "buffer-from": "1.1.1", 338 | "inherits": "2.0.3", 339 | "readable-stream": "2.3.6", 340 | "typedarray": "0.0.6" 341 | }, 342 | "dependencies": { 343 | "isarray": { 344 | "version": "1.0.0", 345 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 346 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" 347 | }, 348 | "readable-stream": { 349 | "version": "2.3.6", 350 | "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", 351 | "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", 352 | "requires": { 353 | "core-util-is": "1.0.2", 354 | "inherits": "2.0.3", 355 | "isarray": "1.0.0", 356 | "process-nextick-args": "2.0.0", 357 | "safe-buffer": "5.1.1", 358 | "string_decoder": "1.1.1", 359 | "util-deprecate": "1.0.2" 360 | } 361 | }, 362 | "string_decoder": { 363 | "version": "1.1.1", 364 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", 365 | "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", 366 | "requires": { 367 | "safe-buffer": "5.1.1" 368 | } 369 | } 370 | } 371 | }, 372 | "content-disposition": { 373 | "version": "0.5.2", 374 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", 375 | "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" 376 | }, 377 | "content-type": { 378 | "version": "1.0.4", 379 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", 380 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" 381 | }, 382 | "cookie": { 383 | "version": "0.3.1", 384 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", 385 | "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" 386 | }, 387 | "cookie-parser": { 388 | "version": "1.4.3", 389 | "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.3.tgz", 390 | "integrity": "sha1-D+MfoZ0AC5X0qt8fU/3CuKIDuqU=", 391 | "requires": { 392 | "cookie": "0.3.1", 393 | "cookie-signature": "1.0.6" 394 | } 395 | }, 396 | "cookie-signature": { 397 | "version": "1.0.6", 398 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 399 | "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" 400 | }, 401 | "core-util-is": { 402 | "version": "1.0.2", 403 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 404 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" 405 | }, 406 | "crypt": { 407 | "version": "0.0.2", 408 | "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", 409 | "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=" 410 | }, 411 | "css": { 412 | "version": "1.0.8", 413 | "resolved": "https://registry.npmjs.org/css/-/css-1.0.8.tgz", 414 | "integrity": "sha1-k4aBHKgrzMnuf7WnMrHioxfIo+c=", 415 | "requires": { 416 | "css-parse": "1.0.4", 417 | "css-stringify": "1.0.5" 418 | } 419 | }, 420 | "css-parse": { 421 | "version": "1.0.4", 422 | "resolved": "https://registry.npmjs.org/css-parse/-/css-parse-1.0.4.tgz", 423 | "integrity": "sha1-OLBQP7+dqfVOnB29pg4UXHcRe90=" 424 | }, 425 | "css-stringify": { 426 | "version": "1.0.5", 427 | "resolved": "https://registry.npmjs.org/css-stringify/-/css-stringify-1.0.5.tgz", 428 | "integrity": "sha1-sNBClG2ylTu50pKQCmy19tASIDE=" 429 | }, 430 | "dashdash": { 431 | "version": "1.14.1", 432 | "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", 433 | "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", 434 | "requires": { 435 | "assert-plus": "1.0.0" 436 | } 437 | }, 438 | "debug": { 439 | "version": "2.6.9", 440 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 441 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 442 | "requires": { 443 | "ms": "2.0.0" 444 | } 445 | }, 446 | "decamelize": { 447 | "version": "1.2.0", 448 | "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", 449 | "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" 450 | }, 451 | "delayed-stream": { 452 | "version": "1.0.0", 453 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 454 | "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" 455 | }, 456 | "depd": { 457 | "version": "1.1.2", 458 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 459 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" 460 | }, 461 | "destroy": { 462 | "version": "1.0.4", 463 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", 464 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" 465 | }, 466 | "dicer": { 467 | "version": "0.2.5", 468 | "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz", 469 | "integrity": "sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=", 470 | "requires": { 471 | "readable-stream": "1.1.14", 472 | "streamsearch": "0.1.2" 473 | } 474 | }, 475 | "dotenv": { 476 | "version": "6.1.0", 477 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-6.1.0.tgz", 478 | "integrity": "sha512-/veDn2ztgRlB7gKmE3i9f6CmDIyXAy6d5nBq+whO9SLX+Zs1sXEgFLPi+aSuWqUuusMfbi84fT8j34fs1HaYUw==" 479 | }, 480 | "ecc-jsbn": { 481 | "version": "0.1.2", 482 | "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", 483 | "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", 484 | "requires": { 485 | "jsbn": "0.1.1", 486 | "safer-buffer": "2.1.2" 487 | } 488 | }, 489 | "ee-first": { 490 | "version": "1.1.1", 491 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 492 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" 493 | }, 494 | "ejs": { 495 | "version": "2.6.1", 496 | "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.6.1.tgz", 497 | "integrity": "sha512-0xy4A/twfrRCnkhfk8ErDi5DqdAsAqeGxht4xkCUrsvhhbQNs7E+4jV0CN7+NKIY0aHE72+XvqtBIXzD31ZbXQ==" 498 | }, 499 | "encodeurl": { 500 | "version": "1.0.2", 501 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 502 | "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" 503 | }, 504 | "errs": { 505 | "version": "0.3.2", 506 | "resolved": "https://registry.npmjs.org/errs/-/errs-0.3.2.tgz", 507 | "integrity": "sha1-eYCZstvTfKK8dJ5TinwTB9C1BJk=" 508 | }, 509 | "escape-html": { 510 | "version": "1.0.3", 511 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 512 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" 513 | }, 514 | "etag": { 515 | "version": "1.8.1", 516 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 517 | "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" 518 | }, 519 | "express": { 520 | "version": "4.16.3", 521 | "resolved": "https://registry.npmjs.org/express/-/express-4.16.3.tgz", 522 | "integrity": "sha1-avilAjUNsyRuzEvs9rWjTSL37VM=", 523 | "requires": { 524 | "accepts": "1.3.5", 525 | "array-flatten": "1.1.1", 526 | "body-parser": "1.18.2", 527 | "content-disposition": "0.5.2", 528 | "content-type": "1.0.4", 529 | "cookie": "0.3.1", 530 | "cookie-signature": "1.0.6", 531 | "debug": "2.6.9", 532 | "depd": "1.1.2", 533 | "encodeurl": "1.0.2", 534 | "escape-html": "1.0.3", 535 | "etag": "1.8.1", 536 | "finalhandler": "1.1.1", 537 | "fresh": "0.5.2", 538 | "merge-descriptors": "1.0.1", 539 | "methods": "1.1.2", 540 | "on-finished": "2.3.0", 541 | "parseurl": "1.3.2", 542 | "path-to-regexp": "0.1.7", 543 | "proxy-addr": "2.0.3", 544 | "qs": "6.5.1", 545 | "range-parser": "1.2.0", 546 | "safe-buffer": "5.1.1", 547 | "send": "0.16.2", 548 | "serve-static": "1.13.2", 549 | "setprototypeof": "1.1.0", 550 | "statuses": "1.4.0", 551 | "type-is": "1.6.16", 552 | "utils-merge": "1.0.1", 553 | "vary": "1.1.2" 554 | } 555 | }, 556 | "express-fileupload": { 557 | "version": "1.0.0", 558 | "resolved": "https://registry.npmjs.org/express-fileupload/-/express-fileupload-1.0.0.tgz", 559 | "integrity": "sha512-6VS9MiPIXXFFKv5+cRS5+iMh3Zw6KadiSEM+SPRMSC3AEoV3ZOfRUk3ogjDtKVr4o9n3EoHTMyyqbuzBj8gMLw==", 560 | "requires": { 561 | "busboy": "0.2.14", 562 | "md5": "2.2.1", 563 | "streamifier": "0.1.1" 564 | } 565 | }, 566 | "extend": { 567 | "version": "3.0.2", 568 | "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", 569 | "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" 570 | }, 571 | "extsprintf": { 572 | "version": "1.3.0", 573 | "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", 574 | "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" 575 | }, 576 | "fast-deep-equal": { 577 | "version": "1.1.0", 578 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", 579 | "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=" 580 | }, 581 | "fast-json-stable-stringify": { 582 | "version": "2.0.0", 583 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", 584 | "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" 585 | }, 586 | "finalhandler": { 587 | "version": "1.1.1", 588 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", 589 | "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", 590 | "requires": { 591 | "debug": "2.6.9", 592 | "encodeurl": "1.0.2", 593 | "escape-html": "1.0.3", 594 | "on-finished": "2.3.0", 595 | "parseurl": "1.3.2", 596 | "statuses": "1.4.0", 597 | "unpipe": "1.0.0" 598 | } 599 | }, 600 | "forever-agent": { 601 | "version": "0.6.1", 602 | "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", 603 | "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" 604 | }, 605 | "form-data": { 606 | "version": "2.3.3", 607 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", 608 | "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", 609 | "requires": { 610 | "asynckit": "0.4.0", 611 | "combined-stream": "1.0.7", 612 | "mime-types": "2.1.19" 613 | } 614 | }, 615 | "forwarded": { 616 | "version": "0.1.2", 617 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", 618 | "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" 619 | }, 620 | "fresh": { 621 | "version": "0.5.2", 622 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 623 | "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" 624 | }, 625 | "getpass": { 626 | "version": "0.1.7", 627 | "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", 628 | "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", 629 | "requires": { 630 | "assert-plus": "1.0.0" 631 | } 632 | }, 633 | "graceful-readlink": { 634 | "version": "1.0.1", 635 | "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", 636 | "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=" 637 | }, 638 | "har-schema": { 639 | "version": "2.0.0", 640 | "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", 641 | "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" 642 | }, 643 | "har-validator": { 644 | "version": "5.1.0", 645 | "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.0.tgz", 646 | "integrity": "sha512-+qnmNjI4OfH2ipQ9VQOw23bBd/ibtfbVdK2fYbY4acTDqKTW/YDp9McimZdDbG8iV9fZizUqQMD5xvriB146TA==", 647 | "requires": { 648 | "ajv": "5.5.2", 649 | "har-schema": "2.0.0" 650 | } 651 | }, 652 | "http-errors": { 653 | "version": "1.6.3", 654 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", 655 | "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", 656 | "requires": { 657 | "depd": "1.1.2", 658 | "inherits": "2.0.3", 659 | "setprototypeof": "1.1.0", 660 | "statuses": "1.4.0" 661 | } 662 | }, 663 | "http-signature": { 664 | "version": "1.2.0", 665 | "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", 666 | "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", 667 | "requires": { 668 | "assert-plus": "1.0.0", 669 | "jsprim": "1.4.1", 670 | "sshpk": "1.15.1" 671 | } 672 | }, 673 | "iconv-lite": { 674 | "version": "0.4.19", 675 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", 676 | "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" 677 | }, 678 | "inherits": { 679 | "version": "2.0.3", 680 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 681 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 682 | }, 683 | "ipaddr.js": { 684 | "version": "1.6.0", 685 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.6.0.tgz", 686 | "integrity": "sha1-4/o1e3c9phnybpXwSdBVxyeW+Gs=" 687 | }, 688 | "is-buffer": { 689 | "version": "1.1.6", 690 | "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", 691 | "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" 692 | }, 693 | "is-promise": { 694 | "version": "2.1.0", 695 | "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", 696 | "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=" 697 | }, 698 | "is-typedarray": { 699 | "version": "1.0.0", 700 | "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", 701 | "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" 702 | }, 703 | "isarray": { 704 | "version": "0.0.1", 705 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", 706 | "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" 707 | }, 708 | "isstream": { 709 | "version": "0.1.2", 710 | "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", 711 | "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" 712 | }, 713 | "jade": { 714 | "version": "1.11.0", 715 | "resolved": "https://registry.npmjs.org/jade/-/jade-1.11.0.tgz", 716 | "integrity": "sha1-nIDlOMEtP7lcjZu5VZ+gzAQEBf0=", 717 | "requires": { 718 | "character-parser": "1.2.1", 719 | "clean-css": "3.4.28", 720 | "commander": "2.6.0", 721 | "constantinople": "3.0.2", 722 | "jstransformer": "0.0.2", 723 | "mkdirp": "0.5.1", 724 | "transformers": "2.1.0", 725 | "uglify-js": "2.8.29", 726 | "void-elements": "2.0.1", 727 | "with": "4.0.3" 728 | }, 729 | "dependencies": { 730 | "acorn": { 731 | "version": "2.7.0", 732 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-2.7.0.tgz", 733 | "integrity": "sha1-q259nYhqrKiwhbwzEreaGYQz8Oc=" 734 | }, 735 | "acorn-globals": { 736 | "version": "1.0.9", 737 | "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-1.0.9.tgz", 738 | "integrity": "sha1-VbtemGkVB7dFedBRNBMhfDgMVM8=", 739 | "requires": { 740 | "acorn": "2.7.0" 741 | } 742 | }, 743 | "asap": { 744 | "version": "1.0.0", 745 | "resolved": "https://registry.npmjs.org/asap/-/asap-1.0.0.tgz", 746 | "integrity": "sha1-sqRdpf36ILBJb8N2jMJ8EvqRan0=" 747 | }, 748 | "character-parser": { 749 | "version": "1.2.1", 750 | "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-1.2.1.tgz", 751 | "integrity": "sha1-wN3kqxgnE7kZuXCVmhI+zBow/NY=" 752 | }, 753 | "commander": { 754 | "version": "2.6.0", 755 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.6.0.tgz", 756 | "integrity": "sha1-nfflL7Kgyw+4kFjugMMQQiXzfh0=" 757 | }, 758 | "constantinople": { 759 | "version": "3.0.2", 760 | "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-3.0.2.tgz", 761 | "integrity": "sha1-S5RdmTeQe82Y7ldRIsOBdRZUQUE=", 762 | "requires": { 763 | "acorn": "2.7.0" 764 | } 765 | }, 766 | "jstransformer": { 767 | "version": "0.0.2", 768 | "resolved": "https://registry.npmjs.org/jstransformer/-/jstransformer-0.0.2.tgz", 769 | "integrity": "sha1-eq4pqQPRls+glz2IXT5HlH7Ndqs=", 770 | "requires": { 771 | "is-promise": "2.1.0", 772 | "promise": "6.1.0" 773 | } 774 | }, 775 | "promise": { 776 | "version": "6.1.0", 777 | "resolved": "https://registry.npmjs.org/promise/-/promise-6.1.0.tgz", 778 | "integrity": "sha1-LOcp9rlLRcJoka0GAsXJDgTG7vY=", 779 | "requires": { 780 | "asap": "1.0.0" 781 | } 782 | }, 783 | "with": { 784 | "version": "4.0.3", 785 | "resolved": "https://registry.npmjs.org/with/-/with-4.0.3.tgz", 786 | "integrity": "sha1-7v0VTp550sjTQXtkeo8U2f7M4U4=", 787 | "requires": { 788 | "acorn": "1.2.2", 789 | "acorn-globals": "1.0.9" 790 | }, 791 | "dependencies": { 792 | "acorn": { 793 | "version": "1.2.2", 794 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-1.2.2.tgz", 795 | "integrity": "sha1-yM4n3grMdtiW0rH6099YjZ6C8BQ=" 796 | } 797 | } 798 | } 799 | } 800 | }, 801 | "jsbn": { 802 | "version": "0.1.1", 803 | "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", 804 | "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" 805 | }, 806 | "json-schema": { 807 | "version": "0.2.3", 808 | "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", 809 | "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" 810 | }, 811 | "json-schema-traverse": { 812 | "version": "0.3.1", 813 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", 814 | "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" 815 | }, 816 | "json-stringify-safe": { 817 | "version": "5.0.1", 818 | "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", 819 | "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" 820 | }, 821 | "jsprim": { 822 | "version": "1.4.1", 823 | "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", 824 | "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", 825 | "requires": { 826 | "assert-plus": "1.0.0", 827 | "extsprintf": "1.3.0", 828 | "json-schema": "0.2.3", 829 | "verror": "1.10.0" 830 | } 831 | }, 832 | "kind-of": { 833 | "version": "3.2.2", 834 | "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", 835 | "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", 836 | "requires": { 837 | "is-buffer": "1.1.6" 838 | } 839 | }, 840 | "lazy-cache": { 841 | "version": "1.0.4", 842 | "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", 843 | "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=" 844 | }, 845 | "lockfile": { 846 | "version": "1.0.3", 847 | "resolved": "https://registry.npmjs.org/lockfile/-/lockfile-1.0.3.tgz", 848 | "integrity": "sha1-Jjj8OaAzHpysGgS3F5mTHJxQ33k=" 849 | }, 850 | "lodash": { 851 | "version": "4.17.10", 852 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", 853 | "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==" 854 | }, 855 | "lodash.isempty": { 856 | "version": "4.4.0", 857 | "resolved": "http://registry.npmjs.org/lodash.isempty/-/lodash.isempty-4.4.0.tgz", 858 | "integrity": "sha1-b4bL7di+TsmHvpqvM8loTbGzHn4=" 859 | }, 860 | "longest": { 861 | "version": "1.0.1", 862 | "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", 863 | "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=" 864 | }, 865 | "md5": { 866 | "version": "2.2.1", 867 | "resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz", 868 | "integrity": "sha1-U6s41f48iJG6RlMp6iP6wFQBJvk=", 869 | "requires": { 870 | "charenc": "0.0.2", 871 | "crypt": "0.0.2", 872 | "is-buffer": "1.1.6" 873 | } 874 | }, 875 | "media-typer": { 876 | "version": "0.3.0", 877 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 878 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" 879 | }, 880 | "merge-descriptors": { 881 | "version": "1.0.1", 882 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 883 | "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" 884 | }, 885 | "methods": { 886 | "version": "1.1.2", 887 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 888 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" 889 | }, 890 | "mime": { 891 | "version": "1.4.1", 892 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", 893 | "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" 894 | }, 895 | "mime-db": { 896 | "version": "1.35.0", 897 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.35.0.tgz", 898 | "integrity": "sha512-JWT/IcCTsB0Io3AhWUMjRqucrHSPsSf2xKLaRldJVULioggvkJvggZ3VXNNSRkCddE6D+BUI4HEIZIA2OjwIvg==" 899 | }, 900 | "mime-types": { 901 | "version": "2.1.19", 902 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.19.tgz", 903 | "integrity": "sha512-P1tKYHVSZ6uFo26mtnve4HQFE3koh1UWVkp8YUC+ESBHe945xWSoXuHHiGarDqcEZ+whpCDnlNw5LON0kLo+sw==", 904 | "requires": { 905 | "mime-db": "1.35.0" 906 | } 907 | }, 908 | "minimist": { 909 | "version": "0.0.8", 910 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", 911 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" 912 | }, 913 | "mkdirp": { 914 | "version": "0.5.1", 915 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", 916 | "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", 917 | "requires": { 918 | "minimist": "0.0.8" 919 | } 920 | }, 921 | "morgan": { 922 | "version": "1.9.0", 923 | "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.9.0.tgz", 924 | "integrity": "sha1-0B+mxlhZt2/PMbPLU6OCGjEdgFE=", 925 | "requires": { 926 | "basic-auth": "2.0.0", 927 | "debug": "2.6.9", 928 | "depd": "1.1.2", 929 | "on-finished": "2.3.0", 930 | "on-headers": "1.0.1" 931 | } 932 | }, 933 | "ms": { 934 | "version": "2.0.0", 935 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 936 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 937 | }, 938 | "multer": { 939 | "version": "1.4.1", 940 | "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.1.tgz", 941 | "integrity": "sha512-zzOLNRxzszwd+61JFuAo0fxdQfvku12aNJgnla0AQ+hHxFmfc/B7jBVuPr5Rmvu46Jze/iJrFpSOsD7afO8SDw==", 942 | "requires": { 943 | "append-field": "1.0.0", 944 | "busboy": "0.2.14", 945 | "concat-stream": "1.6.2", 946 | "mkdirp": "0.5.1", 947 | "object-assign": "4.1.1", 948 | "on-finished": "2.3.0", 949 | "type-is": "1.6.16", 950 | "xtend": "4.0.1" 951 | } 952 | }, 953 | "nan": { 954 | "version": "2.9.2", 955 | "resolved": "https://registry.npmjs.org/nan/-/nan-2.9.2.tgz", 956 | "integrity": "sha512-ltW65co7f3PQWBDbqVvaU1WtFJUsNW7sWWm4HINhbMQIyVyzIeyZ8toX5TC5eeooE6piZoaEh4cZkueSKG3KYw==" 957 | }, 958 | "nano": { 959 | "version": "6.4.4", 960 | "resolved": "https://registry.npmjs.org/nano/-/nano-6.4.4.tgz", 961 | "integrity": "sha512-7sldMrZI1ZH8QE29PnzohxLfR67WNVzMKLa7EMl3x9Hr+0G+YpOUCq50qZ9G66APrjcb0Of2BTOZLNBCutZGag==", 962 | "requires": { 963 | "cloudant-follow": "0.17.0", 964 | "debug": "2.6.9", 965 | "errs": "0.3.2", 966 | "lodash.isempty": "4.4.0", 967 | "request": "2.88.0" 968 | } 969 | }, 970 | "negotiator": { 971 | "version": "0.6.1", 972 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", 973 | "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" 974 | }, 975 | "oauth-sign": { 976 | "version": "0.9.0", 977 | "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", 978 | "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" 979 | }, 980 | "object-assign": { 981 | "version": "4.1.1", 982 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 983 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" 984 | }, 985 | "on-finished": { 986 | "version": "2.3.0", 987 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 988 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", 989 | "requires": { 990 | "ee-first": "1.1.1" 991 | } 992 | }, 993 | "on-headers": { 994 | "version": "1.0.1", 995 | "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz", 996 | "integrity": "sha1-ko9dD0cNSTQmUepnlLCFfBAGk/c=" 997 | }, 998 | "optimist": { 999 | "version": "0.3.7", 1000 | "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.3.7.tgz", 1001 | "integrity": "sha1-yQlBrVnkJzMokjB00s8ufLxuwNk=", 1002 | "requires": { 1003 | "wordwrap": "0.0.2" 1004 | } 1005 | }, 1006 | "os-tmpdir": { 1007 | "version": "1.0.2", 1008 | "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", 1009 | "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" 1010 | }, 1011 | "parseurl": { 1012 | "version": "1.3.2", 1013 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", 1014 | "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" 1015 | }, 1016 | "path-to-regexp": { 1017 | "version": "0.1.7", 1018 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 1019 | "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" 1020 | }, 1021 | "performance-now": { 1022 | "version": "2.1.0", 1023 | "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", 1024 | "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" 1025 | }, 1026 | "process-nextick-args": { 1027 | "version": "2.0.0", 1028 | "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", 1029 | "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" 1030 | }, 1031 | "proxy-addr": { 1032 | "version": "2.0.3", 1033 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.3.tgz", 1034 | "integrity": "sha512-jQTChiCJteusULxjBp8+jftSQE5Obdl3k4cnmLA6WXtK6XFuWRnvVL7aCiBqaLPM8c4ph0S4tKna8XvmIwEnXQ==", 1035 | "requires": { 1036 | "forwarded": "0.1.2", 1037 | "ipaddr.js": "1.6.0" 1038 | } 1039 | }, 1040 | "psl": { 1041 | "version": "1.1.29", 1042 | "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.29.tgz", 1043 | "integrity": "sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ==" 1044 | }, 1045 | "punycode": { 1046 | "version": "1.4.1", 1047 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", 1048 | "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" 1049 | }, 1050 | "qs": { 1051 | "version": "6.5.1", 1052 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", 1053 | "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" 1054 | }, 1055 | "range-parser": { 1056 | "version": "1.2.0", 1057 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", 1058 | "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" 1059 | }, 1060 | "raw-body": { 1061 | "version": "2.3.2", 1062 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz", 1063 | "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=", 1064 | "requires": { 1065 | "bytes": "3.0.0", 1066 | "http-errors": "1.6.2", 1067 | "iconv-lite": "0.4.19", 1068 | "unpipe": "1.0.0" 1069 | }, 1070 | "dependencies": { 1071 | "depd": { 1072 | "version": "1.1.1", 1073 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", 1074 | "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=" 1075 | }, 1076 | "http-errors": { 1077 | "version": "1.6.2", 1078 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", 1079 | "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", 1080 | "requires": { 1081 | "depd": "1.1.1", 1082 | "inherits": "2.0.3", 1083 | "setprototypeof": "1.0.3", 1084 | "statuses": "1.4.0" 1085 | } 1086 | }, 1087 | "setprototypeof": { 1088 | "version": "1.0.3", 1089 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", 1090 | "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" 1091 | } 1092 | } 1093 | }, 1094 | "readable-stream": { 1095 | "version": "1.1.14", 1096 | "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", 1097 | "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", 1098 | "requires": { 1099 | "core-util-is": "1.0.2", 1100 | "inherits": "2.0.3", 1101 | "isarray": "0.0.1", 1102 | "string_decoder": "0.10.31" 1103 | } 1104 | }, 1105 | "repeat-string": { 1106 | "version": "1.6.1", 1107 | "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", 1108 | "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" 1109 | }, 1110 | "request": { 1111 | "version": "2.88.0", 1112 | "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", 1113 | "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", 1114 | "requires": { 1115 | "aws-sign2": "0.7.0", 1116 | "aws4": "1.8.0", 1117 | "caseless": "0.12.0", 1118 | "combined-stream": "1.0.7", 1119 | "extend": "3.0.2", 1120 | "forever-agent": "0.6.1", 1121 | "form-data": "2.3.3", 1122 | "har-validator": "5.1.0", 1123 | "http-signature": "1.2.0", 1124 | "is-typedarray": "1.0.0", 1125 | "isstream": "0.1.2", 1126 | "json-stringify-safe": "5.0.1", 1127 | "mime-types": "2.1.19", 1128 | "oauth-sign": "0.9.0", 1129 | "performance-now": "2.1.0", 1130 | "qs": "6.5.2", 1131 | "safe-buffer": "5.1.2", 1132 | "tough-cookie": "2.4.3", 1133 | "tunnel-agent": "0.6.0", 1134 | "uuid": "3.3.2" 1135 | }, 1136 | "dependencies": { 1137 | "qs": { 1138 | "version": "6.5.2", 1139 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", 1140 | "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" 1141 | }, 1142 | "safe-buffer": { 1143 | "version": "5.1.2", 1144 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 1145 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 1146 | } 1147 | } 1148 | }, 1149 | "right-align": { 1150 | "version": "0.1.3", 1151 | "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", 1152 | "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", 1153 | "requires": { 1154 | "align-text": "0.1.4" 1155 | } 1156 | }, 1157 | "safe-buffer": { 1158 | "version": "5.1.1", 1159 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", 1160 | "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" 1161 | }, 1162 | "safer-buffer": { 1163 | "version": "2.1.2", 1164 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 1165 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 1166 | }, 1167 | "send": { 1168 | "version": "0.16.2", 1169 | "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", 1170 | "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", 1171 | "requires": { 1172 | "debug": "2.6.9", 1173 | "depd": "1.1.2", 1174 | "destroy": "1.0.4", 1175 | "encodeurl": "1.0.2", 1176 | "escape-html": "1.0.3", 1177 | "etag": "1.8.1", 1178 | "fresh": "0.5.2", 1179 | "http-errors": "1.6.3", 1180 | "mime": "1.4.1", 1181 | "ms": "2.0.0", 1182 | "on-finished": "2.3.0", 1183 | "range-parser": "1.2.0", 1184 | "statuses": "1.4.0" 1185 | } 1186 | }, 1187 | "serve-static": { 1188 | "version": "1.13.2", 1189 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", 1190 | "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", 1191 | "requires": { 1192 | "encodeurl": "1.0.2", 1193 | "escape-html": "1.0.3", 1194 | "parseurl": "1.3.2", 1195 | "send": "0.16.2" 1196 | } 1197 | }, 1198 | "setprototypeof": { 1199 | "version": "1.1.0", 1200 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", 1201 | "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" 1202 | }, 1203 | "source-map": { 1204 | "version": "0.4.4", 1205 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", 1206 | "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", 1207 | "requires": { 1208 | "amdefine": "1.0.1" 1209 | } 1210 | }, 1211 | "sqlite": { 1212 | "version": "2.9.2", 1213 | "resolved": "https://registry.npmjs.org/sqlite/-/sqlite-2.9.2.tgz", 1214 | "integrity": "sha512-6rlm1AimZksjZAstmqqnUsmFQvk0LKNaedi9iTSd5e9kWLzN5JRljHFlARLLUudQklmc9nNtVm/vjo3svCKQTw==", 1215 | "requires": { 1216 | "sqlite3": "4.0.0" 1217 | } 1218 | }, 1219 | "sqlite3": { 1220 | "version": "4.0.0", 1221 | "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-4.0.0.tgz", 1222 | "integrity": "sha512-6OlcAQNGaRSBLK1CuaRbKwlMFBb9DEhzmZyQP+fltNRF6XcIMpVIfXCBEcXPe1d4v9LnhkQUYkknDbA5JReqJg==", 1223 | "requires": { 1224 | "nan": "2.9.2", 1225 | "node-pre-gyp": "0.9.0" 1226 | }, 1227 | "dependencies": { 1228 | "abbrev": { 1229 | "version": "1.1.1", 1230 | "bundled": true 1231 | }, 1232 | "ansi-regex": { 1233 | "version": "2.1.1", 1234 | "bundled": true 1235 | }, 1236 | "aproba": { 1237 | "version": "1.2.0", 1238 | "bundled": true 1239 | }, 1240 | "are-we-there-yet": { 1241 | "version": "1.1.4", 1242 | "bundled": true, 1243 | "requires": { 1244 | "delegates": "1.0.0", 1245 | "readable-stream": "2.3.5" 1246 | } 1247 | }, 1248 | "balanced-match": { 1249 | "version": "1.0.0", 1250 | "bundled": true 1251 | }, 1252 | "brace-expansion": { 1253 | "version": "1.1.11", 1254 | "bundled": true, 1255 | "requires": { 1256 | "balanced-match": "1.0.0", 1257 | "concat-map": "0.0.1" 1258 | } 1259 | }, 1260 | "chownr": { 1261 | "version": "1.0.1", 1262 | "bundled": true 1263 | }, 1264 | "code-point-at": { 1265 | "version": "1.1.0", 1266 | "bundled": true 1267 | }, 1268 | "concat-map": { 1269 | "version": "0.0.1", 1270 | "bundled": true 1271 | }, 1272 | "console-control-strings": { 1273 | "version": "1.1.0", 1274 | "bundled": true 1275 | }, 1276 | "core-util-is": { 1277 | "version": "1.0.2", 1278 | "bundled": true 1279 | }, 1280 | "debug": { 1281 | "version": "2.6.9", 1282 | "bundled": true, 1283 | "requires": { 1284 | "ms": "2.0.0" 1285 | } 1286 | }, 1287 | "deep-extend": { 1288 | "version": "0.4.2", 1289 | "bundled": true 1290 | }, 1291 | "delegates": { 1292 | "version": "1.0.0", 1293 | "bundled": true 1294 | }, 1295 | "detect-libc": { 1296 | "version": "1.0.3", 1297 | "bundled": true 1298 | }, 1299 | "fs-minipass": { 1300 | "version": "1.2.5", 1301 | "bundled": true, 1302 | "requires": { 1303 | "minipass": "2.2.1" 1304 | } 1305 | }, 1306 | "fs.realpath": { 1307 | "version": "1.0.0", 1308 | "bundled": true 1309 | }, 1310 | "gauge": { 1311 | "version": "2.7.4", 1312 | "bundled": true, 1313 | "requires": { 1314 | "aproba": "1.2.0", 1315 | "console-control-strings": "1.1.0", 1316 | "has-unicode": "2.0.1", 1317 | "object-assign": "4.1.1", 1318 | "signal-exit": "3.0.2", 1319 | "string-width": "1.0.2", 1320 | "strip-ansi": "3.0.1", 1321 | "wide-align": "1.1.2" 1322 | } 1323 | }, 1324 | "glob": { 1325 | "version": "7.1.2", 1326 | "bundled": true, 1327 | "requires": { 1328 | "fs.realpath": "1.0.0", 1329 | "inflight": "1.0.6", 1330 | "inherits": "2.0.3", 1331 | "minimatch": "3.0.4", 1332 | "once": "1.4.0", 1333 | "path-is-absolute": "1.0.1" 1334 | } 1335 | }, 1336 | "has-unicode": { 1337 | "version": "2.0.1", 1338 | "bundled": true 1339 | }, 1340 | "iconv-lite": { 1341 | "version": "0.4.19", 1342 | "bundled": true 1343 | }, 1344 | "ignore-walk": { 1345 | "version": "3.0.1", 1346 | "bundled": true, 1347 | "requires": { 1348 | "minimatch": "3.0.4" 1349 | } 1350 | }, 1351 | "inflight": { 1352 | "version": "1.0.6", 1353 | "bundled": true, 1354 | "requires": { 1355 | "once": "1.4.0", 1356 | "wrappy": "1.0.2" 1357 | } 1358 | }, 1359 | "inherits": { 1360 | "version": "2.0.3", 1361 | "bundled": true 1362 | }, 1363 | "ini": { 1364 | "version": "1.3.5", 1365 | "bundled": true 1366 | }, 1367 | "is-fullwidth-code-point": { 1368 | "version": "1.0.0", 1369 | "bundled": true, 1370 | "requires": { 1371 | "number-is-nan": "1.0.1" 1372 | } 1373 | }, 1374 | "isarray": { 1375 | "version": "1.0.0", 1376 | "bundled": true 1377 | }, 1378 | "minimatch": { 1379 | "version": "3.0.4", 1380 | "bundled": true, 1381 | "requires": { 1382 | "brace-expansion": "1.1.11" 1383 | } 1384 | }, 1385 | "minimist": { 1386 | "version": "0.0.8", 1387 | "bundled": true 1388 | }, 1389 | "minipass": { 1390 | "version": "2.2.1", 1391 | "bundled": true, 1392 | "requires": { 1393 | "yallist": "3.0.2" 1394 | } 1395 | }, 1396 | "minizlib": { 1397 | "version": "1.1.0", 1398 | "bundled": true, 1399 | "requires": { 1400 | "minipass": "2.2.1" 1401 | } 1402 | }, 1403 | "mkdirp": { 1404 | "version": "0.5.1", 1405 | "bundled": true, 1406 | "requires": { 1407 | "minimist": "0.0.8" 1408 | } 1409 | }, 1410 | "ms": { 1411 | "version": "2.0.0", 1412 | "bundled": true 1413 | }, 1414 | "needle": { 1415 | "version": "2.2.0", 1416 | "bundled": true, 1417 | "requires": { 1418 | "debug": "2.6.9", 1419 | "iconv-lite": "0.4.19", 1420 | "sax": "1.2.4" 1421 | } 1422 | }, 1423 | "node-pre-gyp": { 1424 | "version": "0.9.0", 1425 | "bundled": true, 1426 | "requires": { 1427 | "detect-libc": "1.0.3", 1428 | "mkdirp": "0.5.1", 1429 | "needle": "2.2.0", 1430 | "nopt": "4.0.1", 1431 | "npm-packlist": "1.1.10", 1432 | "npmlog": "4.1.2", 1433 | "rc": "1.2.6", 1434 | "rimraf": "2.6.2", 1435 | "semver": "5.5.0", 1436 | "tar": "4.4.0" 1437 | } 1438 | }, 1439 | "nopt": { 1440 | "version": "4.0.1", 1441 | "bundled": true, 1442 | "requires": { 1443 | "abbrev": "1.1.1", 1444 | "osenv": "0.1.5" 1445 | } 1446 | }, 1447 | "npm-bundled": { 1448 | "version": "1.0.3", 1449 | "bundled": true 1450 | }, 1451 | "npm-packlist": { 1452 | "version": "1.1.10", 1453 | "bundled": true, 1454 | "requires": { 1455 | "ignore-walk": "3.0.1", 1456 | "npm-bundled": "1.0.3" 1457 | } 1458 | }, 1459 | "npmlog": { 1460 | "version": "4.1.2", 1461 | "bundled": true, 1462 | "requires": { 1463 | "are-we-there-yet": "1.1.4", 1464 | "console-control-strings": "1.1.0", 1465 | "gauge": "2.7.4", 1466 | "set-blocking": "2.0.0" 1467 | } 1468 | }, 1469 | "number-is-nan": { 1470 | "version": "1.0.1", 1471 | "bundled": true 1472 | }, 1473 | "object-assign": { 1474 | "version": "4.1.1", 1475 | "bundled": true 1476 | }, 1477 | "once": { 1478 | "version": "1.4.0", 1479 | "bundled": true, 1480 | "requires": { 1481 | "wrappy": "1.0.2" 1482 | } 1483 | }, 1484 | "os-homedir": { 1485 | "version": "1.0.2", 1486 | "bundled": true 1487 | }, 1488 | "os-tmpdir": { 1489 | "version": "1.0.2", 1490 | "bundled": true 1491 | }, 1492 | "osenv": { 1493 | "version": "0.1.5", 1494 | "bundled": true, 1495 | "requires": { 1496 | "os-homedir": "1.0.2", 1497 | "os-tmpdir": "1.0.2" 1498 | } 1499 | }, 1500 | "path-is-absolute": { 1501 | "version": "1.0.1", 1502 | "bundled": true 1503 | }, 1504 | "process-nextick-args": { 1505 | "version": "2.0.0", 1506 | "bundled": true 1507 | }, 1508 | "rc": { 1509 | "version": "1.2.6", 1510 | "bundled": true, 1511 | "requires": { 1512 | "deep-extend": "0.4.2", 1513 | "ini": "1.3.5", 1514 | "minimist": "1.2.0", 1515 | "strip-json-comments": "2.0.1" 1516 | }, 1517 | "dependencies": { 1518 | "minimist": { 1519 | "version": "1.2.0", 1520 | "bundled": true 1521 | } 1522 | } 1523 | }, 1524 | "readable-stream": { 1525 | "version": "2.3.5", 1526 | "bundled": true, 1527 | "requires": { 1528 | "core-util-is": "1.0.2", 1529 | "inherits": "2.0.3", 1530 | "isarray": "1.0.0", 1531 | "process-nextick-args": "2.0.0", 1532 | "safe-buffer": "5.1.1", 1533 | "string_decoder": "1.0.3", 1534 | "util-deprecate": "1.0.2" 1535 | } 1536 | }, 1537 | "rimraf": { 1538 | "version": "2.6.2", 1539 | "bundled": true, 1540 | "requires": { 1541 | "glob": "7.1.2" 1542 | } 1543 | }, 1544 | "safe-buffer": { 1545 | "version": "5.1.1", 1546 | "bundled": true 1547 | }, 1548 | "sax": { 1549 | "version": "1.2.4", 1550 | "bundled": true 1551 | }, 1552 | "semver": { 1553 | "version": "5.5.0", 1554 | "bundled": true 1555 | }, 1556 | "set-blocking": { 1557 | "version": "2.0.0", 1558 | "bundled": true 1559 | }, 1560 | "signal-exit": { 1561 | "version": "3.0.2", 1562 | "bundled": true 1563 | }, 1564 | "string-width": { 1565 | "version": "1.0.2", 1566 | "bundled": true, 1567 | "requires": { 1568 | "code-point-at": "1.1.0", 1569 | "is-fullwidth-code-point": "1.0.0", 1570 | "strip-ansi": "3.0.1" 1571 | } 1572 | }, 1573 | "string_decoder": { 1574 | "version": "1.0.3", 1575 | "bundled": true, 1576 | "requires": { 1577 | "safe-buffer": "5.1.1" 1578 | } 1579 | }, 1580 | "strip-ansi": { 1581 | "version": "3.0.1", 1582 | "bundled": true, 1583 | "requires": { 1584 | "ansi-regex": "2.1.1" 1585 | } 1586 | }, 1587 | "strip-json-comments": { 1588 | "version": "2.0.1", 1589 | "bundled": true 1590 | }, 1591 | "tar": { 1592 | "version": "4.4.0", 1593 | "bundled": true, 1594 | "requires": { 1595 | "chownr": "1.0.1", 1596 | "fs-minipass": "1.2.5", 1597 | "minipass": "2.2.1", 1598 | "minizlib": "1.1.0", 1599 | "mkdirp": "0.5.1", 1600 | "yallist": "3.0.2" 1601 | } 1602 | }, 1603 | "util-deprecate": { 1604 | "version": "1.0.2", 1605 | "bundled": true 1606 | }, 1607 | "wide-align": { 1608 | "version": "1.1.2", 1609 | "bundled": true, 1610 | "requires": { 1611 | "string-width": "1.0.2" 1612 | } 1613 | }, 1614 | "wrappy": { 1615 | "version": "1.0.2", 1616 | "bundled": true 1617 | }, 1618 | "yallist": { 1619 | "version": "3.0.2", 1620 | "bundled": true 1621 | } 1622 | } 1623 | }, 1624 | "sshpk": { 1625 | "version": "1.15.1", 1626 | "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.15.1.tgz", 1627 | "integrity": "sha512-mSdgNUaidk+dRU5MhYtN9zebdzF2iG0cNPWy8HG+W8y+fT1JnSkh0fzzpjOa0L7P8i1Rscz38t0h4gPcKz43xA==", 1628 | "requires": { 1629 | "asn1": "0.2.4", 1630 | "assert-plus": "1.0.0", 1631 | "bcrypt-pbkdf": "1.0.2", 1632 | "dashdash": "1.14.1", 1633 | "ecc-jsbn": "0.1.2", 1634 | "getpass": "0.1.7", 1635 | "jsbn": "0.1.1", 1636 | "safer-buffer": "2.1.2", 1637 | "tweetnacl": "0.14.5" 1638 | } 1639 | }, 1640 | "statuses": { 1641 | "version": "1.4.0", 1642 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", 1643 | "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" 1644 | }, 1645 | "streamifier": { 1646 | "version": "0.1.1", 1647 | "resolved": "https://registry.npmjs.org/streamifier/-/streamifier-0.1.1.tgz", 1648 | "integrity": "sha1-l+mNj6TRBdYqJpHR3AfoINuN/E8=" 1649 | }, 1650 | "streamsearch": { 1651 | "version": "0.1.2", 1652 | "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", 1653 | "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=" 1654 | }, 1655 | "string_decoder": { 1656 | "version": "0.10.31", 1657 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", 1658 | "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" 1659 | }, 1660 | "tmp": { 1661 | "version": "0.0.33", 1662 | "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", 1663 | "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", 1664 | "requires": { 1665 | "os-tmpdir": "1.0.2" 1666 | } 1667 | }, 1668 | "tough-cookie": { 1669 | "version": "2.4.3", 1670 | "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", 1671 | "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", 1672 | "requires": { 1673 | "psl": "1.1.29", 1674 | "punycode": "1.4.1" 1675 | } 1676 | }, 1677 | "transformers": { 1678 | "version": "2.1.0", 1679 | "resolved": "https://registry.npmjs.org/transformers/-/transformers-2.1.0.tgz", 1680 | "integrity": "sha1-XSPLNVYd2F3Gf7hIIwm0fVPM6ac=", 1681 | "requires": { 1682 | "css": "1.0.8", 1683 | "promise": "2.0.0", 1684 | "uglify-js": "2.2.5" 1685 | }, 1686 | "dependencies": { 1687 | "is-promise": { 1688 | "version": "1.0.1", 1689 | "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-1.0.1.tgz", 1690 | "integrity": "sha1-MVc3YcBX4zwukaq56W2gjO++duU=" 1691 | }, 1692 | "promise": { 1693 | "version": "2.0.0", 1694 | "resolved": "https://registry.npmjs.org/promise/-/promise-2.0.0.tgz", 1695 | "integrity": "sha1-RmSKqdYFr10ucMMCS/WUNtoCuA4=", 1696 | "requires": { 1697 | "is-promise": "1.0.1" 1698 | } 1699 | }, 1700 | "source-map": { 1701 | "version": "0.1.43", 1702 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", 1703 | "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=", 1704 | "requires": { 1705 | "amdefine": "1.0.1" 1706 | } 1707 | }, 1708 | "uglify-js": { 1709 | "version": "2.2.5", 1710 | "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.2.5.tgz", 1711 | "integrity": "sha1-puAqcNg5eSuXgEiLe4sYTAlcmcc=", 1712 | "requires": { 1713 | "optimist": "0.3.7", 1714 | "source-map": "0.1.43" 1715 | } 1716 | } 1717 | } 1718 | }, 1719 | "tunnel-agent": { 1720 | "version": "0.6.0", 1721 | "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", 1722 | "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", 1723 | "requires": { 1724 | "safe-buffer": "5.1.1" 1725 | } 1726 | }, 1727 | "tweetnacl": { 1728 | "version": "0.14.5", 1729 | "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", 1730 | "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" 1731 | }, 1732 | "type-is": { 1733 | "version": "1.6.16", 1734 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", 1735 | "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", 1736 | "requires": { 1737 | "media-typer": "0.3.0", 1738 | "mime-types": "2.1.19" 1739 | } 1740 | }, 1741 | "typedarray": { 1742 | "version": "0.0.6", 1743 | "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", 1744 | "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" 1745 | }, 1746 | "uglify-js": { 1747 | "version": "2.8.29", 1748 | "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", 1749 | "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", 1750 | "requires": { 1751 | "source-map": "0.5.7", 1752 | "uglify-to-browserify": "1.0.2", 1753 | "yargs": "3.10.0" 1754 | }, 1755 | "dependencies": { 1756 | "source-map": { 1757 | "version": "0.5.7", 1758 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", 1759 | "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" 1760 | } 1761 | } 1762 | }, 1763 | "uglify-to-browserify": { 1764 | "version": "1.0.2", 1765 | "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", 1766 | "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", 1767 | "optional": true 1768 | }, 1769 | "unpipe": { 1770 | "version": "1.0.0", 1771 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 1772 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" 1773 | }, 1774 | "util-deprecate": { 1775 | "version": "1.0.2", 1776 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 1777 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" 1778 | }, 1779 | "utils-merge": { 1780 | "version": "1.0.1", 1781 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 1782 | "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" 1783 | }, 1784 | "uuid": { 1785 | "version": "3.3.2", 1786 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", 1787 | "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" 1788 | }, 1789 | "vary": { 1790 | "version": "1.1.2", 1791 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 1792 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" 1793 | }, 1794 | "verror": { 1795 | "version": "1.10.0", 1796 | "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", 1797 | "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", 1798 | "requires": { 1799 | "assert-plus": "1.0.0", 1800 | "core-util-is": "1.0.2", 1801 | "extsprintf": "1.3.0" 1802 | } 1803 | }, 1804 | "void-elements": { 1805 | "version": "2.0.1", 1806 | "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", 1807 | "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=" 1808 | }, 1809 | "window-size": { 1810 | "version": "0.1.0", 1811 | "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", 1812 | "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=" 1813 | }, 1814 | "wordwrap": { 1815 | "version": "0.0.2", 1816 | "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", 1817 | "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=" 1818 | }, 1819 | "xtend": { 1820 | "version": "4.0.1", 1821 | "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", 1822 | "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" 1823 | }, 1824 | "yargs": { 1825 | "version": "3.10.0", 1826 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", 1827 | "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", 1828 | "requires": { 1829 | "camelcase": "1.2.1", 1830 | "cliui": "2.1.0", 1831 | "decamelize": "1.2.0", 1832 | "window-size": "0.1.0" 1833 | } 1834 | } 1835 | } 1836 | } 1837 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cameras-app", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "node ./bin/www" 7 | }, 8 | "dependencies": { 9 | "@cloudant/cloudant": "^2.4.0", 10 | "async": "^2.6.1", 11 | "await": "^0.2.6", 12 | "bluebird": "^3.5.1", 13 | "cookie-parser": "~1.4.3", 14 | "debug": "~2.6.9", 15 | "dotenv": "^6.1.0", 16 | "ejs": "^2.6.1", 17 | "express": "~4.16.0", 18 | "express-fileupload": "^1.0.0", 19 | "http-errors": "~1.6.2", 20 | "jade": "~1.11.0", 21 | "morgan": "~1.9.0", 22 | "multer": "^1.4.1", 23 | "sqlite": "^2.9.2" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 50px; 3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; 4 | } 5 | 6 | a { 7 | color: #00B7FF; 8 | } 9 | -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var sqlite = require('sqlite'); 3 | var Promise = require('bluebird'); 4 | var async = require("async"); 5 | var await = require('await') 6 | var fs = require('fs'); 7 | var child_process = require("child_process") //.exec; 8 | var Cloudant = require('@cloudant/cloudant'); 9 | 10 | require('dotenv').load(); 11 | 12 | var multer = require('multer'); 13 | const storage = multer.diskStorage({ 14 | destination: function (req, file, cb) { 15 | cb(null, 'uploads/') 16 | }, 17 | filename: function (req, file, cb) { 18 | // console.log("saving " + file.originalname) 19 | console.log("saving " + req.body.filename) 20 | cb(null, req.body.filename) 21 | } 22 | // onParseEnd:function(req, next){ 23 | // console.log("completed") 24 | // } 25 | // })); 26 | }) 27 | const upload = multer({ 28 | storage: storage 29 | // , 30 | // onFileUploadStart: function (file) { 31 | // console.log('Upload starting for filename: ' + file.originalname); 32 | // } 33 | }) 34 | 35 | 36 | // var upload = multer({dest: './uploads/'}); 37 | 38 | var router = express.Router(); 39 | 40 | /* GET home page. */ 41 | router.get('/', function(req, res, next) { 42 | // res.render('index', { title: 'Express' }); 43 | res.render('index.html'); 44 | res.send(200) 45 | }); 46 | 47 | 48 | const dbPromise = sqlite.open('../camera_data.db', { Promise }); 49 | dbPromise.then( (res) => { 50 | db = res 51 | }) 52 | 53 | router.get('/sql', function(req, res, next) { 54 | // res.render('index', { title: 'Express' }); 55 | try { 56 | // const [post, categories] = await Promise.all([ 57 | // db.get('SELECT * FROM events WHERE id = ?', req.params.id), 58 | // const events = await(Promise.all([ 59 | // db.get('SELECT * FROM events') 60 | // ])) 61 | // Promise.all([ 62 | // db.get('SELECT * FROM events') 63 | // ]) 64 | // db.get('SELECT * FROM events').then() 65 | db.all("select * from events").then( (events) => { 66 | console.log(events) 67 | res.send(events) 68 | }) 69 | // res.render('post', { post, categories }); 70 | } catch (err) { 71 | console.log(err) 72 | res.send(500) 73 | // next(err); 74 | } 75 | }); 76 | 77 | router.get('/status', function(req, res, next) { 78 | res.send(200) 79 | }); 80 | 81 | var initCloudant = function() { 82 | var username = process.env.CLOUDANT_USERNAME || "nodejs"; 83 | var password = process.env.CLOUDANT_PASSWORD; 84 | var cloudant = Cloudant({account:username, password:password}); 85 | // cloudant.db.create('imagedb', function(err, body) { 86 | // console.log(err) 87 | // console.log(body) 88 | // }) 89 | console.log("Initializing Cloudant DB") 90 | imagedb = cloudant.db.use(process.env.CLOUDANT_DB, function(err, body) { 91 | console.log(body) 92 | if (err) { 93 | console.log(err) 94 | console.log("Creating Cloudant Table") 95 | cloudant.db.create(process.env.CLOUDANT_DB, function(err, body) { 96 | 97 | console.log(body) 98 | if (err) { 99 | console.log(err) 100 | } 101 | else { 102 | console.log("Created Cloudant DB table") 103 | } 104 | }) 105 | } else { 106 | console.log("Cloudant DB initialized") 107 | } 108 | }) 109 | 110 | } 111 | 112 | 113 | initCloudant() 114 | 115 | router.get('/initcloudant', function(req, res, next) { 116 | res.send(200) 117 | initCloudant() 118 | }); 119 | 120 | 121 | 122 | 123 | // var uploadToCloudant = function(docID, options) { 124 | // imagedb.insert({ 125 | // _id: 'event1234', // eventID = timestamp + '-' + location + '-' + channel 126 | // // originalImage: "image", 127 | // // processedImage: 'image1', 128 | // timestamp: "123456789", 129 | // cam_channel: "02", 130 | // location: "48", 131 | // classes: [] 132 | // }, function(err, data) { 133 | // console.log('Error:', err); 134 | // console.log('Data:', data); 135 | // // callback(err, data); 136 | // }) 137 | // } 138 | 139 | var createCloudantWithAttachments = function(docID, imagePath, eventProps) { 140 | console.log("creating cloudant doc") 141 | var i = imagePath.split('/') 142 | var imageName = i[i.length - 1] 143 | console.log("loading: " + imagePath) 144 | //fs.readFile(imagePath, function(err, data) { 145 | fs.readFile("detected_images/output.jpg", function(err, data) { 146 | if (!err) { 147 | // imagedb.attachment.insert(docID, imageName, data, 'image/' + imageName.split('.')[1], 148 | console.log("inserting document") 149 | console.log(docID) 150 | console.log(imagePath) 151 | console.log(eventProps) 152 | console.log(data) 153 | console.log('image/' + imageName.split('.')[1]) 154 | // console.log() 155 | imagedb.multipart.insert(eventProps, 156 | [{ 157 | name: imageName, 158 | data: data, 159 | content_type: 'image/' + imageName.split('.')[1] 160 | }], docID, function(err, body) { 161 | if (!err) { 162 | console.log(body); 163 | } else { 164 | console.log("error uploading attachment"); 165 | console.log(err) 166 | } 167 | }) 168 | } else { 169 | console.log("error loading file") 170 | } 171 | }) 172 | } 173 | 174 | // var uploadCloudantAttachment = function(docID, imagePath) { 175 | // var i = imagePath.split('/') 176 | // var imageName = i[i.length - 1] 177 | // fs.readFile(image, function(err, data) { 178 | // if (!err) { 179 | // imagedb.attachment.insert(docID, imageName, data, 'image/' + imageName.split('.')[1], 180 | // { rev: '12-150985a725ec88be471921a54ce91452' }, function(err, body) { 181 | // if (!err) 182 | // console.log(body); 183 | // }); 184 | // // imagedb.attachment.insert('rabbit', 'rabbit.png', data, 'image/png', 185 | // // { rev: '12-150985a725ec88be471921a54ce91452' }, function(err, body) { 186 | // // if (!err) 187 | // // console.log(body); 188 | // // }); 189 | // } 190 | // }); 191 | // } 192 | 193 | var parseClasses = function(original) { 194 | var classes_edited = {} 195 | original['classes'].map(function(key, idx){ 196 | var cls = key['class'] 197 | if (classes_edited[cls]) { 198 | console.log("class key exists, appending") 199 | classes_edited[cls]["data"].push(key) 200 | classes_edited[cls]["count"] += 1 201 | // if (idx == len(original['classes'])) { 202 | // return classes_edited 203 | // } 204 | } else { 205 | console.log("creating new class key") 206 | classes_edited[cls] = { 207 | data: [key], 208 | count: 1 209 | } 210 | // if (idx == len(original['classes'])) { 211 | // return classes_edited 212 | // } 213 | 214 | // classes_edited[cls]["data"] = [key] 215 | // classes_edited[cls]["count"] = 1 216 | } 217 | // return classes_edited 218 | console.log("key", key['class']) 219 | console.log("value", idx) 220 | }) 221 | return classes_edited 222 | } 223 | 224 | // Pull all captured records from Cloudant 225 | router.get('/query/all', function(req, res, next) { 226 | res.setHeader('Content-Type', 'application/json'); 227 | imagedb.list({include_docs:true}, function (err, data) { 228 | console.log(err, data) 229 | // result = data 230 | res.json(data) 231 | }) 232 | }) 233 | 234 | // query by 235 | router.get('/query/and', function(req, res, next) { 236 | res.setHeader('Content-Type', 'application/json'); 237 | // req should contain 238 | imagedb.list({include_docs:true}, function (err, data) { 239 | console.log(err, data) 240 | // result = data 241 | res.json(data) 242 | }) 243 | }) 244 | 245 | var queryCloudant = function () { 246 | var selector = { 247 | "selector": { 248 | "$and": [ 249 | { classes : {car: {"$exists": true}}}, 250 | { time : {"$gt":'20181029182423'}} 251 | ] 252 | } 253 | } 254 | // imagedb.find({selector: { classes : {car: {"$exists": true}}}}, function(er, result) { 255 | imagedb.find(selector, function(er, result) { 256 | if (er) { 257 | throw er; 258 | } 259 | console.log('Found %d matching documents', result.docs.length); 260 | for (var i = 0; i < result.docs.length; i++) { 261 | console.log(' Doc id: %s', result.docs[i]._id); 262 | } 263 | }) 264 | } 265 | 266 | router.get('/query/all', function(req, res, next) { 267 | res.setHeader('Content-Type', 'application/json'); 268 | imagedb.list({include_docs:true}, function (err, data) { 269 | console.log(err, data) 270 | // result = data 271 | res.json(data) 272 | }) 273 | }) 274 | 275 | 276 | // router.post('/test_image', function(req, res, next) { 277 | router.post('/image/upload', 278 | upload.single('file'), function(req, res, next) { 279 | /* 280 | upload.fields ([ 281 | {name: 'file', maxCount: 1}, 282 | {name: 'foo', maxCount: 1} 283 | ]), function(req, res) { 284 | */ 285 | // upload.any('file', 2), function(req, res) { 286 | // console.dir(req.files) 287 | // res.send(200) 288 | // console.log(req.body) 289 | // console.log(req.file) 290 | // console.log(req.files) 291 | // console.log(req) 292 | // TODO, this is hacky, but file upload hangs otherwise 293 | // console.log(req) 294 | if (req.file) { 295 | console.log(req.file) 296 | console.log(req.body) 297 | try { 298 | // var cmd = "/nodejsAction/cv/"example_dnn_object_detection" -c=/nodejsAction/cv/yolov3.cfg -m=/nodejsAction/cv/yolov3.weights --scale=0.00392 --rgb -i= --width=384 --height=384 --classes '/nodejsAction/cv/object_detection_classes_yolov3.txt'" 299 | // var cmd = '/root/opencv-4.0.0-beta/samples/dnn/dnn/example_dnn_object_detection -c=/tmp/yolov3.cfg -m=/tmp/yolov3.weights --scale=0.00392 --rgb -i=/tmp/cam_9_2018-10-09-14_48_00.jpg --width=384 --height=384 --classes ""' 300 | // var path = "/Users/kkbankol@us.ibm.com/projects/" 301 | 302 | // TODO, add a switch flag for running node locally vs in container 303 | // var path = "/opt/cameras_app/" 304 | var path = "/nodejsAction/cv/" 305 | // var modelArtifacts = "/nodejsAction/cv/" 306 | // var object_detection_path = "/Users/kkbankol@us.ibm.com/projects/opencv/samples/dnn/object_detection" 307 | var objectDetectionBin = path + "example_dnn_object_detection" 308 | 309 | // var modelConfigPath = "/Users/kkbankol@us.ibm.com/projects/darknet/cfg/yolov3.cfg" 310 | var modelConfigPath = path + "yolov3.cfg" 311 | // var modelWeightsPath = "/Users/kkbankol@us.ibm.com/projects/darknet/yolov3.weights" 312 | var modelWeightsPath = path + "yolov3.weights" 313 | // var modelClassesPath = "/Users/kkbankol@us.ibm.com/projects/opencv/samples/data/dnn/object_detection_classes_yolov3.txt" 314 | var modelClassesPath = path + "object_detection_classes_yolov3.txt" 315 | 316 | // var imagePath = "/Users/kkbankol@us.ibm.com/projects/smart-city-cameras/cameras_app/uploads/" + req.body.filename 317 | // var imagePath = '/Users/kkbankol@us.ibm.com/projects/smart-city-cameras/dnn-object-detection/scripts/motion_images/' + req.body.filename 318 | var imagePath = "/opt/cameras_app/uploads/" + req.body.filename 319 | var miscOpts = "--width=384 --height=384 --scale=0.00392 --rgb" 320 | var cmd = [objectDetectionBin, " -c=" + modelConfigPath, " -m=" + modelWeightsPath, " -i=" + imagePath, miscOpts, " --classes '" + modelClassesPath + "'"] 321 | // var cmd = "/Users/kkbankol@us.ibm.com/projects/opencv/samples/dnn/object_detection -c=/Users/kkbankol@us.ibm.com/projects/darknet/cfg/yolov3.cfg -m=/Users/kkbankol@us.ibm.com/projects/darknet/yolov3.weights --scale=0.00392 --rgb -i=/Users/kkbankol@us.ibm.com/projects/smart-city-cameras/cameras_app/uploads/file --width=384 --height=384 --classes '/Users/kkbankol@us.ibm.com/projects/opencv/samples/data/dnn/object_detection_classes_yolov3.txt'" 322 | // console.log(req.file.path) 323 | //child_process.execSync(cmd) 324 | //var callFunc = exec(cmd, function (error, stdout, stderr) { 325 | //console.log(stdout) 326 | //result = stdout; 327 | //return {payload: result}; 328 | //return stdout 329 | //}); 330 | // console.log(callFunc) 331 | console.log(cmd.join(" ")) 332 | var extractClasses = child_process.execSync(cmd.join(" ")).toString() 333 | // var listClasses = extractClasses.filter(function(elem, pos) { 334 | // return extractClasses.indexOf(elem) == pos; 335 | // }) 336 | var doc_contents = {} 337 | // console.log("document:", [doc_contents, req.body, extractClasses].reduce(Object.assign)), 338 | // console.log("document:", Object.assign(doc_contents, req.body, JSON.parse(extractClasses))) 339 | console.log("document:", Object.assign(doc_contents, req.body, {classes: parseClasses(JSON.parse(extractClasses))} )) 340 | 341 | createCloudantWithAttachments( 342 | req.body.time + '-' + req.body.channel + '-' + req.body.location, 343 | imagePath, 344 | // Object.assign({}, req.body) 345 | // Object.assign(doc_contents, req.body, JSON.parse(extractClasses)), 346 | Object.assign( 347 | doc_contents, 348 | req.body, { 349 | classes: parseClasses(JSON.parse(extractClasses)) 350 | } 351 | ), 352 | // { 353 | // meta: req.body, 354 | // obj: JSON.parse(extractClasses) 355 | // } 356 | ) 357 | res.send({payload: parseClasses}) 358 | console.log("updated") 359 | // next() 360 | } catch (err) { 361 | console.log(err) 362 | res.send(500) 363 | // next(err); 364 | } 365 | // res.send(200) 366 | } 367 | // res.send(200) 368 | // console.log(req) 369 | // console.log(Object.keys(req)) 370 | // res.send(console.dir(req.files)); // DEBUG: display available fields 371 | }) 372 | 373 | /* 374 | router.post('/image', function(req, res, next) { 375 | // res.render('index', { title: 'Express' }); 376 | 377 | 378 | // if (headers['Content-Type']) == "image" ??? { 379 | // write to 380 | // } 381 | // if request type includes rtsp url / cam { 382 | // connect to rtsp 383 | // take screenshot 384 | // write to file 385 | // run example dnn_object 386 | // } 387 | 388 | try { 389 | var cmd = "/nodejsAction/cv/example_dnn_object_detection -c=/nodejsAction/cv/yolov3.cfg -m=/nodejsAction/cv/yolov3.weights --scale=0.00392 --rgb -i=/nodejsAction/cv/cam_9_2018-10-09-14_48_00.jpg --width=384 --height=384 --classes '/nodejsAction/cv/object_detection_classes_yolov3.txt'" 390 | //child_process.execSync(cmd) 391 | //var callFunc = exec(cmd, function (error, stdout, stderr) { 392 | //console.log(stdout) 393 | //result = stdout; 394 | //return {payload: result}; 395 | //return stdout 396 | //}); 397 | var callFunc = child_process.execSync(cmd).toString() 398 | res.send({payload: callFunc}) 399 | } catch (err) { 400 | console.log(err) 401 | res.send(500) 402 | // next(err); 403 | } 404 | 405 | }); 406 | */ 407 | 408 | 409 | 410 | 411 | // router.get('/query', function(req, res, next) { 412 | // 413 | // }); 414 | 415 | 416 | module.exports = router; 417 | -------------------------------------------------------------------------------- /routes/users.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | 4 | /* GET users listing. */ 5 | router.get('/', function(req, res, next) { 6 | res.send('respond with a resource'); 7 | }); 8 | 9 | module.exports = router; 10 | -------------------------------------------------------------------------------- /sample_videos/vid10.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/dnn-object-detection/ef442ac569fff9d57923967dba6352cda3a1ca62/sample_videos/vid10.mp4 -------------------------------------------------------------------------------- /scripts/bx_auth.sh: -------------------------------------------------------------------------------- 1 | # Copyright [2018] IBM Corp. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | #!/bin/bash -e 16 | 17 | # This script is intended to be run by Travis CI. If running elsewhere, invoke 18 | # it with: TRAVIS_PULL_REQUEST=false [path to script] 19 | # If no credentials are provided at runtime, bx will use the environment 20 | # variable BLUEMIX_API_KEY. If no API key is set, it will prompt for 21 | # credentials. 22 | 23 | # shellcheck disable=SC1090 24 | source "$(dirname "$0")"/../scripts/resources.sh 25 | 26 | BLUEMIX_ORG="Developer Advocacy" 27 | BLUEMIX_SPACE="dev" 28 | 29 | is_pull_request "$0" 30 | 31 | echo "Authenticating to Bluemix" 32 | bx login -a https://api.ng.bluemix.net 33 | 34 | echo "Targeting Bluemix org and space" 35 | bx target -o "$BLUEMIX_ORG" -s "$BLUEMIX_SPACE" 36 | 37 | echo "Initializing Bluemix Container Service" 38 | bx cs init 39 | -------------------------------------------------------------------------------- /scripts/cv_object_detection.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import cv2 3 | import time 4 | import uuid 5 | import sys 6 | from datetime import datetime 7 | import requests 8 | import ImageChops 9 | from threading import Timer 10 | import argparse 11 | import math 12 | # make sure to get opencv 3.4 and up 13 | # pip3.7 install opencv-contrib-python 14 | 15 | # Motion detection flow 16 | 17 | # The cutoff for threshold. A lower number means smaller changes between 18 | # the average and current scene are more readily detected. 19 | THRESHOLD_SENSITIVITY = 50 20 | # Number of pixels in each direction to blur the difference between 21 | # average and current scene. This helps make small differences larger 22 | # and more detectable. 23 | BLUR_SIZE = 20 24 | # The number of square pixels a blob must be before we consider it a 25 | # candidate for tracking. 26 | BLOB_SIZE = 75 # 400 27 | # The number of pixels wide a blob must be before we consider it a 28 | # candidate for tracking. 29 | BLOB_WIDTH = 5 30 | # The weighting to apply to "this" frame when averaging. A higher number 31 | # here means that the average scene will pick up changes more readily, 32 | # thus making the difference between average and current scenes smaller. 33 | DEFAULT_AVERAGE_WEIGHT = 0.08 34 | # The maximum distance a blob centroid is allowed to move in order to 35 | # consider it a match to a previous scene's blob. 36 | BLOB_LOCKON_DISTANCE_PX = 80 37 | # The number of seconds a blob is allowed to sit around without having 38 | # any new blobs matching it. 39 | BLOB_TRACK_TIMEOUT = 10 #0.7 40 | # The left and right X positions of the "poles". These are used to 41 | # track the speed of a vehicle across the scene. 42 | LEFT_POLE_PX = 320 43 | RIGHT_POLE_PX = 500 44 | # Constants for drawing on the frame. 45 | LINE_THICKNESS = 1 46 | CIRCLE_SIZE = 5 47 | RESIZE_RATIO = 0.4 48 | 49 | methods = ['cv2.TM_CCOEFF', 'cv2.TM_CCOEFF_NORMED', 'cv2.TM_CCORR', 50 | 'cv2.TM_CCORR_NORMED', 'cv2.TM_SQDIFF', 'cv2.TM_SQDIFF_NORMED'] 51 | 52 | ### https://gist.githubusercontent.com/bigsnarfdude/cf51fde11fb30ef23628b1547ecdd939/raw/040e6abe88e91779012f8e7a6a5785f43d30afe8/diff.py 53 | from itertools import tee, izip 54 | def pairwise(iterable): 55 | "s -> (s0,s1), (s1,s2), (s2, s3), ..." 56 | a, b = tee(iterable) 57 | next(b, None) 58 | return izip(a, b) 59 | ### 60 | # webcam 61 | # vid = cv2.VideoCapture(0) 62 | # rtsp 63 | # cv.VideoCapture("rtsp://192.168.1.2:8080/out.h264") 64 | frame_count = 0 65 | prev_frame = None 66 | prev_ret = None 67 | avg = None 68 | tracked_blobs = [] 69 | last_frame_captured = None 70 | cam_channel = '9' 71 | 72 | # - Load video source (live RTSP or recorded) 73 | # filename = '/Users/kkbankol@us.ibm.com/projects/smart-city-cameras/sample_videos/HD-D113 CCTV Camera Sample Day Video Footage- Erdington Thie.mp4' 74 | 75 | # parser = argparse.ArgumentParser(description='Process motion detection vars') 76 | # parser.add_argument('source', metavar='source', type=string, nargs='+', 77 | # help='video source, rtsp endpoint or path to file') 78 | # parser.add_argument('endpoint', dest='endpoint', action='store_const', 79 | # const=sum, default=max, 80 | # help='sum the integers (default: find the max)') 81 | # args = parser.parse_args() 82 | 83 | # TODO fix argparser above instead 84 | if len(sys.argv) > 1 : 85 | stream = sys.argv[1] # './sample_videos/vid10.mp4' 86 | obj_detection_ip = sys.argv[2] 87 | obj_detection_port = sys.argv[3] #"3000" 88 | 89 | print("reading from " + stream) 90 | # prerecorded video 91 | vid = cv2.VideoCapture(stream) #rtsp_stream) 92 | # cv2.waitKey() 93 | cv2.startWindowThread() 94 | cv2.namedWindow("preview") 95 | num_blobs = 0 96 | 97 | def update_list_tracked_blobs(): 98 | blob_ids = [ blob['id'] for blob in tracked_blobs ] 99 | print("list blob_ids") 100 | print(blob_ids) 101 | 102 | def save_screenshot(frame): 103 | timestamp = datetime.now().strftime('%Y%m%d%H%M%S') 104 | filename = "cam_" + cam_channel + '_' + timestamp + '.jpg' 105 | print("writing screenshot: " + filename) 106 | cv2.imwrite("motion_images/" + filename, frame) 107 | 108 | 109 | cv2.namedWindow("imgc"); 110 | cv2.moveWindow("imgc", 20,20); 111 | 112 | # set limit on window size, only show last x captured frames 113 | cv2.namedWindow("captured images"); 114 | cv2.moveWindow("captured images", 80,650); 115 | 116 | cv2.namedWindow("threshold images"); 117 | 118 | while(vid.isOpened()): 119 | if cv2.waitKey(1) & 0xFF == ord('q'): 120 | vid.release() 121 | break 122 | # cv2.TrackerCSRT_create() 123 | ret, frame = vid.read() 124 | # if frame is valid and contains a picture 125 | if not ret: 126 | vid.release() 127 | break 128 | else: 129 | frame_time = time.time() 130 | frame_count += 1 131 | if (frame_count % 100 == 0): 132 | print("frame: " + str(frame_count)) 133 | # Convert frame to grayscale 134 | if prev_ret == None: 135 | prev_frame = frame 136 | prev_gray = cv2.cvtColor(prev_frame, cv2.COLOR_BGR2GRAY) #COLOR_BGR2GRAY) 137 | prev_ret = ret 138 | continue 139 | gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) 140 | hsvFrame = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) 141 | (_, _, grayFrame) = cv2.split(hsvFrame) 142 | # reduce noise via gauss blur + smooth 143 | grayFrame = cv2.GaussianBlur(grayFrame, (21, 21), 0) 144 | # cv2.imshow("preview", grayFrame) 145 | if avg is None: 146 | print("Setting average variable") 147 | # Set up the average if this is the first time through. 148 | # if avg doesn't exist, copy 149 | avg = grayFrame.copy().astype("float") 150 | captured_images = np.array((frame)) 151 | continue 152 | cv2.accumulateWeighted(grayFrame, avg, DEFAULT_AVERAGE_WEIGHT) 153 | # cv2.imshow("average", cv2.convertScaleAbs(avg)) 154 | differenceFrame = cv2.absdiff(grayFrame, cv2.convertScaleAbs(avg)) 155 | # cv2.imshow("difference", differenceFrame) 156 | # Apply a threshold to the difference: any pixel value above the sensitivity 157 | # value will be set to 255 and any pixel value below will be set to 0. 158 | retval, thresholdImage = cv2.threshold(differenceFrame, THRESHOLD_SENSITIVITY, 255, cv2.THRESH_BINARY) 159 | thresholdImage = cv2.dilate(thresholdImage, None, iterations=2) 160 | try: 161 | threshold_images 162 | except NameError: 163 | threshold_images = np.array((thresholdImage)) 164 | # cv2.imshow("threshold", thresholdImage) 165 | # eigenface[0] = grayFrame 166 | # eigenface[1] = cv2.convertScaleAbs(avg) 167 | # eigenface[2] = differenceFrame 168 | # eigenface[3] = thresholdImage 169 | # hconcat(eigenface,3,dst); 170 | views1 = np.hstack((gray, cv2.convertScaleAbs(avg))) 171 | views2 = np.hstack((differenceFrame, thresholdImage)) 172 | all_views = np.vstack(( views1, views2 )) 173 | # all_views = np.vstack(( all_views, (differenceFrame, thresholdImage) )) 174 | cv2.imshow('imgc', all_views) 175 | # Find contours aka blobs in the threshold image. 176 | im2, contours, hierarchy = cv2.findContours(thresholdImage, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) 177 | # Technically can stop here if detected object significant enough, follow up with 178 | # - Raise "object detected" flag 179 | # - publish cam number + rtsp url to MQTT 180 | # - server begin recording via RTSP connection, or take multiple screenshots (screenshots should suffice in most cases, but probably want to record video too in case of incident) 181 | # - run object detection algorithm on captured vid/frames. 182 | # - aggregate recognized tags. include screenshot of whole labeled frame (and maybe individual objects?) 183 | # 184 | # Filter out the blobs that are too small to be considered cars. 185 | blobs = filter(lambda c: cv2.contourArea(c) > BLOB_SIZE, contours) 186 | if blobs: 187 | # print(blobs[0]) 188 | # print("motion detected") 189 | # instead of comparing how different the frame is, compare based off num blobs 190 | print("num blobs") 191 | print(len(blobs)) 192 | if len(blobs) > num_blobs: 193 | print('new blob detected') 194 | num_blobs = len(blobs) 195 | elif len(blobs) < num_blobs: 196 | print('object left frame') 197 | num_blobs = len(blobs) 198 | else: 199 | print('same amount of objects in file') 200 | # TODO, take list of tracked blobs and take action when a new one is created 201 | # print("tracked_blobs") 202 | # print(tracked_blobs) 203 | # print(len(tracked_blobs)) tracked_blobs['id'] 204 | 205 | # take screenshot every time number of blobs increases from the last frame 206 | # compare frame to last frame captured, skip iteration if too similar 207 | # print(cv2.matchTemplate(last_frame_captured, frame, eval(methods[1]))) 208 | # print(h) 209 | if last_frame_captured is None: 210 | last_frame_captured = frame 211 | match_threshold = 0.94 # 17873124.0 #0.95 # [[0.99075127]] 212 | calculated_threshold = cv2.matchTemplate(last_frame_captured, frame, eval(methods[1]))[0] 213 | print(calculated_threshold[0]) 214 | print(match_threshold) 215 | if (calculated_threshold[0] > match_threshold) : 216 | print("too similar to last captured frame, skipping") 217 | # time.sleep(0.1) 218 | # continue 219 | time.sleep(0.1) 220 | for c in blobs: 221 | # Find the bounding rectangle and center for each blob 222 | (x, y, w, h) = cv2.boundingRect(c) 223 | center = (int(x + w/2), int(y + h/2)) 224 | 225 | ## Optionally draw the rectangle around the blob on the frame that we'll show in a UI later 226 | # cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 0, 255), LINE_THICKNESS) 227 | 228 | # Look for existing blobs that match this one 229 | closest_blob = None 230 | if tracked_blobs: 231 | print("tracking " + str(len(tracked_blobs)) + " blobs") 232 | # Sort the blobs we have seen in previous frames by pixel distance from this one 233 | closest_blobs = sorted(tracked_blobs, key=lambda b: cv2.norm(b['trail'][0], center)) 234 | # print( tracked_blobs) 235 | update_list_tracked_blobs() 236 | # so if we have new blobs that didn't exist in the last frame, or are being newly tracked, store screenshot? 237 | # if there's a new blob in the tracked list that wasn't already there? 238 | 239 | # Starting from the closest blob, make sure the blob in question is in the expected direction 240 | for close_blob in closest_blobs: 241 | distance = cv2.norm(center, close_blob['trail'][0]) 242 | 243 | # Check if the distance is close enough to "lock on" 244 | if distance < BLOB_LOCKON_DISTANCE_PX: 245 | # If it's close enough, make sure the blob was moving in the expected direction 246 | expected_dir = close_blob['dir'] 247 | if expected_dir == 'left' and close_blob['trail'][0][0] < center[0]: 248 | continue 249 | elif expected_dir == 'right' and close_blob['trail'][0][0] > center[0]: 250 | continue 251 | else: 252 | closest_blob = close_blob 253 | break 254 | 255 | if closest_blob: 256 | # If we found a blob to attach this blob to, we should 257 | # do some math to help us with speed detection 258 | prev_center = closest_blob['trail'][0] 259 | if center[0] < prev_center[0]: 260 | # It's moving left 261 | closest_blob['dir'] = 'left' 262 | closest_blob['bumper_x'] = x 263 | else: 264 | # It's moving right 265 | closest_blob['dir'] = 'right' 266 | closest_blob['bumper_x'] = x + w 267 | 268 | # ...and we should add this centroid to the trail of 269 | # points that make up this blob's history. 270 | closest_blob['trail'].insert(0, center) 271 | closest_blob['last_seen'] = frame_time 272 | 273 | if not closest_blob: 274 | # If we didn't find a blob, let's make a new one and add it to the list 275 | b = dict( 276 | id=str(uuid.uuid4())[:8], 277 | first_seen=frame_time, 278 | last_seen=frame_time, 279 | dir=None, 280 | bumper_x=None, 281 | trail=[center], 282 | ) 283 | print(center) 284 | # distance = math.sqrt( ((p1[0]-p2[0])**2)+((p1[1]-p2[1])**2) ) 285 | # sys.exit(0) 286 | tracked_blobs.append(b) 287 | update_list_tracked_blobs() 288 | # TODO TODO TODO 289 | # update to respond to new blob only if more than x pixels away from others 290 | print("new object detected, taking screenshot, uploading") 291 | # TODO, maybe skip ahead 5 frames later to take screenshot? since object will be just on border 292 | # Or take one screenshot here, another x frames later? 293 | # save_screenshot(frame, cam_channel) 294 | timestamp = datetime.now().strftime('%Y%m%d%H%M%S') 295 | filename = "cam_" + cam_channel + '_' + timestamp + '.jpg' 296 | print("writing to: " + filename) 297 | cv2.imwrite("motion_images/" + filename, frame) 298 | last_frame_captured = frame 299 | # post to kube api 300 | image_path = "motion_images/" + filename 301 | url="http://" + obj_detection_ip + ":" + obj_detection_port + "/image/upload" 302 | files = {'file': open(image_path, 'rb').read()} 303 | data = {'filename': filename, 'time': timestamp, 'channel': cam_channel, 'location': '47'} 304 | # print(data) 305 | print("posting " + filename + " to node") 306 | r = requests.post(url, files=files, data=data) 307 | # print(r.text) 308 | time.sleep(2) # TODO, instead of pausing perhaps we can record video until motion stops? 309 | # Timer(5, save_screenshot(frame), ()).start() 310 | views1 = np.hstack((gray, cv2.convertScaleAbs(avg))) 311 | views2 = np.hstack((differenceFrame, thresholdImage)) 312 | threshold_images = np.hstack((thresholdImage, threshold_images)) 313 | all_views = np.vstack(( views1, views2 )) 314 | captured_images = np.hstack((frame, captured_images)) 315 | # all_views = np.vstack(( all_views, (differenceFrame, thresholdImage) )) 316 | cv2.imshow('imgc', all_views) 317 | cv2.imshow('captured images', captured_images) 318 | cv2.imshow('threshold images', threshold_images) 319 | 320 | 321 | # take screenshot and post 322 | # TODO: should add more logic here...take additional screenshot when tracked blob has existed for more than x seconds? 323 | if tracked_blobs: 324 | # Prune out the blobs that haven't been seen in some amount of time 325 | for i in xrange(len(tracked_blobs) - 1, -1, -1): 326 | if frame_time - tracked_blobs[i]['last_seen'] > BLOB_TRACK_TIMEOUT: 327 | # print "Removing expired track {}".format(tracked_blobs[i]['id']) 328 | del tracked_blobs[i] 329 | update_list_tracked_blobs() 330 | 331 | # Draw the fences 332 | # cv2.line(frame, (LEFT_POLE_PX, 0), (LEFT_POLE_PX, 700), (100, 100, 100), 2) 333 | # cv2.line(frame, (RIGHT_POLE_PX, 0), (RIGHT_POLE_PX, 700), (100, 100, 100), 2) 334 | # Draw information about the blobs on the screen 335 | for blob in tracked_blobs: 336 | for (a, b) in pairwise(blob['trail']): 337 | cv2.circle(frame, a, 3, (255, 0, 0), LINE_THICKNESS) 338 | 339 | if blob['dir'] == 'left': 340 | cv2.line(frame, a, b, (255, 255, 0), LINE_THICKNESS) 341 | else: 342 | cv2.line(frame, a, b, (0, 255, 255), LINE_THICKNESS) 343 | 344 | bumper_x = blob['bumper_x'] 345 | if bumper_x: 346 | cv2.line(frame, (bumper_x, 100), (bumper_x, 500), (255, 0, 255), 3) 347 | 348 | # cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 0, 255), LINE_THICKNESS) 349 | # cv2.circle(frame, center, 10, (0, 255, 0), LINE_THICKNESS) 350 | 351 | # Show the image from the camera (along with all the lines and annotations) 352 | # in a window on the user's screen. 353 | # Only use the Value channel of the frame. 354 | # https://gist.githubusercontent.com/bigsnarfdude/cf51fde11fb30ef23628b1547ecdd939/raw/040e6abe88e91779012f8e7a6a5785f43d30afe8/diff.py 355 | # print("Take value of frame") 356 | # (_, _, grayFrame) = cv2.split(gray) 357 | # print("Blur") 358 | # grayFrame = cv2.GaussianBlur(gray, (21, 21), 0) 359 | # time.sleep(0.10) 360 | # cv2.imshow('frame', gray) 361 | # Build the average scene image by accumulating this frame 362 | # with the existing average. 363 | # cv2.imshow("preview", frame) 364 | cv2.accumulateWeighted(grayFrame, avg, DEFAULT_AVERAGE_WEIGHT) 365 | # cv2.imshow("average", cv2.convertScaleAbs(avg)) 366 | differenceFrame = cv2.absdiff(grayFrame, cv2.convertScaleAbs(avg)) 367 | # cv2.imshow("difference", differenceFrame) 368 | # Apply a threshold to the difference: any pixel value above the sensitivity 369 | # value will be set to 255 and any pixel value below will be set to 0. 370 | retval, thresholdImage = cv2.threshold(differenceFrame, THRESHOLD_SENSITIVITY, 255, cv2.THRESH_BINARY) 371 | thresholdImage = cv2.dilate(thresholdImage, None, iterations=2) 372 | # cv2.imshow("threshold", thresholdImage) 373 | # Find contours aka blobs in the threshold image. 374 | im2, contours, hierarchy = cv2.findContours(thresholdImage, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) 375 | # Filter out the blobs that are too small to be considered cars. 376 | blobs = filter(lambda c: cv2.contourArea(c) > BLOB_SIZE, contours) 377 | # time.sleep(0.01) 378 | # Display frame 379 | # cv2.imshow('frame', gray) 380 | prev_frame = frame 381 | prev_gray = gray 382 | # frame1 = gray 383 | # # if frame1 is None: 384 | # motion_frame = gray # last frame where motion was detected 385 | # # Compare to last frame 386 | # contours = cv.FindContours(grey_image, storage, cv.CV_RETR_CCOMP, cv.CV_CHAIN_APPROX_SIMPLE) 387 | # else: 388 | # # might need to remove this ^, shouldn't exit in case of camera failure 389 | # print("Finished processing " + str(frame_count) + " frames") 390 | # vid.release() 391 | # cv2.destroyAllWindows() 392 | vid.release() 393 | cv2.destroyAllWindows() 394 | 395 | # def process_frame: 396 | # - Convert frame to greyscale 397 | # - Dilate hole, find contour (shape) 398 | 399 | # def subtractFrames(frame1, frame2) 400 | # cv.AbsDiff() 401 | # 402 | # remove noise, return threshold 403 | # https://github.com/RobinDavid/Motion-detection-OpenCV/blob/master/MotionDetector.py#L87:#L90 404 | 405 | 406 | # - Capture frame, set as baseline 407 | # process_frame, set as frame1 408 | 409 | # - Grab next frame 410 | # process_frame, set as frame2 411 | # subtractFrames 412 | 413 | # if threshold > minThreshold 414 | # begin recording 415 | -------------------------------------------------------------------------------- /scripts/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Create Drupal" 4 | IP_ADDR=$(bx cs workers "$CLUSTER_NAME" | grep Ready | awk '{ print $2 }') 5 | if [[ -z "$IP_ADDR" ]]; then 6 | echo "$CLUSTER_NAME not created or workers not ready" 7 | exit 1 8 | fi 9 | 10 | echo -e "Configuring vars" 11 | if ! exp=$(bx cs cluster-config "$CLUSTER_NAME" | grep export); then 12 | echo "Cluster $CLUSTER_NAME not created or not ready." 13 | exit 1 14 | fi 15 | eval "$exp" 16 | 17 | # echo -e "Deleting previous version of Drupal if it exists" 18 | # kubectl delete --ignore-not-found=true svc,pvc,deployment -l app=drupal 19 | # kubectl delete --ignore-not-found=true -f kubernetes/local-volumes.yaml 20 | 21 | echo -e "Creating pods" 22 | # kubectl create -f kubernetes/local-volumes.yaml 23 | # kubectl create -f kubernetes/postgres.yaml 24 | # kubectl create -f kubernetes/drupal.yaml 25 | # kubectl get svc drupal 26 | kubectl apply -f kubernetes/kube-config.yml 27 | 28 | echo "" && echo "View your Opencv cluster at http://$IP_ADDR:30000" 29 | -------------------------------------------------------------------------------- /scripts/install.sh: -------------------------------------------------------------------------------- 1 | # Copyright [2018] IBM Corp. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | #!/bin/bash -e 16 | 17 | # This script is intended to be run by Travis CI. If running elsewhere, invoke 18 | # it with: TRAVIS_PULL_REQUEST=false [path to script] 19 | 20 | # shellcheck disable=SC1090 21 | source "$(dirname "$0")"/../scripts/resources.sh 22 | 23 | is_pull_request "$0" 24 | 25 | echo "Install Bluemix CLI" 26 | curl -L https://public.dhe.ibm.com/cloud/bluemix/cli/bluemix-cli/latest/IBM_Cloud_CLI_amd64.tar.gz > Bluemix_CLI.tar.gz 27 | tar -xvf Bluemix_CLI.tar.gz 28 | sudo ./Bluemix_CLI/install_bluemix_cli 29 | 30 | echo "Install kubectl" 31 | curl -LO https://storage.googleapis.com/kubernetes-release/release/"$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)"/bin/linux/amd64/kubectl 32 | chmod 0755 kubectl 33 | sudo mv kubectl /usr/local/bin 34 | 35 | echo "Configuring bx to disable version check" 36 | bx config --check-version=false 37 | echo "Checking bx version" 38 | bx --version 39 | echo "Install the Bluemix container-service plugin" 40 | bx plugin install container-service -r Bluemix 41 | 42 | if [[ -n "$DEBUG" ]]; then 43 | bx --version 44 | bx plugin list 45 | fi 46 | -------------------------------------------------------------------------------- /scripts/object_detection.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | 11 | // #include 12 | 13 | const char* keys = 14 | "{ help h | | Print help message. }" 15 | "{ device | 0 | camera device number. }" 16 | "{ input i | | Path to input image or video file. Skip this argument to capture frames from a camera. }" 17 | "{ model m | | Path to a binary file of model contains trained weights. " 18 | "It could be a file with extensions .caffemodel (Caffe), " 19 | ".pb (TensorFlow), .t7 or .net (Torch), .weights (Darknet).}" 20 | "{ config c | | Path to a text file of model contains network configuration. " 21 | "It could be a file with extensions .prototxt (Caffe), .pbtxt (TensorFlow), .cfg (Darknet).}" 22 | "{ framework f | | Optional name of an origin framework of the model. Detect it automatically if it does not set. }" 23 | "{ classes | | Optional path to a text file with names of classes to label detected objects. }" 24 | "{ mean | | Preprocess input image by subtracting mean values. Mean values should be in BGR order and delimited by spaces. }" 25 | "{ scale | 1 | Preprocess input image by multiplying on a scale factor. }" 26 | "{ width | -1 | Preprocess input image by resizing to a specific width. }" 27 | "{ height | -1 | Preprocess input image by resizing to a specific height. }" 28 | "{ rgb | | Indicate that model works with RGB input images instead BGR ones. }" 29 | "{ thr | .5 | Confidence threshold. }" 30 | "{ nms | .4 | Non-maximum suppression threshold. }" 31 | "{ backend | 0 | Choose one of computation backends: " 32 | "0: automatically (by default), " 33 | "1: Halide language (http://halide-lang.org/), " 34 | "2: Intel's Deep Learning Inference Engine (https://software.intel.com/openvino-toolkit), " 35 | "3: OpenCV implementation }" 36 | "{ target | 0 | Choose one of target computation devices: " 37 | "0: CPU target (by default), " 38 | "1: OpenCL, " 39 | "2: OpenCL fp16 (half-float precision), " 40 | "3: VPU }"; 41 | 42 | 43 | using namespace cv; 44 | using namespace dnn; 45 | 46 | float confThreshold, nmsThreshold; 47 | std::vector classes; 48 | 49 | void postprocess(Mat& frame, const std::vector& out, Net& net); 50 | 51 | void drawPred(int classId, float conf, int left, int top, int right, int bottom, Mat& frame); 52 | 53 | void callback(int pos, void* userdata); 54 | 55 | std::vector getOutputsNames(const Net& net); 56 | 57 | int main(int argc, char** argv) 58 | { 59 | CommandLineParser parser(argc, argv, keys); 60 | parser.about("Use this script to run object detection deep learning networks using OpenCV."); 61 | if (argc == 1 || parser.has("help")) 62 | { 63 | parser.printMessage(); 64 | return 0; 65 | } 66 | 67 | confThreshold = parser.get("thr"); 68 | nmsThreshold = parser.get("nms"); 69 | float scale = parser.get("scale"); 70 | Scalar mean = parser.get("mean"); 71 | bool swapRB = parser.get("rgb"); 72 | int inpWidth = parser.get("width"); 73 | int inpHeight = parser.get("height"); 74 | 75 | // Open file with classes names. 76 | // if (parser.has("classes")) 77 | // { 78 | // // std::cout << "Parsing classes" << '\n'; 79 | // // TODO, classes flag always being parsed as "true" 80 | // std::string file = parser.get("classes"); 81 | // // std::string file = "/nodejsAction/cv/object_detection_classes_yolov3.txt"; 82 | // //std::string file = "/Users/kkbankol@us.ibm.com/projects/opencv/samples/data/dnn/object_detection_classes_yolov3.txt"; 83 | // // std::cout << "file: " << file << '\n'; 84 | // std::ifstream ifs(file); 85 | // // std::cout << "ifs is_open: " << ifs.is_open() << '\n'; 86 | // if (!ifs.is_open()) { 87 | // CV_Error(Error::StsError, "File " + file + " not found"); 88 | // } 89 | // std::string line; 90 | // while (std::getline(ifs, line)) 91 | // { 92 | // classes.push_back(line); 93 | // // std::cout << "line: " << line << '\n'; 94 | // } 95 | // } 96 | // Classes for YOLO model. Hard coding since loading text file doesn't seems to work 97 | classes = {"person", "bicycle", "car", "motorcycle", "airplane", "bus", "train", "truck", "boat", "traffic light", "fire hydrant", "stop sign", "parking meter", "bench", "bird", "cat", "dog", "horse", "sheep", "cow", "elephant", "bear", "zebra", "giraffe", "backpack", "umbrella", "handbag", "tie", "suitcase", "frisbee", "skis", "snowboard", "sports ball", "kite", "baseball bat", "baseball glove", "skateboard", "surfboard", "tennis racket", "bottle", "wine glass", "cup", "fork", "knife", "spoon", "bowl", "banana", "apple", "sandwich", "orange", "broccoli", "carrot", "hot dog", "pizza", "donut", "cake", "chair", "couch", "potted plant", "bed", "dining table", "toilet", "tv", "laptop", "mouse", "remote", "keyboard", "cell phone", "microwave", "oven", "toaster", "sink", "refrigerator", "book", "clock", "vase", "scissors", "teddy bear", "hair drier", "toothbrush"}; 98 | // Load a model. 99 | CV_Assert(parser.has("model")); 100 | Net net = readNet(parser.get("model"), parser.get("config"), parser.get("framework")); 101 | net.setPreferableBackend(parser.get("backend")); 102 | net.setPreferableTarget(parser.get("target")); 103 | 104 | // Create a window 105 | static const std::string kWinName = "Deep learning object detection in OpenCV"; 106 | // namedWindow(kWinName, WINDOW_NORMAL); 107 | int initialConf = (int)(confThreshold * 100); 108 | // createTrackbar("Confidence threshold, %", kWinName, &initialConf, 99, callback); 109 | 110 | // Open a video file or an image file or a camera stream. 111 | VideoCapture cap; 112 | if (parser.has("input")) 113 | // std::string fname = parser.get("input"); 114 | cap.open(parser.get("input")); 115 | else 116 | cap.open(parser.get("device")); 117 | 118 | // Process frames. 119 | Mat frame, blob; 120 | while (waitKey(1) < 0) 121 | { 122 | cap >> frame; 123 | if (frame.empty()) 124 | { 125 | waitKey(); 126 | break; 127 | } 128 | 129 | // Create a 4D blob from a frame. 130 | Size inpSize(inpWidth > 0 ? inpWidth : frame.cols, 131 | inpHeight > 0 ? inpHeight : frame.rows); 132 | blobFromImage(frame, blob, scale, inpSize, mean, swapRB, false); 133 | 134 | // Run a model. 135 | net.setInput(blob); 136 | if (net.getLayer(0)->outputNameToIndex("im_info") != -1) // Faster-RCNN or R-FCN 137 | { 138 | resize(frame, frame, inpSize); 139 | Mat imInfo = (Mat_(1, 3) << inpSize.height, inpSize.width, 1.6f); 140 | net.setInput(imInfo, "im_info"); 141 | } 142 | std::vector outs; 143 | net.forward(outs, getOutputsNames(net)); 144 | 145 | postprocess(frame, outs, net); 146 | 147 | // Put efficiency information. 148 | std::vector layersTimes; 149 | double freq = getTickFrequency() / 1000; 150 | double t = net.getPerfProfile(layersTimes) / freq; 151 | std::string label = format("Inference time: %.2f ms", t); 152 | putText(frame, label, Point(0, 15), FONT_HERSHEY_SIMPLEX, 0.1, Scalar(0, 255, 0)); 153 | // std::cout << "Label: " << label << "\n"; 154 | // std::cout << "Outs: " ; 155 | // std::cout << outs << "\n"; 156 | // std::cout << "Nets: " << nets << "\n"; 157 | // std::cout << "detected_images/" + parser.get("input"); 158 | 159 | // imshow(kWinName, frame); 160 | // std::string time = 161 | // std::cout << parser.get("input"); 162 | std::string s = parser.get("input"); 163 | std::string filename_delimiter = "."; 164 | // std::filesystem::path p(s); 165 | // std::cout << p.filename() << std::endl; 166 | std::string filename = s.substr(0, s.find(filename_delimiter)); // token is "scott" 167 | std::string extension = s.substr(s.find(filename_delimiter), s.length()); 168 | std::string new_filename = filename + "_processed" + extension ; 169 | imwrite(new_filename, frame); // + parser.get("input"), frame); 170 | // imwrite("detected_images/output.jpg", frame); // + parser.get("input"), frame); 171 | return 0; 172 | } 173 | return 0; 174 | } 175 | 176 | void postprocess(Mat& frame, const std::vector& outs, Net& net) 177 | { 178 | static std::vector outLayers = net.getUnconnectedOutLayers(); 179 | static std::string outLayerType = net.getLayer(outLayers[0])->type; 180 | 181 | std::vector classIds; 182 | std::vector confidences; 183 | std::vector boxes; 184 | if (net.getLayer(0)->outputNameToIndex("im_info") != -1) // Faster-RCNN or R-FCN 185 | { 186 | // Network produces output blob with a shape 1x1xNx7 where N is a number of 187 | // detections and an every detection is a vector of values 188 | // [batchId, classId, confidence, left, top, right, bottom] 189 | CV_Assert(outs.size() == 1); 190 | float* data = (float*)outs[0].data; 191 | for (size_t i = 0; i < outs[0].total(); i += 7) 192 | { 193 | float confidence = data[i + 2]; 194 | if (confidence > confThreshold) 195 | { 196 | int left = (int)data[i + 3]; 197 | int top = (int)data[i + 4]; 198 | int right = (int)data[i + 5]; 199 | int bottom = (int)data[i + 6]; 200 | int width = right - left + 1; 201 | int height = bottom - top + 1; 202 | classIds.push_back((int)(data[i + 1]) - 1); // Skip 0th background class id. 203 | boxes.push_back(Rect(left, top, width, height)); 204 | confidences.push_back(confidence); 205 | } 206 | } 207 | } 208 | else if (outLayerType == "DetectionOutput") 209 | { 210 | // Network produces output blob with a shape 1x1xNx7 where N is a number of 211 | // detections and an every detection is a vector of values 212 | // [batchId, classId, confidence, left, top, right, bottom] 213 | CV_Assert(outs.size() == 1); 214 | float* data = (float*)outs[0].data; 215 | for (size_t i = 0; i < outs[0].total(); i += 7) 216 | { 217 | float confidence = data[i + 2]; 218 | if (confidence > confThreshold) 219 | { 220 | int left = (int)(data[i + 3] * frame.cols); 221 | int top = (int)(data[i + 4] * frame.rows); 222 | int right = (int)(data[i + 5] * frame.cols); 223 | int bottom = (int)(data[i + 6] * frame.rows); 224 | int width = right - left + 1; 225 | int height = bottom - top + 1; 226 | classIds.push_back((int)(data[i + 1]) - 1); // Skip 0th background class id. 227 | boxes.push_back(Rect(left, top, width, height)); 228 | confidences.push_back(confidence); 229 | } 230 | } 231 | } 232 | else if (outLayerType == "Region") 233 | { 234 | for (size_t i = 0; i < outs.size(); ++i) 235 | { 236 | // Network produces output blob with a shape NxC where N is a number of 237 | // detected objects and C is a number of classes + 4 where the first 4 238 | // numbers are [center_x, center_y, width, height] 239 | float* data = (float*)outs[i].data; 240 | for (int j = 0; j < outs[i].rows; ++j, data += outs[i].cols) 241 | { 242 | Mat scores = outs[i].row(j).colRange(5, outs[i].cols); 243 | Point classIdPoint; 244 | double confidence; 245 | minMaxLoc(scores, 0, &confidence, 0, &classIdPoint); 246 | if (confidence > confThreshold) 247 | { 248 | int centerX = (int)(data[0] * frame.cols); 249 | int centerY = (int)(data[1] * frame.rows); 250 | int width = (int)(data[2] * frame.cols); 251 | int height = (int)(data[3] * frame.rows); 252 | int left = centerX - width / 2; 253 | int top = centerY - height / 2; 254 | 255 | classIds.push_back(classIdPoint.x); 256 | confidences.push_back((float)confidence); 257 | boxes.push_back(Rect(left, top, width, height)); 258 | } 259 | } 260 | } 261 | } 262 | else 263 | CV_Error(Error::StsNotImplemented, "Unknown output layer type: " + outLayerType); 264 | 265 | std::vector indices; 266 | NMSBoxes(boxes, confidences, confThreshold, nmsThreshold, indices); 267 | std::cout << "{\"classes\": ["; 268 | for (size_t i = 0; i < indices.size(); ++i) 269 | { 270 | int idx = indices[i]; 271 | Rect box = boxes[idx]; 272 | 273 | std::cout << "{" << "\"class\" : \"" << classes[classIds[idx]] << "\", \"confidence\": " << confidences[classIds[idx]] << ", \"x\":" << (box.x + box.width / 2.0) << ", \"y\":" << (box.y + box.height / 2.0) << "}"; 274 | // TODO, add confidence 275 | if (i != (indices.size() -1)) { 276 | std::cout << ","; 277 | } 278 | drawPred(classIds[idx], confidences[idx], box.x, box.y, 279 | box.x + box.width, box.y + box.height, frame); 280 | } 281 | std::cout << "]}"; 282 | } 283 | 284 | void drawPred(int classId, float conf, int left, int top, int right, int bottom, Mat& frame) 285 | { 286 | rectangle(frame, Point(left, top), Point(right, bottom), Scalar(0, 255, 0)); 287 | 288 | // std::cout << "In drawPred"; 289 | std::string label = format("%.2f", conf); 290 | if (!classes.empty()) 291 | { 292 | CV_Assert(classId < (int)classes.size()); 293 | label = classes[classId] + ": " + label; 294 | // std::cout << classes[classId]; 295 | // std::cout << "label : " << label << '\n'; 296 | // std::cout << "{" << "class : " << classes[classId] << "}" << '\n'; 297 | // std::cout << "classIds size: " << classIds.size() << "\n"; 298 | // for(int i=0; i< classIds.size(); ++i) { 299 | // std::cout << "classIds: " << classIds[i] << '\n'; 300 | // std::cout << "class : "; 301 | // std::cout << classes[classIds[i]] << '\n'; 302 | // std::cout << confidences[i] << '\n'; 303 | // } 304 | } 305 | int baseLine; 306 | Size labelSize = getTextSize(label, FONT_HERSHEY_SIMPLEX, 0.5, 1, &baseLine); 307 | 308 | top = max(top, labelSize.height); 309 | rectangle(frame, Point(left, top - labelSize.height), 310 | Point(left + labelSize.width, top + baseLine), Scalar::all(255), FILLED); 311 | putText(frame, label, Point(left, top), FONT_HERSHEY_SIMPLEX, 0.5, Scalar()); 312 | } 313 | 314 | void callback(int pos, void*) 315 | { 316 | confThreshold = pos * 0.01f; 317 | } 318 | 319 | std::vector getOutputsNames(const Net& net) 320 | { 321 | static std::vector names; 322 | if (names.empty()) 323 | { 324 | std::vector outLayers = net.getUnconnectedOutLayers(); 325 | std::vector layersNames = net.getLayerNames(); 326 | names.resize(outLayers.size()); 327 | for (size_t i = 0; i < outLayers.size(); ++i) 328 | names[i] = layersNames[outLayers[i] - 1]; 329 | } 330 | return names; 331 | } 332 | -------------------------------------------------------------------------------- /scripts/quickstart.sh: -------------------------------------------------------------------------------- 1 | # Copyright [2018] IBM Corp. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | #!/bin/bash 16 | 17 | # kubectl create -f kubernetes/local-volumes.yaml 18 | # kubectl create -f kubernetes/postgres.yaml 19 | # kubectl create -f kubernetes/drupal.yaml 20 | kubectl create -f kubernetes/kube-config.yaml 21 | kubectl get nodes 22 | # kubectl get svc drupal 23 | -------------------------------------------------------------------------------- /scripts/resources.sh: -------------------------------------------------------------------------------- 1 | # Copyright [2018] IBM Corp. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | #!/bin/bash 16 | 17 | # This script contains functions used by many of the scripts found in scripts/ 18 | # and tests/. 19 | 20 | test_failed(){ 21 | echo -e >&2 "\033[0;31m$1 test failed!\033[0m" 22 | exit 1 23 | } 24 | 25 | test_passed(){ 26 | echo -e "\033[0;32m$1 test passed!\033[0m" 27 | } 28 | 29 | is_pull_request(){ 30 | if [[ "$TRAVIS_PULL_REQUEST" != "false" ]]; then 31 | echo -e "\033[0;33mPull Request detected; not running $1!\033[0m" 32 | exit 0 33 | fi 34 | } 35 | -------------------------------------------------------------------------------- /views/error.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | h1= message 5 | h2= error.status 6 | pre #{error.stack} 7 | -------------------------------------------------------------------------------- /views/error.pug: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | h1= message 5 | h2= error.status 6 | pre #{error.stack} 7 | -------------------------------------------------------------------------------- /views/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 |
26 |
27 |
28 |
29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 |
Location
43 | 44 |
45 | 46 |

47 | 48 |
49 | 50 |
51 | 52 | 53 |

54 | 55 | 56 | 57 | 295 | 296 | -------------------------------------------------------------------------------- /views/index.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | h1= title 5 | p Welcome to #{title} 6 | -------------------------------------------------------------------------------- /views/index.pug: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | h1= title 5 | p Welcome to #{title} 6 | -------------------------------------------------------------------------------- /views/layout.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | title= title 5 | link(rel='stylesheet', href='/stylesheets/style.css') 6 | body 7 | block content 8 | -------------------------------------------------------------------------------- /views/layout.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | title= title 5 | link(rel='stylesheet', href='/stylesheets/style.css') 6 | body 7 | block content 8 | --------------------------------------------------------------------------------