Created
November 20, 2019 01:50
-
-
Save AlissonAp/73e3d8ff4da665c73fa5a050f87cb304 to your computer and use it in GitHub Desktop.
Categorização de imagens e análise de conteúdo explícito
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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