gunkiss/src/Pudding.cpp

493 lines
19 KiB
C++

/*
______________
//````````````\\ + by @ohsqueezy [http://ohsqueezy.itch.io] & @sleepin [http://instagram.com/sleepin]
//~~~~~~~~~~~~~~\\ + custom pudding code provided by Nuggets Select [http://nugget.fun]
//================\\ + available for copy, modification and redistribution [http://git.nugget.fun/pudding]
// \\
// ''CUSTOM PUDDING'' \\ 😀 Thank you for choosing Pudding Customs for your business 😀
//______________________\\
``````````````````````````
Generate a custom pudding from food product UPC codes and help a pair of rats take over the video game industry, using
their extraterrestrial ability to turn trash into performance enhancing drug puddings that enable business professionals
to predict the stock market with supernatural accuracy.
*/
#include "Pudding.hpp"
/* Launch the Pudding instance's mainloop */
int main()
{
Pudding pudding = Pudding();
pudding.run();
pudding.quit();
return 0;
}
/* Initialize a Pudding instance */
Pudding::Pudding()
{
/* subscribe to command events */
get_delegate().subscribe(&Pudding::respond, this);
/* initialize an opencv capture device for getting images from an attached camera */
int device_id = 0;
capture.open(device_id);
std::stringstream message;
if (capture.isOpened())
{
message << "opened and initialized " << capture.get(cv::CAP_PROP_FRAME_WIDTH) << "x" <<
capture.get(cv::CAP_PROP_FRAME_HEIGHT) << ", " << capture.get(cv::CAP_PROP_FPS) <<
"fps video capture device ID #" << device_id << " using " << capture.getBackendName();
}
else
{
message << "failed to open video capture device ID #" << device_id;
}
log(message.str());
/* initialize a zbar image scanner for reading barcodes of any format */
image_scanner.set_config(zbar::ZBAR_NONE, zbar::ZBAR_CFG_ENABLE, 1);
/* use gl context so we can draw 3D pudding */
load_gl_context();
}
void Pudding::load_gl_context()
{
super::load_gl_context();
/* Allocate a vertex array object, bind it as current, doesn't need to be a member var because the same one is always bound */
GLuint vao;
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
/* 2D vertices for the video capture texture that are a single plane spanning the screen */
std::array<glm::vec2, 6> camera_vertices = {
{
{-1.0f, 1.0f}, {1.0f, 1.0f}, {-1.0f, -1.0f},
{1.0f, 1.0f}, {1.0f, -1.0f}, {-1.0f, -1.0f}
}};
/* UV map for mapping video capture texture to video capture vertices */
std::array<glm::vec2, 6> camera_uv = {
{
{0.0f, 1.0f}, {1.0f, 1.0f}, {0.0f, 0.0f},
{1.0f, 1.0f}, {1.0f, 0.0f}, {0.0f, 0.0f}
}};
/* generate one vertex buffer object to hold the camera texture and UV vertices */
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
/* allocate space for vertices and UV, copy vertices in at initialization */
GLsizeiptr vbo_size = (camera_vertices.size() + camera_uv.size()) * sizeof(glm::vec2);
glBufferData(GL_ARRAY_BUFFER, vbo_size, camera_vertices.data(), GL_STATIC_DRAW);
/* specify the location and data format of the vertex attributes as consecutive 2D float coords */
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, nullptr);
/* enable index 0 on currently bound VAO */
glEnableVertexAttribArray(0);
/* copy UV data into the VBO, offset to after the vertex data */
glBufferSubData(GL_ARRAY_BUFFER, camera_vertices.size() * sizeof(glm::vec2), camera_uv.size() * sizeof(glm::vec2), camera_uv.data());
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 0, reinterpret_cast<GLvoid*>(camera_vertices.size() * sizeof(glm::vec2)));
glEnableVertexAttribArray(1);
GLuint vertex_shader = load_shader("src/flat.vert", GL_VERTEX_SHADER);
GLuint fragment_shader = load_shader("src/flat.frag", GL_FRAGMENT_SHADER);
world_program = glCreateProgram();
glAttachShader(world_program, vertex_shader);
glAttachShader(world_program, fragment_shader);
glBindAttribLocation(world_program, 0, "in_Position");
glBindAttribLocation(world_program, 1, "vertexUV");
link_shader(world_program);
/* generate the texture that will store the video frame, allocate storage for a video frame, bind and edit texture properties */
glGenTextures(1, &video_capture_texture_id);
glBindTexture(GL_TEXTURE_2D, video_capture_texture_id);
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGB8, capture.get(cv::CAP_PROP_FRAME_WIDTH), capture.get(cv::CAP_PROP_FRAME_HEIGHT));
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
log_gl_errors();
}
/* Respond to command events */
void Pudding::respond(SDL_Event& event)
{
if (get_delegate().compare(event, "up"))
{
increment_item_index();
}
else if (get_delegate().compare(event, "right"))
{
get_current_item().increment_image_index();
}
else if (get_delegate().compare(event, "down"))
{
increment_item_index(-1);
}
else if (get_delegate().compare(event, "left"))
{
get_current_item().increment_image_index(-1);
}
}
/* Build an Item object by submitting the upc parameter to multiple APIs and taking
* relevant results from each. Result JSON will be saved if saving is enabled in the global
* configuration
*/
void Pudding::add_item(const std::string& upc)
{
Item item(this);
item.set_upc(upc);
if (get_configuration()["api"]["open-food-enabled"])
{
incorporate_open_food_api(item);
}
if (get_configuration()["api"]["nutronix-enabled"])
{
incorporate_nutronix_api(item);
}
if (get_configuration()["api"]["edamam-enabled"])
{
incorporate_edamam_api(item);
}
if (get_configuration()["api"]["best-buy-enabled"])
{
incorporate_best_buy_api(item);
}
items.push_back(item);
/* set item index to end so newest item will display */
current_item_index = items.size() - 1;
}
/* Look for item upc in the Open Food API, and use the result to fill out item properties if found
*/
void Pudding::incorporate_open_food_api(Item& item)
{
log("checking Open Food API");
nlohmann::json json = json_from_url(OPEN_FOOD_API_URL + item.get_upc());
// test that should determine if an Open Food API response is not empty
if (json.value("status", 0) && json.contains("product"))
{
if (json["product"].value("image_url", "") != "")
{
std::string url = json["product"]["image_url"];
std::shared_ptr<SDL_Texture> texture = texture_from_image_url(url);
if (texture != nullptr)
{
item.add_image_texture(texture);
}
}
item.set_brand_name(json["product"].value("brands", ""));
item.set_product_name(json["product"].value("product_name", ""));
save_item_json(json, item, "Open_Food_API");
}
else
{
log("no results from Open Food API");
}
}
/* Look for item upc in the Nutronix API, and use the result to fill out item properties if found
*/
void Pudding::incorporate_nutronix_api(Item& item)
{
log("checking Nutronix API");
// Nutronix requires API keys in headers for validation
nlohmann::json json = json_from_url(
NUTRONIX_API_URL + item.get_upc(), {
"x-app-id: " + get_configuration()["api"]["nutronix-app-id"].get<std::string>(),
"x-app-key: " + get_configuration()["api"]["nutronix-app-key"].get<std::string>()
});
// test that should determine if a Nutronix response is not empty
if (!(json.contains("message") && json["message"] == NUTRONIX_NOT_FOUND))
{
nlohmann::json food = json["foods"][0];
if (food.contains("photo") && food["photo"].value("thumb", "") != "")
{
std::string url = food["photo"]["thumb"];
log("adding image listed in Nutronix API at " + url);
std::shared_ptr<SDL_Texture> texture = texture_from_image_url(url);
if (texture != nullptr)
{
item.add_image_texture(texture);
}
}
item.set_brand_name(food.value("brand_name", ""));
item.set_product_name(food.value("food_name", ""));
save_item_json(json, item, "Nutronix_API");
}
else
{
log("no results from Nutronix API");
}
}
/* Submit a query to Edamam API and insert relevant results into supplied Item object
*/
void Pudding::incorporate_edamam_api(Item& item)
{
log("checking Edamam API");
// build API url by concatenating relevant values into query string
std::stringstream url;
url << "https://api.edamam.com/api/food-database/v2/parser?upc=" << item.get_upc() << "&app_id=" <<
get_configuration()["api"]["edamam-app-id"].get<std::string>() << "&app_key=" <<
get_configuration()["api"]["edamam-app-key"].get<std::string>();
nlohmann::json json = json_from_url(url.str());
// test that should determine if a Edamam response has food data
if (json.contains("hints") && json["hints"][0].contains("food"))
{
nlohmann::json food = json["hints"][0]["food"];
if (food.value("image", "") != "")
{
std::string url = food["image"];
std::shared_ptr<SDL_Texture> texture = texture_from_image_url(url);
if (texture != nullptr)
{
item.add_image_texture(texture);
}
item.set_product_name(food.value("label", ""));
}
save_item_json(json, item, "Edamam_API");
}
}
/* Submit a query to the Best Buy API and insert relevant results into supplied Item object
*/
void Pudding::incorporate_best_buy_api(Item& item)
{
log("checking Best Buy API");
// build API url by concatenating relevant values into query string
std::stringstream url;
url << "https://api.bestbuy.com/v1/products(upc=" << item.get_upc() << ")?format=json&apiKey=" <<
get_configuration()["api"]["best-buy-api-key"].get<std::string>();
nlohmann::json json = json_from_url(url.str());
// test that should determine if a Best Buy response has a result
if (json.contains("total") && json["total"].get<int>() > 0)
{
nlohmann::json product = json["products"][0];
// look up image (for games this is box art) and "alternate views image" (for games this is a screen shot)
for (std::string key : {"alternateViewsImage", "image"})
{
if (product.value(key, "") != "")
{
std::string url = product[key];
std::shared_ptr<SDL_Texture> texture = texture_from_image_url(url);
if (texture != nullptr)
{
item.add_image_texture(texture);
}
}
}
item.set_product_name(product.value("name", ""));
save_item_json(json, item, "Best_Buy_API");
}
}
/* Write submitted JSON to file, creating parent directories if necessary, and using item and
* api_name to determine file name prefix
*/
void Pudding::save_item_json(const nlohmann::json& json, const Item& item, const std::string& api_name) const
{
if (get_configuration()["scan"]["json-save"])
{
fs::path path = get_configuration()["scan"]["json-save-directory"];
if (!fs::exists(path))
{
fs::create_directories(path);
}
std::string prefix = api_name;
if (item.get_full_name() != "")
{
prefix += "_" + item.get_full_name();
}
else
{
prefix += "_Unknown";
}
std::replace_if(prefix.begin(), prefix.end(), [](char c) { return !std::isalnum(c); }, '_');
path /= prefix + "_" + item.get_upc() + ".json";
std::ofstream out(path);
out << std::setw(4) << json << std::endl;
log("Saved JSON to " + path.string());
}
else
{
SDL_LogWarn(SDL_LOG_CATEGORY_CUSTOM, "not saving JSON, saving disabled by configuration");
}
}
/* Download the JSON data at the submitted URL, and return it as a JSON object
*/
nlohmann::json Pudding::json_from_url(const std::string& url, const std::vector<std::string>& headers)
{
std::vector<std::uint8_t> storage;
curl_get_bytes(url, storage, headers);
nlohmann::json json = nlohmann::json::parse(storage);
std::stringstream json_formatted;
json_formatted << std::setw(4) << json << std::endl;
debug(json_formatted.str());
return json;
}
/* Store the byte buffer from the submitted URL downloaded by cURL into the supplied storage vector
*/
void Pudding::curl_get_bytes(const std::string& url, std::vector<std::uint8_t>& storage, const std::vector<std::string>& headers)
{
CURL *curl;
CURLcode result;
result = curl_global_init(CURL_GLOBAL_DEFAULT);
if (result != CURLE_OK)
{
std::cout << "curl initialization failed " << curl_easy_strerror(result) << std::endl;
}
else
{
curl = curl_easy_init();
if (curl)
{
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, Pudding::curl_write_response);
std::vector<std::uint8_t> food_barcode_response;
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &storage);
curl_easy_setopt(curl, CURLOPT_USERAGENT, get_configuration()["api"]["user-agent"].get<std::string>().c_str());
struct curl_slist* list = nullptr;
if (headers.size() > 0)
{
for (const std::string& header : headers)
{
list = curl_slist_append(list, header.c_str());
}
}
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, list);
result = curl_easy_perform(curl);
curl_slist_free_all(list);
if (result != CURLE_OK)
{
std::cout << "curl request failed " << curl_easy_strerror(result) << std::endl;
}
}
else
{
std::cout << "curl initialization failed" << std::endl;
}
curl_easy_cleanup(curl);
}
curl_global_cleanup();
}
/* This callback will be called by cURL when it has a response char buffer. The chars will be inserted into the storage
* vector pointed to by the storage parameter.
*/
size_t Pudding::curl_write_response(std::uint8_t* buffer, size_t size, size_t count, std::vector<std::uint8_t>* storage)
{
size_t total_size = size * count;
storage->insert(storage->end(), buffer, buffer + total_size);
return total_size;
}
/* Get an image at the submitted URL as a pointer to SDL_Texture memory
*/
std::shared_ptr<SDL_Texture> Pudding::texture_from_image_url(const std::string& url)
{
log("looking up image at " + url);
std::vector<std::uint8_t> storage;
curl_get_bytes(url, storage);
if (!storage.empty())
{
SDL_RWops* rw = SDL_RWFromConstMem(storage.data(), storage.size());
debug("received image data");
return std::shared_ptr<SDL_Texture>(IMG_LoadTexture_RW(get_renderer(), rw, 0), Pudding::destroy_texture);
}
else
{
SDL_LogWarn(SDL_LOG_CATEGORY_CUSTOM, "image url returned no data");
return nullptr;
}
}
/* Call SDL's destroy texture function, and print a debug statement for testing. This is defined as a static member
* function and uses the SDL function instead of the inherited logging functions from Node since the object may not
* be allocated at destruction time (?)
*/
void Pudding::destroy_texture(SDL_Texture* texture)
{
/* not sure why SDL_Log works here but SDL_LogDebug and SDL_LogInfo don't */
SDL_Log("destroying texture %p", texture);
SDL_DestroyTexture(texture);
}
/* Change the currently selected item */
void Pudding::increment_item_index(int increment)
{
current_item_index = sfw::mod(current_item_index + increment, static_cast<int>(items.size()));
}
Item& Pudding::get_current_item()
{
return items[current_item_index];
}
/* Update parameters and draw the screen */
void Pudding::update()
{
/* reload the config file every frame to check for changes */
get_root()->configuration.load("config.json");
get_root()->configuration.merge();
if (current_config_barcode != get_configuration()["scan"]["barcode"])
{
current_config_barcode = get_configuration()["scan"]["barcode"];
current_barcode = current_config_barcode;
std::stringstream message;
message << "read new barcode from config " << current_barcode;
log(message.str());
}
/* draw the current item image to the left half of the screen if items are available */
Box video_box = get_window_box();
if (items.size() > 0)
{
Box item_box = Box({0.0f, 0.0f}, {get_window_box().get_w() / 2.0f, get_window_box().get_h()});
SDL_RenderCopyF(get_renderer(), get_current_item().get_active_image_texture().get(), nullptr, &item_box);
video_box.set_left(get_window_box().get_center_x(), true);
}
/* draw the camera if the camera has been opened, fullscreen if there aren't any items, or on the right otherwise */
if (capture.isOpened())
{
capture.read(capture_frame);
if (!capture_frame.empty())
{
/* rotate the opencv matrix 180 to work with opengl coords */
cv::flip(capture_frame, capture_frame, -1);
glUseProgram(world_program);
/* bind texture to GLSL sampler */
glBindTexture(GL_TEXTURE_2D, video_capture_texture_id);
GLint base_texture_location = glGetUniformLocation(world_program, "baseTexture");
glUniform1i(base_texture_location, 0);
glActiveTexture(GL_TEXTURE0);
/* convert opencv matrix to GL texture */
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, capture_frame.cols, capture_frame.rows, GL_BGR, GL_UNSIGNED_BYTE, capture_frame.ptr());
glDrawArrays(GL_TRIANGLES, 0, 6);
SDL_GL_SwapWindow(get_window());
log_gl_errors();
/* convert to gray and scan with zbar */
cv::cvtColor(capture_frame, capture_frame, cv::COLOR_BGR2GRAY);
zbar::Image query_image(capture_frame.cols, capture_frame.rows, "Y800", static_cast<void*>(capture_frame.data),
capture_frame.cols * capture_frame.rows);
int result = image_scanner.scan(query_image);
if (result > 0)
{
for (zbar::Image::SymbolIterator symbol = query_image.symbol_begin(); symbol != query_image.symbol_end(); ++symbol)
{
std::stringstream message;
message << "camera scanned " << symbol->get_type_name() << " symbol " << symbol->get_data();
log(message.str());
// current_camera_barcode = symbol->get_data();
// current_barcode = current_camera_barcode;
}
}
query_image.set_data(nullptr, 0);
}
else
{
debug("video capture device frame empty");
}
}
/* add a new item if a new barcode was scanned or entered */
if (current_barcode != previous_barcode)
{
add_item(current_barcode);
previous_barcode = current_barcode;
}
}