├── Makefile ├── blobmark.c ├── csv-blobs.c ├── debugstuff.c ├── quickblob.c ├── quickblob.h └── sample ├── lorem.png └── show-blobs.py /Makefile: -------------------------------------------------------------------------------- 1 | # quickblob - count blobs really fast 2 | 3 | # -ggdb -Werror -std=c99 4 | CFLAGS := -O2 -std=gnu99 -Wall -pedantic -Wextra ${CFLAGS} 5 | LDLIBS = -lpng -lz 6 | 7 | VERSION=$(shell date +%Y%m%d) 8 | 9 | %.a: %.o 10 | strip -d -X $< 11 | ar rvs $@ $< 12 | 13 | all: csv-blobs 14 | 15 | csv-blobs: csv-blobs.o quickblob.a 16 | 17 | quickblob.a: quickblob.o 18 | 19 | blobmark: quickblob.a 20 | 21 | strip: csv-blobs 22 | strip --strip-all $^ 23 | 24 | profile: CFLAGS += -pg 25 | profile: clean blobmark 26 | $(RM) best_case.txt worst_case.txt 27 | ./blobmark 640 480 0 1 1 28 | gprof blobmark gmon.out > worst_case.txt 29 | $(RM) gmon.out 30 | ./blobmark 640 480 3 0 1 31 | gprof blobmark gmon.out > best_case.txt 32 | 33 | tcc-blobs: 34 | tcc -o $@ -lpng quickblob.c csv-blobs.c 35 | 36 | clean: 37 | $(RM) *.o *.a csv-blobs blobmark tcc-blobs gmon.out 38 | 39 | .PHONY: all clean strip profile 40 | -------------------------------------------------------------------------------- /blobmark.c: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | benchmark for the library 4 | no real images, simply a generated video-like stream 5 | some parameters for the size and fill pattern 6 | only for linux because of the simpler timers 7 | */ 8 | 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #include "quickblob.h" 16 | 17 | // gcc -O2 -pg -o blobmark blobmark.c quickblob.a 18 | 19 | // licensed LGPL 20 | 21 | struct misc 22 | { 23 | int width, height; 24 | int fore, back, p; 25 | int target_seconds; 26 | int64_t start_sec, start_nsec; 27 | int64_t stop_sec, stop_nsec; 28 | int64_t frame_counter; 29 | }; 30 | 31 | void log_blob_hook(void* user_struct, struct blob* b) 32 | // benchmark, so do nothing 33 | { 34 | return; 35 | } 36 | 37 | int init_pixel_stream_hook(void* user_struct, struct stream_state* stream) 38 | // get the image ready for streaming 39 | // set the width and height, start the clock 40 | { 41 | struct timespec ts; 42 | struct misc* options = user_struct; 43 | 44 | clock_gettime(CLOCK_MONOTONIC, &ts); 45 | options->start_sec = ts.tv_sec; 46 | options->start_nsec = ts.tv_nsec; 47 | 48 | stream->w = options->width; 49 | stream->h = options->height; 50 | return 0; 51 | } 52 | 53 | int next_row_hook(void* user_struct, struct stream_state* stream) 54 | // y is given, load that row into stream->row 55 | { 56 | struct misc* options = user_struct; 57 | int x; 58 | 59 | for (x=0; x < stream->w; x++) 60 | { 61 | options->p = (options->p + 1) % (options->fore + options->back); 62 | stream->row[x] = options->p < options->fore; 63 | } 64 | return 0; 65 | } 66 | 67 | int next_frame_hook(void* user_struct, struct stream_state* stream) 68 | { 69 | struct timespec ts; 70 | struct misc* options = user_struct; 71 | 72 | clock_gettime(CLOCK_MONOTONIC, &ts); 73 | options->stop_sec = ts.tv_sec; 74 | options->stop_nsec = ts.tv_nsec; 75 | if (options->target_seconds == -1) 76 | {return 1;} 77 | if ((options->stop_sec - options->start_sec) > options->target_seconds) 78 | {return 1;} 79 | 80 | options->frame_counter++; 81 | if (options->target_seconds == 0) 82 | {options->target_seconds = -1; return 0;} 83 | return 0; 84 | } 85 | 86 | 87 | int close_pixel_stream_hook(void* user_struct, struct stream_state* stream) 88 | { 89 | return 0; 90 | } 91 | 92 | void use(void) 93 | { 94 | printf("blobmark -- A basic video throughput benchmark\n\n"); 95 | printf("Use: blobmark width height seconds fore-len back-len\n"); 96 | printf(" width: integer of pixels\n"); 97 | printf(" height: integer of pixels\n"); 98 | printf(" seconds: integer of benchmark duration\n"); 99 | printf(" 0 will run a single frame\n"); 100 | printf(" fore/back len: integer of pixel run\n"); 101 | printf(" 0 1 is best case (blank frames)\n"); 102 | printf(" 1 1 is worst case (tiny stripes)\n"); 103 | printf(" 10 10 or so is fair\n\n"); 104 | printf("all arguments are required\n\n"); 105 | } 106 | 107 | int main(int argc, char *argv[]) 108 | { 109 | int64_t runtime_ns; 110 | struct misc user_struct; 111 | user_struct.frame_counter = 0L; 112 | 113 | if (argc != 6) 114 | {use(); return 1;} 115 | 116 | /* todo, real arg parsing */ 117 | user_struct.width = atoi(argv[1]); 118 | user_struct.height = atoi(argv[2]); 119 | user_struct.target_seconds = atoi(argv[3]); 120 | user_struct.fore = atoi(argv[4]); 121 | user_struct.back = atoi(argv[5]); 122 | 123 | printf("Running, please wait %i seconds...\n", user_struct.target_seconds); 124 | extract_image((void*)&user_struct); 125 | 126 | runtime_ns = (user_struct.stop_sec - user_struct.start_sec) * 1000000000L + (user_struct.stop_nsec - user_struct.start_nsec); 127 | printf("FPS: %f\n", (double)user_struct.frame_counter * 1e9 / (double)runtime_ns); 128 | return 0; 129 | } 130 | 131 | -------------------------------------------------------------------------------- /csv-blobs.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "quickblob.h" 6 | 7 | #include 8 | 9 | // gcc -O2 -lIL -o csv-blobs csv-blobs.c quickblob.a 10 | 11 | // licensed LGPL 12 | 13 | /* 14 | ABOUT 15 | example of how to use QuickBlob with libpng 16 | you define a few functions for your choice of image library 17 | take a look at quickblob.h for more details 18 | 19 | TODO 20 | make quickblob into dynamic library 21 | */ 22 | 23 | struct misc 24 | { 25 | char* filename; 26 | int threshold; 27 | int show_bb; 28 | int frame; 29 | FILE* fp; 30 | png_structp png_ptr; 31 | png_infop info_ptr; 32 | }; 33 | 34 | void log_blob_hook(void* user_struct, struct blob* b) 35 | // center_x, center_y and size are the cumulative stats for a blob 36 | { 37 | struct misc* options = user_struct; 38 | printf("%.2f,%.2f,%i,%i", b->center_x, b->center_y, b->size, b->color); 39 | if (options->show_bb) 40 | {printf(",%i,%i,%i,%i", b->bb_x1, b->bb_y1, b->bb_x2, b->bb_y2);} 41 | printf("\n"); 42 | } 43 | 44 | int init_pixel_stream_hook(void* user_struct, struct stream_state* stream) 45 | // get the image ready for streaming 46 | // set the width and height 47 | // mostly deals with libpng junk (for comparison, DevIL init was 5 lines) 48 | { 49 | struct misc* opt = user_struct; 50 | unsigned int width, height; 51 | int bit_depth, color_type, interlace_type; 52 | unsigned char header[8]; 53 | 54 | // check file 55 | opt->fp = fopen(opt->filename, "rb"); 56 | if (!opt->fp) 57 | {fprintf(stderr, "could not open file\n"); return 1;} 58 | fread(header, 1, 8, opt->fp); 59 | if (png_sig_cmp(header, 0, 8)) 60 | {fprintf(stderr, "file is not a png\n"); return 1;} 61 | 62 | // init 63 | opt->png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); 64 | if (!opt->png_ptr) 65 | {fprintf(stderr, "create_read failure\n"); return 1;} 66 | opt->info_ptr = png_create_info_struct(opt->png_ptr); 67 | if (!opt->info_ptr) 68 | {fprintf(stderr, "create_info failure\n"); return 1;} 69 | if (setjmp(png_jmpbuf(opt->png_ptr))) 70 | {fprintf(stderr, "init_io failure\n"); return 1;} 71 | png_init_io(opt->png_ptr, opt->fp); 72 | png_set_sig_bytes(opt->png_ptr, 8); 73 | png_read_info(opt->png_ptr, opt->info_ptr); 74 | png_get_IHDR(opt->png_ptr, opt->info_ptr, &width, &height, 75 | &bit_depth, &color_type, &interlace_type, NULL, NULL); 76 | if (interlace_type != PNG_INTERLACE_NONE) 77 | {fprintf(stderr, "interlaced PNGs not supported\n"); return 1;} 78 | stream->w = (int)width; 79 | stream->h = (int)height - 1; 80 | 81 | //number_of_passes = png_set_interlace_handling(png_ptr); 82 | 83 | // force to 8 bit grayscale 84 | if (bit_depth == 16) 85 | {png_set_strip_16(opt->png_ptr);} 86 | if (color_type == PNG_COLOR_TYPE_PALETTE) 87 | {png_set_palette_to_rgb(opt->png_ptr);} 88 | if (color_type == PNG_COLOR_TYPE_GRAY_ALPHA || color_type == PNG_COLOR_TYPE_RGB_ALPHA) 89 | {png_set_strip_alpha(opt->png_ptr);} 90 | if (color_type != PNG_COLOR_TYPE_GRAY) 91 | {png_set_rgb_to_gray(opt->png_ptr, 1, -1.0, -1.0);} 92 | if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8) 93 | {png_set_expand_gray_1_2_4_to_8(opt->png_ptr);} 94 | png_set_crc_action(opt->png_ptr, PNG_CRC_QUIET_USE, PNG_CRC_QUIET_USE); 95 | // only one of these? 96 | //png_start_read_image(opt->png_ptr); 97 | png_read_update_info(opt->png_ptr, opt->info_ptr); 98 | 99 | if (setjmp(png_jmpbuf(opt->png_ptr))) 100 | {fprintf(stderr, "read_image failure\n"); return 1;} 101 | 102 | printf("%i,%i,%i,%i\n", stream->w, stream->h, stream->w * stream->h, -1); 103 | return 0; 104 | } 105 | 106 | int next_row_hook(void* user_struct, struct stream_state* stream) 107 | // y is given, load that row into stream->row 108 | { 109 | struct misc* options = user_struct; 110 | int x; 111 | // libpng requires dead reckoning, y is implicit 112 | png_read_row(options->png_ptr, stream->row, NULL); 113 | if (options->threshold < 0) 114 | {return 0;} 115 | for (x=0; x < stream->w; x++) 116 | {stream->row[x] = (stream->row[x] >= options->threshold) * 255;} 117 | return 0; 118 | } 119 | 120 | int next_frame_hook(void* user_struct, struct stream_state* stream) 121 | // single-image application, so this is a no-op 122 | { 123 | struct misc* options = user_struct; 124 | options->frame++; 125 | return options->frame; 126 | } 127 | 128 | int close_pixel_stream_hook(void* user_struct, struct stream_state* stream) 129 | // free anything malloc'd during the init 130 | { 131 | struct misc* opt = user_struct; 132 | //png_read_end(opt->png_ptr, NULL); 133 | png_destroy_read_struct(&opt->png_ptr, &opt->info_ptr, (png_infopp)NULL); 134 | fclose(opt->fp); 135 | return 0; 136 | } 137 | 138 | void use(void) 139 | { 140 | printf("csv-blobs -- Find and count unconnected blobs in an image\n\n"); 141 | printf("Use: csv-blobs [-t threshold] [--bbox] image.png\n"); 142 | printf(" threshold - optional arg for 2-level processing\n\n"); 143 | printf("x_center, y_center, pixel_size, color, printed to stdout\n"); 144 | printf(" --bbox adds bounding box information\n"); 145 | printf(" color -1 is used for image size metadata\n\n"); 146 | } 147 | 148 | int main(int argc, char *argv[]) 149 | { 150 | int i; 151 | struct misc user_struct; 152 | user_struct.threshold = -1; 153 | user_struct.frame = -1; 154 | 155 | if (argc <= 1 || argc >= 6) 156 | {use(); return 1;} 157 | 158 | /* todo, real arg parsing */ 159 | for (i=0; i 2 | #include 3 | #include 4 | 5 | void show(struct blob* b) 6 | { 7 | printf("%i:%i (%i, %i) %.2f, %.2f %X\n", b->x1, b->x2, b->size, b->color, b->center_x, b->center_y, b); 8 | } 9 | 10 | void show_sib(struct blob* b) 11 | { 12 | if (b == NULL) 13 | {printf("NULL\n"); return;} 14 | printf("%X %i:%i @%i (%i) %X %X\n", b, b->x1, b->x2, b->y, b->size, b->sib_p, b->sib_n); 15 | } 16 | 17 | void show_link(struct blob* b) 18 | { 19 | if (b == NULL) 20 | {printf("NULL\n"); return;} 21 | printf("%X (%X %X) (%X %X)\n", b, b->prev, b->next, b->sib_p, b->sib_n); 22 | } 23 | 24 | void show_status(struct blob* bl_start, struct stream_state* stream) 25 | { 26 | struct blob* b; 27 | int i=0, j=0; 28 | printf("stream %i %i %i\n", stream->x, stream->y, stream->wrap); 29 | 30 | b = bl_start; 31 | while (b) 32 | { 33 | if (b->size == 0) 34 | {i++;} 35 | else 36 | {j++;} 37 | b = b->next; 38 | } 39 | printf("blobs %i %i %i\n", i, j, i+j); 40 | } 41 | 42 | void show_blobs(struct blob* bl_start) 43 | { 44 | struct blob* b; 45 | b = bl_start; 46 | while (b) 47 | { 48 | if (b->x1 != -1 && b->size != 0) 49 | {show_sib(b);} 50 | b = b->next; 51 | } 52 | } 53 | 54 | void show_dead_sibs(struct blob* bl_start) 55 | { 56 | struct blob* b; 57 | b = bl_start; 58 | while (b) 59 | { 60 | if ((b->sib_p != NULL && b->sib_p->size == 0) || 61 | (b->sib_n != NULL && b->sib_n->size == 0)) 62 | {printf("DEAD %X\n", b);} 63 | b = b->next; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /quickblob.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "quickblob.h" 6 | //#include "debugstuff.c" 7 | 8 | // licensed LGPL 9 | 10 | /* 11 | ARCHITECTURE 12 | image is streamed row by row 13 | blobs are tracked as line segments on a row 14 | segments are stored in an ordered quad-linked list 15 | unused segments are linked to a stack 16 | 3rd & 4th links are for dealing with V shaped blobs 17 | these occupy multiple segments, but were previously connected 18 | sub-lists of sibling blobs 19 | 20 | TODO 21 | report all pixels (store old segments, dynamic malloc more) 22 | use for stats such as blob eccentricity 23 | binary foreground only mode (1/4 ram) 24 | concentric grayscale identification 25 | remove wrap flag 26 | make into a dynamic library 27 | -fvisibility=internal 28 | #define SYMEXPORT __attribute__((visibility("default"))) 29 | split blob, nest public struct inside private struct 30 | switch from pointers to indexes 31 | not worth it, saves ram but at best 6% faster 32 | */ 33 | 34 | 35 | struct blob_list 36 | { 37 | struct blob* head; 38 | int length; 39 | struct blob** empties; // stack 40 | int empty_i; 41 | }; 42 | 43 | static void blank(struct blob* b) 44 | { 45 | b->size = 0; 46 | b->color = -1; 47 | b->x1 = -1; 48 | b->x2 = -1; 49 | b->y = -1; 50 | b->prev = NULL; 51 | b->next = NULL; 52 | b->sib_p = NULL; 53 | b->sib_n = NULL; 54 | b->center_x = 0.0; 55 | b->center_y = 0.0; 56 | b->bb_x1 = b->bb_y1 = b->bb_x2 = b->bb_y2 = -1; 57 | } 58 | 59 | static int init_pixel_stream(void* user_struct, struct stream_state* stream) 60 | { 61 | memset(stream, 0, sizeof(struct stream_state)); 62 | if (init_pixel_stream_hook(user_struct, stream)) 63 | {return 1;} 64 | stream->row = NULL; 65 | // row is a bunch of 1x8 bit grayscale samples 66 | stream->row = (unsigned char*) malloc(stream->w * sizeof(unsigned char)); 67 | stream->x = 0; 68 | stream->y = -1; // todo, make x and y init the same 69 | stream->wrap = 0; 70 | return 0; 71 | } 72 | 73 | static int close_pixel_stream(void* user_struct, struct stream_state* stream) 74 | { 75 | close_pixel_stream_hook(user_struct, stream); // TODO return status 76 | free(stream->row); 77 | stream->row = NULL; 78 | return 0; 79 | } 80 | 81 | static int malloc_blobs(struct blob_list* blist) 82 | { 83 | blist->head = (struct blob*) malloc(blist->length * sizeof(struct blob)); 84 | if (blist->head == NULL) 85 | {return 1;} 86 | blist->empties = (struct blob**) malloc(blist->length * sizeof(struct blob*)); 87 | if (blist->empties == NULL) 88 | {return 1;} 89 | return 0; 90 | } 91 | 92 | static int init_blobs(struct blob_list* blist) 93 | { 94 | int i, len; 95 | struct blob* head; 96 | len = blist->length; 97 | blist->empty_i = 0; 98 | head = blist->head; 99 | for (i=0; i < len; i++) 100 | {blank(&(head[i]));} 101 | head[0].next = &head[1]; 102 | head[1].prev = &head[0]; 103 | for (i=1; i < len; i++) 104 | { 105 | blist->empties[blist->empty_i] = &(head[i]); 106 | blist->empty_i++; 107 | } 108 | return 0; 109 | } 110 | 111 | static void blob_unlink(struct blob* b2) 112 | // remove from linked list 113 | // (don't use on head, am lazy) 114 | { 115 | struct blob* b1 = NULL; 116 | struct blob* b3 = NULL; 117 | struct blob* s1 = NULL; 118 | struct blob* s3 = NULL; 119 | b1 = b2->prev; 120 | b3 = b2->next; 121 | if (b1 != NULL) 122 | {b1->next = b3;} 123 | if (b3 != NULL) 124 | {b3->prev = b1;} 125 | b2->prev = NULL; 126 | b2->next = NULL; 127 | // unlink sibs 128 | s1 = b2->sib_p; 129 | s3 = b2->sib_n; 130 | if (s1 != NULL) 131 | {s1->sib_n = s3;} 132 | if (s3 != NULL) 133 | {s3->sib_p = s1;} 134 | b2->sib_p = NULL; 135 | b2->sib_n = NULL; 136 | } 137 | 138 | static void blob_insert(struct blob* bl_start, struct blob* b2) 139 | // assume elements are ordered and unique 140 | // never call with head of list, requires bl_start->prev 141 | // will never change head since that is empty element 142 | { 143 | struct blob* b1; 144 | struct blob* b3; 145 | b1 = bl_start->prev; 146 | b3 = b1->next; 147 | while (b1->next != NULL) 148 | { 149 | b3 = b1->next; 150 | // equality for fast reinsertion of empties 151 | if ((b1->x1 <= b2->x1) && (b2->x1 <= b3->x1)) 152 | { 153 | b1->next = b2; 154 | b2->prev = b1; 155 | b2->next = b3; 156 | b3->prev = b2; 157 | return; 158 | } 159 | b1 = b1->next; 160 | } 161 | // append to end 162 | if (b1->x1 > b2->x1) 163 | {printf("insert error\n"); return;} // TODO: should raise an error 164 | b1->next = b2; 165 | b2->prev = b1; 166 | } 167 | 168 | static int next_row(void* user_struct, struct stream_state* stream) 169 | { 170 | if (stream->y >= stream->h) 171 | {return 1;} 172 | stream->wrap = 0; 173 | stream->x = 0; 174 | stream->y++; 175 | return next_row_hook(user_struct, stream); 176 | } 177 | 178 | static int next_frame(void* user_struct, struct stream_state* stream) 179 | { 180 | stream->wrap = 0; 181 | stream->x = 0; 182 | stream->y = -1; 183 | return next_frame_hook(user_struct, stream); 184 | } 185 | 186 | static int scan_segment(struct stream_state* stream, struct blob* b) 187 | // sets color/x1/x2 fields, returns 1 on error 188 | // must call next_row() to continue scanning 189 | { 190 | if (stream->wrap) 191 | {return 1;} 192 | b->x1 = stream->x; 193 | b->color = stream->row[stream->x]; 194 | for (;;) // awkward, but the two exit points are too similar 195 | { 196 | if (stream->x >= stream->w) 197 | {stream->wrap = 1; break;} 198 | if (b->color != (stream->row[stream->x])) 199 | {break;} 200 | stream->x++; 201 | } 202 | b->x2 = stream->x - 1; 203 | return 0; 204 | } 205 | 206 | static void bbox_update(struct blob* b, int x1, int x2, int y1, int y2) 207 | { 208 | if (b->bb_x1 < 0) 209 | {b->bb_x1 = x1;} 210 | if (x1 < b->bb_x1) 211 | {b->bb_x1 = x1;} 212 | if (x2 > b->bb_x2) 213 | {b->bb_x2 = x2;} 214 | 215 | if (b->bb_y1 < 0) 216 | {b->bb_y1 = y1;} 217 | if (y1 < b->bb_y1) 218 | {b->bb_y1 = y1;} 219 | if (y2 > b->bb_y2) 220 | {b->bb_y2 = y2;} 221 | } 222 | 223 | static void blob_update(struct blob* b, int x1, int x2, int y) 224 | { 225 | int s2; 226 | s2 = 1 + x2 - x1; 227 | b->x1 = x1; 228 | b->x2 = x2; 229 | b->y = y; 230 | b->center_x = ((b->center_x * b->size) + (x1+x2)*s2/2)/(b->size + s2); 231 | b->center_y = ((b->center_y * b->size) + (y * s2))/(b->size + s2); 232 | b->size += s2; 233 | bbox_update(b, x1, x2, y, y); 234 | } 235 | 236 | static void sib_link(struct blob* b1, struct blob* b2) 237 | // merge sort two sibling chains into one 238 | { 239 | struct blob* s1 = b1; 240 | struct blob* s2 = b2; 241 | struct blob* s_tmp; 242 | // find heads of each sibling list 243 | while (s1->sib_p != NULL) 244 | {s1 = s1->sib_p;} 245 | while (s2->sib_p != NULL) 246 | {s2 = s2->sib_p;} 247 | if (s1 == s2) 248 | {return;} 249 | while (s1 != NULL && s2 != NULL) 250 | { 251 | // cut code in half by forcing order 252 | if (s2->x1 < s1->x1) 253 | { 254 | s_tmp = s1; 255 | s1 = s2; 256 | s2 = s_tmp; 257 | continue; 258 | } 259 | // so s1 must come before s2 260 | // but what about s1->sib_n ? 261 | if ((s1->sib_n != NULL) && (s1->sib_n->x1 < s2->x1)) 262 | {s1 = s1->sib_n; continue;} 263 | // normal case, interleave s1 and s2 264 | // pointer shell game! 265 | s_tmp = s1->sib_n; 266 | s1->sib_n = s2; 267 | s1->sib_n->sib_p = s1; 268 | s2 = s_tmp; 269 | s1 = s1->sib_n; 270 | } 271 | } 272 | 273 | static void blob_merge(struct blob* b1, struct blob* b2) 274 | // merge b2 into b1, does not deal with sibs 275 | { 276 | b1->center_x = ((b1->center_x * b1->size) + (b2->center_x * b2->size)) / (b1->size + b2->size); 277 | b1->center_y = ((b1->center_y * b1->size) + (b2->center_y * b2->size)) / (b1->size + b2->size); 278 | b1->size += b2->size; 279 | bbox_update(b1, b2->bb_x1, b2->bb_x2, b2->bb_y1, b2->bb_y2); 280 | } 281 | 282 | static int range_overlap(int a1, int a2, int b1, int b2) 283 | // returns 1 for overlap, 0 for none 284 | // pads by one for diagonals 285 | { 286 | // could be less checks, but this is simple 287 | // b1 <= a1 <= b2 288 | if ((b1-1) <= a1 && a1 <= (b2+1)) 289 | {return 1;} 290 | // b1 <= a2 <= b2 291 | if ((b1-1) <= a2 && a2 <= (b2+1)) 292 | {return 1;} 293 | // a1 <= b1 <= a2 294 | if ((a1-1) <= b1 && b1 <= (a2+1)) 295 | {return 1;} 296 | // a1 <= b2 <= a2 297 | if ((a1-1) <= b2 && b2 <= (a2+1)) 298 | {return 1;} 299 | return 0; 300 | } 301 | 302 | static int blob_overlap(struct blob* b, int x1, int x2) 303 | // returns 1 for hit, 0 for miss, -1 for abort 304 | { 305 | if (b->x1 == -1) 306 | {return 0;} 307 | if (x1 == -1) 308 | {return 0;} 309 | if (x2 < (b->x1 - 1)) 310 | {return -1;} 311 | return range_overlap(x1, x2, b->x1, b->x2); 312 | } 313 | 314 | static void blob_reap(struct blob_list* blist, struct blob* b) 315 | { 316 | blob_unlink(b); 317 | blank(b); 318 | blist->empties[blist->empty_i] = b; 319 | blist->empty_i++; 320 | } 321 | 322 | static void sib_cleanup(struct blob_list* blist, struct blob* b) 323 | // seems to do too much 324 | { 325 | struct blob* s1; 326 | struct blob* s3; 327 | if (b->sib_p == NULL && b->sib_n == NULL) 328 | {return;} 329 | // paranoid... 330 | //if (b->sib_p != NULL && b->y > b->sib_p->y) 331 | // {return;} // should raise an error 332 | //if (b->sib_n != NULL && b->y > b->sib_n->y) 333 | // {return;} // should raise an error 334 | s1 = b->sib_p; 335 | s3 = b->sib_n; 336 | if (s1 != NULL && range_overlap(b->x1, b->x2, s1->x1, s1->x2)) 337 | {blob_merge(s1, b); blob_reap(blist, b); return;} 338 | if (s3 != NULL && range_overlap(b->x1, b->x2, s3->x1, s3->x2)) 339 | {blob_merge(s3, b); blob_reap(blist, b); return;} 340 | if (s1 != NULL) 341 | {blob_merge(s1, b); blob_reap(blist, b); return;} 342 | if (s3 != NULL) 343 | {blob_merge(s3, b); blob_reap(blist, b); return;} 344 | } 345 | 346 | static void sib_find(struct blob* blob_start, struct blob* blob_now) 347 | { 348 | struct blob* b = NULL; 349 | int i; 350 | b = blob_start; 351 | while (b) 352 | { 353 | if (b->color != blob_now->color) 354 | {b = b->next; continue;} 355 | if (b->y == blob_now->y) 356 | {b = b->next; continue;} 357 | i = blob_overlap(b, blob_now->x1, blob_now->x2); 358 | if (i == -1) 359 | {break;} 360 | if (i == 1) 361 | {sib_link(b, blob_now);} 362 | b = b->next; 363 | } 364 | } 365 | 366 | static void flush_incremental(void* user_struct, struct blob_list* blist, struct blob* blob_now) 367 | // merges/prints/reaps everything before blob_now 368 | { 369 | struct blob* b; 370 | struct blob* b2; 371 | // pass 1, merge old sibs 372 | b = blob_now; 373 | while (b->sib_p) 374 | { 375 | b = b->sib_p; 376 | if (b->y != blob_now->y-1) 377 | {continue;} 378 | if (b->x1 == -1) 379 | {break;} 380 | if (b->x2 > blob_now->x2) 381 | {continue;} 382 | // merge b into b2 383 | b2 = b->sib_n; 384 | blob_merge(b2, b); 385 | blob_reap(blist, b); 386 | b = b2; 387 | } 388 | // pass 2, log isolated blobs 389 | b = blob_now; 390 | while (b->prev) 391 | { 392 | b = b->prev; 393 | if (b->y != blob_now->y-1) 394 | {continue;} 395 | if (b->x1 == -1) 396 | {break;} 397 | if (b->sib_n || b->sib_p) 398 | {continue;} 399 | b2 = b->next; 400 | log_blob_hook(user_struct, b); 401 | blob_reap(blist, b); 402 | b = b2; 403 | } 404 | } 405 | 406 | static void flush_old_blobs(void* user_struct, struct blob_list* blist, int y) 407 | // merges (or prints) and reaps, y is current row 408 | { 409 | struct blob* b; 410 | struct blob* b2; 411 | b = blist->head->next; 412 | while (b) 413 | { 414 | if (b->size == 0) 415 | {b = b->next; continue;} 416 | if (b->x1 == -1) 417 | {b = b->next; continue;} 418 | if (b->y == y) 419 | {b = b->next; continue;} 420 | // use previous so the scan does not restart every reap 421 | b2 = b; 422 | if (b->prev != NULL) 423 | {b2 = b->prev;} 424 | if (b->sib_p == NULL && b->sib_n == NULL) 425 | {log_blob_hook(user_struct, b); blob_reap(blist, b);} 426 | else 427 | {sib_cleanup(blist, b);} 428 | b = b2->next; 429 | } 430 | } 431 | 432 | static struct blob* empty_blob(struct blob_list* blist) 433 | { 434 | struct blob* b; 435 | blist->empty_i--; 436 | b = blist->empties[blist->empty_i]; 437 | blist->empties[blist->empty_i] = NULL; 438 | return b; 439 | } 440 | 441 | int extract_image(void* user_struct) 442 | { 443 | struct stream_state stream; 444 | struct blob_list blist; 445 | struct blob* blob_now = NULL; 446 | struct blob* blob_prev = NULL; 447 | 448 | if (init_pixel_stream(user_struct, &stream)) 449 | {printf("init malloc error!\n"); return 1;} 450 | if (stream.row == NULL) 451 | {printf("row malloc error!\n"); return 1;} 452 | blist.length = stream.w + 5; 453 | if (malloc_blobs(&blist)) 454 | {printf("blob malloc error!\n"); return 1;} 455 | 456 | while (!next_frame(user_struct, &stream)) 457 | { 458 | init_blobs(&blist); 459 | while (!next_row(user_struct, &stream)) 460 | { 461 | blob_prev = blist.head->next; 462 | while (!stream.wrap) 463 | { 464 | blob_now = empty_blob(&blist); 465 | if (scan_segment(&stream, blob_now)) 466 | {blob_reap(&blist, blob_now); continue;} 467 | blob_update(blob_now, blob_now->x1, blob_now->x2, stream.y); 468 | // update structure 469 | sib_find(blist.head->next, blob_now); 470 | blob_insert(blob_prev, blob_now); 471 | flush_incremental(user_struct, &blist, blob_now); 472 | blob_prev = blob_now; 473 | } 474 | flush_old_blobs(user_struct, &blist, stream.y); 475 | //show_status(blist.head, &stream); 476 | //show_dead_sibs(blist.head); 477 | //show_blobs(blist.head); 478 | //printf("----------\n"); 479 | } 480 | flush_old_blobs(user_struct, &blist, stream.h - 1); 481 | } 482 | 483 | close_pixel_stream(user_struct, &stream); 484 | free(blist.head); 485 | free(blist.empties); 486 | blist.head = NULL; 487 | blist.empties = NULL; 488 | return 0; 489 | } 490 | 491 | -------------------------------------------------------------------------------- /quickblob.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | // gcc -O2 -c quickblob.c -o quickblob.o 6 | // ar rvs quickblob.a quickblob.o 7 | 8 | // licensed LGPL 9 | 10 | /* 11 | ABOUT 12 | image is streamed row by row 13 | stream_state keeps track of this 14 | blobs are assembled incrementally 15 | complete blobs are passed to log_blob_hook 16 | see more details in quickblob.c 17 | */ 18 | 19 | /* some structures you'll be working with */ 20 | 21 | struct blob 22 | // you'll probably only need size, color, center_x and center_y 23 | { 24 | int size; 25 | int color; 26 | // track current line segment 27 | int x1; 28 | int x2; 29 | int y; 30 | // basic linked list 31 | struct blob* prev; 32 | struct blob* next; 33 | // siblings are connected segments 34 | struct blob* sib_p; 35 | struct blob* sib_n; 36 | // incremental average 37 | double center_x; 38 | double center_y; 39 | // bounding box 40 | int bb_x1, bb_y1, bb_x2, bb_y2; 41 | // single linked list for tracking all old pixels 42 | // struct blob* old; 43 | }; 44 | 45 | struct stream_state 46 | // make a struct to hold an state required by the image loader 47 | // and reference in the handle pointer 48 | { 49 | int w, h, x, y; 50 | int wrap; // don't touch this 51 | unsigned char* row; 52 | void* handle; 53 | }; 54 | 55 | /* these are the functions you need to define 56 | * you get void pointer for passing around useful data */ 57 | 58 | void log_blob_hook(void* user_struct, struct blob* b); 59 | // the blob struct will be for a completely finished blob 60 | // you'll probably want to printf() important parts 61 | // or write back to something in user_struct 62 | 63 | int init_pixel_stream_hook(void* user_struct, struct stream_state* stream); 64 | // you need to set several variables: 65 | // stream->w = image width 66 | // stream->h = image hight 67 | // return status (0 for success) 68 | 69 | int close_pixel_stream_hook(void* user_struct, struct stream_state* stream); 70 | // free up anything you allocated in init_pixel_stream_hook 71 | // return status (0 for success) 72 | 73 | int next_row_hook(void* user_struct, struct stream_state* stream); 74 | // load the (grayscale) row at stream->y into the (8 bit) stream->row array 75 | // return status (0 for success) 76 | 77 | int next_frame_hook(void* user_struct, struct stream_state* stream); 78 | // basically a no-op in the library, but useful for applications 79 | // return status (0 for success, otherwise breaks the video loop) 80 | 81 | /* callable functions */ 82 | 83 | int extract_image(void* user_struct); 84 | 85 | -------------------------------------------------------------------------------- /sample/lorem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keenerd/quickblob/7560de3592b9622c183b4e9558cd00b7806164c0/sample/lorem.png -------------------------------------------------------------------------------- /sample/show-blobs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | from PIL import Image, ImageDraw 5 | from math import sqrt, pi, ceil, floor 6 | 7 | def bb(x, y, r): 8 | "bounding box for a circle" 9 | return x-r, y-r, x+r, y+r 10 | 11 | if len(sys.argv) != 4: 12 | print("csv-blobs -t 128 --bbox lorem.png > data.csv") 13 | print("show-blobs.py data.csv lorem.png blobs.png") 14 | sys.exit(2) 15 | 16 | data = sys.argv[1] 17 | source = sys.argv[2] 18 | output = sys.argv[3] 19 | 20 | img = Image.open(source) 21 | img = img.convert("RGB") 22 | draw = ImageDraw.Draw(img) 23 | height = img.size[1] 24 | 25 | for line in open(data): 26 | line = line.strip().split(',') 27 | x,y,a,c = line[:4] 28 | bbox = None 29 | if len(line) == 8: 30 | bbox = line[4:] 31 | 32 | """ 33 | if c == 'white': 34 | gray = 0x95 35 | elif c == 'black': 36 | gray = 0x65 37 | else: 38 | continue 39 | """ 40 | if c == 'color': 41 | continue 42 | if c == '-1': 43 | continue 44 | gray = "rgb(%s,%s,128)" % (c, c) 45 | 46 | try: 47 | x,y,a = map(float, (x,y,a)) 48 | except ValueError: 49 | continue 50 | 51 | r = int(sqrt(a/pi) + 1.5) 52 | if bbox: 53 | bbox = [int(b) for b in bbox] 54 | draw.rectangle(bbox, outline=gray, fill=None) 55 | draw.ellipse(bb(ceil(x), ceil(y), r), outline=gray, fill=None) 56 | draw.ellipse(bb(ceil(x), floor(y), r), outline=gray, fill=None) 57 | draw.ellipse(bb(floor(x), ceil(y), r), outline=gray, fill=None) 58 | draw.ellipse(bb(floor(x), floor(y), r), outline=gray, fill=None) 59 | 60 | img.save(output) 61 | 62 | --------------------------------------------------------------------------------