From e746d9ee2faebaf6db8c007c0d5859454e3be3d7 Mon Sep 17 00:00:00 2001 From: Tim Date: Wed, 26 Oct 2022 09:22:53 +0200 Subject: [PATCH] Bumps version. --- .../behavior/movement/MovementDetector.java | 6 +- .../trophallaxis/TrophallaxisDetector.java | 774 +++++++++--------- .../btools/common/io/record/Converter.java | 198 ++--- .../btools/common/io/record/Indexer.java | 274 +++---- .../btools/tracking/bcode/BCodeDetector.java | 616 +++++++------- .../btools/tracking/bcode/BCodeMaker.java | 514 ++++++------ 6 files changed, 1191 insertions(+), 1191 deletions(-) diff --git a/src/edu/illinois/gernat/btools/behavior/movement/MovementDetector.java b/src/edu/illinois/gernat/btools/behavior/movement/MovementDetector.java index 32ef2e6..4c91973 100644 --- a/src/edu/illinois/gernat/btools/behavior/movement/MovementDetector.java +++ b/src/edu/illinois/gernat/btools/behavior/movement/MovementDetector.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 University of Illinois Board of Trustees. + * Copyright (C) 2021, 2022 University of Illinois Board of Trustees. * * This file is part of bTools. * @@ -93,8 +93,8 @@ private static void detectMovement(String bCodeDetectionFile, String movementFil private static void showVersionAndCopyright() { - System.out.println("Movement detector (bTools) 0.15.0"); - System.out.println("Copyright (C) 2017-2021 University of Illinois Board of Trustees"); + System.out.println("Movement detector (bTools) 0.15.1"); + System.out.println("Copyright (C) 2017-2022 University of Illinois Board of Trustees"); System.out.println("License AGPLv3+: GNU AGPL version 3 or later "); System.out.println("This is free software: you are free to change and redistribute it."); System.out.println("There is NO WARRANTY, to the extent permitted by law."); diff --git a/src/edu/illinois/gernat/btools/behavior/trophallaxis/TrophallaxisDetector.java b/src/edu/illinois/gernat/btools/behavior/trophallaxis/TrophallaxisDetector.java index dca6214..2531712 100644 --- a/src/edu/illinois/gernat/btools/behavior/trophallaxis/TrophallaxisDetector.java +++ b/src/edu/illinois/gernat/btools/behavior/trophallaxis/TrophallaxisDetector.java @@ -1,388 +1,388 @@ -/* - * Copyright (C) 2017, 2018, 2019, 2020, 2021 University of Illinois Board of - * Trustees. - * - * This file is part of bTools. - * - * bTools is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * bTools is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with bTools. If not, see http://www.gnu.org/licenses/. - */ - -package edu.illinois.gernat.btools.behavior.trophallaxis; - -import java.awt.image.BufferedImage; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.nio.file.Files; -import java.text.ParseException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.ListIterator; -import java.util.Map; -import java.util.Map.Entry; - -import javax.imageio.ImageIO; - -import edu.illinois.gernat.btools.behavior.trophallaxis.head.Head; -import edu.illinois.gernat.btools.behavior.trophallaxis.head.HeadDetector; -import edu.illinois.gernat.btools.behavior.trophallaxis.touch.Touch; -import edu.illinois.gernat.btools.behavior.trophallaxis.touch.TouchDetector; -import edu.illinois.gernat.btools.common.geometry.Coordinate; -import edu.illinois.gernat.btools.common.geometry.Vector; -import edu.illinois.gernat.btools.common.image.Images; -import edu.illinois.gernat.btools.common.io.record.IndexedReader; -import edu.illinois.gernat.btools.common.io.record.Record; -import edu.illinois.gernat.btools.common.io.token.TokenReader; -import edu.illinois.gernat.btools.common.io.token.TokenWriter; -import edu.illinois.gernat.btools.common.parameters.Parameters; - -public class TrophallaxisDetector -{ - - public static boolean isContact(Coordinate headCenter1, Coordinate headCenter2, Vector orientation1, Vector orientation2, int minDistance, int maxDistance, double maxAngleSum) - { - - // return true iff the distance between head center is within the - // specified range and the sum of the angles between a line connecting - // the head centers and the specified orientation vectors is smaller - // than the specified threshold - float distance = headCenter1.distanceTo(headCenter2); - if ((distance < minDistance) || (distance > maxDistance)) return false; - float angle1 = Math.abs(new Vector(headCenter1, headCenter2).angleBetween(orientation1)); - if (angle1 > maxAngleSum) return false; - float angle2 = Math.abs(new Vector(headCenter2, headCenter1).angleBetween(orientation2)); - if (angle1 + angle2 > maxAngleSum) return false; - return true; - - } - - public static List predictContacts(List records, int distanceLabelHead, int geometryMinDistance, int geometryMaxDistance, double geometryMaxAngleSum) - { - - // create contact for each pair of bees that meets the specified - // requirements - ArrayList contacts = new ArrayList(); - int recordCount = records.size(); - for (int i = 0; i < recordCount; i++) - { - Record record1 = records.get(i); - Coordinate headCenter1 = record1.orientation.clone().scale(distanceLabelHead).terminal(record1.center); - for (int j = i + 1; j < recordCount; j++) - { - Record record2 = records.get(j); - Coordinate headCenter2 = record2.orientation.clone().scale(distanceLabelHead).terminal(record2.center); - if (isContact(headCenter1, headCenter2, record1.orientation, record2.orientation, geometryMinDistance, geometryMaxDistance, geometryMaxAngleSum)) contacts.add(new Contact(record1.timestamp, record1.id, record2.id)); - } - } - return contacts; - - } - - private static Map predictHeads(BufferedImage image, List contacts, Map records) - { - - // predict position, orientation and shape of the heads of - // potentially interacting bees - HashSet contactRecords = new HashSet(); - for (Contact contact : contacts) - { - contactRecords.add(records.get(contact.id1)); - contactRecords.add(records.get(contact.id2)); - } - HashMap heads = new HashMap(); - for (Record record : contactRecords) heads.put(record.id, HeadDetector.detect(image, record.center, record.orientation)); - return heads; - - } - - private static void filterContacts(List contacts, Map heads, int visionMinDistance, int visionMaxDistance, double visionMaxAngleSum) - { - - // remove each contact for which the relative position and orientation - // of the two head predictions do not meet the specified requirements - ListIterator contactsIterator = contacts.listIterator(); - while (contactsIterator.hasNext()) - { - Contact contact = contactsIterator.next(); - Head head1 = heads.get(contact.id1); - Head head2 = heads.get(contact.id2); - if (!isContact(head1.center, head2.center, head1.orientation, head2.orientation, visionMinDistance, visionMaxDistance, visionMaxAngleSum)) contactsIterator.remove(); - } - - } - - private static void retainTouching(BufferedImage image, List contacts, Map heads) - { - - // remove all potential contacts for bees which are not touching - // each other - Iterator contactsIterator = contacts.iterator(); - while (contactsIterator.hasNext()) - { - Contact contact = contactsIterator.next(); - Touch touch = TouchDetector.detect(image, contact.id1, contact.id2, heads); - if (!touch.isTouching) contactsIterator.remove(); - } - - } - - public static void processImage(String[] args) throws IOException, ParseException - { - - // get arguments - Parameters.INSTANCE.initialize(args); - int distanceLabelHead = Parameters.INSTANCE.getInteger("distance.label.head"); - int geometryMinDistance = Parameters.INSTANCE.getInteger("geometry.min.distance"); - int geometryMaxDistance = Parameters.INSTANCE.getInteger("geometry.max.distance"); - double geometryMaxAngleSum = Math.toRadians(Parameters.INSTANCE.getInteger("geometry.max.angle.sum")); - int visionMinDistance = Parameters.INSTANCE.getInteger("vision.min.distance"); - int visionMaxDistance = Parameters.INSTANCE.getInteger("vision.max.distance"); - double visionMaxAngleSum = Math.toRadians(Parameters.INSTANCE.getInteger("vision.max.angle.sum")); - String filteredDataFile = Parameters.INSTANCE.getString("filtered.data.file"); - String imageFilename = Parameters.INSTANCE.getString("image.filename"); - String rawContactsFile = Parameters.INSTANCE.getString("trophallaxis.file"); - - // delete result file - File file = new File(rawContactsFile); - if (file.exists()) file.delete(); - - // read records corresponding to the image file - IndexedReader indexedReader = new IndexedReader(filteredDataFile); - long timestamp = Images.getTimestampFromFilename(imageFilename); - List records = indexedReader.readThis(timestamp); - indexedReader.close(); - if (records == null) - { - file.createNewFile(); - return; - } - - // predict contacts using the position and orientation of the labels - List contacts = predictContacts(records, distanceLabelHead, geometryMinDistance, geometryMaxDistance, geometryMaxAngleSum); - if (contacts.isEmpty()) - { - file.createNewFile(); - return; - } - - // predict position, orientation and shape of the heads of bees which - // might be engaged in a social contact - BufferedImage image = ImageIO.read(new File(imageFilename)); - HashMap recordLookupTable = new HashMap(); - for (Record record : records) recordLookupTable.put(record.id, record); - Map heads = predictHeads(image, contacts, recordLookupTable); - - // drop contacts for which the relative position and orientation of - // the head of the bees is not within the specified range - filterContacts(contacts, heads, visionMinDistance, visionMaxDistance, visionMaxAngleSum); - if (contacts.isEmpty()) - { - file.createNewFile(); - return; - } - - // predict if the bees potentially engaged in a social contact are - // touching each other's head with their antennae, feet, or tongue and - // filter out those which don't - retainTouching(image, contacts, heads); - if (contacts.isEmpty()) - { - file.createNewFile(); - return; - } - - // write contact data to file - String tmpFilename = rawContactsFile.substring(0, rawContactsFile.length() - 4) + ".tmp"; - TokenWriter tokenWriter = new TokenWriter(tmpFilename); - for (Contact contact : contacts) tokenWriter.writeTokens(contact); - tokenWriter.close(); - - // verify that contact data on disk is equal to the contact data in - // memory - int count = 0; - TokenReader tokenReader = new TokenReader(tmpFilename); - while (tokenReader.hasMoreLines()) - { - Contact contact = new Contact(tokenReader.readTokens()); - if (contact.equals(contacts.get(count))) count++; - else break; - } - tokenReader.close(); - - // delete temporary file if written contact data is different; - // otherwise rename to result file name - file = new File(tmpFilename); - if (count != contacts.size()) Files.delete(file.toPath()); - else file.renameTo(new File(rawContactsFile)); - - } - - private static void showVersionAndCopyright() - { - System.out.println("Trophallaxis Detector (bTools) 0.15.0"); - System.out.println("Copyright (C) 2017-2021 University of Illinois Board of Trustees"); - System.out.println("License AGPLv3+: GNU AGPL version 3 or later "); - System.out.println("This is free software: you are free to change and redistribute it."); - System.out.println("There is NO WARRANTY, to the extent permitted by law."); - } - - private static void showUsageInformation() - { - System.out.println("Usage: java -jar trophallaxis_detector.jar PARAMETER=VALUE..."); - System.out.println("Detect trophllaxis between honey bees."); - System.out.println(); - System.out.println("Parameters:"); - System.out.println("- contrast.threshold contrast threshold used during local thresholding"); - System.out.println("- distance.label.head average distance between the center of a bee's"); - System.out.println(" bCode and the center of her head"); - System.out.println("- filtered.data.file file containing the bCode detection results for"); - System.out.println(" the file named by the image.filename parameter."); - System.out.println(" Must be sorted by timestamp column"); - System.out.println("- geometry.max.angle.sum maximum sum of the angles between a line drawn"); - System.out.println(" between the geometrically predicted centers of the"); - System.out.println(" head of two potential interaction partners and the"); - System.out.println(" orientation vector of their labels"); - System.out.println("- geometry.max.distance maximum distance between the geometrically"); - System.out.println(" predicted centers of the head of two potential"); - System.out.println(" interaction partners"); - System.out.println("- geometry.min.distance minimum distance between the geometrically"); - System.out.println(" predicted centers of the head of two potential"); - System.out.println(" interaction partners"); - System.out.println("- image.filename input image file"); - System.out.println("- image.list.filename plain text file listing on each line one input"); - System.out.println(" image file."); - System.out.println("- leveling.threshold maximum allowed intensity of any pixel in the"); - System.out.println(" image. Pixels with a higher intensity will be set"); - System.out.println(" to this threshold"); - System.out.println("- max.path.thickness maximum thickness of a path connecting the heads"); - System.out.println(" of putative interaction partners"); - System.out.println("- max.thick.segment.length maximum length of the segments that may be thicker"); - System.out.println(" than max.path.thickness at the beginning and at"); - System.out.println(" the end of a path"); - System.out.println("- mean.head.pixel.intensity average expected intensity of bee head pixels"); - System.out.println("- show.credits set to \"true\" or 1 to display credits and exit"); - System.out.println("- thresholding.method name of the local thresholding method used for"); - System.out.println(" converting grayscale images to binary images. Must"); - System.out.println(" be 'Bernsen'"); - System.out.println("- trophallaxis.file output file containing the trophallaxis detections"); - System.out.println("- thresholding.radius radius of the neighborhood over which the local"); - System.out.println(" threshold will be computed"); - System.out.println("- vision.max.angle.sum maximum sum of the angles between a line"); - System.out.println(" connecting the computer-vision-predicted centers"); - System.out.println(" of the heads of two potential interaction partners"); - System.out.println(" and the orientation vectors of their heads"); - System.out.println("- vision.max.distance maximum distance between the"); - System.out.println(" computer-vision-predicted centers of the heads of"); - System.out.println(" two potential interaction partners"); - System.out.println("- vision.min.distance minimum distance between the"); - System.out.println(" computer-vision-predicted centers of the heads of"); - System.out.println(" two potential interaction partners"); - System.out.println(); - System.out.println("Notes:"); - System.out.println("Input image filenames need to be a valid date in the format"); - System.out.println("yyyy-MM-dd-HH-mm-ss-SSS."); - System.out.println(); - System.out.println("Parameters image.filename and trophallaxis.file cannot be specified in"); - System.out.println("conjunction with the image.list.filename parameter."); - System.out.println(); - System.out.println("If the image.list.filename parameter is given, trophallaxis output file names"); - System.out.println("are constructed by replacing the input image file extension with 'txt'. Input"); - System.out.println("image file name and extension must be separated by a dot."); - } - - private static void showCredits() throws IOException - { - showVersionAndCopyright(); - System.out.println(); - System.out.println("This software uses the following third party software that is distributed"); - System.out.println("under its own terms:"); - System.out.println(); - InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("LICENSE-3RD-PARTY"); - BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); - while (reader.ready()) System.out.println(reader.readLine()); - reader.close(); - inputStream.close(); - System.exit(1); - } - - public static void main(String[] args) throws IOException, ParseException - { - - // show version, copyright, and usage information if no arguments were - // given on the command line - if (args.length == 0) - { - showVersionAndCopyright(); - System.out.println(); - showUsageInformation(); - System.exit(1); - } - - // construct constant part of contact predictor argument array - String[] arguments = new String[args.length + 2]; - System.arraycopy(args, 0, arguments, 0, args.length); - - // get arguments - Parameters parameters = Parameters.INSTANCE; - parameters.initialize(args); - if ((parameters.exists("show.credits")) && (parameters.getBoolean("show.credits"))) showCredits(); - - // check arguments - if (parameters.exists("image.list.filename")) - { - if (parameters.exists("image.filename")) throw new IllegalStateException("image.list.filename cannot be specified together with image.filename."); - if (parameters.exists("trophallaxis.file")) throw new IllegalStateException("image.list.filename cannot be specified together with trophallaxis.file."); - } - - // create map from input file to output file - HashMap ioMap = new HashMap<>(); - if (!parameters.exists("image.list.filename")) ioMap.put(parameters.getString("image.filename"), parameters.getString("trophallaxis.file")); - else - { - String imageListFilename = parameters.getString("image.list.filename"); - BufferedReader reader = new BufferedReader(new FileReader(imageListFilename)); - while (reader.ready()) - { - String imageFileName = reader.readLine(); - String resultFileName = imageFileName.substring(0, imageFileName.lastIndexOf(".")) + ".txt"; - ioMap.put(imageFileName, resultFileName); - } - reader.close(); - } - - // process each input image - for (Entry entry : ioMap.entrySet()) - { - arguments[arguments.length - 2] = "image.filename=" + entry.getKey(); - arguments[arguments.length - 1] = "trophallaxis.file=" + entry.getValue(); - try - { - processImage(arguments); - } - catch (Exception e) - { - e.printStackTrace(); - System.err.println("Caused by file: " + entry.getKey()); - } - } - - } - +/* + * Copyright (C) 2017, 2018, 2019, 2020, 2021, 2022 University of Illinois + * Board of Trustees. + * + * This file is part of bTools. + * + * bTools is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * bTools is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with bTools. If not, see http://www.gnu.org/licenses/. + */ + +package edu.illinois.gernat.btools.behavior.trophallaxis; + +import java.awt.image.BufferedImage; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.file.Files; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.Map.Entry; + +import javax.imageio.ImageIO; + +import edu.illinois.gernat.btools.behavior.trophallaxis.head.Head; +import edu.illinois.gernat.btools.behavior.trophallaxis.head.HeadDetector; +import edu.illinois.gernat.btools.behavior.trophallaxis.touch.Touch; +import edu.illinois.gernat.btools.behavior.trophallaxis.touch.TouchDetector; +import edu.illinois.gernat.btools.common.geometry.Coordinate; +import edu.illinois.gernat.btools.common.geometry.Vector; +import edu.illinois.gernat.btools.common.image.Images; +import edu.illinois.gernat.btools.common.io.record.IndexedReader; +import edu.illinois.gernat.btools.common.io.record.Record; +import edu.illinois.gernat.btools.common.io.token.TokenReader; +import edu.illinois.gernat.btools.common.io.token.TokenWriter; +import edu.illinois.gernat.btools.common.parameters.Parameters; + +public class TrophallaxisDetector +{ + + public static boolean isContact(Coordinate headCenter1, Coordinate headCenter2, Vector orientation1, Vector orientation2, int minDistance, int maxDistance, double maxAngleSum) + { + + // return true iff the distance between head center is within the + // specified range and the sum of the angles between a line connecting + // the head centers and the specified orientation vectors is smaller + // than the specified threshold + float distance = headCenter1.distanceTo(headCenter2); + if ((distance < minDistance) || (distance > maxDistance)) return false; + float angle1 = Math.abs(new Vector(headCenter1, headCenter2).angleBetween(orientation1)); + if (angle1 > maxAngleSum) return false; + float angle2 = Math.abs(new Vector(headCenter2, headCenter1).angleBetween(orientation2)); + if (angle1 + angle2 > maxAngleSum) return false; + return true; + + } + + public static List predictContacts(List records, int distanceLabelHead, int geometryMinDistance, int geometryMaxDistance, double geometryMaxAngleSum) + { + + // create contact for each pair of bees that meets the specified + // requirements + ArrayList contacts = new ArrayList(); + int recordCount = records.size(); + for (int i = 0; i < recordCount; i++) + { + Record record1 = records.get(i); + Coordinate headCenter1 = record1.orientation.clone().scale(distanceLabelHead).terminal(record1.center); + for (int j = i + 1; j < recordCount; j++) + { + Record record2 = records.get(j); + Coordinate headCenter2 = record2.orientation.clone().scale(distanceLabelHead).terminal(record2.center); + if (isContact(headCenter1, headCenter2, record1.orientation, record2.orientation, geometryMinDistance, geometryMaxDistance, geometryMaxAngleSum)) contacts.add(new Contact(record1.timestamp, record1.id, record2.id)); + } + } + return contacts; + + } + + private static Map predictHeads(BufferedImage image, List contacts, Map records) + { + + // predict position, orientation and shape of the heads of + // potentially interacting bees + HashSet contactRecords = new HashSet(); + for (Contact contact : contacts) + { + contactRecords.add(records.get(contact.id1)); + contactRecords.add(records.get(contact.id2)); + } + HashMap heads = new HashMap(); + for (Record record : contactRecords) heads.put(record.id, HeadDetector.detect(image, record.center, record.orientation)); + return heads; + + } + + private static void filterContacts(List contacts, Map heads, int visionMinDistance, int visionMaxDistance, double visionMaxAngleSum) + { + + // remove each contact for which the relative position and orientation + // of the two head predictions do not meet the specified requirements + ListIterator contactsIterator = contacts.listIterator(); + while (contactsIterator.hasNext()) + { + Contact contact = contactsIterator.next(); + Head head1 = heads.get(contact.id1); + Head head2 = heads.get(contact.id2); + if (!isContact(head1.center, head2.center, head1.orientation, head2.orientation, visionMinDistance, visionMaxDistance, visionMaxAngleSum)) contactsIterator.remove(); + } + + } + + private static void retainTouching(BufferedImage image, List contacts, Map heads) + { + + // remove all potential contacts for bees which are not touching + // each other + Iterator contactsIterator = contacts.iterator(); + while (contactsIterator.hasNext()) + { + Contact contact = contactsIterator.next(); + Touch touch = TouchDetector.detect(image, contact.id1, contact.id2, heads); + if (!touch.isTouching) contactsIterator.remove(); + } + + } + + public static void processImage(String[] args) throws IOException, ParseException + { + + // get arguments + Parameters.INSTANCE.initialize(args); + int distanceLabelHead = Parameters.INSTANCE.getInteger("distance.label.head"); + int geometryMinDistance = Parameters.INSTANCE.getInteger("geometry.min.distance"); + int geometryMaxDistance = Parameters.INSTANCE.getInteger("geometry.max.distance"); + double geometryMaxAngleSum = Math.toRadians(Parameters.INSTANCE.getInteger("geometry.max.angle.sum")); + int visionMinDistance = Parameters.INSTANCE.getInteger("vision.min.distance"); + int visionMaxDistance = Parameters.INSTANCE.getInteger("vision.max.distance"); + double visionMaxAngleSum = Math.toRadians(Parameters.INSTANCE.getInteger("vision.max.angle.sum")); + String filteredDataFile = Parameters.INSTANCE.getString("filtered.data.file"); + String imageFilename = Parameters.INSTANCE.getString("image.filename"); + String rawContactsFile = Parameters.INSTANCE.getString("trophallaxis.file"); + + // delete result file + File file = new File(rawContactsFile); + if (file.exists()) file.delete(); + + // read records corresponding to the image file + IndexedReader indexedReader = new IndexedReader(filteredDataFile); + long timestamp = Images.getTimestampFromFilename(imageFilename); + List records = indexedReader.readThis(timestamp); + indexedReader.close(); + if (records == null) + { + file.createNewFile(); + return; + } + + // predict contacts using the position and orientation of the labels + List contacts = predictContacts(records, distanceLabelHead, geometryMinDistance, geometryMaxDistance, geometryMaxAngleSum); + if (contacts.isEmpty()) + { + file.createNewFile(); + return; + } + + // predict position, orientation and shape of the heads of bees which + // might be engaged in a social contact + BufferedImage image = ImageIO.read(new File(imageFilename)); + HashMap recordLookupTable = new HashMap(); + for (Record record : records) recordLookupTable.put(record.id, record); + Map heads = predictHeads(image, contacts, recordLookupTable); + + // drop contacts for which the relative position and orientation of + // the head of the bees is not within the specified range + filterContacts(contacts, heads, visionMinDistance, visionMaxDistance, visionMaxAngleSum); + if (contacts.isEmpty()) + { + file.createNewFile(); + return; + } + + // predict if the bees potentially engaged in a social contact are + // touching each other's head with their antennae, feet, or tongue and + // filter out those which don't + retainTouching(image, contacts, heads); + if (contacts.isEmpty()) + { + file.createNewFile(); + return; + } + + // write contact data to file + String tmpFilename = rawContactsFile.substring(0, rawContactsFile.length() - 4) + ".tmp"; + TokenWriter tokenWriter = new TokenWriter(tmpFilename); + for (Contact contact : contacts) tokenWriter.writeTokens(contact); + tokenWriter.close(); + + // verify that contact data on disk is equal to the contact data in + // memory + int count = 0; + TokenReader tokenReader = new TokenReader(tmpFilename); + while (tokenReader.hasMoreLines()) + { + Contact contact = new Contact(tokenReader.readTokens()); + if (contact.equals(contacts.get(count))) count++; + else break; + } + tokenReader.close(); + + // delete temporary file if written contact data is different; + // otherwise rename to result file name + file = new File(tmpFilename); + if (count != contacts.size()) Files.delete(file.toPath()); + else file.renameTo(new File(rawContactsFile)); + + } + + private static void showVersionAndCopyright() + { + System.out.println("Trophallaxis Detector (bTools) 0.15.1"); + System.out.println("Copyright (C) 2017-2022 University of Illinois Board of Trustees"); + System.out.println("License AGPLv3+: GNU AGPL version 3 or later "); + System.out.println("This is free software: you are free to change and redistribute it."); + System.out.println("There is NO WARRANTY, to the extent permitted by law."); + } + + private static void showUsageInformation() + { + System.out.println("Usage: java -jar trophallaxis_detector.jar PARAMETER=VALUE..."); + System.out.println("Detect trophllaxis between honey bees."); + System.out.println(); + System.out.println("Parameters:"); + System.out.println("- contrast.threshold contrast threshold used during local thresholding"); + System.out.println("- distance.label.head average distance between the center of a bee's"); + System.out.println(" bCode and the center of her head"); + System.out.println("- filtered.data.file file containing the bCode detection results for"); + System.out.println(" the file named by the image.filename parameter."); + System.out.println(" Must be sorted by timestamp column"); + System.out.println("- geometry.max.angle.sum maximum sum of the angles between a line drawn"); + System.out.println(" between the geometrically predicted centers of the"); + System.out.println(" head of two potential interaction partners and the"); + System.out.println(" orientation vector of their labels"); + System.out.println("- geometry.max.distance maximum distance between the geometrically"); + System.out.println(" predicted centers of the head of two potential"); + System.out.println(" interaction partners"); + System.out.println("- geometry.min.distance minimum distance between the geometrically"); + System.out.println(" predicted centers of the head of two potential"); + System.out.println(" interaction partners"); + System.out.println("- image.filename input image file"); + System.out.println("- image.list.filename plain text file listing on each line one input"); + System.out.println(" image file."); + System.out.println("- leveling.threshold maximum allowed intensity of any pixel in the"); + System.out.println(" image. Pixels with a higher intensity will be set"); + System.out.println(" to this threshold"); + System.out.println("- max.path.thickness maximum thickness of a path connecting the heads"); + System.out.println(" of putative interaction partners"); + System.out.println("- max.thick.segment.length maximum length of the segments that may be thicker"); + System.out.println(" than max.path.thickness at the beginning and at"); + System.out.println(" the end of a path"); + System.out.println("- mean.head.pixel.intensity average expected intensity of bee head pixels"); + System.out.println("- show.credits set to \"true\" or 1 to display credits and exit"); + System.out.println("- thresholding.method name of the local thresholding method used for"); + System.out.println(" converting grayscale images to binary images. Must"); + System.out.println(" be 'Bernsen'"); + System.out.println("- trophallaxis.file output file containing the trophallaxis detections"); + System.out.println("- thresholding.radius radius of the neighborhood over which the local"); + System.out.println(" threshold will be computed"); + System.out.println("- vision.max.angle.sum maximum sum of the angles between a line"); + System.out.println(" connecting the computer-vision-predicted centers"); + System.out.println(" of the heads of two potential interaction partners"); + System.out.println(" and the orientation vectors of their heads"); + System.out.println("- vision.max.distance maximum distance between the"); + System.out.println(" computer-vision-predicted centers of the heads of"); + System.out.println(" two potential interaction partners"); + System.out.println("- vision.min.distance minimum distance between the"); + System.out.println(" computer-vision-predicted centers of the heads of"); + System.out.println(" two potential interaction partners"); + System.out.println(); + System.out.println("Notes:"); + System.out.println("Input image filenames need to be a valid date in the format"); + System.out.println("yyyy-MM-dd-HH-mm-ss-SSS."); + System.out.println(); + System.out.println("Parameters image.filename and trophallaxis.file cannot be specified in"); + System.out.println("conjunction with the image.list.filename parameter."); + System.out.println(); + System.out.println("If the image.list.filename parameter is given, trophallaxis output file names"); + System.out.println("are constructed by replacing the input image file extension with 'txt'. Input"); + System.out.println("image file name and extension must be separated by a dot."); + } + + private static void showCredits() throws IOException + { + showVersionAndCopyright(); + System.out.println(); + System.out.println("This software uses the following third party software that is distributed"); + System.out.println("under its own terms:"); + System.out.println(); + InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("LICENSE-3RD-PARTY"); + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); + while (reader.ready()) System.out.println(reader.readLine()); + reader.close(); + inputStream.close(); + System.exit(1); + } + + public static void main(String[] args) throws IOException, ParseException + { + + // show version, copyright, and usage information if no arguments were + // given on the command line + if (args.length == 0) + { + showVersionAndCopyright(); + System.out.println(); + showUsageInformation(); + System.exit(1); + } + + // construct constant part of contact predictor argument array + String[] arguments = new String[args.length + 2]; + System.arraycopy(args, 0, arguments, 0, args.length); + + // get arguments + Parameters parameters = Parameters.INSTANCE; + parameters.initialize(args); + if ((parameters.exists("show.credits")) && (parameters.getBoolean("show.credits"))) showCredits(); + + // check arguments + if (parameters.exists("image.list.filename")) + { + if (parameters.exists("image.filename")) throw new IllegalStateException("image.list.filename cannot be specified together with image.filename."); + if (parameters.exists("trophallaxis.file")) throw new IllegalStateException("image.list.filename cannot be specified together with trophallaxis.file."); + } + + // create map from input file to output file + HashMap ioMap = new HashMap<>(); + if (!parameters.exists("image.list.filename")) ioMap.put(parameters.getString("image.filename"), parameters.getString("trophallaxis.file")); + else + { + String imageListFilename = parameters.getString("image.list.filename"); + BufferedReader reader = new BufferedReader(new FileReader(imageListFilename)); + while (reader.ready()) + { + String imageFileName = reader.readLine(); + String resultFileName = imageFileName.substring(0, imageFileName.lastIndexOf(".")) + ".txt"; + ioMap.put(imageFileName, resultFileName); + } + reader.close(); + } + + // process each input image + for (Entry entry : ioMap.entrySet()) + { + arguments[arguments.length - 2] = "image.filename=" + entry.getKey(); + arguments[arguments.length - 1] = "trophallaxis.file=" + entry.getValue(); + try + { + processImage(arguments); + } + catch (Exception e) + { + e.printStackTrace(); + System.err.println("Caused by file: " + entry.getKey()); + } + } + + } + } \ No newline at end of file diff --git a/src/edu/illinois/gernat/btools/common/io/record/Converter.java b/src/edu/illinois/gernat/btools/common/io/record/Converter.java index e2e342a..a55a071 100644 --- a/src/edu/illinois/gernat/btools/common/io/record/Converter.java +++ b/src/edu/illinois/gernat/btools/common/io/record/Converter.java @@ -1,99 +1,99 @@ -/* - * Copyright (C) 2017, 2018, 2019, 2020, 2021 University of Illinois Board of - * Trustees. - * - * This file is part of bTools. - * - * bTools is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * bTools is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with bTools. If not, see http://www.gnu.org/licenses/. - */ - -package edu.illinois.gernat.btools.common.io.record; - -import java.io.IOException; - -import edu.illinois.gernat.btools.common.io.token.TokenWriter; -import edu.illinois.gernat.btools.common.parameters.Parameters; - -public class Converter -{ - - public static final int FIELD_TIMESTAMP = 0; - - public static final int FIELD_X = 1; - - public static final int FIELD_Y = 2; - - public static final int FIELD_DX = 3; - - public static final int FIELD_DY = 4; - - public static final int FIELD_BEE_ID = 5; - - private static void toPlainText(String sourceFileName, String destinationFileName) throws IOException - { - RecordReader reader = new RecordReader(sourceFileName); - TokenWriter writer = new TokenWriter(destinationFileName, ","); - while (reader.hasMoreRecords()) - { - Record record = reader.readRecord(); - writer.writeTokens(record.timestamp, record.center.x, record.center.y, record.orientation.dx, record.orientation.dy, record.id); - } - writer.close(); - reader.close(); - } - - private static void showVersionAndCopyright() - { - System.out.println("Converter (bTools) 0.15.0"); - System.out.println("Copyright (C) 2017-2021 University of Illinois Board of Trustees"); - System.out.println("License AGPLv3+: GNU AGPL version 3 or later "); - System.out.println("This is free software: you are free to change and redistribute it."); - System.out.println("There is NO WARRANTY, to the extent permitted by law."); - } - - private static void showUsageInformation() - { - System.out.println("Usage: java -jar converter.jar PARAMETER=VALUE..."); - System.out.println("Convert raw bCode detections to human-readable plain text."); - System.out.println(); - System.out.println("Parameters:"); - System.out.println("- human.readable.file the human-readable output file"); - System.out.println("- raw.bCode.file the raw input file"); - } - - public static void main(String[] args) throws IOException - { - - // show version, copyright, and usage information if no arguments were - // given on the command line - if (args.length == 0) - { - showVersionAndCopyright(); - System.out.println(); - showUsageInformation(); - System.exit(1); - } - - // get parameters - Parameters parameters = Parameters.INSTANCE; - parameters.initialize(args); - String rawBCodeFile = parameters.getString("raw.bCode.file"); - String humanReadableFile = parameters.getString("human.readable.file"); - - // convert raw data - toPlainText(rawBCodeFile, humanReadableFile); - - } - -} +/* + * Copyright (C) 2017, 2018, 2019, 2020, 2021, 2022 University of Illinois + * Board of Trustees. + * + * This file is part of bTools. + * + * bTools is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * bTools is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with bTools. If not, see http://www.gnu.org/licenses/. + */ + +package edu.illinois.gernat.btools.common.io.record; + +import java.io.IOException; + +import edu.illinois.gernat.btools.common.io.token.TokenWriter; +import edu.illinois.gernat.btools.common.parameters.Parameters; + +public class Converter +{ + + public static final int FIELD_TIMESTAMP = 0; + + public static final int FIELD_X = 1; + + public static final int FIELD_Y = 2; + + public static final int FIELD_DX = 3; + + public static final int FIELD_DY = 4; + + public static final int FIELD_BEE_ID = 5; + + private static void toPlainText(String sourceFileName, String destinationFileName) throws IOException + { + RecordReader reader = new RecordReader(sourceFileName); + TokenWriter writer = new TokenWriter(destinationFileName, ","); + while (reader.hasMoreRecords()) + { + Record record = reader.readRecord(); + writer.writeTokens(record.timestamp, record.center.x, record.center.y, record.orientation.dx, record.orientation.dy, record.id); + } + writer.close(); + reader.close(); + } + + private static void showVersionAndCopyright() + { + System.out.println("Converter (bTools) 0.15.1"); + System.out.println("Copyright (C) 2017-2022 University of Illinois Board of Trustees"); + System.out.println("License AGPLv3+: GNU AGPL version 3 or later "); + System.out.println("This is free software: you are free to change and redistribute it."); + System.out.println("There is NO WARRANTY, to the extent permitted by law."); + } + + private static void showUsageInformation() + { + System.out.println("Usage: java -jar converter.jar PARAMETER=VALUE..."); + System.out.println("Convert raw bCode detections to human-readable plain text."); + System.out.println(); + System.out.println("Parameters:"); + System.out.println("- human.readable.file the human-readable output file"); + System.out.println("- raw.bCode.file the raw input file"); + } + + public static void main(String[] args) throws IOException + { + + // show version, copyright, and usage information if no arguments were + // given on the command line + if (args.length == 0) + { + showVersionAndCopyright(); + System.out.println(); + showUsageInformation(); + System.exit(1); + } + + // get parameters + Parameters parameters = Parameters.INSTANCE; + parameters.initialize(args); + String rawBCodeFile = parameters.getString("raw.bCode.file"); + String humanReadableFile = parameters.getString("human.readable.file"); + + // convert raw data + toPlainText(rawBCodeFile, humanReadableFile); + + } + +} diff --git a/src/edu/illinois/gernat/btools/common/io/record/Indexer.java b/src/edu/illinois/gernat/btools/common/io/record/Indexer.java index 98fb691..78a8317 100644 --- a/src/edu/illinois/gernat/btools/common/io/record/Indexer.java +++ b/src/edu/illinois/gernat/btools/common/io/record/Indexer.java @@ -1,137 +1,137 @@ -/* - * Copyright (C) 2017, 2018, 2019, 2020, 2021 University of Illinois Board of - * Trustees. - * - * This file is part of bTools. - * - * bTools is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * bTools is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with bTools. If not, see http://www.gnu.org/licenses/. - */ - -package edu.illinois.gernat.btools.common.io.record; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileReader; -import java.io.IOException; - -import edu.illinois.gernat.btools.common.io.token.TokenWriter; -import edu.illinois.gernat.btools.common.parameters.Parameters; - -public class Indexer -{ - - private static final String INDEX_FILE_EXTENSION = ".idx"; - - public static String getIndexFilenameFor(String filename) - { - return filename.substring(0, filename.lastIndexOf(".")) + INDEX_FILE_EXTENSION; - } - - public static void index(String recordFileName, int eolByteCount) throws IOException - { - BufferedReader reader = new BufferedReader(new FileReader(new File(recordFileName)), 1024000); - TokenWriter writer = new TokenWriter(getIndexFilenameFor(recordFileName), ","); - String line = null; - long position = 0; - String tMinus1 = ""; - while ((line = reader.readLine()) != null) - { - String t = line.split(",", -1)[Record.TIMESTAMP]; - if (!t.equals(tMinus1)) - { - writer.writeTokens(t, position); - tMinus1 = t; - } - position += line.length() + eolByteCount; - } - writer.close(); - reader.close(); - } - - private static void showVersionAndCopyright() - { - System.out.println("Indexer (bTools) 0.15.0"); - System.out.println("Copyright (C) 2017-2021 University of Illinois Board of Trustees"); - System.out.println("License AGPLv3+: GNU AGPL version 3 or later "); - System.out.println("This is free software: you are free to change and redistribute it."); - System.out.println("There is NO WARRANTY, to the extent permitted by law."); - } - - private static void showUsageInformation() - { - System.out.println("Usage: java -jar indexer.jar PARAMETER=VALUE..."); - System.out.println("Index bCode detection results."); - System.out.println(); - System.out.println("Parameters:"); - System.out.println("- file the file to be indexed"); - } - - public static int determineLineSeparatorLength(String file) throws IOException - { - char c; - FileInputStream fis = new FileInputStream(file); - try - { - while (fis.available() > 0) - { - c = (char) fis.read(); - if (c == '\n') return 1; - if (c == '\r') - { - if (fis.available() > 0) - { - c = (char) fis.read(); - if (c == '\n') return 2; - else return 1; - } - return 1; - } - } - } - finally - { - if (fis!=null) fis.close(); - } - return -1; - } - - public static void main(String[] args) throws NumberFormatException, IOException - { - - // show version, copyright, and usage information if no arguments were - // given on the command line - if (args.length == 0) - { - showVersionAndCopyright(); - System.out.println(); - showUsageInformation(); - System.exit(1); - } - - // get arguments - Parameters parameters = Parameters.INSTANCE; - parameters.initialize(args); - String file = parameters.getString("file"); - - // determine line separator size - int eolByteCount = determineLineSeparatorLength(file); - if (eolByteCount == -1) throw new IllegalStateException("Could not determine line separator size."); - - // index file - Indexer.index(file, eolByteCount); - - } - -} +/* + * Copyright (C) 2017, 2018, 2019, 2020, 2021, 2022 University of Illinois + * Board of Trustees. + * + * This file is part of bTools. + * + * bTools is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * bTools is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with bTools. If not, see http://www.gnu.org/licenses/. + */ + +package edu.illinois.gernat.btools.common.io.record; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileReader; +import java.io.IOException; + +import edu.illinois.gernat.btools.common.io.token.TokenWriter; +import edu.illinois.gernat.btools.common.parameters.Parameters; + +public class Indexer +{ + + private static final String INDEX_FILE_EXTENSION = ".idx"; + + public static String getIndexFilenameFor(String filename) + { + return filename.substring(0, filename.lastIndexOf(".")) + INDEX_FILE_EXTENSION; + } + + public static void index(String recordFileName, int eolByteCount) throws IOException + { + BufferedReader reader = new BufferedReader(new FileReader(new File(recordFileName)), 1024000); + TokenWriter writer = new TokenWriter(getIndexFilenameFor(recordFileName), ","); + String line = null; + long position = 0; + String tMinus1 = ""; + while ((line = reader.readLine()) != null) + { + String t = line.split(",", -1)[Record.TIMESTAMP]; + if (!t.equals(tMinus1)) + { + writer.writeTokens(t, position); + tMinus1 = t; + } + position += line.length() + eolByteCount; + } + writer.close(); + reader.close(); + } + + private static void showVersionAndCopyright() + { + System.out.println("Indexer (bTools) 0.15.1"); + System.out.println("Copyright (C) 2017-2022 University of Illinois Board of Trustees"); + System.out.println("License AGPLv3+: GNU AGPL version 3 or later "); + System.out.println("This is free software: you are free to change and redistribute it."); + System.out.println("There is NO WARRANTY, to the extent permitted by law."); + } + + private static void showUsageInformation() + { + System.out.println("Usage: java -jar indexer.jar PARAMETER=VALUE..."); + System.out.println("Index bCode detection results."); + System.out.println(); + System.out.println("Parameters:"); + System.out.println("- file the file to be indexed"); + } + + public static int determineLineSeparatorLength(String file) throws IOException + { + char c; + FileInputStream fis = new FileInputStream(file); + try + { + while (fis.available() > 0) + { + c = (char) fis.read(); + if (c == '\n') return 1; + if (c == '\r') + { + if (fis.available() > 0) + { + c = (char) fis.read(); + if (c == '\n') return 2; + else return 1; + } + return 1; + } + } + } + finally + { + if (fis!=null) fis.close(); + } + return -1; + } + + public static void main(String[] args) throws NumberFormatException, IOException + { + + // show version, copyright, and usage information if no arguments were + // given on the command line + if (args.length == 0) + { + showVersionAndCopyright(); + System.out.println(); + showUsageInformation(); + System.exit(1); + } + + // get arguments + Parameters parameters = Parameters.INSTANCE; + parameters.initialize(args); + String file = parameters.getString("file"); + + // determine line separator size + int eolByteCount = determineLineSeparatorLength(file); + if (eolByteCount == -1) throw new IllegalStateException("Could not determine line separator size."); + + // index file + Indexer.index(file, eolByteCount); + + } + +} diff --git a/src/edu/illinois/gernat/btools/tracking/bcode/BCodeDetector.java b/src/edu/illinois/gernat/btools/tracking/bcode/BCodeDetector.java index 347ba40..0369a3d 100644 --- a/src/edu/illinois/gernat/btools/tracking/bcode/BCodeDetector.java +++ b/src/edu/illinois/gernat/btools/tracking/bcode/BCodeDetector.java @@ -1,309 +1,309 @@ -/* - * Copyright (C) 2017, 2018, 2019, 2020, 2021 University of Illinois Board of - * Trustees. - * - * This file is part of bTools. - * - * bTools is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * bTools is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with bTools. If not, see http://www.gnu.org/licenses/. - */ - -package edu.illinois.gernat.btools.tracking.bcode; - -import java.awt.image.BufferedImage; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.text.ParseException; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; - -import javax.imageio.ImageIO; - -import org.bytedeco.ffmpeg.global.avutil; -import org.bytedeco.javacv.FFmpegFrameGrabber; -import org.bytedeco.javacv.Frame; -import org.bytedeco.javacv.FrameGrabber.Exception; -import org.bytedeco.javacv.Java2DFrameConverter; - -import com.google.zxing.NotFoundException; - -import edu.illinois.gernat.btools.common.image.Images; -import edu.illinois.gernat.btools.common.io.record.Record; -import edu.illinois.gernat.btools.common.io.record.RecordWriter; -import edu.illinois.gernat.btools.common.parameters.Parameters; - -public class BCodeDetector -{ - - private static LinkedHashMap> processImage(String inputFilename) throws IOException, ParseException - { - BufferedImage image = ImageIO.read(new File(inputFilename)); - List bCodes = detectBCodesIn(image); - long timestamp = Images.getTimestampFromFilename(inputFilename); - LinkedHashMap> result = new LinkedHashMap<>(); - result.put(timestamp, bCodes); - return result; - } - - private static LinkedHashMap> processVideo(String inputFilename, int frameRate) throws ParseException, IOException - { - - // check parameters - if (frameRate == -1) throw new IllegalStateException(); - - // initialize frame grabber and converter - avutil.av_log_set_level(avutil.AV_LOG_QUIET); - FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(inputFilename); - grabber.setVideoOption("threads", "1"); - grabber.start(); - Java2DFrameConverter converter = new Java2DFrameConverter(); - - // initialize bCode detection loop - LinkedHashMap> result = new LinkedHashMap<>(); - long timestamp = Images.getTimestampFromFilename(inputFilename); - int frameNumber = 0; - Frame frame = grabber.grab(); - - // loop over and process video frames - while (frame != null) - { - - // convert frame - BufferedImage image = converter.convert(frame); - - // detect bCodes - List bCodes = detectBCodesIn(image); - result.put(timestamp + Math.round(frameNumber * 1000d / frameRate), bCodes); - - // load next frame - frame = grabber.grab(); - frameNumber++; - - } - - // dispose of frame grabber - grabber.stop(); - grabber.close(); - grabber.release(); - - // done - return result; - - } - - private static List detectBCodesIn(BufferedImage image) - { - - // preprocess image - image = Preprocessor.preprocess(image); - - // detect IDs - List metaIDs = Reader.read(image); - - // postprocess bCode detections - if (Preprocessor.scalingFactor != 1) - { - for (MetaCode metaID : metaIDs) - { - metaID.center.set(metaID.center.x / Preprocessor.scalingFactor, metaID.center.y / Preprocessor.scalingFactor); - metaID.moduleSize /= Preprocessor.scalingFactor; - metaID.nw.set(metaID.nw.x / Preprocessor.scalingFactor, metaID.nw.y / Preprocessor.scalingFactor); - metaID.ne.set(metaID.ne.x / Preprocessor.scalingFactor, metaID.ne.y / Preprocessor.scalingFactor); - metaID.sw.set(metaID.sw.x / Preprocessor.scalingFactor, metaID.sw.y / Preprocessor.scalingFactor); - metaID.se.set(metaID.se.x / Preprocessor.scalingFactor, metaID.se.y / Preprocessor.scalingFactor); - } - } - - // done - return metaIDs; - - } - - private static void showVersionAndCopyright() - { - System.out.println("bCode Detector (bTools) 0.15.0"); - System.out.println("Copyright (C) 2017-2021 University of Illinois Board of Trustees"); - System.out.println("License AGPLv3+: GNU AGPL version 3 or later "); - System.out.println("This is free software: you are free to change and redistribute it."); - System.out.println("There is NO WARRANTY, to the extent permitted by law."); - } - - private static void showUsageInformation() - { - System.out.println("Usage: java -jar bcode_detector.jar PARAMETER=VALUE..."); - System.out.println("Detect bCodes in images or videos."); - System.out.println(); - System.out.println("Parameters:"); - System.out.println("- conserve.margin whether the bCode border is considered to be part"); - System.out.println(" of the bCode template"); - System.out.println("- frame.rate frame rate of any videos to be processed"); - System.out.println("- scaling.factor factor for image scaling prior to detecting bCodes"); - System.out.println("- input.file the input image, video, or plain text file"); - System.out.println("- intensity.step.size increment when going from the lowest to the highest"); - System.out.println(" intensity threshold"); - System.out.println("- max.intensity.threshold highest intensity threshold for converting to a"); - System.out.println(" binary image"); - System.out.println("- min.intensity.threshold lowest intensity threshold for converting to a"); - System.out.println(" binary image"); - System.out.println("- min.template.conservation fraction of bCode modules that need to match the"); - System.out.println(" bCode template"); - System.out.println("- sharpening.amount amount of sharpening during unsharp masking"); - System.out.println("- sharpening.sigma Gaussian blur standard deviation for unsharp"); - System.out.println(" masking"); - System.out.println("- show.credits set to \"true\" or 1 to display credits and exit"); - System.out.println(); - System.out.println("Notes:"); - System.out.println("If the input.file is a plain text file, this file must list one image or."); - System.out.println("video file per line"); - System.out.println(""); - System.out.println("Image and video file names need to be a valid date in the format"); - System.out.println("yyyy-MM-dd-HH-mm-ss-SSS. File name and extension must be separated by a dot."); - System.out.println("Output file names are constructed by replacing the input image file"); - System.out.println("extension with 'txt'."); - System.out.println(); - System.out.println("When processing videos, a constant frame rate if assumed when calculating"); - System.out.println("timestamps from the date encoded by the video file name and the frame number."); - } - - private static void showCredits() throws IOException - { - showVersionAndCopyright(); - System.out.println(); - System.out.println("This software uses the following third party software that is distributed"); - System.out.println("under its own terms:"); - System.out.println(); - InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("LICENSE-3RD-PARTY"); - BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); - while (reader.ready()) System.out.println(reader.readLine()); - reader.close(); - inputStream.close(); - } - - public static void main(String[] args) throws NotFoundException, IOException, ParseException - { - - // show version, copyright, and usage information if no arguments were - // given on the command line - if (args.length == 0) - { - showVersionAndCopyright(); - System.out.println(); - showUsageInformation(); - System.exit(1); - } - - // get arguments - Parameters parameters = Parameters.INSTANCE; - parameters.initialize(args); - - // if requested, show credits and exit - if ((parameters.exists("show.credits")) && (parameters.getBoolean("show.credits"))) - { - showCredits(); - System.exit(1); - } - - // set image processing parameters - Preprocessor.sharpeningSigma = parameters.getDouble("sharpening.sigma"); - Preprocessor.sharpeningAmount = parameters.getDouble("sharpening.amount"); - Preprocessor.scalingFactor = (float) parameters.getDouble("scaling.factor"); - Reader.minBlackThreshold = parameters.getInteger("min.intensity.threshold"); - Reader.maxBlackThreshold = parameters.getInteger("max.intensity.threshold"); - Reader.thresholdStepSize = parameters.getInteger("intensity.step.size"); - Detector.minTemplateConservation = parameters.getDouble("min.template.conservation"); - Detector.checkMargin = parameters.getBoolean("conserve.margin"); - - // map input files to output files - HashMap ioMap = mapInputToOutput(parameters.getString("input.file")); - - // set frame rate parameter, if necessary - int frameRate = -1; - for (String inputFilename : ioMap.keySet()) - { - if (inputFilename.endsWith(".h264") || inputFilename.endsWith(".mp4")) - { - frameRate = parameters.getInteger("frame.rate"); - break; - } - } - - // process each input file - processInputFiles(ioMap, frameRate); - - } - - private static void processInputFiles(HashMap ioMap, int frameRate) throws IOException, ParseException - { - for (String inputFilename : ioMap.keySet()) - { - - // delete output file, if it exists - String outputFilename = ioMap.get(inputFilename); - File outputFile = new File(outputFilename); - if (outputFile.exists()) outputFile.delete(); - - // detect bCodes in input file - LinkedHashMap> bCodeDetectionResults = null; - try - { - if (inputFilename.endsWith(".jpg") || inputFilename.endsWith(".png")) bCodeDetectionResults = processImage(inputFilename); - else if (inputFilename.endsWith(".h264") || inputFilename.endsWith(".mp4")) bCodeDetectionResults = processVideo(inputFilename, frameRate); - } - catch (Exception e) - { - e.printStackTrace(); - System.err.println("Caused by file: " + inputFilename); - } - - // write bCode detections to file - RecordWriter writer = new RecordWriter(outputFilename); - for (Long timestamp : bCodeDetectionResults.keySet()) - { - List bCodes = bCodeDetectionResults.get(timestamp); - for (MetaCode metaCode : bCodes) - { - Record record = new Record(timestamp, metaCode.data, metaCode.nw, metaCode.ne, metaCode.sw, metaCode.support, metaCode.errorCorrectionCount); - record.roundPatternCoordinates(); - writer.writeRecord(record); - } - } - writer.close(); - - } - } - - private static HashMap mapInputToOutput(String inputFilename) throws IOException - { - HashMap ioMap = new HashMap<>(); - if (!inputFilename.endsWith(".txt")) queueInputFile(ioMap, inputFilename); - else - { - BufferedReader reader = new BufferedReader(new FileReader(inputFilename)); - while (reader.ready()) queueInputFile(ioMap, reader.readLine()); - reader.close(); - } - return ioMap; - } - - private static void queueInputFile(HashMap ioMap, String inputFilename) - { - if (inputFilename.endsWith(".jpg") || inputFilename.endsWith(".png") || inputFilename.endsWith(".h264") || inputFilename.endsWith(".mp4")) ioMap.put(inputFilename, inputFilename.substring(0, inputFilename.lastIndexOf(".")) + ".txt"); - else throw new IllegalStateException("bCode detector: unsupported input file extension"); - } - +/* + * Copyright (C) 2017, 2018, 2019, 2020, 2021, 2022 University of Illinois + * Board of Trustees. + * + * This file is part of bTools. + * + * bTools is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * bTools is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with bTools. If not, see http://www.gnu.org/licenses/. + */ + +package edu.illinois.gernat.btools.tracking.bcode; + +import java.awt.image.BufferedImage; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.text.ParseException; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; + +import javax.imageio.ImageIO; + +import org.bytedeco.ffmpeg.global.avutil; +import org.bytedeco.javacv.FFmpegFrameGrabber; +import org.bytedeco.javacv.Frame; +import org.bytedeco.javacv.FrameGrabber.Exception; +import org.bytedeco.javacv.Java2DFrameConverter; + +import com.google.zxing.NotFoundException; + +import edu.illinois.gernat.btools.common.image.Images; +import edu.illinois.gernat.btools.common.io.record.Record; +import edu.illinois.gernat.btools.common.io.record.RecordWriter; +import edu.illinois.gernat.btools.common.parameters.Parameters; + +public class BCodeDetector +{ + + private static LinkedHashMap> processImage(String inputFilename) throws IOException, ParseException + { + BufferedImage image = ImageIO.read(new File(inputFilename)); + List bCodes = detectBCodesIn(image); + long timestamp = Images.getTimestampFromFilename(inputFilename); + LinkedHashMap> result = new LinkedHashMap<>(); + result.put(timestamp, bCodes); + return result; + } + + private static LinkedHashMap> processVideo(String inputFilename, int frameRate) throws ParseException, IOException + { + + // check parameters + if (frameRate == -1) throw new IllegalStateException(); + + // initialize frame grabber and converter + avutil.av_log_set_level(avutil.AV_LOG_QUIET); + FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(inputFilename); + grabber.setVideoOption("threads", "1"); + grabber.start(); + Java2DFrameConverter converter = new Java2DFrameConverter(); + + // initialize bCode detection loop + LinkedHashMap> result = new LinkedHashMap<>(); + long timestamp = Images.getTimestampFromFilename(inputFilename); + int frameNumber = 0; + Frame frame = grabber.grab(); + + // loop over and process video frames + while (frame != null) + { + + // convert frame + BufferedImage image = converter.convert(frame); + + // detect bCodes + List bCodes = detectBCodesIn(image); + result.put(timestamp + Math.round(frameNumber * 1000d / frameRate), bCodes); + + // load next frame + frame = grabber.grab(); + frameNumber++; + + } + + // dispose of frame grabber + grabber.stop(); + grabber.close(); + grabber.release(); + + // done + return result; + + } + + private static List detectBCodesIn(BufferedImage image) + { + + // preprocess image + image = Preprocessor.preprocess(image); + + // detect IDs + List metaIDs = Reader.read(image); + + // postprocess bCode detections + if (Preprocessor.scalingFactor != 1) + { + for (MetaCode metaID : metaIDs) + { + metaID.center.set(metaID.center.x / Preprocessor.scalingFactor, metaID.center.y / Preprocessor.scalingFactor); + metaID.moduleSize /= Preprocessor.scalingFactor; + metaID.nw.set(metaID.nw.x / Preprocessor.scalingFactor, metaID.nw.y / Preprocessor.scalingFactor); + metaID.ne.set(metaID.ne.x / Preprocessor.scalingFactor, metaID.ne.y / Preprocessor.scalingFactor); + metaID.sw.set(metaID.sw.x / Preprocessor.scalingFactor, metaID.sw.y / Preprocessor.scalingFactor); + metaID.se.set(metaID.se.x / Preprocessor.scalingFactor, metaID.se.y / Preprocessor.scalingFactor); + } + } + + // done + return metaIDs; + + } + + private static void showVersionAndCopyright() + { + System.out.println("bCode Detector (bTools) 0.15.1"); + System.out.println("Copyright (C) 2017-2022 University of Illinois Board of Trustees"); + System.out.println("License AGPLv3+: GNU AGPL version 3 or later "); + System.out.println("This is free software: you are free to change and redistribute it."); + System.out.println("There is NO WARRANTY, to the extent permitted by law."); + } + + private static void showUsageInformation() + { + System.out.println("Usage: java -jar bcode_detector.jar PARAMETER=VALUE..."); + System.out.println("Detect bCodes in images or videos."); + System.out.println(); + System.out.println("Parameters:"); + System.out.println("- conserve.margin whether the bCode border is considered to be part"); + System.out.println(" of the bCode template"); + System.out.println("- frame.rate frame rate of any videos to be processed"); + System.out.println("- scaling.factor factor for image scaling prior to detecting bCodes"); + System.out.println("- input.file the input image, video, or plain text file"); + System.out.println("- intensity.step.size increment when going from the lowest to the highest"); + System.out.println(" intensity threshold"); + System.out.println("- max.intensity.threshold highest intensity threshold for converting to a"); + System.out.println(" binary image"); + System.out.println("- min.intensity.threshold lowest intensity threshold for converting to a"); + System.out.println(" binary image"); + System.out.println("- min.template.conservation fraction of bCode modules that need to match the"); + System.out.println(" bCode template"); + System.out.println("- sharpening.amount amount of sharpening during unsharp masking"); + System.out.println("- sharpening.sigma Gaussian blur standard deviation for unsharp"); + System.out.println(" masking"); + System.out.println("- show.credits set to \"true\" or 1 to display credits and exit"); + System.out.println(); + System.out.println("Notes:"); + System.out.println("If the input.file is a plain text file, this file must list one image or."); + System.out.println("video file per line"); + System.out.println(""); + System.out.println("Image and video file names need to be a valid date in the format"); + System.out.println("yyyy-MM-dd-HH-mm-ss-SSS. File name and extension must be separated by a dot."); + System.out.println("Output file names are constructed by replacing the input image file"); + System.out.println("extension with 'txt'."); + System.out.println(); + System.out.println("When processing videos, a constant frame rate if assumed when calculating"); + System.out.println("timestamps from the date encoded by the video file name and the frame number."); + } + + private static void showCredits() throws IOException + { + showVersionAndCopyright(); + System.out.println(); + System.out.println("This software uses the following third party software that is distributed"); + System.out.println("under its own terms:"); + System.out.println(); + InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("LICENSE-3RD-PARTY"); + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); + while (reader.ready()) System.out.println(reader.readLine()); + reader.close(); + inputStream.close(); + } + + public static void main(String[] args) throws NotFoundException, IOException, ParseException + { + + // show version, copyright, and usage information if no arguments were + // given on the command line + if (args.length == 0) + { + showVersionAndCopyright(); + System.out.println(); + showUsageInformation(); + System.exit(1); + } + + // get arguments + Parameters parameters = Parameters.INSTANCE; + parameters.initialize(args); + + // if requested, show credits and exit + if ((parameters.exists("show.credits")) && (parameters.getBoolean("show.credits"))) + { + showCredits(); + System.exit(1); + } + + // set image processing parameters + Preprocessor.sharpeningSigma = parameters.getDouble("sharpening.sigma"); + Preprocessor.sharpeningAmount = parameters.getDouble("sharpening.amount"); + Preprocessor.scalingFactor = (float) parameters.getDouble("scaling.factor"); + Reader.minBlackThreshold = parameters.getInteger("min.intensity.threshold"); + Reader.maxBlackThreshold = parameters.getInteger("max.intensity.threshold"); + Reader.thresholdStepSize = parameters.getInteger("intensity.step.size"); + Detector.minTemplateConservation = parameters.getDouble("min.template.conservation"); + Detector.checkMargin = parameters.getBoolean("conserve.margin"); + + // map input files to output files + HashMap ioMap = mapInputToOutput(parameters.getString("input.file")); + + // set frame rate parameter, if necessary + int frameRate = -1; + for (String inputFilename : ioMap.keySet()) + { + if (inputFilename.endsWith(".h264") || inputFilename.endsWith(".mp4")) + { + frameRate = parameters.getInteger("frame.rate"); + break; + } + } + + // process each input file + processInputFiles(ioMap, frameRate); + + } + + private static void processInputFiles(HashMap ioMap, int frameRate) throws IOException, ParseException + { + for (String inputFilename : ioMap.keySet()) + { + + // delete output file, if it exists + String outputFilename = ioMap.get(inputFilename); + File outputFile = new File(outputFilename); + if (outputFile.exists()) outputFile.delete(); + + // detect bCodes in input file + LinkedHashMap> bCodeDetectionResults = null; + try + { + if (inputFilename.endsWith(".jpg") || inputFilename.endsWith(".png")) bCodeDetectionResults = processImage(inputFilename); + else if (inputFilename.endsWith(".h264") || inputFilename.endsWith(".mp4")) bCodeDetectionResults = processVideo(inputFilename, frameRate); + } + catch (Exception e) + { + e.printStackTrace(); + System.err.println("Caused by file: " + inputFilename); + } + + // write bCode detections to file + RecordWriter writer = new RecordWriter(outputFilename); + for (Long timestamp : bCodeDetectionResults.keySet()) + { + List bCodes = bCodeDetectionResults.get(timestamp); + for (MetaCode metaCode : bCodes) + { + Record record = new Record(timestamp, metaCode.data, metaCode.nw, metaCode.ne, metaCode.sw, metaCode.support, metaCode.errorCorrectionCount); + record.roundPatternCoordinates(); + writer.writeRecord(record); + } + } + writer.close(); + + } + } + + private static HashMap mapInputToOutput(String inputFilename) throws IOException + { + HashMap ioMap = new HashMap<>(); + if (!inputFilename.endsWith(".txt")) queueInputFile(ioMap, inputFilename); + else + { + BufferedReader reader = new BufferedReader(new FileReader(inputFilename)); + while (reader.ready()) queueInputFile(ioMap, reader.readLine()); + reader.close(); + } + return ioMap; + } + + private static void queueInputFile(HashMap ioMap, String inputFilename) + { + if (inputFilename.endsWith(".jpg") || inputFilename.endsWith(".png") || inputFilename.endsWith(".h264") || inputFilename.endsWith(".mp4")) ioMap.put(inputFilename, inputFilename.substring(0, inputFilename.lastIndexOf(".")) + ".txt"); + else throw new IllegalStateException("bCode detector: unsupported input file extension"); + } + } \ No newline at end of file diff --git a/src/edu/illinois/gernat/btools/tracking/bcode/BCodeMaker.java b/src/edu/illinois/gernat/btools/tracking/bcode/BCodeMaker.java index c75bd02..e272b24 100644 --- a/src/edu/illinois/gernat/btools/tracking/bcode/BCodeMaker.java +++ b/src/edu/illinois/gernat/btools/tracking/bcode/BCodeMaker.java @@ -1,257 +1,257 @@ -/* - * Copyright (C) 2017, 2018, 2019, 2020, 2021 University of Illinois Board of - * Trustees. - * - * This file is part of bTools. - * - * bTools is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * bTools is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with bTools. If not, see http://www.gnu.org/licenses/. - */ - -package edu.illinois.gernat.btools.tracking.bcode; - -import java.awt.Color; -import java.awt.Graphics2D; -import java.awt.image.BufferedImage; -import java.io.BufferedReader; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; - -import javax.imageio.ImageIO; - -import com.google.zxing.NotFoundException; - -import edu.illinois.gernat.btools.common.parameters.Parameters; - -public class BCodeMaker -{ - - private static final int ID_START = 1; // This should never be 0 - - private static final int ID_INCREMENT = 1; - - private static final int ID_COUNT = 2047; - - private static final boolean LABEL_INVERT = false; - - private static final int LABEL_TYPE = Writer.TYPE_CORNERS; - - private static final float LABEL_SPACER_WIDTH = 0f; // mm - - private static final int CUTTING_LINE_WIDTH = 3; // dots - - private static final float WHISKER_LENGTH = 5f; // mm - - private static final int GROUP_X_COUNT = 2; - - private static final int GROUP_Y_COUNT = 4; - - private static final int GROUP_SIDE_LENGTH = 16; - - private static final float GROUP_SPACER_WIDTH = 30; // mm - - private static final int PRINTER_DPI = 1200; // dots - - public static final float MM_PER_INCH = 25.4f; - - private static void drawLines(Graphics2D graphics, int groupSideLength, int groupSpacerWidth, int squareSideLength, int squareSpacerWidth, int cuttingLineExcessLength, int cuttingLineWidth) - { - - // loop over group rows and columns - for (int groupRow = 0; groupRow < GROUP_Y_COUNT; groupRow++) - { - for (int groupColumn = 0; groupColumn < GROUP_X_COUNT; groupColumn++) - { - - // calculate group coordinate offsets - int groupXOffset = groupSpacerWidth + groupColumn * (groupSideLength + groupSpacerWidth); - int groupYOffset = groupSpacerWidth + groupRow * (groupSideLength + groupSpacerWidth); - - // loop over rows - for (int squareRow = -1; squareRow < GROUP_SIDE_LENGTH; squareRow++) - { - - // calculate offset - int y = groupYOffset + squareRow * (squareSideLength + cuttingLineWidth + squareSpacerWidth); - - // draw whiskers - graphics.setColor(Color.BLACK); - for (int i = 0; i < cuttingLineWidth; i++) graphics.drawLine(groupXOffset - cuttingLineExcessLength - cuttingLineWidth, y + squareSideLength + i, groupXOffset + groupSideLength + cuttingLineExcessLength - cuttingLineWidth - 1, y + squareSideLength + i); - - // draw dividers - if (LABEL_INVERT) graphics.setColor(Color.WHITE); - else graphics.setColor(Color.BLACK); - for (int i = 0; i < cuttingLineWidth; i++) graphics.drawLine(groupXOffset - cuttingLineWidth, y + squareSideLength + i, groupXOffset + groupSideLength - cuttingLineWidth - 1, y + squareSideLength + i); - - } - - // loop over columns - for (int squareColumn = - 1; squareColumn < GROUP_SIDE_LENGTH; squareColumn++) - { - - // calculate offset - int x = groupXOffset + squareColumn * (squareSideLength + cuttingLineWidth + squareSpacerWidth); - - // draw whiskers - graphics.setColor(Color.BLACK); - for (int i = 0; i < cuttingLineWidth; i++) graphics.drawLine(x + squareSideLength + i, groupYOffset - cuttingLineExcessLength - cuttingLineWidth, x + squareSideLength + i, groupYOffset + groupSideLength + cuttingLineExcessLength - cuttingLineWidth - 1); - - // draw dividers - if (LABEL_INVERT) graphics.setColor(Color.WHITE); - else graphics.setColor(Color.BLACK); - for (int i = 0; i < cuttingLineWidth; i++) graphics.drawLine(x + squareSideLength + i, groupYOffset - cuttingLineWidth, x + squareSideLength + i, groupYOffset + groupSideLength - cuttingLineWidth - 1); - - } - - } - } - - } - - private static void drawLabels(Graphics2D graphics, int groupSideLength, int groupSpacerWidth, int squareSideLength, int squareSpacerWidth, int cuttingLineWidth, int labelZoom, int labelExtraMargin) - { - - // loop over group rows and columns - int data = ID_START; - for (int groupRow = 0; groupRow < GROUP_Y_COUNT; groupRow++) - { - for (int groupColumn = 0; groupColumn < GROUP_X_COUNT; groupColumn++) - { - - // calculate group coordinate offsets - int groupXOffset = groupSpacerWidth + groupColumn * (groupSideLength + groupSpacerWidth); - int groupYOffset = groupSpacerWidth + groupRow * (groupSideLength + groupSpacerWidth); - - // fill background - if (LABEL_INVERT) graphics.setColor(Color.BLACK); - else graphics.setColor(Color.WHITE); - graphics.fillRect(groupXOffset, groupYOffset, groupSideLength, groupSideLength); - - // loop over squares - for (int squareRow = 0; squareRow < GROUP_SIDE_LENGTH; squareRow++) - { - for (int squareColumn = 0; squareColumn < GROUP_SIDE_LENGTH; squareColumn++) - { - - // draw label - if (data <= ID_START + ID_INCREMENT * (ID_COUNT - 1)) - { - BufferedImage label = Writer.create(data % BCode.UNIQUE_ID_COUNT, LABEL_TYPE, labelZoom, LABEL_INVERT); - int x = groupXOffset + squareColumn * (squareSideLength + squareSpacerWidth + cuttingLineWidth) + labelExtraMargin; - int y = groupYOffset + squareRow * (squareSideLength + squareSpacerWidth + cuttingLineWidth) + labelExtraMargin; - graphics.drawImage(label, x, y, null); - data += ID_INCREMENT; - } - - } - } - - } - } - - } - - private static float toPixels(float mm) - { - return mm * PRINTER_DPI / MM_PER_INCH; - } - - private static void showVersionAndCopyright() - { - System.out.println("bCode Maker (bTools) 0.15.0"); - System.out.println("Copyright (C) 2017-2021 University of Illinois Board of Trustees"); - System.out.println("License AGPLv3+: GNU AGPL version 3 or later "); - System.out.println("This is free software: you are free to change and redistribute it."); - System.out.println("There is NO WARRANTY, to the extent permitted by law."); - } - - private static void showUsageInformation() - { - System.out.println("Usage: java -jar bcode_maker.jar PARAMETER=VALUE..."); - System.out.println("Draw 2048 unique bCodes."); - System.out.println(); - System.out.println("Parameters:"); - System.out.println("- bcode.file output image file"); - System.out.println("- padding amount of extra space (in pixels) between the cutting"); - System.out.println(" guides and the (invisible) bCode border"); - System.out.println("- show.credits set to \"true\" or 1 to display credits and exit"); - System.out.println("- square.side.length size (in pixels) of the squares are that make up a bCode"); - } - - private static void showCredits() throws IOException - { - showVersionAndCopyright(); - System.out.println(); - System.out.println("This software uses the following third party software that is distributed"); - System.out.println("under its own terms:"); - System.out.println(); - InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("LICENSE-3RD-PARTY"); - BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); - while (reader.ready()) System.out.println(reader.readLine()); - reader.close(); - inputStream.close(); - System.exit(1); - } - - public static void main(String[] args) throws IOException, NotFoundException - { - - // show version, copyright, and usage information if no arguments were - // given on the command line - if (args.length == 0) - { - showVersionAndCopyright(); - System.out.println(); - showUsageInformation(); - System.exit(1); - } - - // get arguments - Parameters parameters = Parameters.INSTANCE; - parameters.initialize(args); - if ((parameters.exists("show.credits")) && (parameters.getBoolean("show.credits"))) showCredits(); - - // set parameters - int labelZoom = parameters.getInteger("square.side.length"); // default: 5 dots - int labelExtraMargin = parameters.getInteger("padding"); // default: 2 dots - String labelFilename = parameters.getString("bcode.file"); - - // perform some calculations - int cuttingLineExcessLength = Math.round(toPixels(WHISKER_LENGTH)); - int labelWidthOffset = ((LABEL_TYPE == Writer.TYPE_SOLID) || (LABEL_TYPE == Writer.TYPE_CORNERS)) ? 0 : -1; - int squareSideLength = Writer.getLabelSideLength(LABEL_TYPE, labelZoom) + 2 * labelExtraMargin + labelWidthOffset; - int squareSpacerWidth = Math.round(toPixels(LABEL_SPACER_WIDTH)); - int groupSideLength = GROUP_SIDE_LENGTH * (squareSideLength + CUTTING_LINE_WIDTH) + (GROUP_SIDE_LENGTH - 1) * squareSpacerWidth + CUTTING_LINE_WIDTH; - - int groupSpacerWidth = Math.round(toPixels(GROUP_SPACER_WIDTH / 2)) * 2; - int width = GROUP_X_COUNT * groupSideLength + (GROUP_X_COUNT + 1) * groupSpacerWidth; - int height = GROUP_Y_COUNT * groupSideLength + (GROUP_Y_COUNT + 1) * groupSpacerWidth; - - // create a blank image - BufferedImage canvas = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); - Graphics2D graphics = canvas.createGraphics(); - graphics.setColor(Color.WHITE); - graphics.fillRect(0, 0, canvas.getWidth(), canvas.getHeight()); - - // draw cutting lines and labels - drawLabels(graphics, groupSideLength, groupSpacerWidth, squareSideLength, squareSpacerWidth, CUTTING_LINE_WIDTH, labelZoom, labelExtraMargin); - drawLines(graphics, groupSideLength, groupSpacerWidth, squareSideLength, squareSpacerWidth, cuttingLineExcessLength, CUTTING_LINE_WIDTH); - - // write labels to disk - ImageIO.write(canvas, "png", new File(labelFilename)); - - } - -} +/* + * Copyright (C) 2017, 2018, 2019, 2020, 2021, 2022 University of Illinois + * Board of Trustees. + * + * This file is part of bTools. + * + * bTools is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * bTools is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with bTools. If not, see http://www.gnu.org/licenses/. + */ + +package edu.illinois.gernat.btools.tracking.bcode; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; + +import javax.imageio.ImageIO; + +import com.google.zxing.NotFoundException; + +import edu.illinois.gernat.btools.common.parameters.Parameters; + +public class BCodeMaker +{ + + private static final int ID_START = 1; // This should never be 0 + + private static final int ID_INCREMENT = 1; + + private static final int ID_COUNT = 2047; + + private static final boolean LABEL_INVERT = false; + + private static final int LABEL_TYPE = Writer.TYPE_CORNERS; + + private static final float LABEL_SPACER_WIDTH = 0f; // mm + + private static final int CUTTING_LINE_WIDTH = 3; // dots + + private static final float WHISKER_LENGTH = 5f; // mm + + private static final int GROUP_X_COUNT = 2; + + private static final int GROUP_Y_COUNT = 4; + + private static final int GROUP_SIDE_LENGTH = 16; + + private static final float GROUP_SPACER_WIDTH = 30; // mm + + private static final int PRINTER_DPI = 1200; // dots + + public static final float MM_PER_INCH = 25.4f; + + private static void drawLines(Graphics2D graphics, int groupSideLength, int groupSpacerWidth, int squareSideLength, int squareSpacerWidth, int cuttingLineExcessLength, int cuttingLineWidth) + { + + // loop over group rows and columns + for (int groupRow = 0; groupRow < GROUP_Y_COUNT; groupRow++) + { + for (int groupColumn = 0; groupColumn < GROUP_X_COUNT; groupColumn++) + { + + // calculate group coordinate offsets + int groupXOffset = groupSpacerWidth + groupColumn * (groupSideLength + groupSpacerWidth); + int groupYOffset = groupSpacerWidth + groupRow * (groupSideLength + groupSpacerWidth); + + // loop over rows + for (int squareRow = -1; squareRow < GROUP_SIDE_LENGTH; squareRow++) + { + + // calculate offset + int y = groupYOffset + squareRow * (squareSideLength + cuttingLineWidth + squareSpacerWidth); + + // draw whiskers + graphics.setColor(Color.BLACK); + for (int i = 0; i < cuttingLineWidth; i++) graphics.drawLine(groupXOffset - cuttingLineExcessLength - cuttingLineWidth, y + squareSideLength + i, groupXOffset + groupSideLength + cuttingLineExcessLength - cuttingLineWidth - 1, y + squareSideLength + i); + + // draw dividers + if (LABEL_INVERT) graphics.setColor(Color.WHITE); + else graphics.setColor(Color.BLACK); + for (int i = 0; i < cuttingLineWidth; i++) graphics.drawLine(groupXOffset - cuttingLineWidth, y + squareSideLength + i, groupXOffset + groupSideLength - cuttingLineWidth - 1, y + squareSideLength + i); + + } + + // loop over columns + for (int squareColumn = - 1; squareColumn < GROUP_SIDE_LENGTH; squareColumn++) + { + + // calculate offset + int x = groupXOffset + squareColumn * (squareSideLength + cuttingLineWidth + squareSpacerWidth); + + // draw whiskers + graphics.setColor(Color.BLACK); + for (int i = 0; i < cuttingLineWidth; i++) graphics.drawLine(x + squareSideLength + i, groupYOffset - cuttingLineExcessLength - cuttingLineWidth, x + squareSideLength + i, groupYOffset + groupSideLength + cuttingLineExcessLength - cuttingLineWidth - 1); + + // draw dividers + if (LABEL_INVERT) graphics.setColor(Color.WHITE); + else graphics.setColor(Color.BLACK); + for (int i = 0; i < cuttingLineWidth; i++) graphics.drawLine(x + squareSideLength + i, groupYOffset - cuttingLineWidth, x + squareSideLength + i, groupYOffset + groupSideLength - cuttingLineWidth - 1); + + } + + } + } + + } + + private static void drawLabels(Graphics2D graphics, int groupSideLength, int groupSpacerWidth, int squareSideLength, int squareSpacerWidth, int cuttingLineWidth, int labelZoom, int labelExtraMargin) + { + + // loop over group rows and columns + int data = ID_START; + for (int groupRow = 0; groupRow < GROUP_Y_COUNT; groupRow++) + { + for (int groupColumn = 0; groupColumn < GROUP_X_COUNT; groupColumn++) + { + + // calculate group coordinate offsets + int groupXOffset = groupSpacerWidth + groupColumn * (groupSideLength + groupSpacerWidth); + int groupYOffset = groupSpacerWidth + groupRow * (groupSideLength + groupSpacerWidth); + + // fill background + if (LABEL_INVERT) graphics.setColor(Color.BLACK); + else graphics.setColor(Color.WHITE); + graphics.fillRect(groupXOffset, groupYOffset, groupSideLength, groupSideLength); + + // loop over squares + for (int squareRow = 0; squareRow < GROUP_SIDE_LENGTH; squareRow++) + { + for (int squareColumn = 0; squareColumn < GROUP_SIDE_LENGTH; squareColumn++) + { + + // draw label + if (data <= ID_START + ID_INCREMENT * (ID_COUNT - 1)) + { + BufferedImage label = Writer.create(data % BCode.UNIQUE_ID_COUNT, LABEL_TYPE, labelZoom, LABEL_INVERT); + int x = groupXOffset + squareColumn * (squareSideLength + squareSpacerWidth + cuttingLineWidth) + labelExtraMargin; + int y = groupYOffset + squareRow * (squareSideLength + squareSpacerWidth + cuttingLineWidth) + labelExtraMargin; + graphics.drawImage(label, x, y, null); + data += ID_INCREMENT; + } + + } + } + + } + } + + } + + private static float toPixels(float mm) + { + return mm * PRINTER_DPI / MM_PER_INCH; + } + + private static void showVersionAndCopyright() + { + System.out.println("bCode Maker (bTools) 0.15.1"); + System.out.println("Copyright (C) 2017-2022 University of Illinois Board of Trustees"); + System.out.println("License AGPLv3+: GNU AGPL version 3 or later "); + System.out.println("This is free software: you are free to change and redistribute it."); + System.out.println("There is NO WARRANTY, to the extent permitted by law."); + } + + private static void showUsageInformation() + { + System.out.println("Usage: java -jar bcode_maker.jar PARAMETER=VALUE..."); + System.out.println("Draw 2048 unique bCodes."); + System.out.println(); + System.out.println("Parameters:"); + System.out.println("- bcode.file output image file"); + System.out.println("- padding amount of extra space (in pixels) between the cutting"); + System.out.println(" guides and the (invisible) bCode border"); + System.out.println("- show.credits set to \"true\" or 1 to display credits and exit"); + System.out.println("- square.side.length size (in pixels) of the squares are that make up a bCode"); + } + + private static void showCredits() throws IOException + { + showVersionAndCopyright(); + System.out.println(); + System.out.println("This software uses the following third party software that is distributed"); + System.out.println("under its own terms:"); + System.out.println(); + InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("LICENSE-3RD-PARTY"); + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); + while (reader.ready()) System.out.println(reader.readLine()); + reader.close(); + inputStream.close(); + System.exit(1); + } + + public static void main(String[] args) throws IOException, NotFoundException + { + + // show version, copyright, and usage information if no arguments were + // given on the command line + if (args.length == 0) + { + showVersionAndCopyright(); + System.out.println(); + showUsageInformation(); + System.exit(1); + } + + // get arguments + Parameters parameters = Parameters.INSTANCE; + parameters.initialize(args); + if ((parameters.exists("show.credits")) && (parameters.getBoolean("show.credits"))) showCredits(); + + // set parameters + int labelZoom = parameters.getInteger("square.side.length"); // default: 5 dots + int labelExtraMargin = parameters.getInteger("padding"); // default: 2 dots + String labelFilename = parameters.getString("bcode.file"); + + // perform some calculations + int cuttingLineExcessLength = Math.round(toPixels(WHISKER_LENGTH)); + int labelWidthOffset = ((LABEL_TYPE == Writer.TYPE_SOLID) || (LABEL_TYPE == Writer.TYPE_CORNERS)) ? 0 : -1; + int squareSideLength = Writer.getLabelSideLength(LABEL_TYPE, labelZoom) + 2 * labelExtraMargin + labelWidthOffset; + int squareSpacerWidth = Math.round(toPixels(LABEL_SPACER_WIDTH)); + int groupSideLength = GROUP_SIDE_LENGTH * (squareSideLength + CUTTING_LINE_WIDTH) + (GROUP_SIDE_LENGTH - 1) * squareSpacerWidth + CUTTING_LINE_WIDTH; + + int groupSpacerWidth = Math.round(toPixels(GROUP_SPACER_WIDTH / 2)) * 2; + int width = GROUP_X_COUNT * groupSideLength + (GROUP_X_COUNT + 1) * groupSpacerWidth; + int height = GROUP_Y_COUNT * groupSideLength + (GROUP_Y_COUNT + 1) * groupSpacerWidth; + + // create a blank image + BufferedImage canvas = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + Graphics2D graphics = canvas.createGraphics(); + graphics.setColor(Color.WHITE); + graphics.fillRect(0, 0, canvas.getWidth(), canvas.getHeight()); + + // draw cutting lines and labels + drawLabels(graphics, groupSideLength, groupSpacerWidth, squareSideLength, squareSpacerWidth, CUTTING_LINE_WIDTH, labelZoom, labelExtraMargin); + drawLines(graphics, groupSideLength, groupSpacerWidth, squareSideLength, squareSpacerWidth, cuttingLineExcessLength, CUTTING_LINE_WIDTH); + + // write labels to disk + ImageIO.write(canvas, "png", new File(labelFilename)); + + } + +}