├── .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 | [](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 | Location
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | Car
49 |
50 | Person
51 |
52 | Truck
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 |
--------------------------------------------------------------------------------