{"action":"created","comment":{"url":"https://api.github.com/repos/CellProfiler/CellProfiler/pulls/comments/57712349","id":57712349,"diff_hunk":"@@ -0,0 +1,952 @@\n+\"\"\"Classify - train or use a classifier\"\"\"\n+\n+import bisect\n+import cellprofiler.cpimage\n+import cellprofiler.cpmodule\n+import cellprofiler.settings\n+import h5py\n+import hashlib\n+import logging\n+import numpy\n+import os\n+import pickle\n+import sklearn.decomposition\n+import sklearn.ensemble\n+\n+logger = logging.getLogger(__name__)\n+\n+A_CLASS = \"Class\"\n+A_DIGEST = \"MD5Digest\"\n+A_VERSION = \"Version\"\n+AA_ADVANCED = \"Advanced\"\n+AA_AUTOMATIC = \"Automatic\"\n+CLASS_CLASSIFIER = \"Classifier\"\n+CLASS_FILTER = \"Filter\"\n+CLASS_GROUND_TRUTH = \"GroundTruth\"\n+CLASS_KERNEL = \"Kernel\"\n+CLASS_SAMPLING = \"Sampling\"\n+DATASET_COORDINATES = \"Coords\"\n+DATASET_IMAGE_NUMBER = \"ImageNumber\"\n+DATASET_KERNEL = \"Kernel\"\n+DEFAULT_MIN_SAMPLES_PER_LEAF = 10\n+DEFAULT_N_ESTIMATORS = 25\n+DEFAULT_N_FEATURES = 100\n+DEFAULT_RADIUS = 9\n+G_CLASSIFIERS = \"Classifiers\"\n+G_FILTERS = \"Filters\"\n+G_IMAGES = \"Images\"\n+G_SAMPLING = \"Sampling\"\n+G_TRAINING_SET = \"TrainingSet\"\n+MODE_CLASSIFY = \"Classify\"\n+MODE_TRAIN = \"Train\"\n+ROUNDS = [(\"initial\", 100000, 0), (\"middle\", 75000, 25000), (\"final\", 50000, 50000)]\n+SRC_ILASTIK = \"Ilastik\"\n+SRC_OBJECTS = \"Objects\"\n+USE_DOT = True\n+\n+\n+class Classify(cellprofiler.cpmodule.CPModule):\n+    category = \"Image Processing\"\n+\n+    module_name = \"Classify\"\n+\n+    variable_revision_number = 1\n+\n+    def create_settings(self):\n+        def get_directory_fn():\n+            \"\"\"Get the directory for the file name\"\"\"\n+            return self.path.get_absolute_path()\n+\n+        def set_directory_fn(path):\n+            dir_choice, custom_path = self.path.get_parts_from_path(path)\n+\n+            self.path.join_parts(dir_choice, custom_path)\n+\n+        self.mode = cellprofiler.settings.Choice(\"Classify or train?\", [MODE_CLASSIFY, MODE_TRAIN])\n+\n+        self.advanced_or_automatic = cellprofiler.settings.Choice(\"Configuration mode\", [AA_AUTOMATIC, AA_ADVANCED],\n+                                                                  doc=\"Do you want to automatically choose the training parameters or use the defaults?\")\n+\n+        self.radius = cellprofiler.settings.Integer(\"Radius\", DEFAULT_RADIUS, 1)\n+\n+        self.n_features = cellprofiler.settings.Integer(\"Number of features\", DEFAULT_N_FEATURES, 1,\n+                                                        doc=\"The classifier runs a feature reduction set. This creates <i>Eigentextures</i> which are representative texture patches found throughout the image. The module scores each patch around a pixel according to how much it has each of these textures and those scores are fed into the final classifier. Raise the number of features if some of the textures or edges of your classes are misclassified. Lower the number of features to improve processing time or to reduce overfitting if you have a smaller amount of ground truth.\")\n+\n+        self.n_estimators = cellprofiler.settings.Integer(\"Number of estimators\", DEFAULT_N_ESTIMATORS, 1,\n+                                                          doc=\"The classifier uses a voting scheme where it trains this many estimators. It purposefully does a bad job training and makes up for this deficit by having many poor classification judges. This protects against overfitting by not relying on having a single classifier that is very good at classifying the ground truth, but mistakenly uses irrelevant information to do so. Raise the number of estimators if the classifier is making obvious mistakes with unwarranted certainty. Lower the number of estimators to improve processing speed.\")\n+\n+        self.min_samples_per_leaf = cellprofiler.settings.Integer(\"Minimum samples per leaf\", DEFAULT_MIN_SAMPLES_PER_LEAF, 1,\n+                                                                  doc=\"This setting determines the minimum number of ground truth pixels that the classifier will use to split a decision tree. There must be at least this number of example pixels in each branch for the classifier to have confidence that the split is real and not just an artifact of an irrelevant measurement.\\n Lower this setting if the classifier does a good job on most of the pixels but does not draw sharp distinctions between one class and another at the border between the classes (e.g. at the edges of cells). Raise this setting if the classifier misclassifies pixels that are clearly not the right class - this is overtraining.\")\n+\n+        self.path = cellprofiler.settings.DirectoryPath(\"Classifier folder\")\n+\n+        self.filename = cellprofiler.settings.FilenameText(\"Classifier file\", \"Classifier.cpclassifier\", get_directory_fn=get_directory_fn,\n+                                                           set_directory_fn=set_directory_fn,\n+                                                           exts=[(\"Pixel classifier (*.cpclassifier)\", \"*.cpclassifier\"), (\"All files (*.*)\", \"*.*\")])\n+\n+        self.gt_source = cellprofiler.settings.Choice(\"Ground truth source\", [SRC_OBJECTS, SRC_ILASTIK],\n+                                                      doc=\"The ground truth data can either be taken from objects or can be the exported TIF \\\"labels\\\" output of Ilastik.\")\n+\n+        self.labels_image = cellprofiler.settings.ImageNameSubscriber(\"Ilastik labels image\", \"labels.tif\",\n+                                                                      doc=\"<i>Used only if the ground truth source is \\\"Ilastik\\\"</i> <br> This image should be the exported labels image from Ilastik.\")\n+\n+        self.wants_background_class = cellprofiler.settings.Binary(\"Do you want a background class?\", True)\n+\n+        self.background_class_name = cellprofiler.settings.Text(\"Background class name\", \"Background\")\n+\n+        self.object_classes = []\n+\n+        self.object_class_count = cellprofiler.settings.HiddenCount(self.object_classes, \"Object class count\")\n+\n+        self.add_objects(False)\n+\n+        self.add_objects_button = cellprofiler.settings.DoSomething(\"Add another class\", \"Add\", self.add_objects)\n+\n+        self.label_classes = []\n+\n+        self.label_class_count = cellprofiler.settings.HiddenCount(self.label_classes, \"Label class count\")\n+\n+        self.add_labels(False)\n+\n+        self.add_labels_button = cellprofiler.settings.DoSomething(\"Add another class\", \"Add\", self.add_labels)\n+\n+        self.images = []\n+\n+        self.image_count = cellprofiler.settings.HiddenCount(self.images, \"Image count\")\n+\n+        self.add_image(False)\n+\n+        self.add_image_button = cellprofiler.settings.DoSomething(\"Add another image\", \"Add\", self.add_image)\n+\n+        self.outputs = []\n+\n+        self.output_count = cellprofiler.settings.HiddenCount(self.outputs, \"Output count\")\n+\n+        self.add_output(False)\n+\n+        self.add_output_button = cellprofiler.settings.DoSomething(\"Add another output\", \"Add\", self.add_output)\n+\n+    def get_class_names(self, ignore=None):\n+        result = []\n+\n+        if self.mode == MODE_TRAIN:\n+            if self.gt_source == SRC_OBJECTS:\n+                if self.wants_background_class:\n+                    result.append(self.background_class_name.value)\n+\n+                result += [group.object_name.value for group in self.object_classes]\n+            else:\n+                result += [group.class_name.value for group in self.label_classes]\n+        else:\n+            try:\n+                with self.get_classifier(\"r\") as classifier:\n+                    return classifier.get_class_names()\n+            except IOError:\n+                result.append(\"None\")\n+\n+        return result\n+\n+    def add_objects(self, can_remove=True):\n+        group = cellprofiler.settings.SettingsGroup()\n+\n+        group.append(\"object_name\", cellprofiler.settings.ObjectNameSubscriber(\"Object name\", \"Nuclei\"))\n+\n+        if can_remove:\n+            group.append(\"remover\", cellprofiler.settings.RemoveSettingButton(\"Remove object\", \"Remove\", self.object_classes, group))\n+\n+        self.object_classes.append(group)\n+\n+    def add_labels(self, removable=True):\n+        group = cellprofiler.settings.SettingsGroup()\n+\n+        group.append(\"class_name\", cellprofiler.settings.AlphanumericText(\"Class name\", \"Class {0:d}\".format(len(self.label_classes) + 1),\n+                                                                          doc=\"The name to give to pixels of this class (e.g. \\\"Foreground\\\")\\nYou should add one class for each class you defined in Ilastik\"))\n+\n+        if removable:\n+            group.append(\"remover\", cellprofiler.settings.RemoveSettingButton(\"Remove object\", \"Remove\", self.label_classes, group))\n+\n+        self.label_classes.append(group)\n+\n+    def add_image(self, removable=True):\n+        group = cellprofiler.settings.SettingsGroup()\n+\n+        group.append(\"image_name\", cellprofiler.settings.ImageNameSubscriber(\"Image name\", \"DNA\"))\n+\n+        if removable:\n+            group.append(\"remover\", cellprofiler.settings.RemoveSettingButton(\"Remove object\", \"Remove\", self.images, group))\n+\n+        self.images.append(group)\n+\n+    def add_output(self, removable=True):\n+        group = cellprofiler.settings.SettingsGroup()\n+\n+        group.append(\"output_image\", cellprofiler.settings.ImageNameProvider(\"Output image\", \"Probability\"))\n+\n+        group.append(\"class_name\", cellprofiler.settings.Choice(\"Class name\", choices=self.get_class_names(), choices_fn=self.get_class_names))\n+\n+        if removable:\n+            group.append(\"remover\", cellprofiler.settings.RemoveSettingButton(\"Remove object\", \"Remove\", self.outputs, group))\n+\n+        self.outputs.append(group)\n+\n+    def settings(self):\n+        result = [\n+            self.object_class_count,\n+            self.label_class_count,\n+            self.image_count,\n+            self.output_count,\n+            self.mode,\n+            self.path,\n+            self.filename,\n+            self.advanced_or_automatic,\n+            self.radius,\n+            self.n_features,\n+            self.n_estimators,\n+            self.min_samples_per_leaf,\n+            self.gt_source,\n+            self.labels_image,\n+            self.wants_background_class,\n+            self.background_class_name\n+        ]\n+\n+        for group in self.object_classes:\n+            result += group.pipeline_settings()\n+\n+        for group in self.label_classes:\n+            result += group.pipeline_settings()\n+\n+        for group in self.images:\n+            result += group.pipeline_settings()\n+\n+        for group in self.outputs:\n+            result += group.pipeline_settings()\n+\n+        return result\n+\n+    def visible_settings(self):\n+        result = [\n+            self.mode,\n+            self.path,\n+            self.filename\n+        ]\n+\n+        if self.mode == MODE_TRAIN:\n+            result.append(self.advanced_or_automatic)\n+\n+            if self.advanced_or_automatic == AA_ADVANCED:\n+                result += [self.radius, self.n_features, self.n_estimators, self.min_samples_per_leaf]\n+\n+        for group in self.images:\n+            result += group.visible_settings()\n+\n+        result.append(self.add_image_button)\n+\n+        if self.mode == MODE_TRAIN:\n+            self.filename.mode = cellprofiler.settings.FilenameText.MODE_OVERWRITE\n+\n+            result.append(self.gt_source)\n+\n+            if self.gt_source == SRC_OBJECTS:\n+                result.append(self.wants_background_class)\n+\n+                if self.wants_background_class:\n+                    result.append(self.background_class_name)\n+\n+                for group in self.object_classes:\n+                    result += group.visible_settings()\n+\n+                result.append(self.add_objects_button)\n+            else:\n+                result.append(self.labels_image)\n+\n+                for group in self.label_classes:\n+                    result += group.visible_settings()\n+\n+                result.append(self.add_labels_button)\n+        else:\n+            self.filename.mode = cellprofiler.settings.FilenameText.MODE_OPEN\n+\n+            for group in self.outputs:\n+                result += group.visible_settings()\n+\n+            result.append(self.add_output_button)\n+\n+        return result\n+\n+    def prepare_settings(self, settings):\n+        for count, sequence, add_fn in zip([int(_) for _ in settings[:4]], (self.object_classes, self.label_classes, self.images, self.outputs),\n+                                           (self.add_objects, self.add_labels, self.add_image, self.add_output)):\n+            del sequence[:]\n+\n+            for index in range(count):\n+                add_fn()\n+\n+    def is_aggregation_module(self):\n+        return self.mode == MODE_TRAIN\n+\n+    def get_classifier(self, mode):\n+        return PixelClassifier(os.path.join(self.path.get_absolute_path(), self.filename.value), mode)\n+\n+    def get_radius(self):\n+        if self.advanced_or_automatic == AA_AUTOMATIC:\n+            return DEFAULT_RADIUS\n+\n+        return self.radius.value\n+\n+    def get_n_features(self):\n+        if self.advanced_or_automatic == AA_AUTOMATIC:\n+            return DEFAULT_N_FEATURES\n+\n+        return self.n_features.value\n+\n+    def get_n_estimators(self):\n+        if self.advanced_or_automatic == AA_AUTOMATIC:\n+            return DEFAULT_N_ESTIMATORS\n+\n+        return self.n_estimators.value\n+\n+    def get_min_samples_per_leaf(self):\n+        if self.advanced_or_automatic == AA_AUTOMATIC:\n+            return DEFAULT_MIN_SAMPLES_PER_LEAF\n+\n+        return self.min_samples_per_leaf.value\n+\n+    def prepare_group(self, workspace, grouping, image_numbers):\n+        if self.mode == MODE_TRAIN:\n+            with self.get_classifier(\"w\") as classifier:\n+                assert isinstance(classifier, PixelClassifier)\n+\n+                radius = self.get_radius()\n+\n+                i, j = numpy.mgrid[-radius:radius + 1, -radius:radius + 1]\n+\n+                kernel_mask = i * i + j * j <= radius * radius\n+\n+                n_features = numpy.sum(kernel_mask)\n+\n+                classifier.set_kernel(numpy.vstack([numpy.column_stack(\n+                        [numpy.ones(n_features, int) * channel, numpy.zeros(n_features, int), numpy.zeros(n_features, int), i[kernel_mask], j[kernel_mask]]) for\n+                                                    channel in range(len(self.images))]))\n+\n+                for class_name in self.get_class_names():\n+                    classifier.add_class(class_name)\n+\n+    def run(self, workspace):\n+        if self.mode == MODE_TRAIN:\n+            self.run_train(workspace)\n+        else:\n+            self.run_classify(workspace)\n+\n+    def get_5d_image(self, workspace):\n+        \"\"\"\n+        Compile a 5d image from the channel planes\n+\n+        :param workspace:\n+\n+        :return:\n+        \"\"\"\n+        pixels = []\n+\n+        for group in self.images:\n+            #\n+            # [[img]] adds Z and T of dimension 1 to the uber array\n+            #\n+            pixels.append([[workspace.image_set.get_image(group.image_name.value, must_be_grayscale=True).pixel_data]])\n+\n+        return numpy.array(pixels)\n+\n+    def run_train(self, workspace):\n+        pixels = self.get_5d_image(workspace)\n+\n+        image_number = workspace.measurements.image_number\n+\n+        with self.get_classifier(\"a\") as classifier:\n+            assert isinstance(classifier, PixelClassifier)\n+\n+            classifier.add_image(pixels, image_number)\n+\n+            ground_truth = []\n+\n+            if self.gt_source == SRC_OBJECTS:\n+                background = numpy.ones(pixels.shape[-2:], bool)\n+\n+                for group in self.object_classes:\n+                    object_name = group.object_name.value\n+\n+                    foreground = numpy.zeros(background.shape, bool)\n+\n+                    for plane, _ in workspace.object_set.get_objects(object_name).get_labels():\n+                        foreground[plane > 0] = True\n+\n+                    background[foreground] = False\n+\n+                    ground_truth.append((object_name, foreground))\n+\n+                if self.wants_background_class:\n+                    ground_truth.append((self.background_class_name.value, background))\n+            else:\n+                image = workspace.image_set.get_image(self.labels_image.value)\n+\n+                for index, group in enumerate(self.label_classes):\n+                    ground_truth.append((group.class_name.value, (image.pixel_data * image.scale).astype(int) == index + 1))\n+\n+            for object_name, foreground in ground_truth:\n+                i, j = numpy.where(foreground)\n+\n+                classifier.add_ground_truth(object_name, image_number, numpy.column_stack([numpy.zeros(len(i), int)] * 3 + [i, j]))\n+\n+    def post_group(self, workspace, grouping):\n+        if self.mode == MODE_TRAIN:\n+            for round_name, n_random, n_error in ROUNDS:\n+                self.do_training_round(None, round_name, n_random / len(self.get_class_names()), n_error / len(self.get_class_names()))\n+\n+            with self.get_classifier(\"a\") as classifier:\n+                assert isinstance(classifier, PixelClassifier)\n+\n+                classifier.config_final_pipeline(\"final\", \"final\")\n+\n+    def do_training_round(self, name_in, name_out, n_random_samples, n_error_samples):\n+        \"\"\"\n+        Perform a round of training\n+\n+        :param name_in: name of the filter bank and classifier to use to find the error samples. None if no error samples.\n+        :param name_out: name for output classifier\n+        :param n_random_samples: # of samples randomly drawn from each class\n+        :param n_error_samples: # of samples drawn from errors\n+\n+        :return:\n+        \"\"\"\n+        with self.get_classifier(\"a\") as classifier:\n+            assert isinstance(classifier, PixelClassifier)\n+\n+            #\n+            # Sample\n+            #\n+            filter_bank_sample_name = \"{0}_filter_bank\".format(name_out)\n+\n+            classifier_sample_name = \"{0}_classifier\".format(name_out)\n+\n+            for sample_name in filter_bank_sample_name, classifier_sample_name:\n+                d = {}\n+\n+                for index, class_name in enumerate(classifier.get_class_names()):\n+                    ground_truth = classifier.get_ground_truth(class_name)\n+\n+                    if name_in is not None and n_error_samples > 0:\n+                        order = numpy.argsort(classifier.run_pipeline(name_in, name_in, ground_truth)[:, index])\n+\n+                        error_index = order[:n_error_samples]\n+\n+                        other_index = order[n_error_samples:]\n+                    else:\n+                        error_index = numpy.zeros(0, int)\n+\n+                        other_index = numpy.arange(ground_truth.shape[0])\n+                    if len(other_index) > n_random_samples:\n+                        other_index = classifier.random_state(str(name_in) + name_out + class_name).choice(other_index, size=n_random_samples, replace=False)\n+\n+                    d[class_name] = numpy.hstack((error_index, other_index))\n+\n+                classifier.add_sampling(sample_name, d)\n+\n+            samples, classes = classifier.sample(filter_bank_sample_name)\n+\n+            classifier.make_filter_bank(samples, classes, name_out, self.get_n_features())\n+\n+            samples, classes = classifier.sample(classifier_sample_name)\n+\n+            classifier.fit(name_out, classifier.use_filter_bank(name_out, samples), classes,\n+                           sklearn.ensemble.ExtraTreesClassifier(n_estimators=self.get_n_estimators(), min_samples_leaf=self.get_min_samples_per_leaf()))\n+\n+    def run_classify(self, workspace):\n+        pixels = self.get_5d_image(workspace)\n+\n+        #\n+        # Process the image in chunks\n+        #\n+        with self.get_classifier(\"r\") as classifier:\n+            assert isinstance(classifier, PixelClassifier)\n+\n+            probabilities_indices = numpy.array([classifier.get_class_names().index(group.class_name.value) for group in self.outputs])\n+\n+            chunk_size = 128\n+\n+            probabilities = numpy.zeros((len(probabilities_indices), pixels.shape[3], pixels.shape[4]))\n+\n+            for index in range(0, pixels.shape[3], chunk_size):\n+                iend = min(index + chunk_size, pixels.shape[3])\n+\n+                for j in range(0, pixels.shape[4], chunk_size):\n+                    jend = min(j + chunk_size, pixels.shape[4])\n+\n+                    ii, jj = [_.flatten() for _ in numpy.mgrid[index:iend, j:jend]]\n+\n+                    probabilities[:, index:iend, j:jend] = classifier.run_final_pipeline(classifier.get_samples(pixels, numpy.column_stack(\n+                            (numpy.zeros(len(ii), int), numpy.zeros(len(ii), int), numpy.zeros(len(ii), int), ii, jj))))[:, probabilities_indices].reshape(\n+                        iend - index, jend - j, len(probabilities)).transpose(2, 0, 1)\n+\n+        for index, group in enumerate(self.outputs):\n+            workspace.image_set.add(group.output_image.value, cellprofiler.cpimage.Image(probabilities[index]))\n+\n+        if self.show_window:\n+            workspace.display_data.input_images = [pixels[index].reshape(*pixels.shape[-2:]) for index in range(pixels.shape[0])]\n+\n+            workspace.display_data.output_images = [probabilities[index] for index in range(len(probabilities))]\n+\n+    def display(self, workspace, figure):\n+        if self.mode == MODE_CLASSIFY:\n+            figure.set_subplots((2, max(len(self.images), len(self.outputs))))\n+\n+            for index, (group, image) in enumerate(zip(self.images, workspace.display_data.input_images)):\n+                figure.subplot_imshow_bw(0, index, image, title=group.image_name.value)\n+\n+            for index, (group, image) in enumerate(zip(self.outputs, workspace.display_data.output_images)):\n+                figure.subplot_imshow_bw(1, index, image, title=group.output_image.value)\n+\n+\n+#\n+# The classifier class is here mostly to simplify the initial development\n+# and it would get moved out to Centrosome eventually.\n+#\n+class PixelClassifier:\n+    \"\"\"Represents a classifier stored in an HDF5 file\n+\n+    The parts:\n+\n+    Kernel - this is the patch that's taken from the pixel's neighborhood. It has the shape, NxM where N is the # of points in the patch and M are the indices relative to the pixel. The kernel dimensions are C, T, Z, Y, X.\n+    TrainingSet - the ground truth data. Each label has a corresponding dataset stored using the name of the class. The dataset has the shape, S x N, where N is the # of points in the patch and S is the # of samples for that class.\n+    Filters - the filters applied to the kernel to derive features. Each filter is a vector of length N.\n+    Classifier - the classifier is pickled after being trained and is stored in a dataset.\n+    \"\"\"\n+    version = 1\n+\n+    def __init__(self, path, mode, classifier_path=None):\n+        \"\"\"Either create or load the classifier from a file\n+\n+        path - path to the file\n+        mode - \"r\" for read-only, \"w\" for overwrite (new file) or \"a\" for read-write access\n+        classifier_path - path to the sub-groups within the HDF file, defaults to the root.\n+        \"\"\"\n+        self.f = h5py.File(path, mode)\n+\n+        self.root = self.f if classifier_path is None else self.f[classifier_path]\n+\n+        if mode == \"w\":\n+            self.f.attrs[A_VERSION] = self.version\n+\n+            self.g_training_set = self.root.create_group(G_TRAINING_SET)\n+\n+            self.g_filters = self.root.create_group(G_FILTERS)\n+\n+            self.g_sampling = self.root.create_group(G_SAMPLING)\n+\n+            self.g_classifiers = self.root.create_group(G_CLASSIFIERS)\n+\n+            self.g_images = self.root.create_group(G_IMAGES)\n+        else:\n+            self.g_training_set = self.root[G_TRAINING_SET]\n+\n+            self.g_filters = self.root[G_FILTERS]\n+\n+            self.g_sampling = self.root[G_SAMPLING]\n+\n+            self.g_classifiers = self.root[G_CLASSIFIERS]\n+\n+            self.g_images = self.root[G_IMAGES]\n+\n+        self.classifier_cache = {}\n+\n+    def __enter__(self):\n+        return self\n+\n+    def __exit__(self, exc_type, exc_value, exc_traceback):\n+        self.f.flush()\n+\n+        del self.root\n+\n+        del self.g_training_set\n+\n+        del self.g_filters\n+\n+        del self.g_classifiers\n+\n+        del self.g_sampling\n+\n+        self.f.close()\n+\n+        del self.f\n+\n+    def random_state(self, extra=''):\n+        \"\"\"\n+        Return a random state based on the ground truth sampled\n+\n+        :param extra:\n+\n+        :return:\n+        \"\"\"\n+        if A_DIGEST not in self.g_training_set.attrs:\n+            md5 = hashlib.md5()\n+\n+            for class_name in self.get_class_names():\n+                md5.update(self.get_ground_truth(class_name).value.data)\n+\n+            self.g_training_set.attrs[A_DIGEST] = md5.hexdigest()\n+\n+        return numpy.random.RandomState(numpy.frombuffer(self.g_training_set.attrs[A_DIGEST] + extra, numpy.uint8))\n+\n+    @staticmethod\n+    def get_instances(group, class_name):\n+        \"\"\"\n+        Get the keys for a group for objects of a given class\n+\n+        :param group:\n+        :param class_name:\n+\n+        :return:\n+        \"\"\"\n+        return sorted([k for k in group.keys() if group[k].attrs[A_CLASS] == class_name])\n+\n+    def set_kernel(self, kernel):\n+        \"\"\"\n+        Set the kernel used to sample pixels from a neighborhood\n+\n+        :param kernel: an N x 5 matrix of N offsets by 5 dimensions (C, T, Z, Y, X)\n+\n+        :return:\n+        \"\"\"\n+        if DATASET_KERNEL in self.root.keys():\n+            del self.root[DATASET_KERNEL]\n+\n+        self.root.create_dataset(DATASET_KERNEL, data=kernel).attrs[A_CLASS] = CLASS_KERNEL\n+\n+    def get_kernel(self):\n+        return self.root[DATASET_KERNEL][:]\n+\n+    def add_class(self, class_name):\n+        self.g_training_set.create_dataset(class_name, shape=(0, 6), dtype=numpy.int32, chunks=(4096, 6), maxshape=(None, 6)).attrs[\n+            A_CLASS] = CLASS_GROUND_TRUTH\n+\n+    def get_class_names(self):\n+        \"\"\"Get the names of the classifier's classes\"\"\"\n+        return self.get_instances(self.g_training_set, CLASS_GROUND_TRUTH)\n+\n+    def get_ground_truth(self, class_name):\n+        \"\"\"\n+        Get the ground truth for a class\n+\n+        :param class_name: the name of the class\n+\n+        :return: an S X 6 where the first index is the image number and the remaining are the coordinates of the GT pixel in C, T, Z, Y, X form\n+        \"\"\"\n+        return self.g_training_set[class_name]\n+\n+    @property\n+    def ground_truth_chunk_size(self):\n+        \"\"\"The size of a chunk of ground truth that fits in memory\"\"\"\n+        return int(50 * 1000 * 1000 / self.get_kernel().shape[0])\n+\n+    @property\n+    def pix_chunk_size(self):\n+        return 1000 * 1000\n+\n+    def add_image(self, image, image_number):\n+        image_number = str(image_number)\n+\n+        if image_number in self.g_images.keys():\n+            del self.g_images[image_number]\n+\n+        self.g_images.create_dataset(image_number, data=image)\n+\n+    def get_image(self, image_number):\n+        return self.g_images[str(image_number)].value\n+\n+    def add_ground_truth(self, class_name, image_number, coordinates):\n+        \"\"\"\n+        Add ground truth to a class\n+\n+        :param class_name: name of the class\n+        :param image_number: the image number as reported in add_image\n+        :param coordinates: an S x 5 matrix of S samples and 5 pixel coordinates\n+\n+        :return:\n+        \"\"\"\n+        coordinates = numpy.column_stack((numpy.ones(coordinates.shape[0], coordinates.dtype) * image_number, coordinates))\n+\n+        dataset = self.get_ground_truth(class_name)\n+\n+        dataset_index = dataset.shape[0]\n+\n+        dataset.resize(dataset_index + coordinates.shape[0], axis=0)\n+\n+        dataset[dataset_index:] = coordinates\n+\n+        if A_DIGEST in self.g_training_set.attrs:\n+            del self.g_training_set.attrs[A_DIGEST]\n+\n+    def get_samples(self, image, pixels):\n+        \"\"\"\n+        Extract samples from an image at given pixels\n+\n+        :param image: a C, T, Z, Y, X image\n+        :param pixels: an S x 5 matrix where the columns are the C, T, Z, Y, X coordinates and the rows are the samples to collect\n+\n+        :return: an S x N matrix where N is the size of the kernel\n+        \"\"\"\n+        coordinates = pixels[:, numpy.newaxis, :] + self.get_kernel()[numpy.newaxis, :, :]\n+\n+        #\n+        # Boundary reflection\n+        #\n+        coordinates[coordinates < 0] = numpy.abs(coordinates[coordinates < 0])\n+\n+        for index, axis_size in enumerate(image.shape):\n+            mask = coordinates[:, :, index] >= axis_size\n+\n+            coordinates[mask, index] = axis_size * 2 - coordinates[mask, index] - 1\n+\n+        #\n+        # Samples\n+        #\n+        return image[\n+            coordinates[:, :, 0],\n+            coordinates[:, :, 1],\n+            coordinates[:, :, 2],\n+            coordinates[:, :, 3],\n+            coordinates[:, :, 4]\n+        ]\n+\n+    def add_sampling(self, sampling_name, d_index):\n+        \"\"\"\n+        Add a sampling of the ground truth\n+\n+        :param sampling_name: a name for the sampling\n+        :param d_index: a dictionary of indices. The key is the class name and the value is a vector of indices into the class's ground truth\n+\n+        :return:\n+        \"\"\"\n+        if sampling_name in self.g_sampling.keys():\n+            del self.g_sampling[sampling_name]\n+\n+        for key, value in d_index.iteritems():\n+            self.g_sampling.create_group(sampling_name).create_dataset(key, data=value)\n+\n+    def sample(self, sampling_name):\n+        \"\"\"\n+        Return sample and vector of class indexes\n+\n+        :param sampling_name: the name of the sampling\n+\n+        :return: a sample which is S x N and vector of length S which is composed of indexes into the class names returned by get_class_names.  S is the length of the sum of all samples in the sampling.\n+        \"\"\"\n+        g = self.g_sampling[sampling_name]\n+\n+        samples = []\n+\n+        classes = []\n+\n+        for index, class_name in enumerate(self.get_class_names()):\n+            if class_name in g.keys():\n+                sampling = g[class_name][:]\n+\n+                classes.append(numpy.ones(len(sampling), numpy.uint8) * index)\n+\n+                ground_truth = self.get_ground_truth(class_name)\n+\n+                #\n+                # h5py datasets are not addressable via an array of indices\n+                # in the way that numpy arrays are. A mask of the array elements\n+                # to be processed is handled by a loop through each selected\n+                # element; it takes ~ hours to process arrays of our size. The\n+                # ground truth may be too large to bring into memory as a\n+                # Numpy array.\n+                #\n+                # We sort the sampling indices and then process in chunks.\n+                #\n+                if len(ground_truth) == len(sampling):\n+                    logger.debug(\"Extracting {0:d} samples from {1:s}\".format(len(sampling), class_name))\n+\n+                    samples.append(ground_truth[:])\n+                else:\n+                    chunk_size = self.pix_chunk_size\n+\n+                    sampling.sort()\n+\n+                    sample_index = 0\n+\n+                    for ground_truth_index in range(0, len(ground_truth), chunk_size):\n+                        ground_truth_index_end = min(ground_truth_index + chunk_size, len(ground_truth))\n+\n+                        if sampling[sample_index] >= ground_truth_index_end:\n+                            continue\n+\n+                        sindx_end = bisect.bisect_left(sampling[sample_index:], ground_truth_index_end) + sample_index\n+\n+                        logger.debug(\"Extracting {0:d} samples from {1:s} {2:d}:{3:d}\".format(sindx_end - sample_index, class_name, ground_truth_index,\n+                                                                                              ground_truth_index_end))\n+\n+                        samples.append(ground_truth[:][sampling[sample_index:sindx_end], :])\n+\n+                        sample_index = sindx_end\n+\n+                        if sample_index >= len(ground_truth):\n+                            break\n+\n+        samples = numpy.vstack(samples)\n+\n+        classes = numpy.hstack(classes)\n+\n+        #\n+        # Order by image number.\n+        #\n+        order = numpy.lexsort([samples[:, _] for _ in reversed(range(samples.shape[1]))])\n+\n+        samples = samples[order]\n+\n+        classes = classes[order]\n+\n+        counts = numpy.bincount(samples[:, 0])\n+\n+        image_numbers = numpy.where(counts > 0)[0]\n+\n+        counts = counts[image_numbers]\n+\n+        indices = numpy.hstack([[0], numpy.cumsum(counts)])\n+\n+        result = []\n+\n+        for image_number, index, index_end in zip(image_numbers, indices[:-1], indices[1:]):\n+            image = self.get_image(image_number)\n+\n+            result.append(self.get_samples(image, samples[index:index_end, 1:]))\n+\n+        return numpy.vstack(result), classes\n+\n+    def make_filter_bank(self, sampling, classes, filter_bank_name, n_filters, algorithm=None):\n+        \"\"\"\n+        Make a filter bank using PCA\n+\n+        :param sampling: a sampling of the ground truth\n+        :param classes: a vector of the same length as the sampling giving the indexes of the classes of each sample\n+        :param filter_bank_name: the name to assign to the filter bank\n+        :param n_filters: # of filters to create\n+        :param algorithm: an object that can be fitted using algorithm.fit(X, Y) and can transform using algorithm.transform(X). Default is RandomizedPCA.\n+\n+        :return:\n+        \"\"\"\n+        if algorithm is None:\n+            algorithm = sklearn.decomposition.RandomizedPCA(n_filters, random_state=self.random_state(filter_bank_name))\n+\n+        algorithm.fit(sampling, classes)\n+\n+        if hasattr(algorithm, \"components_\"):\n+            components = algorithm.components_\n+\n+            if len(components) > n_filters:\n+                components = components[:n_filters]\n+\n+            if filter_bank_name in self.g_filters.keys():\n+                del self.g_filters[filter_bank_name]\n+\n+            self.g_filters.create_dataset(filter_bank_name, data=components).attrs[A_CLASS] = CLASS_FILTER\n+        else:\n+            self.g_filters.create_dataset(filter_bank_name, data=pickle.dumps(algorithm)).attrs[A_CLASS] = CLASS_CLASSIFIER\n+\n+    def use_filter_bank(self, filter_bank_name, sample):\n+        \"\"\"\n+        Transform a sample using a filter bank\n+\n+        :param filter_bank_name:\n+        :param sample:\n+\n+        :return:\n+        \"\"\"\n+        dataset = self.g_filters[filter_bank_name]\n+\n+        if dataset.attrs[A_CLASS] == CLASS_FILTER:\n+            if USE_DOT:\n+                result = numpy.dot(sample, dataset[:].T)\n+            else:\n+                #\n+                # A dot product... but cluster's np.dot is so XXXXed\n+                #\n+                chunk_size = self.ground_truth_chunk_size\n+\n+                result = []\n+\n+                for index in range(0, len(sample), chunk_size):\n+                    index_end = min(index + chunk_size, len(sample))\n+\n+                    logger.debug(\"Processing dot product chunk {0:d}:{1:d} of {2:d}\".format(index, index_end, len(sample)))\n+\n+                    result.append(numpy.sum(sample[index:index_end, :, numpy.newaxis] * dataset[:].T[numpy.newaxis, :, :], 1))\n+\n+                result = numpy.vstack(result)\n+        else:\n+            algorithm = pickle.loads(dataset.value)\n+\n+            result = algorithm.transform(sample)\n+\n+        return result\n+\n+    def fit(self, classifier_name, sample, classes, algorithm=None):\n+        \"\"\"\n+        Fit an algorithm to data and save\n+\n+        :param classifier_name: save using this name\n+        :param sample: S samples x N features\n+        :param classes: S class labels indexing into the class names\n+        :param algorithm: algorithm to use to train\n+\n+        :return:\n+        \"\"\"\n+        if algorithm is None:\n+            algorithm = sklearn.ensemble.ExtraTreesClassifier(n_estimators=N_ESTIMATORS, min_samples_leaf=MIN_SAMPLES_PER_LEAF)","path":"cellprofiler/modules/classify.py","position":900,"original_position":903,"commit_id":"28bb4ca8b5388fe0f039b32e7d478a85178490d3","original_commit_id":"d2aa438eef5a71a034e1bc232264475fe4a6364d","user":{"login":"LeeKamentsky","id":660469,"avatar_url":"https://avatars.githubusercontent.com/u/660469?v=3","gravatar_id":"","url":"https://api.github.com/users/LeeKamentsky","html_url":"https://github.com/LeeKamentsky","followers_url":"https://api.github.com/users/LeeKamentsky/followers","following_url":"https://api.github.com/users/LeeKamentsky/following{/other_user}","gists_url":"https://api.github.com/users/LeeKamentsky/gists{/gist_id}","starred_url":"https://api.github.com/users/LeeKamentsky/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/LeeKamentsky/subscriptions","organizations_url":"https://api.github.com/users/LeeKamentsky/orgs","repos_url":"https://api.github.com/users/LeeKamentsky/repos","events_url":"https://api.github.com/users/LeeKamentsky/events{/privacy}","received_events_url":"https://api.github.com/users/LeeKamentsky/received_events","type":"User","site_admin":false},"body":"Good catch on N_ESTIMATORS and MIN_SAMPLES_PER_LEAF. That's a legacy of me fooling around with the classifier when those were static variables.","created_at":"2016-03-29T12:10:22Z","updated_at":"2016-03-29T12:10:22Z","html_url":"https://github.com/CellProfiler/CellProfiler/pull/1774#discussion_r57712349","pull_request_url":"https://api.github.com/repos/CellProfiler/CellProfiler/pulls/1774","_links":{"self":{"href":"https://api.github.com/repos/CellProfiler/CellProfiler/pulls/comments/57712349"},"html":{"href":"https://github.com/CellProfiler/CellProfiler/pull/1774#discussion_r57712349"},"pull_request":{"href":"https://api.github.com/repos/CellProfiler/CellProfiler/pulls/1774"}}},"pull_request":{"url":"https://api.github.com/repos/CellProfiler/CellProfiler/pulls/1774","id":58481455,"html_url":"https://github.com/CellProfiler/CellProfiler/pull/1774","diff_url":"https://github.com/CellProfiler/CellProfiler/pull/1774.diff","patch_url":"https://github.com/CellProfiler/CellProfiler/pull/1774.patch","issue_url":"https://api.github.com/repos/CellProfiler/CellProfiler/issues/1774","number":1774,"state":"open","locked":false,"title":"WIP: pixel classifier","user":{"login":"LeeKamentsky","id":660469,"avatar_url":"https://avatars.githubusercontent.com/u/660469?v=3","gravatar_id":"","url":"https://api.github.com/users/LeeKamentsky","html_url":"https://github.com/LeeKamentsky","followers_url":"https://api.github.com/users/LeeKamentsky/followers","following_url":"https://api.github.com/users/LeeKamentsky/following{/other_user}","gists_url":"https://api.github.com/users/LeeKamentsky/gists{/gist_id}","starred_url":"https://api.github.com/users/LeeKamentsky/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/LeeKamentsky/subscriptions","organizations_url":"https://api.github.com/users/LeeKamentsky/orgs","repos_url":"https://api.github.com/users/LeeKamentsky/repos","events_url":"https://api.github.com/users/LeeKamentsky/events{/privacy}","received_events_url":"https://api.github.com/users/LeeKamentsky/received_events","type":"User","site_admin":false},"body":"The idea here is to use CellProfiler to train a classifier by feeding it objects representing ground truth and gray-scale images representing the channels. Underlying, the data structure is 5D, but only channel, I and J are used.\r\n\r\nA sampling window is taken around each pixel to create a feature vector. Dimensionality reduction is done via PCA to get a set of eigentextures. The output of these eigentexture filters creates a reduced feature vector for a classifier, e.g. random forests. Currently, things are trained using randomly selected pixels and then two rounds of training are done with random pixels augmented with misclassified pixels.\r\n\r\nClassification applies the kernel to the image (in 128x128 chunks), runs the eigentexture filters to reduce the dimensionality and then classifies using the trained random forest.","created_at":"2016-02-05T20:40:08Z","updated_at":"2016-03-29T12:10:22Z","closed_at":null,"merged_at":null,"merge_commit_sha":"00e34475e2c5fecf1fc5b1d40f5f87e361aef7c6","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/CellProfiler/CellProfiler/pulls/1774/commits","review_comments_url":"https://api.github.com/repos/CellProfiler/CellProfiler/pulls/1774/comments","review_comment_url":"https://api.github.com/repos/CellProfiler/CellProfiler/pulls/comments{/number}","comments_url":"https://api.github.com/repos/CellProfiler/CellProfiler/issues/1774/comments","statuses_url":"https://api.github.com/repos/CellProfiler/CellProfiler/statuses/28bb4ca8b5388fe0f039b32e7d478a85178490d3","head":{"label":"CellProfiler:features/pixel_classifier","ref":"features/pixel_classifier","sha":"28bb4ca8b5388fe0f039b32e7d478a85178490d3","user":{"login":"CellProfiler","id":710590,"avatar_url":"https://avatars.githubusercontent.com/u/710590?v=3","gravatar_id":"","url":"https://api.github.com/users/CellProfiler","html_url":"https://github.com/CellProfiler","followers_url":"https://api.github.com/users/CellProfiler/followers","following_url":"https://api.github.com/users/CellProfiler/following{/other_user}","gists_url":"https://api.github.com/users/CellProfiler/gists{/gist_id}","starred_url":"https://api.github.com/users/CellProfiler/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/CellProfiler/subscriptions","organizations_url":"https://api.github.com/users/CellProfiler/orgs","repos_url":"https://api.github.com/users/CellProfiler/repos","events_url":"https://api.github.com/users/CellProfiler/events{/privacy}","received_events_url":"https://api.github.com/users/CellProfiler/received_events","type":"Organization","site_admin":false},"repo":{"id":1571820,"name":"CellProfiler","full_name":"CellProfiler/CellProfiler","owner":{"login":"CellProfiler","id":710590,"avatar_url":"https://avatars.githubusercontent.com/u/710590?v=3","gravatar_id":"","url":"https://api.github.com/users/CellProfiler","html_url":"https://github.com/CellProfiler","followers_url":"https://api.github.com/users/CellProfiler/followers","following_url":"https://api.github.com/users/CellProfiler/following{/other_user}","gists_url":"https://api.github.com/users/CellProfiler/gists{/gist_id}","starred_url":"https://api.github.com/users/CellProfiler/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/CellProfiler/subscriptions","organizations_url":"https://api.github.com/users/CellProfiler/orgs","repos_url":"https://api.github.com/users/CellProfiler/repos","events_url":"https://api.github.com/users/CellProfiler/events{/privacy}","received_events_url":"https://api.github.com/users/CellProfiler/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/CellProfiler/CellProfiler","description":"An open-source application for measuring phenotypes","fork":false,"url":"https://api.github.com/repos/CellProfiler/CellProfiler","forks_url":"https://api.github.com/repos/CellProfiler/CellProfiler/forks","keys_url":"https://api.github.com/repos/CellProfiler/CellProfiler/keys{/key_id}","collaborators_url":"https://api.github.com/repos/CellProfiler/CellProfiler/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/CellProfiler/CellProfiler/teams","hooks_url":"https://api.github.com/repos/CellProfiler/CellProfiler/hooks","issue_events_url":"https://api.github.com/repos/CellProfiler/CellProfiler/issues/events{/number}","events_url":"https://api.github.com/repos/CellProfiler/CellProfiler/events","assignees_url":"https://api.github.com/repos/CellProfiler/CellProfiler/assignees{/user}","branches_url":"https://api.github.com/repos/CellProfiler/CellProfiler/branches{/branch}","tags_url":"https://api.github.com/repos/CellProfiler/CellProfiler/tags","blobs_url":"https://api.github.com/repos/CellProfiler/CellProfiler/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/CellProfiler/CellProfiler/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/CellProfiler/CellProfiler/git/refs{/sha}","trees_url":"https://api.github.com/repos/CellProfiler/CellProfiler/git/trees{/sha}","statuses_url":"https://api.github.com/repos/CellProfiler/CellProfiler/statuses/{sha}","languages_url":"https://api.github.com/repos/CellProfiler/CellProfiler/languages","stargazers_url":"https://api.github.com/repos/CellProfiler/CellProfiler/stargazers","contributors_url":"https://api.github.com/repos/CellProfiler/CellProfiler/contributors","subscribers_url":"https://api.github.com/repos/CellProfiler/CellProfiler/subscribers","subscription_url":"https://api.github.com/repos/CellProfiler/CellProfiler/subscription","commits_url":"https://api.github.com/repos/CellProfiler/CellProfiler/commits{/sha}","git_commits_url":"https://api.github.com/repos/CellProfiler/CellProfiler/git/commits{/sha}","comments_url":"https://api.github.com/repos/CellProfiler/CellProfiler/comments{/number}","issue_comment_url":"https://api.github.com/repos/CellProfiler/CellProfiler/issues/comments{/number}","contents_url":"https://api.github.com/repos/CellProfiler/CellProfiler/contents/{+path}","compare_url":"https://api.github.com/repos/CellProfiler/CellProfiler/compare/{base}...{head}","merges_url":"https://api.github.com/repos/CellProfiler/CellProfiler/merges","archive_url":"https://api.github.com/repos/CellProfiler/CellProfiler/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/CellProfiler/CellProfiler/downloads","issues_url":"https://api.github.com/repos/CellProfiler/CellProfiler/issues{/number}","pulls_url":"https://api.github.com/repos/CellProfiler/CellProfiler/pulls{/number}","milestones_url":"https://api.github.com/repos/CellProfiler/CellProfiler/milestones{/number}","notifications_url":"https://api.github.com/repos/CellProfiler/CellProfiler/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/CellProfiler/CellProfiler/labels{/name}","releases_url":"https://api.github.com/repos/CellProfiler/CellProfiler/releases{/id}","deployments_url":"https://api.github.com/repos/CellProfiler/CellProfiler/deployments","created_at":"2011-04-05T12:10:12Z","updated_at":"2016-03-21T11:24:49Z","pushed_at":"2016-03-28T21:25:35Z","git_url":"git://github.com/CellProfiler/CellProfiler.git","ssh_url":"git@github.com:CellProfiler/CellProfiler.git","clone_url":"https://github.com/CellProfiler/CellProfiler.git","svn_url":"https://github.com/CellProfiler/CellProfiler","homepage":"http://cellprofiler.org","size":162687,"stargazers_count":124,"watchers_count":124,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":91,"mirror_url":null,"open_issues_count":220,"forks":91,"open_issues":220,"watchers":124,"default_branch":"master"}},"base":{"label":"CellProfiler:master","ref":"master","sha":"89908c18a6034afc3a5c844b758375563b554d7c","user":{"login":"CellProfiler","id":710590,"avatar_url":"https://avatars.githubusercontent.com/u/710590?v=3","gravatar_id":"","url":"https://api.github.com/users/CellProfiler","html_url":"https://github.com/CellProfiler","followers_url":"https://api.github.com/users/CellProfiler/followers","following_url":"https://api.github.com/users/CellProfiler/following{/other_user}","gists_url":"https://api.github.com/users/CellProfiler/gists{/gist_id}","starred_url":"https://api.github.com/users/CellProfiler/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/CellProfiler/subscriptions","organizations_url":"https://api.github.com/users/CellProfiler/orgs","repos_url":"https://api.github.com/users/CellProfiler/repos","events_url":"https://api.github.com/users/CellProfiler/events{/privacy}","received_events_url":"https://api.github.com/users/CellProfiler/received_events","type":"Organization","site_admin":false},"repo":{"id":1571820,"name":"CellProfiler","full_name":"CellProfiler/CellProfiler","owner":{"login":"CellProfiler","id":710590,"avatar_url":"https://avatars.githubusercontent.com/u/710590?v=3","gravatar_id":"","url":"https://api.github.com/users/CellProfiler","html_url":"https://github.com/CellProfiler","followers_url":"https://api.github.com/users/CellProfiler/followers","following_url":"https://api.github.com/users/CellProfiler/following{/other_user}","gists_url":"https://api.github.com/users/CellProfiler/gists{/gist_id}","starred_url":"https://api.github.com/users/CellProfiler/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/CellProfiler/subscriptions","organizations_url":"https://api.github.com/users/CellProfiler/orgs","repos_url":"https://api.github.com/users/CellProfiler/repos","events_url":"https://api.github.com/users/CellProfiler/events{/privacy}","received_events_url":"https://api.github.com/users/CellProfiler/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/CellProfiler/CellProfiler","description":"An open-source application for measuring phenotypes","fork":false,"url":"https://api.github.com/repos/CellProfiler/CellProfiler","forks_url":"https://api.github.com/repos/CellProfiler/CellProfiler/forks","keys_url":"https://api.github.com/repos/CellProfiler/CellProfiler/keys{/key_id}","collaborators_url":"https://api.github.com/repos/CellProfiler/CellProfiler/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/CellProfiler/CellProfiler/teams","hooks_url":"https://api.github.com/repos/CellProfiler/CellProfiler/hooks","issue_events_url":"https://api.github.com/repos/CellProfiler/CellProfiler/issues/events{/number}","events_url":"https://api.github.com/repos/CellProfiler/CellProfiler/events","assignees_url":"https://api.github.com/repos/CellProfiler/CellProfiler/assignees{/user}","branches_url":"https://api.github.com/repos/CellProfiler/CellProfiler/branches{/branch}","tags_url":"https://api.github.com/repos/CellProfiler/CellProfiler/tags","blobs_url":"https://api.github.com/repos/CellProfiler/CellProfiler/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/CellProfiler/CellProfiler/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/CellProfiler/CellProfiler/git/refs{/sha}","trees_url":"https://api.github.com/repos/CellProfiler/CellProfiler/git/trees{/sha}","statuses_url":"https://api.github.com/repos/CellProfiler/CellProfiler/statuses/{sha}","languages_url":"https://api.github.com/repos/CellProfiler/CellProfiler/languages","stargazers_url":"https://api.github.com/repos/CellProfiler/CellProfiler/stargazers","contributors_url":"https://api.github.com/repos/CellProfiler/CellProfiler/contributors","subscribers_url":"https://api.github.com/repos/CellProfiler/CellProfiler/subscribers","subscription_url":"https://api.github.com/repos/CellProfiler/CellProfiler/subscription","commits_url":"https://api.github.com/repos/CellProfiler/CellProfiler/commits{/sha}","git_commits_url":"https://api.github.com/repos/CellProfiler/CellProfiler/git/commits{/sha}","comments_url":"https://api.github.com/repos/CellProfiler/CellProfiler/comments{/number}","issue_comment_url":"https://api.github.com/repos/CellProfiler/CellProfiler/issues/comments{/number}","contents_url":"https://api.github.com/repos/CellProfiler/CellProfiler/contents/{+path}","compare_url":"https://api.github.com/repos/CellProfiler/CellProfiler/compare/{base}...{head}","merges_url":"https://api.github.com/repos/CellProfiler/CellProfiler/merges","archive_url":"https://api.github.com/repos/CellProfiler/CellProfiler/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/CellProfiler/CellProfiler/downloads","issues_url":"https://api.github.com/repos/CellProfiler/CellProfiler/issues{/number}","pulls_url":"https://api.github.com/repos/CellProfiler/CellProfiler/pulls{/number}","milestones_url":"https://api.github.com/repos/CellProfiler/CellProfiler/milestones{/number}","notifications_url":"https://api.github.com/repos/CellProfiler/CellProfiler/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/CellProfiler/CellProfiler/labels{/name}","releases_url":"https://api.github.com/repos/CellProfiler/CellProfiler/releases{/id}","deployments_url":"https://api.github.com/repos/CellProfiler/CellProfiler/deployments","created_at":"2011-04-05T12:10:12Z","updated_at":"2016-03-21T11:24:49Z","pushed_at":"2016-03-28T21:25:35Z","git_url":"git://github.com/CellProfiler/CellProfiler.git","ssh_url":"git@github.com:CellProfiler/CellProfiler.git","clone_url":"https://github.com/CellProfiler/CellProfiler.git","svn_url":"https://github.com/CellProfiler/CellProfiler","homepage":"http://cellprofiler.org","size":162687,"stargazers_count":124,"watchers_count":124,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":91,"mirror_url":null,"open_issues_count":220,"forks":91,"open_issues":220,"watchers":124,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/CellProfiler/CellProfiler/pulls/1774"},"html":{"href":"https://github.com/CellProfiler/CellProfiler/pull/1774"},"issue":{"href":"https://api.github.com/repos/CellProfiler/CellProfiler/issues/1774"},"comments":{"href":"https://api.github.com/repos/CellProfiler/CellProfiler/issues/1774/comments"},"review_comments":{"href":"https://api.github.com/repos/CellProfiler/CellProfiler/pulls/1774/comments"},"review_comment":{"href":"https://api.github.com/repos/CellProfiler/CellProfiler/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/CellProfiler/CellProfiler/pulls/1774/commits"},"statuses":{"href":"https://api.github.com/repos/CellProfiler/CellProfiler/statuses/28bb4ca8b5388fe0f039b32e7d478a85178490d3"}}}}