| // Copyright 2018 Google LLC |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // https://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| #include "src/decoder/partition.h" |
| #include "src/base/bottom_n.h" |
| #include "src/base/utils.h" |
| #include "src/decoder/footprint.h" |
| |
| #include <algorithm> |
| #include <array> |
| #include <limits> |
| #include <memory> |
| #include <numeric> |
| #include <queue> |
| #include <set> |
| #include <unordered_set> |
| #include <utility> |
| |
| namespace astc_codec { |
| |
| namespace { |
| |
| // The maximum number of partitions supported by ASTC is four. |
| constexpr int kMaxNumSubsets = 4; |
| |
| // Partition selection function based on the ASTC specification. |
| // See section C.2.21 |
| int SelectASTCPartition(int seed, int x, int y, int z, int partitioncount, |
| int num_pixels) { |
| if (partitioncount <= 1) { |
| return 0; |
| } |
| |
| if (num_pixels < 31) { |
| x <<= 1; |
| y <<= 1; |
| z <<= 1; |
| } |
| |
| seed += (partitioncount - 1) * 1024; |
| |
| uint32_t rnum = seed; |
| rnum ^= rnum >> 15; |
| rnum -= rnum << 17; |
| rnum += rnum << 7; |
| rnum += rnum << 4; |
| rnum ^= rnum >> 5; |
| rnum += rnum << 16; |
| rnum ^= rnum >> 7; |
| rnum ^= rnum >> 3; |
| rnum ^= rnum << 6; |
| rnum ^= rnum >> 17; |
| |
| uint8_t seed1 = rnum & 0xF; |
| uint8_t seed2 = (rnum >> 4) & 0xF; |
| uint8_t seed3 = (rnum >> 8) & 0xF; |
| uint8_t seed4 = (rnum >> 12) & 0xF; |
| uint8_t seed5 = (rnum >> 16) & 0xF; |
| uint8_t seed6 = (rnum >> 20) & 0xF; |
| uint8_t seed7 = (rnum >> 24) & 0xF; |
| uint8_t seed8 = (rnum >> 28) & 0xF; |
| uint8_t seed9 = (rnum >> 18) & 0xF; |
| uint8_t seed10 = (rnum >> 22) & 0xF; |
| uint8_t seed11 = (rnum >> 26) & 0xF; |
| uint8_t seed12 = ((rnum >> 30) | (rnum << 2)) & 0xF; |
| |
| seed1 *= seed1; |
| seed2 *= seed2; |
| seed3 *= seed3; |
| seed4 *= seed4; |
| seed5 *= seed5; |
| seed6 *= seed6; |
| seed7 *= seed7; |
| seed8 *= seed8; |
| seed9 *= seed9; |
| seed10 *= seed10; |
| seed11 *= seed11; |
| seed12 *= seed12; |
| |
| int sh1, sh2, sh3; |
| if (seed & 1) { |
| sh1 = (seed & 2 ? 4 : 5); |
| sh2 = (partitioncount == 3 ? 6 : 5); |
| } else { |
| sh1 = (partitioncount == 3 ? 6 : 5); |
| sh2 = (seed & 2 ? 4 : 5); |
| } |
| sh3 = (seed & 0x10) ? sh1 : sh2; |
| |
| seed1 >>= sh1; |
| seed2 >>= sh2; |
| seed3 >>= sh1; |
| seed4 >>= sh2; |
| seed5 >>= sh1; |
| seed6 >>= sh2; |
| seed7 >>= sh1; |
| seed8 >>= sh2; |
| |
| seed9 >>= sh3; |
| seed10 >>= sh3; |
| seed11 >>= sh3; |
| seed12 >>= sh3; |
| |
| int a = seed1 * x + seed2 * y + seed11 * z + (rnum >> 14); |
| int b = seed3 * x + seed4 * y + seed12 * z + (rnum >> 10); |
| int c = seed5 * x + seed6 * y + seed9 * z + (rnum >> 06); |
| int d = seed7 * x + seed8 * y + seed10 * z + (rnum >> 02); |
| |
| a &= 0x3F; |
| b &= 0x3F; |
| c &= 0x3F; |
| d &= 0x3F; |
| |
| if (partitioncount <= 3) { |
| d = 0; |
| } |
| if (partitioncount <= 2) { |
| c = 0; |
| } |
| |
| if (a >= b && a >= c && a >= d) { |
| return 0; |
| } else if (b >= c && b >= d) { |
| return 1; |
| } else if (c >= d) { |
| return 2; |
| } else { |
| return 3; |
| } |
| } |
| |
| // A partition hash that we can pass to containers like std::unordered_set |
| struct PartitionHasher { |
| size_t operator()(const Partition& part) const { |
| // The issue here is that if we have two different partitions, A and B, then |
| // their hash should be equal if A and B are equal. We define the distance |
| // between A and B using PartitionMetric, but internally that finds a 1-1 |
| // mapping from labels in A to labels in B. |
| // |
| // With that in mind, when we define a hash for partitions, we need to find |
| // a 1-1 mapping to a 'universal' labeling scheme. Here we define that as |
| // the heuristic: the first encountered label will be 0, the second will be |
| // 1, etc. This creates a unique 1-1 mapping scheme from any partition. |
| // |
| // Note, we can't use this heuristic for the PartitionMetric, as it will |
| // generate very large discrepancies between similar labellings (for example |
| // 000...001 vs 011...111). We are just looking for a boolean distinction |
| // whether or not two partitions are different and don't care how different |
| // they are. |
| std::array<int, kMaxNumSubsets> mapping {{ -1, -1, -1, -1 }}; |
| int next_subset = 0; |
| for (int subset : part.assignment) { |
| if (mapping[subset] < 0) { |
| mapping[subset] = next_subset++; |
| } |
| } |
| assert(next_subset <= kMaxNumSubsets); |
| |
| // The return value will be the hash of the assignment according to this |
| // mapping |
| const auto seed = 0; |
| return std::accumulate(part.assignment.begin(), part.assignment.end(), seed, |
| [&mapping](size_t seed, const int& subset) { |
| std::hash<size_t> hasher; |
| const int s = mapping[subset]; |
| return hasher(seed) ^ hasher(static_cast<size_t>(s)); |
| }); |
| } |
| }; |
| |
| // Construct a VP-Tree of partitions. Since our PartitionMetric satisfies |
| // the triangle inequality, we can use this general higher-dimensional space |
| // partitioning tree to organize our partitions. |
| // |
| // TODO(google): !SPEED! Right now this tree stores an actual linked |
| // structure of pointers which is likely very slow during construction and |
| // very not cache-coherent during traversal, so it'd probably be good to |
| // switch to a flattened binary tree structure if performance becomes an |
| // issue. |
| class PartitionTree { |
| public: |
| // Unclear what it means to have an uninitialized tree, so delete default |
| // constructors, but allow the tree to be moved |
| PartitionTree() = delete; |
| PartitionTree(const PartitionTree&) = delete; |
| PartitionTree(PartitionTree&& t) = default; |
| |
| // Generate a PartitionTree from iterators over |Partition|s |
| template<typename Itr> |
| PartitionTree(Itr begin, Itr end) : parts_(begin, end) { |
| std::vector<int> part_indices(parts_.size()); |
| std::iota(part_indices.begin(), part_indices.end(), 0); |
| root_ = std::unique_ptr<PartitionTreeNode>( |
| new PartitionTreeNode(parts_, part_indices)); |
| } |
| |
| // Search for the k-nearest partitions that are closest to part based on |
| // the result of PartitionMetric |
| void Search(const Partition& part, int k, |
| std::vector<const Partition*>* const results, |
| std::vector<int>* const distances) const { |
| ResultHeap heap(k); |
| SearchNode(root_, part, &heap); |
| |
| results->clear(); |
| if (nullptr != distances) { |
| distances->clear(); |
| } |
| |
| std::vector<ResultNode> search_results = heap.Pop(); |
| for (const auto& result : search_results) { |
| results->push_back(&parts_[result.part_idx]); |
| if (nullptr != distances) { |
| distances->push_back(result.distance); |
| } |
| } |
| |
| assert(results->size() == k); |
| } |
| |
| private: |
| // Heap elements to be stored while searching the tree. The two relevant |
| // pieces of information are the partition index and it's distance from the |
| // queried partition. |
| struct ResultNode { |
| int part_idx; |
| int distance; |
| |
| // Heap based on distance from query point. |
| bool operator<(const ResultNode& other) const { |
| return distance < other.distance; |
| } |
| }; |
| |
| using ResultHeap = base::BottomN<ResultNode>; |
| |
| struct PartitionTreeNode { |
| int part_idx; |
| int split_dist; |
| |
| std::unique_ptr<PartitionTreeNode> left; |
| std::unique_ptr<PartitionTreeNode> right; |
| |
| PartitionTreeNode(const std::vector<Partition> &parts, |
| const std::vector<int> &part_indices) |
| : split_dist(-1) { |
| assert(part_indices.size() > 0); |
| |
| right.reset(nullptr); |
| left.reset(nullptr); |
| |
| // Store the first node as our vantage point |
| part_idx = part_indices[0]; |
| const Partition& vantage_point = parts[part_indices[0]]; |
| |
| // Calculate the distances of the remaining nodes against the vantage |
| // point. |
| std::vector<std::pair<int, int>> part_dists; |
| for (int i = 1; i < part_indices.size(); ++i) { |
| const int idx = part_indices[i]; |
| const int dist = PartitionMetric(vantage_point, parts[idx]); |
| if (dist > 0) { |
| part_dists.push_back(std::make_pair(idx, dist)); |
| } |
| } |
| |
| // If there are no more different parts, then this is a leaf node |
| if (part_dists.empty()) { |
| return; |
| } |
| |
| struct OrderBySecond { |
| typedef std::pair<int, int> PairType; |
| bool operator()(const PairType& lhs, const PairType& rhs) { |
| return lhs.second < rhs.second; |
| } |
| }; |
| |
| // We want to partition the set such that the points are ordered |
| // based on their distances from the vantage point. We can do this |
| // using the partial sort of nth element. |
| std::nth_element( |
| part_dists.begin(), part_dists.begin() + part_dists.size() / 2, |
| part_dists.end(), OrderBySecond()); |
| |
| // Once that's done, our split position is in the middle |
| const auto split_iter = part_dists.begin() + part_dists.size() / 2; |
| split_dist = split_iter->second; |
| |
| // Recurse down the right and left sub-trees with the indices of the |
| // parts that are farther and closer respectively |
| std::vector<int> right_indices; |
| for (auto itr = split_iter; itr != part_dists.end(); ++itr) { |
| right_indices.push_back(itr->first); |
| } |
| |
| if (!right_indices.empty()) { |
| right.reset(new PartitionTreeNode(parts, right_indices)); |
| } |
| |
| std::vector<int> left_indices; |
| for (auto itr = part_dists.begin(); itr != split_iter; ++itr) { |
| left_indices.push_back(itr->first); |
| } |
| |
| if (!left_indices.empty()) { |
| left.reset(new PartitionTreeNode(parts, left_indices)); |
| } |
| } |
| }; |
| |
| void SearchNode(const std::unique_ptr<PartitionTreeNode>& node, |
| const Partition& p, ResultHeap* const heap) const { |
| if (nullptr == node) { |
| return; |
| } |
| |
| // Calculate distance against current node |
| const int dist = PartitionMetric(parts_[node->part_idx], p); |
| |
| // Push it onto the heap and remove the top-most nodes to maintain |
| // closest k indices. |
| ResultNode result; |
| result.part_idx = node->part_idx; |
| result.distance = dist; |
| heap->Push(result); |
| |
| // If the split distance is uninitialized, it means we have no children. |
| if (node->split_dist < 0) { |
| assert(nullptr == node->left); |
| assert(nullptr == node->right); |
| return; |
| } |
| |
| // Next we need to check the left and right trees if their distance |
| // is closer/farther than the farthest element on the heap |
| const int tau = heap->Top().distance; |
| if (dist + tau < node->split_dist || dist - tau < node->split_dist) { |
| SearchNode(node->left, p, heap); |
| } |
| |
| if (dist + tau > node->split_dist || dist - tau > node->split_dist) { |
| SearchNode(node->right, p, heap); |
| } |
| } |
| |
| std::vector<Partition> parts_; |
| std::unique_ptr<PartitionTreeNode> root_; |
| }; |
| |
| // A helper function that generates all of the partitions for each number of |
| // subsets in ASTC blocks and stores them in a PartitionTree for fast retrieval. |
| const int kNumASTCPartitionIDBits = 10; |
| PartitionTree GenerateASTCPartitionTree(Footprint footprint) { |
| std::unordered_set<Partition, PartitionHasher> parts; |
| for (int num_parts = 2; num_parts <= kMaxNumSubsets; ++num_parts) { |
| for (int id = 0; id < (1 << kNumASTCPartitionIDBits); ++id) { |
| Partition part = GetASTCPartition(footprint, num_parts, id); |
| |
| // Make sure we're not using a degenerate partition assignment that wastes |
| // an endpoint pair... |
| bool valid_part = true; |
| for (int i = 0; i < num_parts; ++i) { |
| if (std::find(part.assignment.begin(), part.assignment.end(), i) == |
| part.assignment.end()) { |
| valid_part = false; |
| break; |
| } |
| } |
| |
| if (valid_part) { |
| parts.insert(std::move(part)); |
| } |
| } |
| } |
| |
| return PartitionTree(parts.begin(), parts.end()); |
| } |
| |
| // To avoid needing any fancy boilerplate for mapping from a width, height |
| // tuple, we can define a simple encoding for the block mode: |
| constexpr int EncodeDims(int width, int height) { |
| return (width << 16) | height; |
| } |
| |
| } // namespace |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| |
| int PartitionMetric(const Partition& a, const Partition& b) { |
| // Make sure that one partition is at least a subset of the other... |
| UTILS_RELEASE_ASSERT(a.footprint == b.footprint); |
| |
| // Make sure that the number of parts is within our limits. ASTC has a maximum |
| // of four subsets per block according to the specification. |
| UTILS_RELEASE_ASSERT(a.num_parts <= kMaxNumSubsets); |
| UTILS_RELEASE_ASSERT(b.num_parts <= kMaxNumSubsets); |
| |
| const int w = a.footprint.Width(); |
| const int h = b.footprint.Height(); |
| |
| struct PairCount { |
| int a; |
| int b; |
| int count; |
| |
| // Comparison needed for sort below. |
| bool operator>(const PairCount& other) const { |
| return count > other.count; |
| } |
| }; |
| |
| // Since we need to find the smallest mapping from labels in A to labels in B, |
| // we need to store each label pair in a structure that can later be sorted. |
| // The maximum number of subsets in an ASTC block is four, meaning that |
| // between the two partitions, we can have up to sixteen different pairs. |
| std::array<PairCount, 16> pair_counts; |
| for (int y = 0; y < 4; ++y) { |
| for (int x = 0; x < 4; ++x) { |
| const int idx = y * 4 + x; |
| pair_counts[idx].a = x; |
| pair_counts[idx].b = y; |
| pair_counts[idx].count = 0; |
| } |
| } |
| |
| // Count how many times we see each pair of assigned values (order matters!) |
| for (int y = 0; y < h; ++y) { |
| for (int x = 0; x < w; ++x) { |
| const int idx = y * w + x; |
| |
| const int a_val = a.assignment[idx]; |
| const int b_val = b.assignment[idx]; |
| |
| assert(a_val >= 0); |
| assert(b_val >= 0); |
| |
| assert(a_val < 4); |
| assert(b_val < 4); |
| |
| ++(pair_counts[b_val * 4 + a_val].count); |
| } |
| } |
| |
| // Sort the pairs in descending order based on their count |
| std::sort(pair_counts.begin(), pair_counts.end(), std::greater<PairCount>()); |
| |
| // Now assign pairs one by one until we have no more pairs to assign. Once |
| // a value from A is assigned to a value in B, it can no longer be reassigned, |
| // so we can keep track of this in a matrix. Similarly, to keep the assignment |
| // one-to-one, once a value in B has been assigned to, it cannot be assigned |
| // to again. |
| std::array<std::array<bool, kMaxNumSubsets>, kMaxNumSubsets> assigned { }; |
| |
| int pixels_matched = 0; |
| for (const auto& pair_count : pair_counts) { |
| bool is_assigned = false; |
| for (int i = 0; i < kMaxNumSubsets; ++i) { |
| is_assigned |= assigned.at(pair_count.a).at(i); |
| is_assigned |= assigned.at(i).at(pair_count.b); |
| } |
| |
| if (!is_assigned) { |
| assigned.at(pair_count.a).at(pair_count.b) = true; |
| pixels_matched += pair_count.count; |
| } |
| } |
| |
| // The difference is the number of pixels that had an assignment versus the |
| // total number of pixels. |
| return w * h - pixels_matched; |
| } |
| |
| // Generates the partition assignment for the given block attributes. |
| Partition GetASTCPartition(const Footprint& footprint, int num_parts, |
| int partition_id) { |
| // Partitions must have at least one subset but may have at most four |
| assert(num_parts >= 0); |
| assert(num_parts <= kMaxNumSubsets); |
| |
| // Partition ID can be no more than 10 bits. |
| assert(partition_id >= 0); |
| assert(partition_id < 1 << 10); |
| |
| Partition part = {footprint, num_parts, partition_id, /* assignment = */ {}}; |
| part.assignment.reserve(footprint.NumPixels()); |
| |
| // Maintain column-major order so that we match all of the image processing |
| // algorithms that depend on this class. |
| for (int y = 0; y < footprint.Height(); ++y) { |
| for (int x = 0; x < footprint.Width(); ++x) { |
| const int p = SelectASTCPartition(partition_id, x, y, 0, num_parts, |
| footprint.NumPixels()); |
| part.assignment.push_back(p); |
| } |
| } |
| |
| return part; |
| } |
| |
| const std::vector<const Partition*> FindKClosestASTCPartitions( |
| const Partition& candidate, int k) { |
| const int encoded_dims = EncodeDims(candidate.footprint.Width(), |
| candidate.footprint.Height()); |
| |
| int index = 0; |
| switch (encoded_dims) { |
| case EncodeDims(4, 4): index = 0; break; |
| case EncodeDims(5, 4): index = 1; break; |
| case EncodeDims(5, 5): index = 2; break; |
| case EncodeDims(6, 5): index = 3; break; |
| case EncodeDims(6, 6): index = 4; break; |
| case EncodeDims(8, 5): index = 5; break; |
| case EncodeDims(8, 6): index = 6; break; |
| case EncodeDims(8, 8): index = 7; break; |
| case EncodeDims(10, 5): index = 8; break; |
| case EncodeDims(10, 6): index = 9; break; |
| case EncodeDims(10, 8): index = 10; break; |
| case EncodeDims(10, 10): index = 11; break; |
| case EncodeDims(12, 10): index = 12; break; |
| case EncodeDims(12, 12): index = 13; break; |
| default: |
| assert(false && "Unknown footprint dimensions. This should have been caught sooner."); |
| break; |
| } |
| |
| static const auto* const kASTCPartitionTrees = |
| new std::array<PartitionTree, Footprint::NumValidFootprints()> {{ |
| GenerateASTCPartitionTree(Footprint::Get4x4()), |
| GenerateASTCPartitionTree(Footprint::Get5x4()), |
| GenerateASTCPartitionTree(Footprint::Get5x5()), |
| GenerateASTCPartitionTree(Footprint::Get6x5()), |
| GenerateASTCPartitionTree(Footprint::Get6x6()), |
| GenerateASTCPartitionTree(Footprint::Get8x5()), |
| GenerateASTCPartitionTree(Footprint::Get8x6()), |
| GenerateASTCPartitionTree(Footprint::Get8x8()), |
| GenerateASTCPartitionTree(Footprint::Get10x5()), |
| GenerateASTCPartitionTree(Footprint::Get10x6()), |
| GenerateASTCPartitionTree(Footprint::Get10x8()), |
| GenerateASTCPartitionTree(Footprint::Get10x10()), |
| GenerateASTCPartitionTree(Footprint::Get12x10()), |
| GenerateASTCPartitionTree(Footprint::Get12x12()), |
| }}; |
| |
| const PartitionTree& parts_vptree = kASTCPartitionTrees->at(index); |
| std::vector<const Partition*> results; |
| parts_vptree.Search(candidate, k, &results, nullptr); |
| return results; |
| } |
| |
| // Returns the valid ASTC partition that is closest to the candidate based on |
| // the PartitionMetric defined above. |
| const Partition& FindClosestASTCPartition(const Partition& candidate) { |
| // Given a candidate, the closest valid partition will likely not be an exact |
| // match. Consider all of the texels for which this valid partition differs |
| // with the candidate. |
| // |
| // If the valid partition has more subsets than the candidate, then all of the |
| // highest subset will be included in the mismatched texels. Since the number |
| // of possible partitions with increasing subsets grows exponentially, the |
| // chance that a valid partition with fewer subsets appears within the first |
| // few closest partitions is relatively high. Empirically, we can usually find |
| // a partition with at most |candidate.num_parts| number of subsets within the |
| // first four closest partitions. |
| constexpr int kSearchItems = 4; |
| |
| const std::vector<const Partition*> results = |
| FindKClosestASTCPartitions(candidate, kSearchItems); |
| |
| // Optimistically, look for result with the same number of subsets. |
| for (const auto& result : results) { |
| if (result->num_parts == candidate.num_parts) { |
| return *result; |
| } |
| } |
| |
| // If all else fails, then at least find the result with fewer subsets than |
| // we asked for. |
| for (const auto& result : results) { |
| if (result->num_parts < candidate.num_parts) { |
| return *result; |
| } |
| } |
| |
| assert(false && |
| "Could not find partition with acceptable number of subsets!"); |
| return *(results[0]); |
| } |
| |
| } // namespace astc_codec |