Introduction: IPlantação: Introdução

About: I Like to integrate all

Autores

  • João André da Costa Franco Anunciação: joaoandre.eng@gmail.com
  • Rafael Munhoz Almeida da Silva: rafael.munhoz@gmail.com
  • Rédi Vildo dos Santos Rosa: redidosantosrosa@gmail.com

Componentes utilizados

  • DragonBoard 410c
  • Raspberry Pi 3
  • Modulo de Camera para Raspberry V2

Detalhes do Projeto

Resumo do Projeto: Redução de custos e aumento de confiabilidade na coleta de dados para plataforma de análise da qualidade da plantação de soja através da completa automatização do procedimento. Atualmente, esta plataforma se baseia na altura da plantação e na imagem.

Descrição do funcionamento básico do protótipo

A cada intervalo de tempo definido pelo usuário o Raspberry produz uma foto no formato de paisagem da plantação onde ele está posicionado. Esta imagem é então transferida para a placa DragonBoard 410C via bluetooth e em seguida tratada por um algoritmo que quebra essa imagem em várias outras de menor dimensão, no caso usamos 15 x 15 pixels, para fazer esse tratamento utilizamos uma versão da biblioteca OpenCV para a plataforma Android. Teremos como resultado deste projeto o input para o algoritmo de Machine Learning, que foi previamente construido e treinado utilizando-se as ferramentas Keras e TensorFlow, para rodarmos esse algoritmo localmente na DragonBoard, foi necessária uma conversão do Tensor Flow para o Tensor Flow Light, que é mais leve e foi especialmente desenvolvido para em dispositivos móveis. Como saída do modelo do Tensor Flow Light teremos para cada “tile”, ou seja para cada pedaço de 15 x 15 pixels, teremos entre 0 e 1 que indicará qual a qualidade daquele pedacinho da imagem, quanto mais próximo de 1 melhor a qualidade e quanto mais próximo de 0 pior a qualidade. Por fim calculamos a média de qualidade levando em consideração todos os tiles e enviamos esses dados junto com a imagem que foi produzida para o Firebase. Estes dados então serão visualizados por um aplicativo web escrito em React.

Description of the basic operation of the prototype

At each user-defined time interval, Raspberry produces a photo in the landscape format of the plantation where it is positioned. This image is then transferred to the DragonBoard 410C card via Bluetooth and then treated by an algorithm that breaks this image into several smaller ones, in the case we use 15 x 15 pixels, to do this treatment we use a version of the OpenCV library for the Android platform. We will have as a result of this project the input to the Machine Learning algorithm, which was previously constructed and trained using the tools Keras and TensorFlow, to run this algorithm locally in the DragonBoard, it was necessary a conversion of the Tensor Flow to the Tensor Flow Light, which is lighter and has been specially developed for mobile devices. As output of the Tensor Flow Light model we will have for each tile, that is for each piece of 15 x 15 pixels, we will have between 0 and 1 that will indicate what the quality of that bit of the image, the closer to 1 the better the quality and the closer to 0 the worse the quality. Finally, we calculate the average quality taking into account all tiles and send this data along with the image that was produced for Firebase. This data will then be viewed by a web application written in React.

Step 1: Objetivo

Resumo

Tentamos com esse projeto envolver todas as técnologias que estão na moda, tais como Machine Learning, Tensor Flow, Keras, Iot, Android, React, Firebase, Serveless, entre outras, de maneira a integrar todas de ponta a ponta e construir um protótipo de uma solução que faz uma foto de uma plantação, prepara os dados da foto para serem processados pelo modelo construído no TensorFlow e extrai como resultado um idicador que diz se a plantação está com uma qualidade boa ou ruim, esses dados são então enviados para uma conta do Firebase e exibidos em um frotend escrito em React.

O problema da camera escura

Na proposta inicial tinhamos definido que iriamos utilizar a camera ov7670, pois eu tinha uma sobrando em casa de um outro projeto que quase dominou o mundo, só que não. Voltemos ao nosso problema então, o que aconteceu foi que prontamente tantamos conectar a ov7670 diretamente na GPIO da 410 C porém a única imagem que viamos era uma imagem escura e mais nada, o que fazer então, novamente seguindo o método Go Horse e utilizando o que tinhamos em mãos partimos para o uso de um Raspberry com a sua camera, a ideia era então produzir as fotos com o Rasp e enviar essa foto para a placa 410C por bluetooth e funcionou !!!! todo mundo para o bar da esquina comemorar.

O problema do tempo

Seguindo a proposta inicial iriamos utilizar um sensor ultrasonico e um motor de passo e muita trigonometria para medir a altura média da plantação, como perdemos muito tempo tentando fazer a camera ov7670 funcionar desistimos dessa parte e até que não ficou ruim pois conseguimos bons resultados de acurácia apenas analisando a imagem da plantação no momento do treinamento da rede neural.
Com os “workarounds” feitos acima finalizamos a implementação do projeto baseado na arquitetura descrita no diagrama em anexo


Step 2: O Desafio Do Machine Learning Abordando O Descritor

Tinhamos a nossa disposição um conjunto de dados com 610 imagens já classificadas pelo engenheiro agronomo, para cada uma das imagens foi dado um valor entre 1 e 5 onde 1 era muito ruim e 5 era muito bom, o primeiro desafio aqui foi pensar em um descritor para a imagem, por simplicidade optamos por consturir o descritor baseado no RGB da imagem, então fizemos o seguinte, calculamos a média de todos os valores de Red, de Green e de Blue da imagem, nosso vetor de entrada ficaria então com dimensão pequena o que favoreceria a execução do treinamento da rede. Nesse ponto ainda não tinhamos decidido se iriamos usar o Tensor Flow ou o scikit-learn, e neste momento optamos por usar o scikit-learn, veja abaixo um pedaço do código que constroi esse descritor:

mean_descriptor.py

def__create_feature_vector_mean(self, file_name, piquete_id, score, height):
rgb_mean = [0] *3
image_path =self.__image_files_root_folder+str(piquete_id)+'/'+file_name
image = cv2.imread(image_path)
print(image_path)
means = cv2.mean(image)
if means isnotNone:
#raw = image.flatten()
print(str(means[:3])+'\n')
rgb_mean[0] = means[0]
rgb_mean[1] = means[1]
rgb_mean[2] = means[2]
#rgb_mean[3] = height * 1000
#print(preprocessing.scale(rgb_mean))
#means[3] = height
#scaled_means = preprocessing.scale(means[:3])
ifself.__check_for_bad_images(file_name, piquete_id, score):
self.__field_dataset.feature_vector.append(preprocessing.scale(rgb_mean))
#self.__field_dataset.feature_vector.append(preprocessing.scale(means))
returnTrue
else:
returnFalse
else:
returnFalse

Step 3: O Desafio Do Machine Learning Atacando Com Scikit-learn

Com esse descritor implementado partimos para a execução do treinamento do algoritmo utilizando o scikit-learn, fizemos isso neste momento pois a curva de aprendizado do scikit-learn é mais rápida que a do Tensor Flow, neste instante ainda estavamos considerando este problema com um problema do tipo multiclasse, pois como dito anteriormente nossa classificação foi baseada em valores de 1 a 5, veja abaixo o código que implementa o treinamento do algoritmo no scikit-learn

sckikit.py

defrunCrossValidation(self, farm_dataset):
print('start cross validation')
x_train, x_test, y_train, y_test = cross_validation.train_test_split(
farm_dataset.feature_vector,farm_dataset.target, test_size=0.15)
#x_train_scaled = preprocessing.scale(x_train)
#y_train_scaled = preprocessing.scale(y_train)
#x_test_scaled = preprocessing.scale(x_test)
#y_test_scaled = preprocessing.scale(y_test)
print('end cross validation ')
print('start grid search')
rfc_parameters = { 'n_estimators': [200, 700], 'max_features': ['auto', 'sqrt', 'log2'] }
rfc_parameters_2 = {'bootstrap': [True, False], 'max_depth': [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, None],
'max_features': ['auto', 'sqrt'],'min_samples_leaf': [1, 2, 4],'min_samples_split': [2, 5, 10],
'n_estimators': [200, 400, 600, 800, 1000, 1200, 1400, 1600, 1800, 2000]}
parameters = {'kernel': ['rbf'], 'C': [1, 10, 100, 1000, 10000],
'gamma': [0.01, 0.001, 0.0001, 0.00001]}
clf = grid_search.RandomizedSearchCV(RandomForestClassifier(), rfc_parameters_2, n_iter=10, cv=3, verbose=5, random_state=42, n_jobs=-1).fit(x_train, y_train)
#clf = grid_search.GridSearchCV(RandomForestClassifier(), rfc_parameters_2, verbose = 5).fit(x_train, y_train)
#clf = grid_search.RandomizedSearchCV(RandomForestClassifier(), rfc_parameters, verbose = 5).fit(x_train, y_train)
#clf = grid_search.GridSearchCV(svm.SVC(), parameters, verbose = 5).fit(x_train, y_train)
#clf = grid_search.RandomizedSearchCV(svm.SVC(), parameters, verbose = 5).fit(x_train, y_train)
print('end grid search')
self.__classifier = clf.best_estimator_
print()
print('Parameters:', clf.best_params_)
print()
print('Best classifier score')
predicted =self.__classifier.predict(x_test)
print(metrics.classification_report(y_test,predicted))
print(predicted)
print(y_test)
print(accuracy_score(y_test,predicted))
view rawsckikit.py hosted with ❤ by GitHub

Step 4: O Desafio Do Machine Learning a Missão De Melhorar O Descritor

Com o descritor e o algoritmo escolhido, que no caso foi o RandomForest, começamos a rodar o treinamento, e percebemos que os resultados de acurácia ficavam entre 50% e 60% ocilando muito. Resolvemos então partir para o Tensor Flow, utilizando o Keras. O Keras simplifica em muito o uso do Tensor Flow, pois com ele podemos construir a rede que será treinada de maneira mais simples, sem ter que aprender todos os detalhes do Tensor Flow.

Porém resolvemos modificar o descritor também, tivemos a idéia de fatiar a imagem original em “tiles”, retangulos de 15 x 15 pixels, desta maneira conseguiriamos mais amostras pois de uma imagem de dimensão 1000 x 750 conseguiriamos até 3250 pequenos retângulos de 15 x 15 pixels. Veja abaixo um trecho da implementação deste descritor:

improvedDesc.py

def__create_feature_vector_raw(self, file_name, piquete_id, score, flatten=False):
sample_size = (self.CROP_WIDTH, self.CROP_HEIGHT, 3)
image_path =self.__image_files_root_folder+str(piquete_id)+'/'+file_name
print(image_path)
image_raw = cv2.imread(image_path)
if image_raw isnotNone:
ifnotself.__check_for_bad_images(file_name, piquete_id, score):
return (False ,0)
cropped_images = []
image_square_counter =0
image_raw_width = image_raw.shape[0]
image_raw_height = image_raw.shape[1]
print(image_raw_height)
print(image_raw_width)
square_qtd_x =int(image_raw_width / sample_size[0])
square_qtd_y =int(image_raw_height / sample_size[1])
print(square_qtd_x)
print(square_qtd_y)
for sq_indx_x inrange(square_qtd_x -2, square_qtd_x):
for sq_indx_y inrange(0, square_qtd_y):
cropped_images.append(np.zeros(sample_size, np.uint8))
for i inrange(0, sample_size[0]):
for j inrange(0, sample_size[1]):
cropped_images[image_square_counter][i][j] = image_raw[i + sq_indx_x * sample_size[0]][j + sq_indx_y * sample_size[1]]
image_square_counter = image_square_counter +1
print(' ******* Squares counter ******** '+str(image_square_counter))
print(' ******* Cropped image size ***** '+str(len(cropped_images)))
for img_index inrange(0, len(cropped_images)):
if flatten:
expanded = np.array(cropped_images[img_index])
self.__farm_dataset.feature_vector.append(expanded.flatten())
else:
self.__farm_dataset.feature_vector.append(cropped_images[img_index])
return (True, image_square_counter)
else:
return (False, 0)
view rawimprovedDesc.py hosted with ❤ by GitHub

Step 5: O Desafio Do Machine Learning O Ataque Do Tensor Flow

Agora utilizando o novo descritor e com o Tensor Flow conseguimos chegar na marca dos 73% de acurácia e a ocilação caiu para próximo de 0%. Durante todo esse processo descobrimos várias inconsistências nos dados, como no caso de encontrarmos uma mesma imagem com com classificações diferentes, dos 610 exemplos que tinhamos 110 estavam com problema. Além disso notamos que o dataset estava desbalanceado, para resolver o desbalanceamento, decidimos reduzir o problema a duas classes, uma que seria caracterizada como boa e outra como ruim.

tensorflow.py

model = Sequential()
model.add(Conv2D(32, kernel_size=(3, 3),
activation='relu',
input_shape=input_shape))
model.add(Conv2D(64, (3, 3), activation='relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.40)) #0.4 removed as it is bad of tflite # 0.25
model.add(Flatten())
model.add(Dense(128, activation='relu'))
model.add(Dropout(0.25)) #0.25 removed as it is bad of tflite # 0.5
model.add(Dense(num_classes, activation='sigmoid')) #sigmoid
model.compile(loss=keras.losses.binary_crossentropy,
optimizer=keras.optimizers.Adadelta(),
metrics=['accuracy'])
filepath="weights-improvement-{epoch:02d}-{val_acc:.2f}.hdf5"
checkpoint = ModelCheckpoint(filepath, monitor='val_acc', verbose=1, save_best_only=True, mode='max', period=5)
callbacks_list = [checkpoint]
model.fit(x_train, y_train,
batch_size=batch_size,
epochs=epochs,
verbose=1,
callbacks=callbacks_list,
validation_data=(x_test, y_test))
score = model.evaluate(x_test, y_test, verbose=5)
view rawtensorflow.py hosted with ❤ by GitHub

Step 6: O Desafio Do Machine Learning Convertendo Keras Para TFLight

Agora é hora de subir o modelo do Tensor Flow já treinado para o Firebase, desta maneira quando rodarmos a aplicação desenvolvida no Android será possível baixar/atualizar o modelo e rodar o processamento localmente na Dragonboard, achamos interessante implementar o processamento local na placa pois na maioria das fazendas não temos conexão com internet.

Para rodar o algoritmo localmente primeiramente precisamos converter o modelo do Tensor Flow Desenvolvido no Keras para o formato Tensor Flow Light que é suportado pelo firebase e pode ser distribuido e rodado localmente na aplicação, veja abaixo o código para conversão:

kerasToTflight.py

from__future__import print_function
import keras
#from keras.datasets import mnist
from keras.models import Sequential
from keras.layers import Dense, Dropout, Flatten
from keras.layers import Conv2D, MaxPooling2D
from keras import backend as K
import numpy as np
import ml_bullgreen_dataset_handler
from sklearn import cross_validation
from keras.callbacks import ModelCheckpoint
from keras.models import load_model
import tensorflow as tf
print('Loading persisted block')
converter = tf.contrib.lite.TocoConverter.from_keras_model_file('weights-improvement-60-0.72_softmax.hdf5')
tflite_model = converter.convert()
open("plantacao-qualidade-detector.tflite", "wb").write(tflite_model)

Step 7: O Desafio Do Machine Learning Publicando No ML Do Firebase

Step 8: Android Na Dragonboard Instalando OpenCV

Vamos agora partir para a implementação da parte Android do projeto, inicialmente vamos construir o código que prepara o descritor de cada imagem que foi capturada, da mesma maneira que fizemos em python quando treinamos o a rede.

Como no python utilizamos o OpenCV, achamos que seria interessante usar o OpenCV no Android também, para instalar o OpenCV para Android na Dragonboard siga a instrução abaixo:

Utilizei o opencv para fazer o crop da imagem da mesma maneira que foi feito no código python durante a fase de treino/validação do modelo .Para o opencv funcionar temos que instalar o .apk baixe o sdk do android no link

https://sourceforge.net/projects/opencvlibrary/files/opencv-android/3.4.3/opencv-3.4.3-android-sdk.zip/download

Abra o arquivo .zip e instale no device o seguinte apk.

	adb install /Users/vntraal/Downloads/OpenCV-android-sdk/apk/OpenCV_3.4.3_Manager_3.43_arm64-v8a.apk

Step 9: Android Na Dragonboard Implementando O Descritor

Com o OpenCV instaladona Dragonboard, vamos implemenar o código que pega cada uma das imagens produzidas e faz o crop em retângulos de 15 x 15 pixels, da mesma maneira como fizemos no python. Veja o resultado no trecho de código abaixo:

crop.java

privatefloat[][][][] cropImage(String absolutePath) {
Mat imageRaw =Imgcodecs.imread(absolutePath);
float[][][][] imgData =newfloat [DIM_BATCH_SIZE] [DIM_IMG_SIZE_X] [DIM_IMG_SIZE_Y] [DIM_PIXEL_SIZE];
int square_qtd_x =20;
int square_qtd_y =20;
Log.d(TAG,"Squares in x "+square_qtd_x);
Log.d(TAG,"Squares in y "+square_qtd_y);
Log.d(TAG, "Total Crop Squares "+square_qtd_x * square_qtd_y);
int square_counter =0;
for (int idx =0 ; idx < square_qtd_x ; idx ++) {
for(int idy =0 ; idy < square_qtd_y ; idy ++) {
for (int i =DIM_IMG_SIZE_X-1 ; i >=0 ; i --) {
for (int j =DIM_IMG_SIZE_Y-1 ; j >=0 ; j --) {
float[] bgrColorsNormalized =newfloat[3];
bgrColorsNormalized[0] =newFloat(imageRaw.get(i +DIM_IMG_SIZE_X* idx, j +DIM_IMG_SIZE_Y* idy)[2]) /255; // b
bgrColorsNormalized[1] =newFloat(imageRaw.get(i +DIM_IMG_SIZE_X* idx, j +DIM_IMG_SIZE_Y* idy)[1]) /255; // g
bgrColorsNormalized[2] =newFloat(imageRaw.get(i +DIM_IMG_SIZE_X* idx, j +DIM_IMG_SIZE_Y* idy)[0]) /255; // r
imgData[square_counter][i][j] = bgrColorsNormalized;
}
}
square_counter ++;
}
}
Log.d(TAG,"raw image size "+imageRaw.size());
return imgData;
}
view rawcrop.java hosted with ❤ by GitHub

Step 10: Android Na Dragonboard Preparando Para O Firebase

Com o algoritmo que gera o descritor da imagem implementado, podemos rodar o modelo de predição armazenado no formato TFLight, localmente na Dragonboard 410C, para isso vamos usar o código do serviço ML do Firebase, mas antes precisamos instalar 3 APK's do google que são necessários quando vamos usar o firebase. veja abaixo o passo a passo, não se esqueça de usar as mesmas versões, senão muitos erros vão acontecer

A Dragon Board 410C vem sem o trio ternura descrito abaixo, para que a biblioteca do firebase funcione precisamos do google service funcionando, no meu caso eu tentei várias versões e a seguinte combinação que deu certo foi a mostrada abaixo

com.android.vending_12.0.19.apk
com.google.android.gms_13.2.83.apk
com.google.android.gsf_5.1-1743759.apk

veja os links para download de cada arquivo

https://www.apkmirror.com/apk/google-inc/google-play-store/google-play-store-12-0-19-release/

https://www.apkmirror.com/apk/google-inc/google-pl...
https://www.apkmirror.com/apk/google-inc/google-s...

Faça o download e copie para dentro do device como mostrado nos comandos abaixo

adb root 
adb remount<br>adb push /Users/vntraal/Downloads/com.android.vending_12.0.19.apk /system/priv-app<br>adb push /Users/vntraal/Downloads/com.google.android.gms_13.2.83.apk /system/priv-app<br>adb push /Users/vntraal/Downloads/com.google.android.gsf_5.1-1743759.apk /system/priv-app <br>adb shell sync<br>adb reboot

Step 11: Android Na Dragonboard Rodando O Algoritmo De Predição

Com a placa preparada para o Firebase podemos agora escrever o código que fará a predição das imagens produzidas pela câmera, veja abaixo o trecho do código responsável por essa implementação:

firebaseRunML.java

publicvoid runModel(String absolutePath) {
if (mInterpreter ==null) {
Log.e(TAG, "Image classifier has not been initialized; Skipped.");
return;
}
float[][][][] imgData = cropImage(absolutePath);
try {
FirebaseModelInputs inputs =newFirebaseModelInputs.Builder().add(imgData).build();
// Here's where the magic happens!!
Log.d(TAG,"Here's where the magic happens!!");
mInterpreter
.run(inputs, mDataOptions)
.addOnFailureListener(newOnFailureListener() {
@Override
publicvoidonFailure(@NonNullExceptione) {
e.printStackTrace();
showToast("Error running model inference "+e.toString());
mlKitListener.machineLearningError(e);
}
})
.continueWith(
newContinuation<FirebaseModelOutputs, Boolean>() {
@Override
publicBooleanthen(Task<FirebaseModelOutputs>task) {
float[][] result = task.getResult().<float[][]>getOutput(0);
mlKitListener.machineLearningResult(result);
showToast("- Class 1 - "+result[0][0]+" - Class 2 - "+result[0][1]);
calculateMLSimpleResult(result);
returntrue;
}
});
} catch (FirebaseMLException e) {
e.printStackTrace();
showToast("Error running model inference");
}
}

Step 12: Android Na Dragonboard Enviando Os Dados Para O Firebase

Depois de rodar a predição na imagem e classificar com boa ou ruim, vamos enviar esses dados junto com a imagem para o Database e para o Storage do firebase, o código responsável por essa operação é o seguinte:

sendDataFirebase.java

privatevoid savePrediction(Prediction prediction) {
Log.d(TAG,"savePrediction");
FirebaseDatabase firebaseDatabase =FirebaseDatabase.getInstance();
DatabaseReference databaseReference = firebaseDatabase.getReference();
databaseReference.child("prediction").child(prediction.getId()).setValue(prediction);
}
privatevoid uploadImage(String absolutePath, finalPrediction prediction) {
Uri file =Uri.fromFile(newFile(absolutePath));
Long timestamp =System.currentTimeMillis() /1000;
StorageReference imageRef = storageReference.child("images/"+timestamp.toString()+".jpg");
prediction.setTimestamp(timestamp);
imageRef.putFile(file)
.addOnSuccessListener(newOnSuccessListener<UploadTask.TaskSnapshot>() {
@Override
publicvoidonSuccess(UploadTask.TaskSnapshottaskSnapshot) {
Uri downloadUrl = taskSnapshot.getDownloadUrl();
Log.d(TAG,"image uploaded with success "+downloadUrl.toString());
prediction.setImageDownloadUrl(downloadUrl.toString());
savePrediction(prediction);
}
})
.addOnFailureListener(newOnFailureListener() {
@Override
publicvoidonFailure(@NonNullExceptionexception) {
exception.printStackTrace();
Log.d(TAG,"error cause "+exception.getCause().toString());
Log.d(TAG,"error on upload image "+exception.toString());
}
});
}
view rawgistfile1.java hosted with ❤ by GitHub

Step 13: Consumindo Os Dados Do Firebase Conectando O React

Para consumir os dados armazenados no Firebase e exibir ao usuário, vamos constuir um aplicativo web, optamos por utilizar o React por simplicidade e como oportunidade para ganharmos competência. Antes de mais nada precisamos conectar o aplicativo web com o firebase para isso crie um arquivo com o nome firebaseUtils.js.

No site do firebase baixe o arquivo com as configurações da sua conta, o arquivo terá o seguinte formato:

FirebaseUtils.js

importfirebasefrom"firebase";
constconfig= {
apiKey:"Minha Chave Tira o Zóio",
authDomain:"iplantacao-72b97.firebaseapp.com",
databaseURL:"https://iplantacao-72b97.firebaseio.com",
projectId:"iplantacao-72b97",
storageBucket:"iplantacao-72b97.appspot.com",
messagingSenderId:"Tira o Zoio"
};
exportconstfirebaseImpl=firebase.initializeApp(config);
exportconstfirebaseDatabase=firebase.database();
view rawFirebaseUtils.js hosted with ❤ by GitHub

Step 14: Consumindo Os Dados Do Firebase Escolhendo Os Componentes

A ideia do aplicativo web é mostrar um gráfico com a qualidade da plantação por tempo. Além deste gráfico vamos exibir uma tabela com os dados na integra e por fim um componente "Caroucel" que vai mostrar as ultimas 20 imagens que foram capturadas da plantação.

Utilizamos o Firebase como host para publicarmos o aplicativo web

Aplicativo Web

Step 15: Referências

Como desligar o log do NFC da DragonBard 410C que não para de sujar o console

https://discuss.96boards.org/t/install-google-playstore-on-android/917/12

Como instalar o Google Playstore no Android

https://discuss.96boards.org/t/nfc-logcat-spam-in-...

Como fazer autenticação anomima no Firebase (Necessário para fazer o storage da imagem) https://firebase.google.com/docs/auth/android/ano...

Como treinar e finalizar o algoritmo de Machine Learning

https://machinelearningmastery.com/train-final-ma...

Explicação do bug de conversão do Tensor Flow para o Tensor Flow Lite

https://stackoverflow.com/questions/51966486/diff...

Fazendo Predições com o Keras

https://machinelearningmastery.com/how-to-make-cl...

Rodando o modelo TF lite

https://stackoverflow.com/questions/50443411/how-...

Como subir o modelo no Firebase

https://medium.com/google-developer-experts/explo...

Como usar o TF Lite com Firebase no Android

https://firebase.google.com/docs/ml-kit/android/u... https://proandroiddev.com/exporting-tensorflow-mo...

Ferramenta que mostra a composição do modelo do Tensor Flow

https://github.com/lutzroeder/Netron

Manipulando imagens com OpenCV

https://docs.opencv.org/3.0-beta/doc/py_tutorials...

https://docs.opencv.org/3.0-beta/doc/tutorials/co...

https://docs.opencv.org/3.0-beta/doc/py_tutorials...

https://docs.opencv.org/java/3.0.0/

Como instalar o OpenCV no Android

https://medium.com/@sukritipaul005/a-beginners-gu...

https://stackoverflow.com/questions/17767557/how-...

React e Firebase

https://www.robinwieruch.de/complete-firebase-aut...

https://blog.tecsinapse.com.br/criando-uma-aplica...

Crash Course em React

https://www.youtube.com/watch?v=Ke90Tje7VS0

Usando o Chart.js

https://www.youtube.com/watch?v=Ly-9VTXJlnA http://www.chartjs.org/docs/latest/charts/line.ht...

https://www.youtube.com/watch?v=Ly-9VTXJlnA https://stackoverflow.com/questions/48689876/how-...

Material Design com React

https://mdbootstrap.com/react/content/datatables/

Hosting React no Firebase

https://medium.com/@bensigo/hosting-your-react-ap...

Tabela com scroll no React

https://mdbootstrap.com/content/bootstrap-table-s...

https://stackoverflow.com/questions/8522673/make-...