commit b0758dcc73ecb8752ee51722b87bf8cd82b9b8c0 Author: Joshua Jerred Date: Sun Mar 12 20:23:47 2023 -0600 Initial Commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1899660 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +build +.vscode \ No newline at end of file diff --git a/.timetracker b/.timetracker new file mode 100644 index 0000000..5713386 --- /dev/null +++ b/.timetracker @@ -0,0 +1 @@ +{"total":15240,"sessions":[{"begin":"2023-03-12T13:44:37-06:00","end":"2023-03-12T13:48:55-06:00","duration":258},{"begin":"2023-03-12T13:49:38-06:00","end":"2023-03-12T13:55:19-06:00","duration":341},{"begin":"2023-03-12T13:55:28-06:00","end":"2023-03-12T14:12:39-06:00","duration":1030},{"begin":"2023-03-12T14:12:42-06:00","end":"2023-03-12T14:53:07-06:00","duration":2424},{"begin":"2023-03-12T14:56:47-06:00","end":"2023-03-12T15:03:52-06:00","duration":425},{"begin":"2023-03-12T15:05:24-06:00","end":"2023-03-12T15:32:37-06:00","duration":1632},{"begin":"2023-03-12T15:32:42-06:00","end":"2023-03-12T15:48:18-06:00","duration":936},{"begin":"2023-03-12T15:49:19-06:00","end":"2023-03-12T15:59:42-06:00","duration":623},{"begin":"2023-03-12T16:00:06-06:00","end":"2023-03-12T16:07:04-06:00","duration":417},{"begin":"2023-03-12T16:07:33-06:00","end":"2023-03-12T16:13:28-06:00","duration":354},{"begin":"2023-03-12T16:17:33-06:00","end":"2023-03-12T16:36:24-06:00","duration":1131},{"begin":"2023-03-12T16:38:20-06:00","end":"2023-03-12T17:24:24-06:00","duration":2764},{"begin":"2023-03-12T17:25:18-06:00","end":"2023-03-12T17:27:19-06:00","duration":121},{"begin":"2023-03-12T18:08:42-06:00","end":"2023-03-12T18:08:57-06:00","duration":15},{"begin":"2023-03-12T18:09:00-06:00","end":"2023-03-12T18:11:02-06:00","duration":122},{"begin":"2023-03-12T19:14:25-06:00","end":"2023-03-12T19:17:15-06:00","duration":170},{"begin":"2023-03-12T19:18:33-06:00","end":"2023-03-12T19:20:50-06:00","duration":137},{"begin":"2023-03-12T19:21:10-06:00","end":"2023-03-12T19:24:03-06:00","duration":173},{"begin":"2023-03-12T19:25:08-06:00","end":"2023-03-12T19:33:11-06:00","duration":483},{"begin":"2023-03-12T19:35:37-06:00","end":"2023-03-12T19:37:53-06:00","duration":135},{"begin":"2023-03-12T19:39:51-06:00","end":"2023-03-12T19:41:53-06:00","duration":122},{"begin":"2023-03-12T19:48:10-06:00","end":"2023-03-12T20:11:58-06:00","duration":1427}]} \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..55af566 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,25 @@ +cmake_minimum_required(VERSION 3.14) +project(SSTV-Image-Tools VERSION 0.4) +set(CMAKE_CXX_STANDARD 20) + +set(CMAKE_BUILD_TYPE Debug) # Change to Release for production +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} \ + -fno-omit-frame-pointer \ + -pedantic \ + -Wall \ + -Wextra \ + -Weffc++ \ + -Wdisabled-optimization \ + -Wno-unused-variable") + +include_directories(src) + +add_executable(example + example/example.cpp + src/sstv-image-tools.cpp +) +add_definitions(-DMAGICKCORE_QUANTUM_DEPTH=8) +add_definitions(-DMAGICKCORE_HDRI_ENABLE=1) # Required or there are linking errors with Magick::Color::Color +find_package(ImageMagick COMPONENTS Magick++) +include_directories(${ImageMagick_INCLUDE_DIRS}) +target_link_libraries(example ${ImageMagick_LIBRARIES}) \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..a2b305b --- /dev/null +++ b/README.md @@ -0,0 +1,33 @@ +# SSTV Image Tools C++ Library + +Currently in development but it's functional. Will be used with the MWAV library to create SSTV images. + +A simple library for manipulating images into common SSTV image formats using Magick++. + +Basic Features: +- Overlay call sign and ~~message text~~ +- Convert the image to the proper color space +- Get the values for individual pixels +- ~~Add Data to the image~~ + +*** + +Supported SSTV Image Formats: +- Robot24 +- Robot36 +- Robot72 +- ~~Robot8 B/W~~ +- ~~Robot16 B/W~~ + +Tested with jpeg, and png images, support for other Magick++ supported formats has not been tested. + +Magick++ (7.1) with ``libjpeg62-dev``, ``libpng-dev``, ``libfreetype6-dev`` is required. + +*** + +Example Image Sources: +|Image File|Source|License| +|:--------:|:----:|:-----:| +|`example/test1.png`|[Wikipedia](https://en.wikipedia.org/wiki/File:Philips_PM5544.svg)|GNU 1.2| +|`example/test2.png`|[Wikipedia](https://en.wikipedia.org/wiki/File:TwibrightLinksTestCard.png)|GNU 1.2| +|`example/test3.jpg`|[Wikipedia](https://en.wikipedia.org/wiki/File:SMPTE_Color_Bars.svg)|Public Domain| diff --git a/example/converted_test1.png b/example/converted_test1.png new file mode 100644 index 0000000..8527cd9 Binary files /dev/null and b/example/converted_test1.png differ diff --git a/example/converted_test2.png b/example/converted_test2.png new file mode 100644 index 0000000..41055c8 Binary files /dev/null and b/example/converted_test2.png differ diff --git a/example/converted_test3.jpg b/example/converted_test3.jpg new file mode 100644 index 0000000..e94db1f Binary files /dev/null and b/example/converted_test3.jpg differ diff --git a/example/example.cpp b/example/example.cpp new file mode 100644 index 0000000..c93ab49 --- /dev/null +++ b/example/example.cpp @@ -0,0 +1,27 @@ +#include +#include +#include + +#include "sstv-image-tools.h" + +int main() { + std::array test_images = {"test1.png", "test2.png", + "test3.jpg"}; + + for (auto &image_path : test_images) { + SstvImage image(SstvImage::Mode::ROBOT_36_COLOR, image_path, + "converted_" + image_path); + image.AddCallSign("N0CALL"); + + SstvImage::Pixel pixel; + if (!image.GetPixel(128, 91, pixel)) { + std::cout << "Failed to get pixel" << std::endl; + } + + std::cout << (int)pixel.r << " " << (int)pixel.g << " " << (int)pixel.b + << std::endl; + + image.Write(); + } + return 0; +} \ No newline at end of file diff --git a/example/test1.png b/example/test1.png new file mode 100644 index 0000000..45e137b Binary files /dev/null and b/example/test1.png differ diff --git a/example/test2.png b/example/test2.png new file mode 100644 index 0000000..5e8fd33 Binary files /dev/null and b/example/test2.png differ diff --git a/example/test3.jpg b/example/test3.jpg new file mode 100644 index 0000000..fb269d9 Binary files /dev/null and b/example/test3.jpg differ diff --git a/src/sstv-image-tools.cpp b/src/sstv-image-tools.cpp new file mode 100644 index 0000000..d8526c3 --- /dev/null +++ b/src/sstv-image-tools.cpp @@ -0,0 +1,195 @@ +#include "sstv-image-tools.h" + +#include +#include +#include + +SstvImage::Color::Color(int red, int green, int blue) { + this->r = red; + this->g = green; + this->b = blue; +} + +SstvImage::SstvImage(SstvImage::Mode mode, std::string source_image_path, + std::string destination_image_path, bool crop) + : mode_(mode), crop_(crop) { + source_path_ = source_image_path; + if (destination_image_path == "") { + destination_path_ = source_image_path; + } else { + destination_path_ = destination_image_path; + } + try { + image_.read(source_image_path); + Scale(); + } catch (Magick::Exception &error_) { + throw SstvImageToolsException("Failed to read image: " + source_image_path); + } +} + +void SstvImage::Write() { + try { + image_.write(destination_path_); + } catch (Magick::Exception &error_) { + throw SstvImageToolsException("Failed to write image: " + + destination_path_); + } +} + +void SstvImage::AddCallSign(const std::string &callsign, + const SstvImage::Color &color) { + (void)color; + + int font_size = 20 * height_scaler_; + + Magick::DrawableFont font("Arial-Bold"); + Magick::DrawablePointSize point_size(font_size); + Magick::DrawableText text(0, 0 + font_size, callsign); + Magick::DrawableStrokeColor stroke_color("red"); + Magick::DrawableFillColor fill("green"); + + std::vector draw_list({font, point_size, text, stroke_color, + fill}); + + image_.draw(draw_list); +} + +bool SstvImage::GetPixel(int x, int y, SstvImage::Pixel &pixel) { + if (x < 0 || x >= width_ || y < 0 || y >= height_) { + return false; + } + + MagickCore::Quantum *pixels = image_.getPixels(0, 0, width_, height_); + unsigned index = (y * width_ + x) * image_.channels(); + + pixel.r = pixels[index]; + pixel.g = pixels[index + 1]; + pixel.b = pixels[index + 2]; + return true; +} + +void SstvImage::Scale() { + width_ = 0; + height_ = 0; + int scale_factor = 1; + switch (mode_) { + case SstvImage::Mode::ROBOT_8_BW: + case SstvImage::Mode::ROBOT_12_BW: + case SstvImage::Mode::ROBOT_12_COLOR: + width_ = 160; + height_ = 120; + scale_factor = 1.2; + break; + case SstvImage::Mode::ROBOT_24_BW: + case SstvImage::Mode::ROBOT_36_BW: + case SstvImage::Mode::ROBOT_24_COLOR: + case SstvImage::Mode::ROBOT_36_COLOR: + width_ = 320; + height_ = 240; + scale_factor = 1.0; + break; + case SstvImage::Mode::ROBOT_72_COLOR: + width_ = 640; + height_ = 480; + break; + default: + throw SstvImageToolsException("Not yet implemented"); + } + + height_scaler_ = (height_ / 100.0) * scale_factor; + + Magick::Geometry crop_size(width_, height_); + if (crop_) { + crop_size.fillArea(true); + image_.resize(crop_size); + image_.extent(crop_size, Magick::CenterGravity); + + + } else { + crop_size.aspect(true); + image_.resize(crop_size); + } +} + + + + + + + + + + + + + + + + + + +bool ConvertToRobot8(std::string image_path) { + Magick::Image image; + Magick::Geometry crop_size(160, 120); + crop_size.aspect(true); + try { + image.read(image_path); + image.scale(crop_size); + image.quantizeColorSpace(Magick::GRAYColorspace); + image.quantizeColors(8); + image.quantize(); + image.write(image_path + ".r8.png"); + } catch (Magick::Exception &error_) { + std::cout << "Caught exception: " << error_.what() << std::endl; + return 1; + } + return 0; +} + +bool ConvertToRobot36(std::string image_path) { + Magick::Image image; + Magick::Geometry crop_size(320, 240); + crop_size.aspect(true); + try { + image.read(image_path); + image.scale(crop_size); + image.quantizeColorSpace(Magick::YUVColorspace); + image.quantize(); + image.write(image_path + ".r36.png"); + } catch (Magick::Exception &error_) { + std::cout << "Caught exception: " << error_.what() << std::endl; + return 1; + } + return 0; +} + +bool ConvertToCustom8(std::string image_path) { + Magick::Image image; + try { + image.read(image_path); + Magick::Geometry crop_size(400, 400); + crop_size.aspect(true); + image.scale(crop_size); + image.crop(crop_size); + image.quantizeColorSpace(Magick::GRAYColorspace); + image.quantizeColors(200); + image.quantize(); + image.write(image_path + ".c8.png"); + } catch (Magick::Exception &error_) { + std::cout << "exception: " << error_.what() << std::endl; + return 1; + } + return 0; +} + +bool Pixels(std::string image_path) { + Magick::Image image; + try { + image.read(image_path); + Magick::Pixels view(image); + } catch (Magick::Exception &error_) { + std::cout << "exception: " << error_.what() << std::endl; + return 1; + } + return 0; +} \ No newline at end of file diff --git a/src/sstv-image-tools.h b/src/sstv-image-tools.h new file mode 100644 index 0000000..990b5f6 --- /dev/null +++ b/src/sstv-image-tools.h @@ -0,0 +1,92 @@ +#ifndef IMAGE_TOOLS_H_ +#define IMAGE_TOOLS_H_ + +#include + +#include +#include +#include + +class SstvImage { + public: + enum class Mode { + ROBOT_8_BW, // 160x120 (4:3) Black and White + ROBOT_12_BW, // 160x120 (4:3) Black and White + ROBOT_24_BW, // 320x240 (4:3) Black and White + ROBOT_36_BW, // 320x240 (4:3) Black and White + ROBOT_12_COLOR, // 160x120 (4:3) Color + ROBOT_24_COLOR, // 320x240 (4:3) Color + ROBOT_36_COLOR, // 320x240 (4:3) Color + ROBOT_72_COLOR, // 640x480 (4:3) Color + CUSTOM_TEST }; + + struct Color { + int r = -1; + int g = -1; + int b = -1; + float gray_scale_value = -1; + Color(int r, int g, int b); // 0 - 255 + Color(int gray_scale_value); // 0.0 - 1.0 + }; + + struct Pixel { + uint8_t r = -1; + uint8_t g = -1; + uint8_t b = -1; + }; + + /** + * @brief Construct a new SSTV Image Tools object + * + * @param source_image_path The path to the image to be converted + * @param destination_image_path *Optional* If not specified, the source image + * will be overwritten + * @exception SstvImageToolsException If the image cannot be read + */ + SstvImage(SstvImage::Mode mode, std::string source_image_path, + std::string destination_image_path = "", bool crop = true); + ~SstvImage() {} + + void Write(); + + void AddCallSign(const std::string &callsign, + const SstvImage::Color &color = {-2, -2, -2}); + void AddMessage(std::string message); + + bool GetPixel(int x, int y, SstvImage::Pixel &pixel); + int GetWidth() { return width_; } + int GetHeight() { return height_; } + + private: + void Scale(); + + std::pair WorkspaceToImageCoordinates(int x, int y); + + int width_ = 0; + int height_ = 0; + double height_scaler_ = 1; + + std::string source_path_ = ""; + std::string destination_path_ = ""; + Magick::Image image_ = Magick::Image(); + SstvImage::Mode mode_; + bool crop_; +}; + +class SstvImageToolsException : public std::exception { + public: + SstvImageToolsException(std::string message) : message_(message) {} + ~SstvImageToolsException() throw() {} + const char *what() const throw() { return message_.c_str(); } + + private: + std::string message_; +}; + +bool ConvertToRobot8(std::string image_path); +bool ConvertToRobot36(std::string image_path); +bool ConvertToCustom8(std::string image_path); + +bool Pixels(std::string image_path); + +#endif \ No newline at end of file