Compare commits

...

48 Commits

Author SHA1 Message Date
al 9e0d7a34cd update README with win32 build info 12 months ago
al 773734e6be added crossfile for win32 12 months ago
al 45ef16522d add license headers to source 1 year ago
al 3775fcfb1a add license to README 1 year ago
al f4beff5fc9 add license 1 year ago
al 476cd5f28a move shared functionality to WorkerPool 1 year ago
al aec946622c reduce complexity by getting queue ptrs from worker pools 1 year ago
al 53978fe62c update buildsystem to allow for cross-compilation (win32) 1 year ago
al 609997f878 remove old filtering code 1 year ago
al d8a45f7e36 move assets -> data 1 year ago
al 25e9e79159 update buildsystem to reflect changes to directory structure 1 year ago
al d7a438b14c move source files to src/ 1 year ago
al 1a6f619db6 bump cairomm version 1 year ago
al c069025ee6 correct email in books.rc 1 year ago
al 5b9dbbb286 bump version to 0.2.2 1 year ago
al 9f177afc05 change progress window message to follow GNOME HIG 1 year ago
al 10e22bdfaf add rejection threshold control; change autocrop -> crop 1 year ago
al b12e6ef392 use gtksnapshot instead of passing visual offsets to draw funcs 1 year ago
al cea91493db use more appropriate icon for settings; change margins 1 year ago
al fea2b31c15 fix ui hang while re-importing images 1 year ago
al 6b1fd2f0e3 dilate kernel size > 0 to avoid breaking opencv 1 year ago
al 0baa99402d slim down win32 distributions by only including needed icons 1 year ago
al cefc4dc4c5 remove unused method 1 year ago
al d5703a23fd fix gtkoverlay again 1 year ago
al b1852b8259 fix gtkoverlay blocking mouse events 1 year ago
al fdfc884cf3 use G_PI 1 year ago
al 58422814e6 add cmath.h 1 year ago
al 6abd14cfa7 add prev/next image preview overlay 1 year ago
al 391d304fef fix margin button style 1 year ago
al f889f0dfc2 re-add show feature functionality 1 year ago
al b2f0db9b90 pass crop info to image exporter 1 year ago
al 6641250cc2 fix miscalculation of crop bounds 1 year ago
al 116efb461a construct box from Gdk::Rectangle 1 year ago
al ffeb1beb1d re-add cropping functionality to support custom tweaks 1 year ago
al fa8a5423ef implement manual tweaking 1 year ago
al 550db09287 use std::move to avoid copying shared_ptrs 1 year ago
al 72a7dd0e1c import windows module 1 year ago
al c28a56a3e1 compile windows resources 1 year ago
al cf3f6b8658 add stuff for building windows resources 1 year ago
al 91fae55dd3 add application icons 1 year ago
al 1cb3fdb21f grammar 1 year ago
al f42ef02b39 tweak headerbar buttons 1 year ago
al 1fd4478521 fix memory consumption in ImageData by adding a destructor to explicitly unload data 1 year ago
al 307d0be80c UI overhaul 1 year ago
al a792c7e06d removed unused source files 1 year ago
al 72e3c72374 add filter parameters to ui 1 year ago
al 6ce7283502 use native file selector 1 year ago
al 02679b6a9c avoid crash when no features are detected in an image 1 year ago
  1. 1
      .gitignore
  2. 7
      LICENSE.md
  3. 69
      README.md
  4. 200
      assets/ui/MainWindow.ui
  5. 19
      books.cpp
  6. 102
      crop.cpp
  7. 24
      crop.hpp
  8. BIN
      data/icons/org.sugol.books-16.png
  9. BIN
      data/icons/org.sugol.books-256.png
  10. BIN
      data/icons/org.sugol.books-32.png
  11. 16
      data/icons/org.sugol.books.svg
  12. 0
      data/ui-resources.xml
  13. 0
      data/ui/MainWindow.glade
  14. 318
      data/ui/MainWindow.ui
  15. 8
      data/ui/ProgressWindow.ui
  16. BIN
      data/win32/books.ico
  17. 24
      data/win32/books.rc
  18. 22
      data/win32/make-icon.sh
  19. 30
      featuredetector.hpp
  20. 63
      featuredetectorpool.cpp
  21. 32
      featuredetectorpool.hpp
  22. 13
      filter/bgr2grey.cpp
  23. 11
      filter/bgr2grey.hpp
  24. 25
      filter/canny.cpp
  25. 13
      filter/canny.hpp
  26. 17
      filter/dilate.cpp
  27. 15
      filter/dilate.hpp
  28. 11
      filter/filter.hpp
  29. 34
      filter/filterchain.cpp
  30. 19
      filter/filterchain.hpp
  31. 8
      filter/filters_all.hpp
  32. 16
      filter/medianblur.cpp
  33. 15
      filter/medianblur.hpp
  34. 13
      filter/normalise.cpp
  35. 11
      filter/normalise.hpp
  36. 11
      filter/threshold.cpp
  37. 11
      filter/threshold.hpp
  38. 16
      fitnessmetrics.hpp
  39. 59
      imageexporter.cpp
  40. 21
      imageexporter.hpp
  41. 54
      imageexporterpool.cpp
  42. 31
      imageexporterpool.hpp
  43. 27
      imageloader.cpp
  44. 14
      imageloader.hpp
  45. 57
      imageloaderpool.cpp
  46. 29
      imageloaderpool.hpp
  47. 112
      imagepreview.cpp
  48. 37
      imagepreview.hpp
  49. 37
      imagestore.cpp
  50. 21
      imagestore.hpp
  51. 312
      mainwindow.cpp
  52. 102
      mainwindow.hpp
  53. 53
      meson.build
  54. 35
      progresswindow.cpp
  55. 23
      progresswindow.hpp
  56. 43
      scripts/dist-win32.sh
  57. 40
      src/books.cpp
  58. 70
      src/crophandle.cpp
  59. 95
      src/crophandle.hpp
  60. 106
      src/croprect.cpp
  61. 41
      src/croprect.hpp
  62. 61
      src/featuredetector.cpp
  63. 54
      src/featuredetector.hpp
  64. 39
      src/featuredetectorparams.hpp
  65. 44
      src/featuredetectorpool.cpp
  66. 42
      src/featuredetectorpool.hpp
  67. 21
      src/fitnessmetrics.cpp
  68. 37
      src/fitnessmetrics.hpp
  69. 34
      src/imagedata.cpp
  70. 36
      src/imagedata.hpp
  71. 66
      src/imageexporter.cpp
  72. 39
      src/imageexporter.hpp
  73. 38
      src/imageexporterpool.cpp
  74. 35
      src/imageexporterpool.hpp
  75. 44
      src/imageloader.cpp
  76. 32
      src/imageloader.hpp
  77. 39
      src/imageloaderpool.cpp
  78. 34
      src/imageloaderpool.hpp
  79. 301
      src/imagepreview.cpp
  80. 68
      src/imagepreview.hpp
  81. 58
      src/imagestore.cpp
  82. 42
      src/imagestore.hpp
  83. 477
      src/mainwindow.cpp
  84. 146
      src/mainwindow.hpp
  85. 36
      src/meson.build
  86. 86
      src/progresswindow.cpp
  87. 49
      src/progresswindow.hpp
  88. 40
      src/util/box.cpp
  89. 52
      src/util/box.hpp
  90. 41
      src/util/point.hpp
  91. 49
      src/worker.cpp
  92. 56
      src/worker.hpp
  93. 21
      src/workerpool.cpp
  94. 162
      src/workerpool.hpp
  95. 59
      src/workqueue.cpp
  96. 29
      src/workqueue.hpp
  97. 27
      util/box.hpp
  98. 20
      util/point.hpp
  99. 31
      win32-cross.txt
  100. 28
      worker.cpp
  101. Some files were not shown because too many files have changed in this diff Show More

1
.gitignore vendored

@ -1,6 +1,5 @@
build/
builddir/
data/
output/
*.code-workspace
.cache

@ -0,0 +1,7 @@
Copyright © 2021 Alister Sanders (alister@sugol.org)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

@ -1,8 +1,20 @@
# books
Automatic cropping tool for cropping to books
An intelligent bulk cropping tool
## Build
## Download
Linux and Windows are both supported.
Windows binaries are available [here](https://git.sugol.org/al/books/releases).
## Building from Source
### Dependencies
- opencv4
- gtkmm-4.0
- cairomm-1.16
- meson
- GCC or clang. C++11 support required
### Linux
@ -12,10 +24,59 @@ This project uses the meson build system:
$ meson build
$ cd build
$ ninja
```
And install with
```
$ sudo ninja install
```
### Windows
Since this project uses GTK4, it's a bit difficult here (there's no GTK4 installer for Win32 just yet). However, I have built using cygwin & it seems to work ok.
There are a couple of options for building Windows binaries. Either you can
compile natively using `msys2`, or cross-compile from Linux.
*MSVC is unsupported*
#### Native
Install `msys2` and build as you would on Linux
#### Cross-compilation
A cross-file is included for building Windows binaries on Linux
(`win32-cross.txt`). This file may need tweaking depending on how your `mingw`
installation is configured. Make sure to install the relevant mingw
dependencies.
Build as you would for Linux, but substitute the first step with `meson setup
build --cross-file=win32-cross.txt`.
Good luck!
#### Packaging
A script for packaging Windows builds is included under `scripts/dist-win32.sh`.
This should work with either `msys2` or a cross-compiled build, but you may need
to tweak the `MINGW` variable to match your mingw installation path.
The script will package the compiled executable, `.dll`s and icons into a zip
archive for distribution.
## License
Copyright © 2021 Alister Sanders (alister@sugol.org)
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the “Software”), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
TODO: Actual build instructions
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

@ -1,200 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk+" version="3.24"/>
<object class="GtkApplicationWindow" id="mainWindow">
<property name="name">mainWindow</property>
<property name="title" translatable="yes">Books</property>
<property name="default-width">1200</property>
<property name="default-height">800</property>
<child>
<object class="GtkPaned" id="content">
<property name="shrink-start-child">0</property>
<child>
<object class="GtkBox">
<property name="width-request">300</property>
<property name="margin-start">8</property>
<property name="margin-end">8</property>
<property name="margin-top">16</property>
<property name="margin-bottom">16</property>
<property name="orientation">vertical</property>
<property name="spacing">8</property>
<child>
<object class="GtkScrolledWindow">
<property name="vexpand">1</property>
<property name="propagate-natural-width">1</property>
<child>
<object class="GtkViewport">
<child>
<object class="GtkTreeView" id="filesTreeView">
<property name="activate-on-single-click">1</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox">
<property name="can-focus">0</property>
<property name="orientation">vertical</property>
<property name="spacing">8</property>
<child>
<object class="GtkGrid">
<property name="can-focus">0</property>
<property name="row-spacing">6</property>
<property name="column-spacing">6</property>
<child>
<object class="GtkLabel">
<property name="can-focus">0</property>
<property name="halign">start</property>
<property name="hexpand">1</property>
<property name="label" translatable="yes">Image directory</property>
<layout>
<property name="column">0</property>
<property name="row">0</property>
</layout>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="can-focus">0</property>
<property name="halign">start</property>
<property name="hexpand">1</property>
<property name="label" translatable="yes">Export directory</property>
<layout>
<property name="column">0</property>
<property name="row">1</property>
</layout>
</object>
</child>
<child>
<object class="GtkButton" id="btnExportDirectory">
<property name="label" translatable="yes">(None)</property>
<property name="receives-default">1</property>
<property name="hexpand">1</property>
<layout>
<property name="column">1</property>
<property name="row">1</property>
</layout>
</object>
</child>
<child>
<object class="GtkButton" id="btnImageDirectory">
<property name="label" translatable="yes">(None)</property>
<property name="receives-default">1</property>
<property name="hexpand">1</property>
<layout>
<property name="column">1</property>
<property name="row">0</property>
</layout>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="label">Margins</property>
<property name="can-focus">0</property>
<property name="halign">start</property>
<property name="hexpand">1</property>
<layout>
<property name="column">0</property>
<property name="row">2</property>
</layout>
</object>
</child>
<child>
<object class="GtkScale" id="scaleMargins">
<property name="hexpand">1</property>
<property name="adjustment">
<object class="GtkAdjustment">
<property name="lower">-50</property>
<property name="upper">200</property>
<property name="value">0</property>
</object>
</property>
<layout>
<property name="column">1</property>
<property name="row">2</property>
</layout>
</object>
</child>
</object>
</child>
<child>
<object class="GtkExpander">
<property name="label">Advanced</property>
<child>
<object class="GtkBox">
<property name="margin-start">8</property>
<property name="margin-end">8</property>
<property name="margin-top">16</property>
<property name="margin-bottom">16</property>
<property name="orientation">vertical</property>
<property name="spacing">8</property>
<child>
<object class="GtkCheckButton" id="chkShowFeatures">
<property name="label">Display features</property>
</object>
</child>
<child>
<object class="GtkCheckButton" id="chkShowFitness">
<property name="label">Display fitness scores</property>
<property name="sensitive">0</property>
</object>
</child>
<child>
<object class="GtkBox">
<property name="visible">0</property>
<property name="orientation">horizontal</property>
<child>
<object class="GtkLabel">
<property name="label">Show filter step</property>
<property name="hexpand">1</property>
<property name="halign">start</property>
</object>
</child>
<child>
<object class="GtkSpinButton" id="spinBtnLayer">
<property name="adjustment">
<object class="GtkAdjustment">
<property name="lower">-1</property>
<property name="upper">10</property>
<property name="step-increment">1</property>
</object>
</property>
<property name="value">-1</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkButton" id="btnExport">
<property name="label" translatable="yes">Export</property>
<property name="receives-default">1</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkScrolledWindow">
<child>
<object class="GtkViewport">
<property name="can-focus">0</property>
<child>
<object class="GtkDrawingArea" id="preview">
<property name="can-focus">0</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</interface>

@ -1,19 +0,0 @@
#include <gtkmm.h>
#include <mainwindow.hpp>
#include <ui-resources.h>
static void on_startup(Glib::RefPtr<Gtk::Application> app) {
ui::MainWindow* window = ui::MainWindow::create();
app->add_window(*window);
window->show();
}
int main(int argc, char *argv[]) {
auto resource_bundle = Glib::wrap(ui_res_get_resource());
resource_bundle->register_global();
auto app = Gtk::Application::create("org.sugol.books");
app->signal_startup().connect([&]() { on_startup(app); });
return app->run(argc, argv);
}

@ -1,102 +0,0 @@
#include <vector>
#include <algorithm>
#include <opencv2/objdetect.hpp>
#include <util/box.hpp>
#include <crop.hpp>
Cropper::Cropper() {}
cv::Rect Cropper::auto_crop(CVImage& image, int margin, bool remove_whiteboard, int kernel_size) {
float image_area = image.total();
cv::Mat edges = m_filterchain.apply_filters(image);
std::vector<std::vector<cv::Point>> contours;
cv::findContours(edges, contours, cv::RETR_TREE, cv::CHAIN_APPROX_SIMPLE);
std::vector<cv::Rect> bounding_boxes;
for (auto c : contours) {
cv::Rect box = cv::boundingRect(c);
bounding_boxes.push_back(box);
// if (box.area() / image_area >= MIN_RELATIVE_BOX_SIZE) {
// bounding_boxes.push_back(box);
// }
}
if (bounding_boxes.size() == 0) {
return cv::Rect(0, 0, 0, 0);
}
cv::groupRectangles(bounding_boxes, 0, 10);
util::Point image_center = {(unsigned int)image.size().width / 2, (unsigned int)image.size().height / 2};
util::Box midbox = bounding_boxes[0];
// Find the box that is closest to the image center
for (util::Box box : bounding_boxes) {
if (image_center.distance_to(box.midpoint()) < image_center.distance_to(midbox.midpoint())) {
midbox = box;
}
}
return midbox;
// if (remove_whiteboard) {
// if (bounding_boxes.size() == 1) {
// return cv::Rect(0, 0, 0, 0);
// }
// // Sort bounding boxes by x-position
// std::sort(bounding_boxes.begin(), bounding_boxes.end(), [](cv::Rect r1, cv::Rect r2) {
// return r1.x < r2.x;
// });
// // Remove the first bounding box (the whiteboard)
// bounding_boxes.erase(bounding_boxes.begin());
// }
// for (auto b : bounding_boxes) {
// cv::Point pt1(b.x, b.y);
// cv::Point pt2(b.x + b.width, b.y + b.height);
// cv::rectangle(image, pt1, pt2, cv::Scalar(0, 255, 0), 2);
// }
// Get the enclosing box
// cv::Rect enclosing = enclose_bounding_boxes(bounding_boxes);
// // Pad out the enclosing box with a margin
// enclosing.x -= margin; enclosing.x = std::max(enclosing.x, 0);
// enclosing.y -= margin; enclosing.y = std::max(enclosing.y, 0);
// enclosing.width += margin * 2;
// enclosing.width = std::min(enclosing.width, image.size().width - enclosing.x);
// enclosing.height += margin * 2;
// enclosing.height = std::min(enclosing.height, image.size().height - enclosing.y);
//
}
filter::FilterChain& Cropper::get_filterchain() {
return m_filterchain;
}
cv::Rect Cropper::enclose_bounding_boxes(std::vector<util::Box> boxes) {
std::vector<int> x_vals;
std::vector<int> y_vals;
for (auto& box : boxes) {
x_vals.push_back(box.top_left().x);
x_vals.push_back(box.bottom_right().x);
y_vals.push_back(box.top_left().y);
y_vals.push_back(box.bottom_right().y);
}
// TODO: Sort the two lists beforehand so we don't have to use
// min/max_element
int x1 = *std::min_element(x_vals.begin(), x_vals.end());
int y1 = *std::min_element(y_vals.begin(), y_vals.end());
int x2 = *std::max_element(x_vals.begin(), x_vals.end());
int y2 = *std::max_element(y_vals.begin(), y_vals.end());
return cv::Rect(x1, y1, x2 - x1, y2 - y1);
}

@ -1,24 +0,0 @@
#pragma once
#define MIN_RELATIVE_BOX_SIZE 0.03
#include <util/box.hpp>
#include <filter/filterchain.hpp>
class Cropper {
public:
Cropper();
cv::Rect auto_crop(cv::Mat& image,
int margin = 32,
bool remove_whiteboard = true,
int kernel_size = 11);
filter::FilterChain& get_filterchain();
private:
cv::Rect enclose_bounding_boxes(std::vector<util::Box> boxes);
private:
filter::FilterChain m_filterchain;
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 522 B

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="128px" viewBox="0 0 128 128" width="128px" xmlns="http://www.w3.org/2000/svg">
<path d="m 29.617188 17.828125 h 68.765624 c 3.101563 0 5.617188 2.113281 5.617188 4.722656 v 86.726563 c 0 2.609375 -2.515625 4.722656 -5.617188 4.722656 h -68.765624 c -3.101563 0 -5.617188 -2.113281 -5.617188 -4.722656 v -86.726563 c 0 -2.609375 2.515625 -4.722656 5.617188 -4.722656 z m 0 0" fill="#1a5fb4"/>
<path d="m 24 17.304688 h 10 v 96.695312 h -10 z m 0 0" fill="#3d3846"/>
<path d="m 29.648438 15.203125 h 67.453124 c 3.121094 0 5.648438 2.128906 5.648438 4.75 v 87.195313 c 0 2.621093 -2.527344 4.75 -5.648438 4.75 h -67.453124 c -3.121094 0 -5.648438 -2.128907 -5.648438 -4.75 v -87.195313 c 0 -2.621094 2.527344 -4.75 5.648438 -4.75 z m 0 0" fill="#deddda"/>
<path d="m 24.035156 107.695312 h 9.964844 v 4.203126 h -9.964844 z m 0 0" fill="#deddda"/>
<path d="m 24 17.304688 h 2.5 v 96.695312 h -2.5 z m 0 0" fill="#3d3846"/>
<path d="m 29.648438 11 h 68.703124 c 3.121094 0 5.648438 2.125 5.648438 4.75 v 87.195312 c 0 2.621094 -2.527344 4.75 -5.648438 4.75 h -68.703124 c -3.121094 0 -5.648438 -2.128906 -5.648438 -4.75 v -87.195312 c 0 -2.625 2.527344 -4.75 5.648438 -4.75 z m 0 0" fill="#1a5fb4"/>
<path d="m 29.527344 11 h 68.945312 c 3.054688 0 5.527344 2.082031 5.527344 4.648438 v 85.296874 c 0 2.566407 -2.472656 4.648438 -5.527344 4.648438 h -68.945312 c -3.054688 0 -5.527344 -2.082031 -5.527344 -4.648438 v -85.296874 c 0 -2.566407 2.472656 -4.648438 5.527344 -4.648438 z m 0 0" fill="#3584e4"/>
<path d="m 24 11 h 10 v 96.695312 h -10 z m 0 0" fill="#3d3846"/>
<path d="m 24 11 h 10 v 94.59375 h -10 z m 0 0" fill="#5e5c64"/>
<path d="m 42 2 h -28 v 30" fill="none" stroke="#deddda" stroke-linecap="round" stroke-linejoin="round" stroke-width="3.26929"/>
<path d="m 43 40 h 50 c 0.550781 0 1 0.449219 1 1 v 54 c 0 0.550781 -0.449219 1 -1 1 h -50 c -0.550781 0 -1 -0.449219 -1 -1 v -54 c 0 -0.550781 0.449219 -1 1 -1 z m 0 0" fill="#1c71d8"/>
<path d="m 43 22 h 50 c 0.550781 0 1 0.449219 1 1 v 8 c 0 0.550781 -0.449219 1 -1 1 h -50 c -0.550781 0 -1 -0.449219 -1 -1 v -8 c 0 -0.550781 0.449219 -1 1 -1 z m 0 0" fill="#1c71d8"/>
<path d="m 86 124 h 28 v -30" fill="none" stroke="#deddda" stroke-linecap="round" stroke-linejoin="round" stroke-width="3.26929"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

@ -0,0 +1,318 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk+" version="4.0"/>
<object class="GtkApplicationWindow" id="mainWindow">
<property name="name">mainWindow</property>
<property name="title" translatable="yes">Books</property>
<property name="default-width">1200</property>
<property name="default-height">800</property>
<child type="titlebar">
<object class="GtkHeaderBar">
<child type="end">
<object class="GtkMenuButton">
<property name="icon-name">open-menu-symbolic</property>
<property name="popover">
<object class="GtkPopover">
<property name="width-request">300</property>
<property name="halign">end</property>
<child>
<object class="GtkGrid">
<property name="can-focus">0</property>
<property name="margin-top">8</property>
<property name="margin-start">8</property>
<property name="margin-bottom">8</property>
<property name="margin-end">8</property>
<child>
<object class="GtkLabel">
<property name="label">Margins</property>
<property name="halign">start</property>
<property name="hexpand">1</property>
<layout>
<property name="column">0</property>
<property name="row">0</property>
</layout>
</object>
</child>
<child>
<object class="GtkBox">
<style>
<class name="linked" />
</style>
<property name="halign">end</property>
<property name="orientation">horizontal</property>
<child>
<object class="GtkButton" id="btnMarginAdd">
<property name="icon-name">list-add-symbolic</property>
</object>
</child>
<child>
<object class="GtkButton" id="btnMarginSubtract">
<property name="icon-name">list-remove-symbolic</property>
</object>
</child>
<layout>
<property name="column">1</property>
<property name="row">0</property>
</layout>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="label">Rejection threshold</property>
<property name="halign">start</property>
<property name="hexpand">1</property>
<layout>
<property name="column">0</property>
<property name="row">1</property>
</layout>
</object>
</child>
<child>
<object class="GtkScale" id="scaleRejection">
<property name="adjustment">
<object class="GtkAdjustment">
<property name="lower">0</property>
<property name="upper">1</property>
<property name="value">0.1</property>
</object>
</property>
<property name="digits">2</property>
<property name="draw-value">1</property>
<property name="hexpand">1</property>
<layout>
<property name="column">1</property>
<property name="row">1</property>
</layout>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="label">Display features</property>
<property name="halign">start</property>
<property name="hexpand">1</property>
<layout>
<property name="column">0</property>
<property name="row">2</property>
</layout>
</object>
</child>
<child>
<object class="GtkSwitch" id="switchShowFeatures">
<property name="halign">end</property>
<property name="hexpand">0</property>
<layout>
<property name="column">1</property>
<property name="row">2</property>
</layout>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="label">Display fitness score</property>
<property name="halign">start</property>
<property name="hexpand">1</property>
<layout>
<property name="column">0</property>
<property name="row">3</property>
</layout>
</object>
</child>
<child>
<object class="GtkSwitch" id="switchShowFitness">
<property name="halign">end</property>
<property name="hexpand">0</property>
<property name="sensitive">0</property>
<layout>
<property name="column">1</property>
<property name="row">3</property>
</layout>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="label">Blur kernel size</property>
<property name="can-focus">0</property>
<property name="halign">start</property>
<property name="hexpand">1</property>
<layout>
<property name="column">0</property>
<property name="row">4</property>
</layout>
</object>
</child>
<child>
<object class="GtkSpinButton" id="spinBlurKernel">
<property name="adjustment">
<object class="GtkAdjustment">
<property name="lower">1</property>
<property name="upper">49</property>
<property name="step-increment">2</property>
<property name="value">21</property>
</object>
</property>
<layout>
<property name="column">1</property>
<property name="row">4</property>
</layout>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="label">Dilate kernel size</property>
<property name="can-focus">0</property>
<property name="halign">start</property>
<property name="hexpand">1</property>
<layout>
<property name="column">0</property>
<property name="row">5</property>
</layout>
</object>
</child>
<child>
<object class="GtkSpinButton" id="spinDilateKernel">
<property name="adjustment">
<object class="GtkAdjustment">
<property name="lower">1</property>
<property name="upper">50</property>
<property name="step-increment">1</property>
<property name="value">16</property>
</object>
</property>
<layout>
<property name="column">1</property>
<property name="row">5</property>
</layout>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="label">Threshold</property>
<property name="can-focus">0</property>
<property name="halign">start</property>
<property name="hexpand">1</property>
<layout>
<property name="column">0</property>
<property name="row">6</property>
</layout>
</object>
</child>
<child>
<object class="GtkSpinButton" id="spinThreshold">
<property name="adjustment">
<object class="GtkAdjustment">
<property name="lower">0</property>
<property name="upper">255</property>
<property name="step-increment">1</property>
<property name="value">70</property>
</object>
</property>
<layout>
<property name="column">1</property>
<property name="row">6</property>
</layout>
</object>
</child>
</object>
</child>
</object>
</property>
</object>
</child>
<child>
<object class="GtkButton" id="btnImport">
<style>
<class name="flat" />
</style>
<property name="icon-name">folder-new-symbolic</property>
<property name="tooltip-text">Add folder</property>
</object>
</child>
<child>
<object class="GtkButton" id="btnExport">
<style>
<class name="flat" />
</style>
<property name="icon-name">document-save-symbolic</property>
<property name="tooltip-text">Export</property>
</object>
</child>
<child>
<object class="GtkButton" id="btnReload">
<style>
<class name="flat" />
</style>
<property name="icon-name">view-refresh-symbolic</property>
<property name="tooltip-text">Reload</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkPaned" id="content">
<child>
<object class="GtkScrolledWindow">
<property name="width-request">300</property>
<child>
<object class="GtkTreeView" id="filesTreeView">
<property name="activate-on-single-click">1</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox">
<property name="hexpand">1</property>
<property name="width-request">300</property>
<property name="height-request">200</property>
<child>
<object class="GtkOverlay">
<property name="hexpand">1</property>
<child type="overlay">
<object class="GtkButton" id="btnPrevImage">
<property name="halign">start</property>
<property name="valign">center</property>
<property name="icon-name">go-previous-symbolic</property>
<style>
<class name="flat" />
</style>
</object>
</child>
<child type="overlay">
<object class="GtkButton" id="btnNextImage">
<property name="halign">end</property>
<property name="valign">center</property>
<property name="icon-name">go-next-symbolic</property>
<style>
<class name="flat" />
</style>
</object>
</child>
<child>
<object class="GtkPicture" id="preview">
<property name="can-focus">0</property>
<property name="halign">center</property>
<property name="hexpand">1</property>
<property name="vexpand">1</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</interface>

@ -5,17 +5,19 @@
<property name="can-focus">False</property>
<child>
<object class="GtkBox">
<property name="margin-top">16</property>
<property name="margin-start">16</property>
<property name="margin-bottom">16</property>
<property name="margin-end">16</property>
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkLabel" id="lblAction">
<property name="margin-bottom">8</property>
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Working...</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
</child>
<child>

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

@ -0,0 +1,24 @@
1 VERSIONINFO
FILEVERSION 1,0,0,0
PRODUCTVERSION 1,0,0,0
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904E4"
BEGIN
VALUE "CompanyName", "Sugol"
VALUE "FileDescription", "Books"
VALUE "FileVersion", "0.2.2"
VALUE "InternalName", "Books"
VALUE "LegalCopyright", "al (alister@sugol.org)"
VALUE "OriginalFilename", "books.exe"
VALUE "ProductName", "Books"
VALUE "ProductVersion", "0.2.2"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1252
END
END
id ICON "books.ico"

@ -0,0 +1,22 @@
#!/bin/bash
# Copyright (C) 2021 Alister Sanders
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
convert ../icons/org.sugol.books-16.png ../icons/org.sugol.books-32.png ../icons/org.sugol.books-256.png books.ico

@ -1,30 +0,0 @@
#pragma once
#include <fitnessmetrics.hpp>
#include <imagedata.hpp>
#include <worker.hpp>
namespace worker {
class FeatureDetector : public Worker<img::ImageData, img::ImageData> {
public:
const double CV_IMAGE_TARGET_DIMENSION = 400.0;
FeatureDetector(std::shared_ptr<InputQueue> input_queue,
std::shared_ptr<OutputQueue> output_queue,
const std::vector<ft::FitnessMetric>& fitness_metrics,
const std::vector<float>& fitness_metric_weights);
void run(IWorkerPool* wp) override;
private:
cv::UMat apply_filters(const cv::UMat& mat);
void find_features(img::ImageData& image_data);
double resize_mat(const cv::UMat& src, cv::UMat& dest);
float calculate_fitness(const cv::Mat& mat, const util::Box& box);
img::ImageData::Feature& best_candidate_box(std::vector<img::ImageData::Feature>& features);
private:
std::vector<ft::FitnessMetric> m_fitness_metrics;
std::vector<float> m_fitness_weights;
};
}

@ -1,63 +0,0 @@
#include <featuredetectorpool.hpp>
namespace worker {
FeatureDetectorPool::FeatureDetectorPool(size_t n_workers,
std::vector<ft::FitnessMetric> fitness_metrics,
std::vector<float> fitness_weights) {
m_active_workers = n_workers;
m_input_queue = std::make_shared<InputQueue>();
m_output_queue = std::make_shared<OutputQueue>();
for (size_t i = 0; i < n_workers; i++) {
m_workers.push_back(std::make_unique<FeatureDetector>(m_input_queue,
m_output_queue,
fitness_metrics,
fitness_weights));
}
}
FeatureDetectorPool::~FeatureDetectorPool() {
join_all();
}
std::shared_ptr<FeatureDetectorPool::InputQueue> FeatureDetectorPool::input() {
return m_input_queue;
}
std::shared_ptr<FeatureDetectorPool::OutputQueue> FeatureDetectorPool::output() {
return m_output_queue;
}
void FeatureDetectorPool::set_input(std::shared_ptr<InputQueue> in) {
m_input_queue = in;
for (auto& worker : m_workers) {
worker->set_input_queue(m_input_queue);
}
}
void FeatureDetectorPool::set_output(std::shared_ptr<OutputQueue> out) {
m_output_queue = out;
}
void FeatureDetectorPool::run_workers() {
for (auto& worker : m_workers) {
worker->run(this);
}
}
void FeatureDetectorPool::join_all() {
for (auto& worker : m_workers) {
worker->join();
}
}
void FeatureDetectorPool::signal_done() {
std::lock_guard lock(m_mutex);
m_active_workers--;
if (m_active_workers == 0) {
m_output_queue->finish();
}
}
}

@ -1,32 +0,0 @@
#pragma once
#include <featuredetector.hpp>
#include <workerpool.hpp>
namespace worker {
class FeatureDetectorPool : public WorkerPool<FeatureDetector> {
public:
FeatureDetectorPool() { }
FeatureDetectorPool(size_t n_workers,
std::vector<ft::FitnessMetric> fitness_metrics,
std::vector<float> fitness_weights);
~FeatureDetectorPool();
std::shared_ptr<InputQueue> input() override;
std::shared_ptr<OutputQueue> output() override;
void set_input(std::shared_ptr<InputQueue> in) override;
void set_output(std::shared_ptr<OutputQueue> out) override;
void run_workers() override;
void join_all() override;
void signal_done() override;
private:
std::shared_ptr<InputQueue> m_input_queue;
std::shared_ptr<OutputQueue> m_output_queue;
mutable std::mutex m_mutex;
size_t m_active_workers;
std::vector<std::unique_ptr<FeatureDetector>> m_workers;
};
}

@ -1,13 +0,0 @@
#include "opencv2/imgproc.hpp"
#include <filter/bgr2grey.hpp>
namespace filter {
BGR2GreyFilter::~BGR2GreyFilter() {}
cv::Mat BGR2GreyFilter::apply(const cv::Mat &img) {
cv::Mat img_grey;
cv::cvtColor(img, img_grey, cv::COLOR_BGR2GRAY);
return img_grey;
}
}

@ -1,11 +0,0 @@
#pragma once
#include <filter/filter.hpp>
namespace filter {
class BGR2GreyFilter : public Filter {
public:
~BGR2GreyFilter();
cv::Mat apply(cv::Mat const& img) override;
};
}

@ -1,25 +0,0 @@
#include "opencv2/imgproc.hpp"
#include <filter/canny.hpp>
namespace filter {
CannyFilter::~CannyFilter() {
}
cv::Mat CannyFilter::apply(const cv::Mat& img) {
double low_thres, high_thres;
get_thresholds(img, low_thres, high_thres);
cv::Mat canny;
cv::Canny(img, canny, low_thres, high_thres);
return canny;
}
void CannyFilter::get_thresholds(const cv::Mat& img, double& low, double& high) {
cv::Mat dummy;
// Automagically determine threshold values for the canny filter. Do you know how this works? I sure don't.
high = cv::threshold(img, dummy, 0, 255, cv::THRESH_BINARY | cv::THRESH_OTSU);
low = 0.5 * high;
}
}

@ -1,13 +0,0 @@
#pragma once
#include <filter/filter.hpp>
namespace filter {
class CannyFilter : public Filter {
public:
~CannyFilter();
cv::Mat apply(cv::Mat const& img) override;
private:
void get_thresholds(cv::Mat const& img, double& low, double& high);
};
}

@ -1,17 +0,0 @@
#include <filter/dilate.hpp>
namespace filter {
DilateFilter::DilateFilter(int kernel_size)
: m_kernelSize(kernel_size) { }
DilateFilter::~DilateFilter() {}
cv::Mat DilateFilter::apply(const cv::Mat &img) {
cv::Mat dilated;
cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(m_kernelSize, m_kernelSize));
cv::dilate(img, dilated, kernel);
return dilated;
}
}

@ -1,15 +0,0 @@
#pragma once
#include <filter/filter.hpp>
namespace filter {
class DilateFilter : public Filter {
public:
DilateFilter(int kernel_size = 8);
~DilateFilter();
cv::Mat apply(cv::Mat const& img) override;
private:
int m_kernelSize;
};
}

@ -1,11 +0,0 @@
#pragma once
#include <opencv2/imgproc.hpp>
namespace filter {
class Filter {
public:
virtual ~Filter() { };
virtual cv::Mat apply(cv::Mat const& img) = 0;
};
}

@ -1,34 +0,0 @@
#include "filter/canny.hpp"
#include "filter/medianblur.hpp"
#include <filter/filterchain.hpp>
#include <filter/filters_all.hpp>
namespace filter {
FilterChain::FilterChain() {
// Construct the default filter chain
// TODO: Allow for custom filter chains
m_filters.emplace_back(new BGR2GreyFilter);
m_filters.emplace_back(new MedianBlurFilter(11));
m_filters.emplace_back(new NormaliseFilter);
m_filters.emplace_back(new DilateFilter(4));
m_filters.emplace_back(new ThresholdFilter);
m_filters.emplace_back(new CannyFilter);
}
FilterChain::~FilterChain() {
for (auto filter : m_filters){
delete filter;
}
}
cv::Mat FilterChain::apply_filters(const cv::Mat& img) {
cv::Mat filter_result = img;
for (auto& filter : m_filters) {
filter_result = filter->apply(filter_result);
}
return filter_result;
}
}

@ -1,19 +0,0 @@
#pragma once
#include <opencv2/imgproc.hpp>
#include <vector>
#include <map>
#include <filter/filter.hpp>
namespace filter {
class FilterChain {
public:
FilterChain();
~FilterChain();
cv::Mat apply_filters(const cv::Mat& img);
private:
std::vector<Filter*> m_filters;
};
}

@ -1,8 +0,0 @@
#pragma once
#include <filter/bgr2grey.hpp>
#include <filter/medianblur.hpp>
#include <filter/normalise.hpp>
#include <filter/dilate.hpp>
#include <filter/threshold.hpp>
#include <filter/canny.hpp>

@ -1,16 +0,0 @@
#include "opencv2/imgproc.hpp"
#include <filter/medianblur.hpp>
namespace filter {
MedianBlurFilter::MedianBlurFilter(int kernel_size)
: m_kernel_size(kernel_size) { }
MedianBlurFilter::~MedianBlurFilter() {}
cv::Mat MedianBlurFilter::apply(const cv::Mat& img) {
cv::Mat blurred;
cv::medianBlur(img, blurred, m_kernel_size);
return blurred;
}
}

@ -1,15 +0,0 @@
#pragma once
#include <filter/filter.hpp>
namespace filter {
class MedianBlurFilter : public Filter {
public:
MedianBlurFilter(int kernel_size = 11);
~MedianBlurFilter();
cv::Mat apply(cv::Mat const& img) override;
private:
int m_kernel_size;
};
}

@ -1,13 +0,0 @@
#include "opencv2/core.hpp"
#include "opencv2/core/base.hpp"
#include <filter/normalise.hpp>
namespace filter {
NormaliseFilter::~NormaliseFilter() {}
cv::Mat NormaliseFilter::apply(const cv::Mat& img) {
cv::Mat normalised;
cv::normalize(img, normalised, 255, 0, cv::NORM_MINMAX);
return normalised;
}
}

@ -1,11 +0,0 @@
#pragma once
#include <filter/filter.hpp>
namespace filter {</