Canola  0.8.D001
canola-card-scanner/main.cc
Go to the documentation of this file.
00001 //
00002 // canola - canon canola 1614p emulator
00003 // Copyright (C) 2011, 2012 Peter Miller
00004 //
00005 // This program is free software; you can redistribute it and/or modify
00006 // it under the terms of the GNU General Public License, version 3, as
00007 // published by the Free Software Foundation.
00008 //
00009 // This program is distributed in the hope that it will be useful,
00010 // but WITHOUT ANY WARRANTY; without even the implied warranty of
00011 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
00012 // General Public License for more details.
00013 //
00014 // You should have received a copy of the GNU General Public License along
00015 // with this program. If not, see <http://www.gnu.org/licenses/>.
00016 //
00017 
00018 #include <lib/ac/stdio.h>
00019 #include <lib/ac/stdlib.h>
00020 #include <lib/ac/string.h>
00021 #include <lib/ac/unistd.h>
00022 #include <libexplain/output.h>
00023 #include <libexplain/program_name.h>
00024 
00025 #include <lib/calculator/asm.h>
00026 #include <lib/image.h>
00027 #include <lib/image/invert.h>
00028 #include <lib/image/monochrome.h>
00029 #include <lib/image/threshold.h>
00030 #include <lib/opcode.h>
00031 #include <lib/polyfit.h>
00032 #include <lib/print_version.h>
00033 #include <lib/sizeof.h>
00034 
00035 
00036 // #define FUZZY 1
00037 
00038 
00039 static void
00040 usage(void)
00041 {
00042     const char *prog = explain_program_name_get();
00043     fprintf(stderr, "Usage: %s [ <option>... ] <card-image-file> "
00044         "<binary-code-file>\n", prog);
00045     fprintf(stderr, "       %s -V\n", prog);
00046     exit(1);
00047 }
00048 
00049 
00050 #ifdef FUZZY
00051 
00052 static bool
00053 is_black(const image::pointer &img, int x, int y)
00054 {
00055     icon_pixel p;
00056     img->get_pixel(x, y, p);
00057     return (p.get_red_short() < 0x8000);
00058 }
00059 
00060 #endif
00061 
00062 
00063 static bool
00064 is_white(const image::pointer &img, int x, int y)
00065 {
00066     icon_pixel p;
00067     img->get_pixel(x, y, p);
00068     return (p.get_red_short() >= 0x8000);
00069 }
00070 
00071 
00072 #ifdef FUZZY
00073 
00074 static bool
00075 hole_inner(const image::pointer &img, int x, int y, int hx, int hy)
00076 {
00077     //           0 | 0 | 0      y + hy * 0.6
00078     //        -+---+---+---+
00079     //             |   |
00080     //             | 1 |        y + hy * 0.2
00081     //           0 | 1 | 0      y
00082     //             | 1 |        y - hy * 0.2
00083     //             |   |
00084     //        -+---+---+---+
00085     //           0 | 0 | 0      y - hy * 0.6
00086     //
00087     //           |   |   ^--- x + hx * 0.6
00088     //           |   `------- x
00089     //           `------------x - hx * 0.6
00090     icon_pixel p;
00091 
00092     int x1 = x - hx * 3/5;
00093     int x2 = x;
00094     int x3 = x + hx * 3/5;
00095 
00096     int y1 = y - hy * 7/10;
00097     int y2 = y - hy / 5;
00098     int y3 = y;
00099     int y4 = y + hy / 5;
00100     int y5 = y + hy * 7/10;
00101 
00102     return
00103         (
00104             is_black(img, x1, y1)
00105         &&
00106             is_black(img, x2, y1)
00107         &&
00108             is_black(img, x3, y1)
00109         &&
00110             is_white(img, x2, y2)
00111         &&
00112             is_black(img, x1, y3)
00113         &&
00114             is_white(img, x2, y3)
00115         &&
00116             is_black(img, x3, y3)
00117         &&
00118             is_white(img, x2, y4)
00119         &&
00120             is_black(img, x1, y5)
00121         &&
00122             is_black(img, x2, y5)
00123         &&
00124             is_black(img, x3, y5)
00125         );
00126 }
00127 
00128 
00129 static bool
00130 hole_fuzzy(const image::pointer &img, int x, int y, int hx, int hy)
00131 {
00132     for (int sdy = 1; sdy < hy; ++sdy)
00133     {
00134         int dy = (sdy >> 1) * ((sdy & 1) ? 1 : -1);
00135         for (int sdx = 1; sdx < hx; ++sdx)
00136         {
00137             int dx = (sdx >> 1) * ((sdx & 1) ? 1 : -1);
00138             if (hole_inner(img, x + dx, y + dy, hx, hy))
00139                 return true;
00140         }
00141     }
00142     return false;
00143 }
00144 
00145 #endif
00146 
00147 static bool invert;
00148 
00149 static void
00150 box(const image::pointer &img, int xlo, int ylo, int xhi, int yhi)
00151 {
00152     icon_pixel green(0., 1., 0.);
00153     for (int x = xlo; x < xhi; ++x)
00154     {
00155         img->set_pixel(x, ylo, green);
00156         img->set_pixel(x, yhi, green);
00157     }
00158     for (int y = ylo; y < yhi; ++y)
00159     {
00160         img->set_pixel(xlo, y, green);
00161         img->set_pixel(xhi, y, green);
00162     }
00163 }
00164 
00165 static bool debug;
00166 
00167 //
00168 // Referencing http://www.quadibloc.com/comp/cardint.htm
00169 //
00170 // IBM card width 7.75 inch by 3.25 inch
00171 // at 600dpi that would be 4650 (wider than our cards)
00172 //                         1950 (higher than our cards)
00173 // row pitch 0.25 inch @ 600dpi = 150 pixels which very close to the
00174 // calculated row pitch of 151.666 pixels.
00175 //
00176 //                  13/150 = 0.08666... @600 dpi = 52.0
00177 // column pitch is stated as 0.087 inch @600 dpi = 52.2 pixels which is
00178 // very close to half of the calulated pitch of 104.641 pixels
00179 // (alternatively 0.0885 @ 600 dpi = 53.1 compared to 104.641 (52.32) less good)
00180 //
00181 // vertical height of holes stated as 0.125 inch @ 600dpi = 75 pixels,
00182 // horizontal width of holes not stated
00183 //
00184 // also 0.375 inch unpunched top and bottom @ 600dpi = 225 pixels
00185 // which is significantly different than the 310 calculated.
00186 //
00187 
00188 static void
00189 process_image(const char *filename, const calculator::pointer &data)
00190 {
00191     // The ideal image is the card image measured to figure out where
00192     // the holes are.
00193     const int ideal_image_width = 4433;
00194     const int ideal_image_height = 1370;
00195 
00196     // open image.png as greyscale
00197     image::pointer img = image::create_from_file(filename);
00198 
00199     // verify proportions look right
00200     {
00201         double ratio = img->get_width() / (double)img->get_height();
00202         // We want approximately 4432x1375
00203         double want = (double)ideal_image_width / (double)ideal_image_height;
00204         if (ratio < want * 0.98 || ratio > want * 1.02)
00205         {
00206             explain_output_error_and_die
00207             (
00208                 "%s: file has proportions %dx%d (%g:1) but was expecting %g:1",
00209                 filename,
00210                 img->get_width(),
00211                 img->get_height(),
00212                 ratio,
00213                 want
00214             );
00215         }
00216     }
00217 
00218     // we only want gray
00219     img = image_monochrome::create(img);
00220 
00221     // threshold based on averages
00222     if (debug)
00223         fprintf(stderr, "averaging...\n");
00224     icon_pixel avg = img->average();
00225 
00226     img = image_threshold::create(img, (1. + avg.get_red()) / 2);
00227 
00228     if (invert)
00229         img = image_invert::create(img);
00230 
00231     // debug: write the image out so I can look at it
00232     if (debug)
00233     {
00234         fprintf(stderr, "writing debug1.png\n");
00235         {
00236             const char *fn2 = "debug1.png";
00237             image::pointer img2 =
00238                 image::create_blank_from_file
00239                 (
00240                     fn2,
00241                     img->get_width(),
00242                     img->get_height()
00243                 );
00244             img2->copy(img);
00245             img2->save_to_file(fn2);
00246         }
00247     }
00248 
00249     double scale = img->get_width() / (double)ideal_image_width;
00250     if (debug)
00251         fprintf(stderr, "scale = %g\n", scale);
00252 
00253     // punched holes are 48x79
00254     // (1/12 inch x 1/8 inch = 50x75)
00255     // (2mm x 3.5mm = 47x83)
00256     int hx = 34 * scale + 0.5;
00257     int hy = 75 * scale + 0.5;
00258 
00259     // Get a bit smarter about detecting each row of holes.
00260     int row_centre[40];
00261     int nrows = 0;
00262     {
00263         int w = img->get_width();
00264         int h = img->get_height();
00265         int *pixel_column_total = new int [w];
00266         for (int x = 0; x < w; ++x)
00267         {
00268             int sum = 0;
00269             for (int y = 0; y < h; ++y)
00270             {
00271                 if (is_white(img, x, y))
00272                 {
00273                     ++sum;
00274                 }
00275             }
00276             pixel_column_total[x] = (2 * sum > hy) ? sum : 0;
00277         }
00278 
00279         int x = 0;
00280         while (x < w)
00281         {
00282             while (x < w && pixel_column_total[x] == 0)
00283                 ++x;
00284             // start of row
00285             int x2 = x + 1;
00286             while (x2 < w && pixel_column_total[x2] != 0)
00287                 ++x2;
00288             int rw = x2 - x;
00289             if (2 * rw >= hx)
00290             {
00291                 row_centre[nrows++] = (x + x2) / 2;
00292                 if (debug)
00293                 {
00294                     fprintf(stderr, "row%3d, at%5d,%3d pixels wide\n", nrows,
00295                         row_centre[nrows - 1], rw);
00296                 }
00297             }
00298             x = x2;
00299         }
00300         delete [] pixel_column_total;
00301     }
00302 
00303     // now we look for each column of bits
00304     int col_centre[20];
00305     {
00306         int ncols = 0;
00307         int w = img->get_width();
00308         int h = img->get_height();
00309         int *pixel_row_total = new int [h];
00310         for (int y = 0; y < h; ++y)
00311         {
00312             if (y < 240 * scale || y >= 1285 * scale)
00313             {
00314                 pixel_row_total[y] = 0;
00315                 continue;
00316             }
00317             int sum = 0;
00318             // only scan the row centres, not the whole image
00319             for (int x = 0; x < w; ++x)
00320             {
00321                 if (is_white(img, x, y))
00322                     ++sum;
00323             }
00324             pixel_row_total[y] = (2 * sum > hx) ? sum : 0;
00325         }
00326 
00327         int y = 0;
00328         while (y < h && ncols < (int)SIZEOF(col_centre))
00329         {
00330             while (y < h && pixel_row_total[y] == 0)
00331                 ++y;
00332             // start of row
00333             int y2 = y + 1;
00334             while (y2 < h && pixel_row_total[y2] != 0)
00335                 ++y2;
00336             int rh = y2 - y;
00337             if (2 * rh >= hy)
00338             {
00339                 col_centre[ncols++] = (y + y2) / 2;
00340                 if (debug)
00341                 {
00342                     fprintf(stderr, "col %d, at %3d, %2d pixels high\n",
00343                         ncols - 1, col_centre[ncols - 1], rh);
00344                 }
00345             }
00346             y = y2;
00347         }
00348         if (ncols == 1)
00349         {
00350             // assuming ~150 pixels between columns
00351             int c = (col_centre[0]/scale - 310 + 75) / 150;
00352             int offset = col_centre[0] - c * 150 * scale;
00353             for (int j = 0; j < 7; ++j)
00354                 col_centre[j] = offset + j * 150 * scale;
00355             ncols = 7;
00356         }
00357         else if (ncols < 7)
00358         {
00359             // fit a line to the available data
00360             polyfit p(2);
00361             for (int j = 0; j < ncols; ++j)
00362             {
00363                 int x = (col_centre[j]/scale - 310 + 75) / 150;
00364                 int y = col_centre[j];
00365                 p.enter(x, y);
00366             }
00367             if (p.solve() != polyfit::solve_ok)
00368                 goto yuck;
00369             for (int j = 0; j < 7; ++j)
00370             {
00371                 col_centre[j] = p.evaluate(j);
00372                 if (debug)
00373                     fprintf(stderr, "col %d, at %3d\n", j, col_centre[j]);
00374             }
00375             ncols = 7;
00376         }
00377         if (ncols != 7)
00378         {
00379             yuck:
00380             fprintf(stderr, "found %d bit columns, expected 7\n", ncols);
00381             exit(1);
00382         }
00383         delete [] pixel_row_total;
00384     }
00385     // debug: write the image out so I can look at it
00386     image::pointer img2;
00387     if (debug)
00388     {
00389         fprintf(stderr, "write debug2.png\n");
00390         const char *fn2 = "debug2.png";
00391         img2 =
00392             image::create_blank_from_file
00393             (
00394                 fn2,
00395                 img->get_width(),
00396                 img->get_height()
00397             );
00398         img2->copy(img);
00399 
00400         icon_pixel red(1., 0., 0.);
00401         unsigned dy = col_centre[1] - col_centre[0];
00402         unsigned ylo = col_centre[0] - dy / 2;
00403         unsigned yhi = col_centre[6] + dy / 2;
00404         for (int row = 0; row < 40 && row < nrows; ++row)
00405         {
00406             // int x = scale * (203 + 104.641025641 * row);
00407             // int x = scale * (203 + 104 * row);
00408             unsigned x = row_centre[row];
00409             for (unsigned y = ylo; y < yhi; ++y)
00410                 if (!is_white(img2, x, y))
00411                     img2->set_pixel(x, y, red);
00412         }
00413         unsigned dx = (nrows > 1 ? row_centre[1] - row_centre[0] : 104*scale);
00414         unsigned xlo = row_centre[0] - dx / 2;
00415         unsigned xhi = row_centre[nrows - 1] + dx / 2;
00416         for (int col = 6; col >= 0; --col)
00417         {
00418             // int y = scale * (310 + 151.666666667 * col);
00419             // int y = scale * (310 + 150 * col);
00420             int y = col_centre[col];
00421             for (unsigned x = xlo; x < xhi; ++x)
00422                 if (!is_white(img2, x, y))
00423                     img2->set_pixel(x, y, red);
00424         }
00425         img2->save_to_file(fn2);
00426     }
00427 
00428     // interpret holes as data
00429     bool something = false;
00430     data->program_mode_set(calculator::program_mode_learn);
00431     for (int row = 0; row < 40 && row < nrows; ++row)
00432     {
00433         // int x = scale * (203 + 104.641025641 * row);
00434         // int x = scale * (203 + 104 * row);
00435         int x = row_centre[row];
00436         int code = 0;
00437         for (int col = 6; col >= 0; --col)
00438         {
00439             // int y = scale * (310 + 151.666666667 * col);
00440             // int y = scale * (310 + 150 * col);
00441             int y = col_centre[col];
00442 #ifdef FUZZY
00443             if (hole_fuzzy(img, x, y, hx, hy))
00444 #else
00445             if (is_white(img, x, y))
00446 #endif
00447             {
00448                 code |= 1 << col;
00449                 if (debug)
00450                     box(img2, x - 7, y - 10, x + 7, y + 10);
00451             }
00452         }
00453         if (code != 0 && code != 0x7F)
00454         {
00455             data->on_opcode_lrn(opcode_t(code), location::pointer());
00456             something = true;
00457         }
00458     }
00459 
00460     if (!something)
00461         explain_output_error_and_die("%s: could not find any data", filename);
00462     if (debug)
00463     {
00464         fprintf(stderr, "writing debug3.png\n");
00465         img2->save_to_file("debug3.png");
00466     }
00467 }
00468 
00469 
00470 int
00471 main(int argc, char **argv)
00472 {
00473     bool binary = false;
00474     for (;;)
00475     {
00476         int c = getopt(argc, argv, "bdiV");
00477         if (c == EOF)
00478             break;
00479         switch (c)
00480         {
00481         case 'b':
00482             binary = true;
00483             break;
00484 
00485         case 'd':
00486             debug = true;
00487             break;
00488 
00489         case 'i':
00490             invert = true;
00491             break;
00492 
00493         case 'V':
00494             print_version();
00495             return 0;
00496 
00497         default:
00498             usage();
00499         }
00500     }
00501     if (optind + 2 != argc)
00502         usage();
00503     const char *input_file_name = argv[optind];
00504     const char *output_file_name = argv[optind + 1];
00505 
00506     // read the data on the card
00507     calculator::pointer data = calculator_asm::create();
00508     process_image(input_file_name, data);
00509 
00510     // write the code out.
00511     data->save_program_as(output_file_name, binary);
00512 
00513     return 0;
00514 }