今天你喝水了吗?喵~

4,660 阅读9分钟

一起用代码吸猫!本文正在参与【喵星人征文活动】

事情的开始是这样的,带着我们家的两只小猫咪去一年一度打疫苗的时候顺便做了个小体检

结果猫1被诊断为缺水,医生说 RBC、HGB、HCT 指标都不正常有脱水的情况 问在家是不是不怎么喝水,因为家里有两只猫, 饮水机的水一直也有消耗但是你并不知道是被哪只猫喝了,回医生说不太清楚,说罢医生给了我这个工具

医生说要是它不喝水就用这个给它灌水!

回去给它灌了一针筒,场面堪比杀猫....

这工作没法干~

有没有办法监控它们的喝水情况呢?如果喝得不够再灌水,而且猫子们不会说话要是生病了不喝水了能不能及时发现?因为家里有多只毛孩子要是其他都正常喝有只不喝有没有办法及时发现呢?

👨:你也有脸说你知道你自己的身价吗???

看来只能做个监控工具来监控它们的喝水情况

今天你喝水了吗? v0.0.1

方案框架如下

硬件设备准备

设备采集端用什么呢? 废旧手机?树莓派? 其他廉价IoT设备?

基于成本考虑以及硬件扩展我选了ESP32 CAM 万能淘宝价 25-35一大把大概长这样

image.png

这个板子比较坑的的是没有usb接口,作为垃圾大佬的我翻出一个cp2102 转接口接上

实物接线后长这样(建议购买CP2102兼容性好一些,淘宝上另外一个是CH340便宜一些但是兼容性更差一些

image.png

服务端设备

服务端用什么呢? 拿出了我多年收集的吃灰套装

小公鸡点到谁我就选谁~

软件部分方案

设备端使用Arduino吧,网上资源丰富

AI部分采用Pytorch, 模型的话可以考虑用MobileNet 或者 SqueezeNet,

推理端也可以用Pytorch,也可以用淘宝出品的MNN 不仅在手机端上性能出众,在树莓派上也可以跑

服务端部分可以采用蚂蚁的Eggjs 前端展示还是App吧毕竟我是一个落魄的Android开发🐶~

好了整体架构理论上都可以实现~ 开搞!

设备端

嵌入式系统采用Arduion,官方IDE实在有点难用这里使用 VSCode+PlatformIO 进行开发 vscode 里面直接搜platformIO 插件安装即可,Arduion 环境配置就自行Google吧

相机配置

    camera_config_t config;
    config.ledc_channel = LEDC_CHANNEL_0;
    config.ledc_timer = LEDC_TIMER_0;
    config.pin_d0 = Y2_GPIO_NUM;
    config.pin_d1 = Y3_GPIO_NUM;
    config.pin_d2 = Y4_GPIO_NUM;
    config.pin_d3 = Y5_GPIO_NUM;
    config.pin_d4 = Y6_GPIO_NUM;
    config.pin_d5 = Y7_GPIO_NUM;
    config.pin_d6 = Y8_GPIO_NUM;
    config.pin_d7 = Y9_GPIO_NUM;
    config.pin_xclk = XCLK_GPIO_NUM;
    config.pin_pclk = PCLK_GPIO_NUM;
    config.pin_vsync = VSYNC_GPIO_NUM;
    config.pin_href = HREF_GPIO_NUM;
    config.pin_sscb_sda = SIOD_GPIO_NUM;
    config.pin_sscb_scl = SIOC_GPIO_NUM;
    config.pin_pwdn = PWDN_GPIO_NUM;
    config.pin_reset = RESET_GPIO_NUM;
    config.xclk_freq_hz = 20000000;
    config.pixel_format = PIXFORMAT_JPEG;
    config.frame_size = frameSize;
    config.jpeg_quality = 10;
    config.fb_count = 1;

    bool ok = esp_camera_init(&config) == ESP_OK;

然后调用拍照API就可以拍照了

    camera_fb_t *frame_buffer = esp_camera_fb_get();

如何知道有小猫咪正在喝水呢?加个红外人体感应模块?

万能淘宝上5块以下就有一大把大概长这样

image.png

它能够感应人体热释电,当有人或者动物经过的时候就会输出接口输出高电平

部署在猫子门喝水的地(经费有限~😭 有点丑

实际测试下来发现拍了非常多的照片由于红外人体感应它的感应角度能到120度,距离3~7米,然后由于家里比较小,喝水的地方在过道当有人经过的时候它也会拍照, 由于是集成模块无法调整电路参数,如果外加遮挡物的也比较麻烦。看来得调整下方案

怎么办?要不只能将所有照片都发到服务端上进行检测?如果每5s拍一张照片进行检测 一天下来有1.7w照片需要检测这也太不环保了虽然树莓派的功耗只有5w....

家里的小米摄像头有 motion 检测功能即当有物体移动的时候可以记录下来大致其实就是对比前后两帧差异如果变化比较大则说明有物体移动。

那我们是不是也可以这么干呢?在检测到前后两帧有比较大的变化的时候再进行将拍照的图片上传到树莓派上进行检测。

ESP32 Cam 的 motion 检测

首先我们看看它是否能拍出灰度图, 这是它支持的图片格式

typedef enum {
    PIXFORMAT_RGB565,    // 2BPP/RGB565
    PIXFORMAT_YUV422,    // 2BPP/YUV422
    PIXFORMAT_GRAYSCALE, // 1BPP/GRAYSCALE
    PIXFORMAT_JPEG,      // JPEG/COMPRESSED
    PIXFORMAT_RGB888,    // 3BPP/RGB888
    PIXFORMAT_RAW,       // RAW
    PIXFORMAT_RGB444,    // 3BP2P/RGB444
    PIXFORMAT_RGB555,    // 3BP2P/RGB555
} pixformat_t;

可以看到 PIXFORMAT_GRAYSCALE 这个就是我们想要的格式,为什么要用这个格式呢? 因为 这个格式不仅数据量小而且格式也比较好处理就是一张图片就是一个宽x长的二维数组表示

如何对比两张灰度图片是否变化了 如果是将所有像素进行遍历对比来做判断的话

  1. 是计算量比较大
  2. 是这样泛化能力比较差

所以我们可以降低采样率来缩小对比计算量,同时进行分块比较提升泛化能力

降低采样率

假设图片分辨率为240 * 320 在灰度图格式下即1BPP一个像素点1字节即有 76800 数组长度数据 你可以理解为图片的第一行即第一个 240 字节,第二行为第二个240字节假如想降低10倍采样即将 240 * 320 映射成 24 * 32 大致代码实现就是将 240 * 320 的 10 * 10 映射成 24 * 32 的一个点代码实现如下

        const uint16_t x = i % 240;
        const uint16_t y = floor(i / 320);
        const uint8_t block_x = floor(x / 10);
        const uint8_t block_y = floor(y / 10);
        const uint8_t pixel = frame_buffer->buf[i]
        current_frame[block_y][block_x] += pixel;

之后你需要对 current_frame[block_y][block_x] / 100;这一个像素点的值就包含了原来 10 * 10 的大致信息 之后你就只要对比 24 * 32 这些数据的差异,每个点可以定义一个阀值比如差异超过 25% 就判定这个区块有变化 当所有区块比例变化超过10%就认定这两帧图片发生了变化。

服务端

安装Nodejs参考Google,安装eggjs 参考官网,

上传和统计接口就不细说了就是一个简单的CURD,不过在上传图片这里遇到了一些问题简单记录下

一开始我的bodyParser配置为text大概是这样

  config.bodyParser = {
     formLimit: '8mb',
     enableTypes: ['json', 'form', 'text'],
     extendTypes: {
       text: ['*/*']
     }
  }

结果发现了一个神奇的问题接收的body总比header 中长度content-length 小了10几字节收到的图片在电脑上无法打开, 写了个Python的Flask demo来接收发现是ok的可以说明设备端发送应该是没问题的,这种情况大概率应该是二进制编码出了一些问题只要对发送的内容base64以下应该就没问题了, 但是base64后内存会大30%左右设备端没内存了尴尬(ESP32 官方总共大概520k,系统摄像头http库等剩余你能用的大概只有不到100k了)找了js大神咨询了下如何获取原始数据不编码成Text 可以使用这个库来获取原始数据 ('raw-body') 调整上传代码

  async upload() {
    const { ctx } = this;
    const buffer = await getRawBody(ctx.req, { length: ctx.request.headers['content-length'] })
    const time = sd.format(new Date(), 'YYYY-MM-DD_HH-mm-ss');
    try {
      fs.writeFileSync('./file-data/' + time + ".jpg", buffer);
    } finally {
    }
    ctx.body = {
      "code": 1001,
      "msg": "ok"
    }
  }

图片终于正常了

AI

环境

PyTorch安装自行Google,由于我台机装的是黑苹果N卡CUDA环境这折腾的要了半条命 还是不行,算了算了改天还是装Ubuntu再战CUDA,暂时直接用CPU训练吧

准备数据

准备训练数据"两只猫子照片" 整个手机都是你们照片! 暂时没找到好的打标方法只能手动打了各1~2百张应该就够了吧,(ps: 我们家猫一只黑一只灰平均下色值应该就可以了? 万一明年我又有新猫了怎么办呢?!!

训练数据如下 两个文件夹分别放入主子照片

吵吵 image.png

闹闹

image.png

各自准备了100张左右

预处理数据

主要是将图片尺寸压到224大小, 然后根据文件路径打好标 如果路径有 naonao 定义lable 为1

# coding:utf8
import os
from PIL import Image
from torch.utils import data
import numpy as np
from torchvision import transforms as T


class ChaochaoNaonao(data.Dataset):

    def __init__(self, root, transforms=None, train=True, test=False):

        self.test = test
        imgs = []

        for rootDir, dirs, files in os.walk(root):
          for fileItem in files:
            if "DS_Store" in fileItem:
              continue
            imgs.append(os.path.join(rootDir, fileItem)) 

        imgs_num = len(imgs)
        self.imgs = imgs
        if transforms is None:
            normalize = T.Normalize(mean=[0.485, 0.456, 0.406],
                                    std=[0.229, 0.224, 0.225])

            if self.test or not train:
                self.transforms = T.Compose([
                    T.Resize(224),
                    T.CenterCrop(224),
                    T.ToTensor(),
                    normalize
                ])
            else:
                self.transforms = T.Compose([
                    T.Resize(256),
                    T.RandomResizedCrop(224),
                    T.RandomHorizontalFlip(),
                    T.ToTensor(),
                    normalize
                ])

    def __getitem__(self, index):
        """
            返回一张图片的数据
        """
        img_path = self.imgs[index]
        print(img_path)
        if self.test:
            label = int(img_path.split('/')[-1].split('.')[0])
        else:
            #  如果路径里有naonoa 定义为1 吵吵定义为 0
            label = 1 if 'naonao' in img_path.split('/')[-2] else 0
        data = Image.open(img_path)
        data = self.transforms(data)
        return data, label

    def __len__(self):
        return len(self.imgs)
定义网络

这里选择用 squeezenet 只是个二分类应该什么都可以😄

from torchvision.models import  squeezenet1_1
from models.basic_module import  BasicModule
from torch import nn
from torch.optim import Adam

class SqueezeNet(BasicModule):
  	 # 修改为二分类
    def __init__(self, num_classes=2):
        super(SqueezeNet, self).__init__()
        self.model_name = 'squeezenet'
        self.model = squeezenet1_1(pretrained=True)
        self.model.num_classes = num_classes
        self.model.classifier = nn.Sequential(
            nn.Dropout(p=0.5),
            nn.Conv2d(512, num_classes, 1),
            nn.ReLU(inplace=True),
            nn.AvgPool2d(13, stride=1)
        )

    def forward(self,x):
        return self.model(x)

    def get_optimizer(self, lr, weight_decay):
        return Adam(self.model.classifier.parameters(), lr, weight_decay=weight_decay) 
加载数据训练
  # train
  for epoch in range(opt.max_epoch):
      
      for ii,(data,label) in tqdm(enumerate(train_dataloader)):

          # train model 
          input = data.to(opt.device)
          target = label.to(opt.device)

          optimizer.zero_grad()
          score = model(input)
          loss = criterion(score,target)
          loss.backward()
          optimizer.step()

          loss_meter.add(loss.item())

          if (ii + 1)%opt.print_freq == 0:
              vis.plot('loss', loss_meter.value()[0])
              
              # 进入debug模式
              if os.path.exists(opt.debug_file):
                  import ipdb;
                  ipdb.set_trace()
          print("loss : %s " %  loss_meter.value()[0])

  model.save()

epoch 50次后最终loss在0.2左右也不在拟合效果如何直接测试吧

测试了些数据全部都对(毕竟一只黑一只灰AI应该很容易学会吧😆

拿到模型挺小的只有2.9M

部署到树莓派

安装pytorch,自己编译or找三方已经编译好的库  自己编译大概需要10几个小时还得调整swap 大概率还有各种错误 找个现成编译好的吧 搜了一下 找了一个这个 github.com/marcusvlc/p…  参照步骤完成安装~

直接用上面训练好的模型然后使用验证脚本即可

App

采用Flutter开发 教程网上搜吧。

网络请求使用 dio pub.dev/packages/di…

图表库使用 charts_flutter pub.dev/packages/ch…

简单堆两个页面

image.png

具体代码等后续整理好了开源吧

尾声

👩:医生都说了这只猫子不喝水你做这个有什么用能让它多喝水吗??

我:...... 医院没有误诊,这猫真不喜欢喝水!!!不说了我得去灌水了

闹闹🐱:喵!喵!喵!喵!杀猫啦 ! 你们是魔鬼吗!!

参考

github.com/fengdu78/Co…

github.com/chenyuntc/p…

github.com/espressif/e…

eggjs.org/zh-cn/

flutter.dev/