From a31c6967332360f85d190950bec3e57bf4275a7c Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Tue, 22 Feb 2022 21:14:47 -0800 Subject: [PATCH] Initial import. --- .gitignore | 3 + README.md | 24 ++++++ go.mod | 34 ++++++++ go.sum | 149 +++++++++++++++++++++++++++++++++ lifecam.go | 73 ++++++++++++++++ lifecam/image.go | 29 +++++++ lifecam/mjpeg.go | 212 +++++++++++++++++++++++++++++++++++++++++++++++ lifecam/s3.go | 81 ++++++++++++++++++ lifecam/zip.go | 158 +++++++++++++++++++++++++++++++++++ main.py | 87 +++++++++++++++++++ 10 files changed, 850 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 go.mod create mode 100644 go.sum create mode 100644 lifecam.go create mode 100644 lifecam/image.go create mode 100644 lifecam/mjpeg.go create mode 100644 lifecam/s3.go create mode 100644 lifecam/zip.go create mode 100644 main.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..405aaf2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.jpg +*.avi +*.zip diff --git a/README.md b/README.md new file mode 100644 index 0000000..e6c7cf5 --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +lifecam +======= + +I was reading a [blog post](https://writings.stephenwolfram.com/2019/02/seeking-the-productive-life-some-details-of-my-personal-infrastructure/) by Stephen Wolfram: + +> But when I go to events like trade shows I typically bring a tiny +> camera with me, that takes a picture every half-minute. If I’m +> wearing one of those lanyard name tags I’ll typically clip the +> camera on the top of the name tag, among other things putting it at +> an ideal height to capture name tags of people I meet. When I write +> my personal trip report I’ll typically review the pictures, and +> sometimes copy a few into my trip notebook. + +I thought it would be cool to build something like this, and maybe use +the photos to do some interesting photo analysis. I picked up an +[OpenMV H7 R2](https://www.sparkfun.com/products/18982) with a +[wide-angle lens](https://www.sparkfun.com/products/16778). I wrote a +[quick little program](main.py) to take a picture every thirty seconds +and write it to an image directory. This code collects those images +into a video. + +I haven't used this for anything yet on account of not having a +case. I might try to replicate this with an OAK-D for the inside of my +house. diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e3e5ae0 --- /dev/null +++ b/go.mod @@ -0,0 +1,34 @@ +module git.wntrmute.dev/kyle/lifecam + +go 1.17 + +require ( + git.sr.ht/~kisom/goutils v1.5.3 + github.com/disintegration/imaging v1.6.2 + github.com/icza/mjpeg v0.0.0-20210726201846-5ff75d3c479f + github.com/minio/minio-go/v7 v7.0.23 +) + +require ( + github.com/dustin/go-humanize v1.0.0 // indirect + github.com/google/uuid v1.1.1 // indirect + github.com/gopherjs/gopherjs v0.0.0-20220221023154-0b2280d3ff96 // indirect + github.com/json-iterator/go v1.1.10 // indirect + github.com/jtolds/gls v4.20.0+incompatible // indirect + github.com/klauspost/compress v1.13.5 // indirect + github.com/klauspost/cpuid v1.3.1 // indirect + github.com/minio/md5-simd v1.1.0 // indirect + github.com/minio/sha256-simd v0.1.1 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.1 // indirect + github.com/rs/xid v1.2.1 // indirect + github.com/sirupsen/logrus v1.8.1 // indirect + github.com/smartystreets/assertions v1.2.1 // indirect + golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect + golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 // indirect + golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 // indirect + golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect + golang.org/x/text v0.3.3 // indirect + gopkg.in/ini.v1 v1.57.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..45494ff --- /dev/null +++ b/go.sum @@ -0,0 +1,149 @@ +bitbucket.org/liamstask/goose v0.0.0-20150115234039-8488cc47d90c/go.mod h1:hSVuE3qU7grINVSwrmzHfpg9k87ALBk+XaualNyUzI4= +git.sr.ht/~kisom/goutils v1.5.3 h1:Q0BI2Q8/gs5uYdi2e2op4uRt9hTLt5zgH9HdGDcx4pM= +git.sr.ht/~kisom/goutils v1.5.3/go.mod h1:azq1B7nq8lmva1FWeuw+tZ6xdJZCtJIlFskB3N2v9E4= +github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0= +github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0= +github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= +github.com/certifi/gocertifi v0.0.0-20180118203423-deb3ae2ef261/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4= +github.com/cloudflare/backoff v0.0.0-20161212185259-647f3cdfc87a/go.mod h1:rzgs2ZOiguV6/NpiDgADjRLPNyZlApIWxKpkT+X8SdY= +github.com/cloudflare/cfssl v1.5.0/go.mod h1:sPPkBS5L8l8sRc/IOO1jG51Xb34u+TYhL6P//JdODMQ= +github.com/cloudflare/go-metrics v0.0.0-20151117154305-6a9aea36fb41/go.mod h1:eaZPlJWD+G9wseg1BuRXlHnjntPMrywMsyxf+LTOdP4= +github.com/cloudflare/redoctober v0.0.0-20171127175943-746a508df14c/go.mod h1:6Se34jNoqrd8bTxrmJB2Bg2aoZ2CdSXonils9NsiNgo= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= +github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/getsentry/raven-go v0.0.0-20180121060056-563b81fc02b7/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= +github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gopherjs/gopherjs v0.0.0-20220221023154-0b2280d3ff96 h1:QJq7UBOuoynsywLk+aC75rC2Cbi2+lQRDaLaizhA+fA= +github.com/gopherjs/gopherjs v0.0.0-20220221023154-0b2280d3ff96/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= +github.com/icza/mjpeg v0.0.0-20210726201846-5ff75d3c479f h1:907GjB0sqx3+XKOz5tSeAi8Z9ED7WwQU9hXoclDp+jg= +github.com/icza/mjpeg v0.0.0-20210726201846-5ff75d3c479f/go.mod h1:Eja3x31oRrEOzl6ihhsxY23gXaTYWLP3Gwj5nMAJ7m0= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jmhodges/clock v0.0.0-20160418191101-880ee4c33548/go.mod h1:hGT6jSUVzF6no3QaDSMLGLEHtHSBSefs+MgcDWnmhmo= +github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= +github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/kisielk/sqlstruct v0.0.0-20150923205031-648daed35d49/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= +github.com/kisom/goutils v1.1.0/go.mod h1:+UBTfd78habUYWFbNWTJNG+jNG/i/lGURakr4A/yNRw= +github.com/klauspost/compress v1.13.5 h1:9O69jUPDcsT9fEm74W92rZL9FQY7rCdaXVneq+yyzl4= +github.com/klauspost/compress v1.13.5/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/cpuid v1.2.3/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/cpuid v1.3.1 h1:5JNjFYYQrZeKRJ0734q51WCEEn2huer72Dc7K+R/b6s= +github.com/klauspost/cpuid v1.3.1/go.mod h1:bYW4mA6ZgKPob1/Dlai2LviZJO7KGI3uoWLd42rAQw4= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/go-gypsy v0.0.0-20160905020020-08cad365cd28/go.mod h1:T/T7jsxVqf9k/zYOqbgNAsANsjxTd1Yq3htjDhQ1H0c= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/minio/md5-simd v1.1.0 h1:QPfiOqlZH+Cj9teu0t9b1nTBfPbyTl16Of5MeuShdK4= +github.com/minio/md5-simd v1.1.0/go.mod h1:XpBqgZULrMYD3R+M28PcmP0CkI7PEMzB3U77ZrKZ0Gw= +github.com/minio/minio-go/v7 v7.0.23 h1:NleyGQvAn9VQMU+YHVrgV4CX+EPtxPt/78lHOOTncy4= +github.com/minio/minio-go/v7 v7.0.23/go.mod h1:ei5JjmxwHaMrgsMrn4U/+Nmg+d8MKS1U2DAn1ou4+Do= +github.com/minio/sha256-simd v0.1.1 h1:5QHSlgo3nt5yKOJrC7W8w7X+NFl8cMPZm96iu8kKUJU= +github.com/minio/sha256-simd v0.1.1/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8= +github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E= +github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.12.0/go.mod h1:fUqqXB5vEgVCZ131L+9say31RAri6aF6KDViawhxKK8= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc= +github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/smartystreets/assertions v1.2.1 h1:bKNHfEv7tSIjZ8JbKaFjzFINljxG4lzZvmHUnElzOIg= +github.com/smartystreets/assertions v1.2.1/go.mod h1:wDmR7qL282YbGsPy6H/yAsesrxfxaaSlJazyFLYVFx8= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= +github.com/weppos/publicsuffix-go v0.4.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k= +github.com/weppos/publicsuffix-go v0.13.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k= +github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= +github.com/zmap/rc2 v0.0.0-20131011165748-24b9757f5521/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE= +github.com/zmap/zcertificate v0.0.0-20180516150559-0e3d58b1bac4/go.mod h1:5iU54tB79AMBcySS0R2XIyZBAVmeHranShAFELYx7is= +github.com/zmap/zcrypto v0.0.0-20200513165325-16679db567ff/go.mod h1:TxpejqcVKQjQaVVmMGfzx5HnmFMdIU+vLtaCyPBfGI4= +github.com/zmap/zcrypto v0.0.0-20200911161511-43ff0ea04f21/go.mod h1:TxpejqcVKQjQaVVmMGfzx5HnmFMdIU+vLtaCyPBfGI4= +github.com/zmap/zlint/v2 v2.2.1/go.mod h1:ixPWsdq8qLxYRpNUTbcKig3R7WgmspsHGLhCCs6rFAM= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200124225646-8b5121be2f68/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U= +golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww= +gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/lifecam.go b/lifecam.go new file mode 100644 index 0000000..b47edc5 --- /dev/null +++ b/lifecam.go @@ -0,0 +1,73 @@ +package main + +import ( + "flag" + "log" + "os" + "path/filepath" + + "git.sr.ht/~kisom/goutils/config" + "git.sr.ht/~kisom/goutils/die" + "git.wntrmute.dev/kyle/lifecam/lifecam" +) + +var defaultConfigFile = config.DefaultConfigPath("lifecam", "lifecam.conf") + +func main() { + var err error + var minio *lifecam.Server + + clean := flag.Bool("c", false, "remove files after zipping") + cfgFile := flag.String("f", defaultConfigFile, "path to config file") + upload := flag.Bool("u", false, "upload zipfile to S3") + flag.Parse() + + if *cfgFile != "" { + err := config.LoadFile(*cfgFile) + if err != nil { + if !os.IsNotExist(err) { + die.If(err) + } + } else { + log.Println("read config file", *cfgFile) + } + } + + if *upload { + minio, err = lifecam.NewMinio() + die.If(err) + } + + paths := flag.Args() + if len(paths) == 0 { + if defaultPath := config.Get("DEFAULT_IMAGE_DIR"); defaultPath != "" { + paths = append(paths, defaultPath) + } else { + log.Println("nothing to do") + } + } + + var frameMap map[int]lifecam.FrameList + for _, path := range paths { + log.Println("loading all frames in", path) + path = filepath.Clean(path) + frameMap, err = lifecam.LoadDir(path, frameMap) + die.If(err) + } + + for _, fl := range frameMap { + pkg, err := fl.Package() + die.If(err) + err = lifecam.PackageZip(pkg, *clean) + die.If(err) + + if *upload { + path, err := pkg.Frames.Name() + die.If(err) + n, err := minio.Upload(path, lifecam.MimeZip) + die.If(err) + log.Printf("uploaded %s [%d bytes]", path, n) + } + } + log.Println("complete") +} diff --git a/lifecam/image.go b/lifecam/image.go new file mode 100644 index 0000000..67ca9f4 --- /dev/null +++ b/lifecam/image.go @@ -0,0 +1,29 @@ +package lifecam + +import ( + "image" + "os" + + "github.com/disintegration/imaging" +) + +func getImageBounds(img image.Image) (w, h int) { + edges := img.Bounds().Max + return edges.X, edges.Y +} + +func getImageSize(path string) (w, h int, err error) { + file, err := os.Open(path) + if err != nil { + return + } + defer file.Close() + + img, err := imaging.Decode(file, imaging.AutoOrientation(true)) + if err != nil { + return 0, 0, err + } + + w, h = getImageBounds(img) + return w, h, nil +} diff --git a/lifecam/mjpeg.go b/lifecam/mjpeg.go new file mode 100644 index 0000000..821d123 --- /dev/null +++ b/lifecam/mjpeg.go @@ -0,0 +1,212 @@ +package lifecam + +import ( + "errors" + "fmt" + "io/ioutil" + "log" + "os" + "path/filepath" + "sort" + "strconv" + "strings" + + "github.com/icza/mjpeg" +) + +const ( + fps = 1 +) + +type Frame struct { + Session int + Frame int + Path string + Width, Height int +} + +func (f *Frame) String() string { + return fmt.Sprintf("[FRAME] S: %02d F: %04d P: %s", + f.Session, f.Frame, f.Path) +} + +func FrameFromPath(path string) (*Frame, error) { + names := strings.Split(path, "-") + if len(names) != 3 { + return nil, fmt.Errorf("lifecam: %s isn't a lifecam frame", path) + } + + f := &Frame{ + Path: path, + } + + var err error + f.Session, err = strconv.Atoi(names[1]) + if err != nil { + return nil, err + } + + names[2] = strings.TrimSuffix(names[2], filepath.Ext(names[2])) + f.Frame, err = strconv.Atoi(names[2]) + if err != nil { + return nil, err + } + + f.Width, f.Height, err = getImageSize(path) + if err != nil { + return nil, err + } + + return f, nil +} + +var ( + ErrEmptyFrameList = errors.New("lifecam: empty framelist") + ErrFrameListSequence = errors.New("lifecam: framelist out of sequence") + ErrFrameListSession = errors.New("lifecam: mismatched sessions in framelist") + ErrFrameDimensions = errors.New("lifecam: frames have mismatched dimensions") +) + +type FrameList []*Frame + +func (fl FrameList) Check() error { + if len(fl) == 0 { + return ErrEmptyFrameList + } + + sort.Sort(fl) + + for i := 1; i < len(fl); i++ { + if fl[i].Frame <= fl[i-1].Frame { + return ErrFrameListSequence + } + + if fl[i].Session != fl[i-1].Session { + return ErrFrameListSession + } + + if fl[i].Width != fl[i-1].Width { + return ErrFrameDimensions + } + + if fl[i].Height != fl[i-1].Height { + return ErrFrameDimensions + } + } + + return nil +} + +func (fl FrameList) String() string { + if len(fl) == 0 { + return "empty framelist" + } + + return fmt.Sprintf("S: %d F: %d", fl[0].Session, len(fl)) +} + +func (fl FrameList) Len() int { + return len(fl) +} + +func (fl FrameList) Less(i, j int) bool { + return fl[i].Frame < fl[j].Frame +} + +func (fl FrameList) Swap(i, j int) { + fl[i], fl[j] = fl[j], fl[i] +} + +func (fl FrameList) Name() (string, error) { + fi, err := os.Stat(fl[0].Path) + if err != nil { + panic(err.Error()) + } + + ts := fi.ModTime().Format("2006-01-02_1534") + return fmt.Sprintf("lifecam_%s_%02d_%04d.avi", + ts, fl[0].Session, len(fl)), nil +} + +func LoadDir(imageDir string, frameMap map[int]FrameList) (map[int]FrameList, error) { + if frameMap == nil { + frameMap = map[int]FrameList{} + } + + dir, err := os.ReadDir(imageDir) + if err != nil { + return nil, err + } + + for _, dirent := range dir { + path := filepath.Join(imageDir, dirent.Name()) + fi, err := os.Stat(path) + if err != nil { + return nil, err + } + + if !fi.Mode().IsRegular() { + continue + } + + f, err := FrameFromPath(path) + if err != nil { + return nil, err + } + + frameMap[f.Session] = append(frameMap[f.Session], f) + } + + return frameMap, nil +} + +func (fl FrameList) ToMJPEG(outPath string) error { + err := fl.Check() + if err != nil { + return err + } + + width := int32(fl[0].Width) + height := int32(fl[0].Height) + aviFile, err := mjpeg.New(outPath, width, height, fps) + if err != nil { + return err + } + defer aviFile.Close() + + for _, frame := range fl { + img, err := ioutil.ReadFile(frame.Path) + if err != nil { + return err + } + + if err = aviFile.AddFrame(img); err != nil { + return err + } + } + + log.Printf("encoded %s", outPath) + return aviFile.Close() +} + +type Package struct { + Frames FrameList + Video string +} + +func (fl FrameList) Package() (*Package, error) { + path, err := fl.Name() + if err != nil { + return nil, err + } + + err = fl.ToMJPEG(path) + if err != nil { + return nil, err + } + + return &Package{ + Frames: fl, + Video: path, + }, nil +} diff --git a/lifecam/s3.go b/lifecam/s3.go new file mode 100644 index 0000000..0e6f06b --- /dev/null +++ b/lifecam/s3.go @@ -0,0 +1,81 @@ +package lifecam + +import ( + "context" + "fmt" + "log" + "path/filepath" + "strings" + + "git.sr.ht/~kisom/goutils/config" + "github.com/minio/minio-go/v7" + "github.com/minio/minio-go/v7/pkg/credentials" +) + +const MimeZip = "application/zip" + +type Server struct { + client *minio.Client + bucket string +} + +func NewMinio() (*Server, error) { + var fields []string + server := config.Get("MINIO_SERVER") + if server == "" { + fields = append(fields, "MINIO_SERVER") + } + + access := config.Get("MINIO_ACCESS") + if access == "" { + fields = append(fields, "MINIO_ACCESS") + } + + secret := config.Get("MINIO_SECRET") + if secret == "" { + fields = append(fields, "MINIO_SECRET") + } + + bucket := config.Get("MINIO_BUCKET") + if bucket == "" { + fields = append(fields, "MINIO_BUCKET") + } + + srv := &Server{ + bucket: bucket, + } + + if len(fields) != 0 { + err := fmt.Errorf("lifecam: missing fields in S3 config: %s", + strings.Join(fields, ", ")) + return nil, err + } + + var err error + srv.client, err = minio.New(server, &minio.Options{ + Creds: credentials.NewStaticV4(access, secret, ""), + Secure: true, + }) + if err != nil { + return nil, err + } + + return srv, nil +} + +func (srv *Server) Upload(path, contentType string) (int64, error) { + log.Printf("uploading %s to minio", path) + ui, err := srv.client.FPutObject( + context.Background(), + srv.bucket, + filepath.Base(path), + path, + minio.PutObjectOptions{ + ContentType: contentType, + }) + + if err != nil { + return 0, err + } + return ui.Size, nil +} diff --git a/lifecam/zip.go b/lifecam/zip.go new file mode 100644 index 0000000..816ceea --- /dev/null +++ b/lifecam/zip.go @@ -0,0 +1,158 @@ +package lifecam + +import ( + "archive/zip" + "io" + "log" + "os" + "path/filepath" + "strings" +) + +func zipName(p string, top bool) string { + if top { + return filepath.Base(p) + } + return filepath.Join("frames", filepath.Base(p)) +} + +func pathHeader(p string, top bool) (*zip.FileHeader, error) { + + fi, err := os.Stat(p) + if err != nil { + return nil, err + } + + h, err := zip.FileInfoHeader(fi) + if err != nil { + return nil, err + } + + h.Name = zipName(p, top) + h.Method = zip.Deflate + return h, nil +} + +type zipFile struct { + path string + file *os.File + zw *zip.Writer + err error + clean bool + + zipped []string +} + +func (zf *zipFile) Close() error { + zf.zw.Close() + err := zf.file.Close() + if zf.err != nil { + os.Remove(zf.path) + } + if zf.clean { + zf.Clean() + } + return err +} + +func (zf *zipFile) Clean() (err error) { + targets := zf.zipped[:] + for i := range targets { + err = os.Remove(targets[i]) + if err != nil { + zf.zipped = zf.zipped[i:] + break + } + log.Println("removed", targets[i]) + zf.zipped = zf.zipped[1:] + } + + if len(zf.zipped) != 0 { + log.Printf("failed to remove %s", + strings.Join(zf.zipped, " ")) + return err + } + return nil +} + +func (zf *zipFile) addPath(path string, top bool) error { + var ( + f *os.File + h *zip.FileHeader + w io.Writer + ) + + h, zf.err = pathHeader(path, top) + if zf.err != nil { + return zf.err + } + log.Println("zipping", h.Name) + + w, zf.err = zf.zw.CreateHeader(h) + if zf.err != nil { + return zf.err + } + + f, zf.err = os.Open(path) + if zf.err != nil { + return zf.err + } + defer f.Close() + + _, zf.err = io.Copy(w, f) + if zf.err != nil { + return zf.err + } + + zf.zipped = append(zf.zipped, path) + return nil +} + +func (zf *zipFile) AddVideo(path string) error { + return zf.addPath(path, true) +} + +func (zf *zipFile) AddFrame(path string) error { + return zf.addPath(path, false) +} + +func newZipFile(path string, clean bool) (*zipFile, error) { + zf := &zipFile{path: path, clean: clean} + zf.file, zf.err = os.Create(path) + if zf.err != nil { + return nil, zf.err + } + + zf.zw = zip.NewWriter(zf.file) + return zf, nil +} + +func PackageZip(p *Package, clean bool) (err error) { + path := strings.TrimSuffix(p.Video, + filepath.Ext(p.Video)) + ".zip" + zf, err := newZipFile(path, clean) + if err != nil { + return err + } + defer zf.Close() + + err = zf.AddVideo(p.Video) + if err != nil { + return err + } + + for _, frame := range p.Frames { + err = zf.AddFrame(frame.Path) + if err != nil { + return err + } + } + + zf.zw.Close() + + if clean { + err = zf.Clean() + } + + return err +} diff --git a/main.py b/main.py new file mode 100644 index 0000000..c3c3fa7 --- /dev/null +++ b/main.py @@ -0,0 +1,87 @@ +# Hello World Example +# +# Welcome to the OpenMV IDE! Click on the green run arrow button below to run the script! + +import sensor +import image +import pyb +import time +import uos + +blue = pyb.LED(3) +green = pyb.LED(2) +sleep_cycle = 30 + +def reset(): + sensor.reset() # Reset and initialize the sensor. + sensor.set_pixformat(sensor.RGB565) # Set pixel format to RGB565 (or GRAYSCALE) + sensor.set_framesize(sensor.VGA) # Set frame size to QVGA (320x240) + sensor.skip_frames(time = 2000) # Wait for settings take effect. + +# if the fs is <= 1M in size, it's probably flash and we don't want to +# write images. +def sd_is_mounted(): + st = uos.statvfs('.') + sz = st[1] * st[2] + return sz > 1048576 + +def image_path(sess, imno): + return 'images/image-%02d-%04d.jpg' % (sess, imno) + +def check_dir(): + try: + uos.stat('images') + except OSError: + uos.mkdir('images') + +def find_session(): + sess = -1 + for dir in uos.listdir('images'): + try: + _, imsess, _ = dir.split('-') + except ValueError: + continue + imsess = int(imsess) + if imsess > sess: + sess = imsess + return sess + 1 + +def snapshot(sess, imno): + path = image_path(sess, imno) + green.on() + img = sensor.snapshot() + green.off() + + img.save(path) + print('wrote path', path) + return imno + 1 + +def cycle(cycle, sess, imno): + while cycle > 0: + if cycle % 5 == 0: + print('blue on') + blue.on() + if cycle % 5 == 4: + print('blue off') + blue.off() + time.sleep(1) + cycle -= 1 + return snapshot(sess, imno) + +def main(): + reset() + if not sd_is_mounted(): + return + + check_dir() + sess = find_session() + imno = 0 + + print('session', sess) + + while(True): + print('start cycle', imno) + imno = cycle(sleep_cycle, sess, imno) + +if __name__ == '__main__': + main()