Skip to content

Instantly share code, notes, and snippets.

@AlissonAp
Created November 20, 2019 01:50
Show Gist options
  • Save AlissonAp/73e3d8ff4da665c73fa5a050f87cb304 to your computer and use it in GitHub Desktop.
Save AlissonAp/73e3d8ff4da665c73fa5a050f87cb304 to your computer and use it in GitHub Desktop.
Categorização de imagens e análise de conteúdo explícito
const projectId = 'fb-functions-2019';
const uploadsBucketName = 'my-image-upload-bucket'
const notSafedBucketName = 'my-not-safed-images'
const databaseTable = '/uploads'
// SDK do Cloud Functions para Firebase para criar Cloud Functions e escutar e executar eventos.
const functions = require('firebase-functions');
const cors = require('cors')({ origin: true });
// SDK do Firebase para acessar a base de dados em tempo real.
const admin = require('firebase-admin');
admin.initializeApp();
const db = admin.database();
// Importa a biblioteca do Cloud Storage para acessar os buckets criados e salvar objetos
const { Storage } = require('@google-cloud/storage');
// Importa a bilioteca da Vision API para realizar a análise das imagens inseridas no bucket
const { ImageAnnotatorClient } = require('@google-cloud/vision');
// Importa a biblioteca do imageMagick para realizar o desfoque das imagens explícitas encontradas
const gm = require('gm').subClass({ imageMagick: true });
const { promisify } = require('util');
//Por padrão o Node JS não tem suporte para parsear requisições multipart/form-data
//Dessa forma foi utilizada a biblioteca Bysboy para prestar esse suporte de parse das requisições
const Busboy = require('busboy');
const path = require('path');
const os = require('os');
const fs = require('fs');
/**
* Realiza o parse de uma requisição de upload 'multipart/form-data'
*
* @param {Object} req Cloud Function request context.
* @param {Object} res Cloud Function response context.
*/
// Instancia o cliente do Storage com o id do projeto no GCP
const storage = new Storage({
projectId: projectId,
});
/* Funções Serverless */
const blurImage = async (object) => {
const tempLocalPath = `/tmp/${path.parse(object.name).base}`;
const file = storage.bucket(object.bucket).file(object.name);
try {
await file.download({ destination: tempLocalPath })
console.log(`O ${file.name} foi baixado para ${tempLocalPath}.`);
} catch (err) {
throw new Error(`Falha ao realizar o download do arquivo: ${err}`);
}
await new Promise((resolve, reject) => {
gm(tempLocalPath)
.blur(0, 16)
.write(tempLocalPath, (err, stdout) => {
if (err) {
console.error('Falha ao desfocar a imagem.', err);
reject(err);
} else {
console.log(`Imagem desfocada, arquivo: ${file.name} `);
resolve(stdout);
}
});
});
// Define o bucket que será utilizado para inserir a imagem desfocada
const blurredBucket = storage.bucket(notSafedBucketName);
// Realiza o upload da imagem desfocada no bucket
const gcsPath = `gs://${notSafedBucketName}/${object.name}`;
try {
blurredBucket.upload(tempLocalPath, { destination: object.name }).then(async blurredObject => {
console.log(`Imagem desfocada inserida no bucket: ${gcsPath}`);
const parsedObject = path.parse(object.name);
if (!parsedObject.name) {
console.error("Referência da imagem não encontrada!")
return null
}
db.ref(`${databaseTable}/${parsedObject.name}`).update({
urlBlurred: `https://storage.cloud.google.com/${notSafedBucketName}/${object.name}`
}).finally(() => {
// Exclui o arquivo temporário da memória
const unlink = promisify(fs.unlink);
return unlink(tempLocalPath);
})
}).catch(err => {
throw new Error(`Não foi possível salvar a imagem desfocada no banco de dados ${gcsPath}: ${err}`);
});
} catch (err) {
throw new Error(`Não foi possível inserir a imagem desfocada no bucket ${gcsPath}: ${err}`);
}
}
/* Rota para consulta de imagens já inseridas */
exports.search = functions.https.onRequest((req, res) => {
return cors(req, res, () => {
if (req.method !== 'GET') {
// Método não permitido
return res.status(405).end();
}
getData().then(result => {
res.send(result)
}).catch(err => {
res.send(err)
})
/* Busca todas as referências de imagens do banco de dados, bem como as informações se são imagens
explícitas ou não */
function getData() {
return new Promise((resolve, reject) => {
const imagesData = []
db.ref(databaseTable).once('value').then(async data => {
await data.forEach((elm) => {
let newItem = {
checked: elm.val().checked || false,
explicit: elm.val().explicitContent || false,
url: elm.val().url || "",
urlExplicit: elm.val().urlBlurred || "",
labels: elm.val().labels || ""
}
imagesData.push(newItem);
});
resolve(imagesData);
}).catch(err => {
reject(err);
});
});
}
});
})
/* Rota para envio de novas imagens em formato multipart/form-data */
exports.upload = functions.https.onRequest((req, res) => {
return cors(req, res, () => {
if (req.method !== 'POST') {
// Return a "method not allowed" error
return res.status(405).end();
}
const busboy = new Busboy({ headers: req.headers });
const tmpdir = os.tmpdir();
// Objeto acumula todos os campos recebidos no multipart/form-data
const fields = {};
// Objeto acumula todos as imagens para upload
const uploads = {};
// Realiza a leitura de campos que não são do tipo File
busboy.on('field', (fieldname, val) => {
console.log(`Campo ${fieldname} processado com o valor : ${val}.`);
fields[fieldname] = val;
});
const fileWrites = [];
// Percorre todos os arquivos de upload, do tipo File
busboy.on('file', (fieldname, file, filename) => {
console.log(`Arquivo ${filename} processado!`);
//Recupera a extensão do arquivo
const extension = filename.substring(filename.indexOf("."), filename.length)
//Substitui o nome original do arquivo por um timeStamp
filename = new Date().getTime().toString() + extension;
const filepath = path.join(tmpdir, filename);
uploads[fieldname] = filepath;
const writeStream = fs.createWriteStream(filepath);
file.pipe(writeStream);
// Arquivo processado, salva o mesmo na memória
const promise = new Promise((resolve, reject) => {
file.on('end', () => {
writeStream.end();
});
writeStream.on('finish', resolve);
writeStream.on('error', reject);
});
fileWrites.push(promise);
});
// Evento é disparado quando todos os arquivos foram processados pelo Busboy.
busboy.on('finish', () => {
//Aguarda a gravação dos arquivos em memória
Promise.all(fileWrites).then(() => {
for (const name in uploads) {
const file = uploads[name]
async function upload2bucket() {
/* Criar registro de referência da imagem no banco de dados, com a flag checked = false
checked = false, significa que a imagem ainda não foi validada
*/
const newImageRef = await db.ref(databaseTable).push({ checked: false });
const storageOptions = {
destination: `${newImageRef.key}${file.substring(file.lastIndexOf('.'))}`
}
storage.bucket(uploadsBucketName).upload(file, storageOptions).then(fileRes => {
return `https://storage.cloud.google.com/${uploadsBucketName}/${newImageRef.key}${file.substring(file.lastIndexOf('.'))}`
}).then(url => {
//Salvou o arquivo de imagem no bucket principal, exclui o arquivo da memória
fs.unlinkSync(file);
newImageRef.update({ url }).then(
res.send("Upload realizado com sucesso e arquivo salvo no banco de dados! -> " + newImageRef.key)
).catch(err => {
res.send("Falha ao salvar o arquivo no banco de dados -> " + err)
})
}).catch(err => {
res.send("Falha ao salvar o arquivo no Storage -> " + err)
})
}
upload2bucket()
}
});
});
busboy.end(req.rawBody);
});
});
/* Evento é disparado toda vez que um novo arquivo é inserido no bucket principal */
exports.onUploadImageToBucket = functions.storage.bucket(uploadsBucketName).object().onFinalize(async object => {
const parts = path.parse(object.name);
if (!parts.name) {
console.error("Referência da imagem não encontrada!")
return null
}
// Instancia a classe de análise de imagens da Vision API
const client = new ImageAnnotatorClient();
// Monta a localização do arquivo inserido no bucket
const pathStorage = `gs://${object.bucket}/${object.name}`
//Retorna os labels (propriedades) encontradas na imagem,
const [results] = await client.labelDetection(pathStorage);
//Retorna possíveis conteúdos explícitos presentes na imagem
const [safes] = await client.safeSearchDetection(pathStorage);
const labels = results.labelAnnotations;
const safeLabels = safes.safeSearchAnnotation;
let explicitContent = false;
/*Verifica a propriedade de violência e conteúdo adulto presente na imagem, caso encontrado
irá salvar no banco como explicitContent = true
*/
if (safeLabels.adult === 'LIKELY' || safeLabels.violence === 'LIKELY' ||
safeLabels.adult === 'VERY_LIKELY' || safeLabels.violence === 'VERY_LIKELY') {
explicitContent = true;
}
//Atualiza as informações de referência da imagem no banco de dados
await db.ref(`${databaseTable}/${parts.name}`).update({
labels: labels.map(label => label.description).join(', '),
safeLabels,
explicitContent,
checked: true
})
/* Se encontrou conteúdo explícito na imagem, realiza o desfoque com base na imagem original e salva no bucket
secundário */
if (explicitContent) {
await blurImage(object)
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment