diff --git a/devin_32017267.py b/devin_32017267.py index a778708cdb6511d2037fd6428aed09ecda694ea7..69dfded23d29083dc8431d7f0a74e10998181cf0 100644 --- a/devin_32017267.py +++ b/devin_32017267.py @@ -1,4 +1,6 @@ -from reedsolo import RSCodec, ReedSolomonError +import math +from reedsolo import RSCodec +import numpy def generate_code(text): """Given an input string (can be a plain text message or a URL), converts it into a QR code. @@ -89,21 +91,26 @@ def __RSencode_bits(data, num_codewords=7): def make_matrix(code, version=1): """ Given the complete encoded binary (with metadata, padding & error correction added), - generate from it the QR code matrix for a byte-mode QR code. + generate from it a QR code matrix containing: + - requisite finder, timing & seperator patterns + - modules explicitly reserved for formatting info + - the data itself THIS METHOD ONLY WORKS FOR VERSIONS 1 AND 2 QR CODES: IT DOES NOT ADD/ENCODE THE VERSION INFORMATION REQUIRED BY HIGHER VERSIONS. Args: - code (String): Binary strinng representing the complete binary encoding of the original string, with metadata, padding & error correction added + code (String): Binary strinng representing the complete binary encoding of the original data string, with metadata, padding & error correction added version(int): Version of the QR code (version 1 by default) Returns: - int[][]: Binary matrix representing the QR code (0s represent a whi bn vbnte pixel, 1s represent a black pixel) + int[][]: Binary matrix representing the QR code (0s represent a white pixel, 1s represent a black pixel, 'R' for any modules reserved for formatting information) """ n = 4*(version-1)+21 # Calculate # of pixels on each side from version number - QR_matrix = [[0]*n for _ in range(n)] # All squares are white (0) by default. Can be set to black (1) + QR_matrix = [['E']*n for _ in range(n)] # All squares are empty 'E' by default. Can be set to white (0) or black (1) + + # |__________________________________ FINDER PATTERNS _______________________________________________________| # Paint finder patterns in the topleft, topright and bottomleft corners @@ -128,17 +135,27 @@ def make_matrix(code, version=1): else: # Paint inner square black QR_matrix[y_offset+i][x_offset+j] = 1 - # Add white seperators on edges of finder patters that touch the inside of the QR code - # First index of each tuple denotes row idx in matrix for the horizontal part of the seperator, - # Second idx denotes column idx for the vertical part - seperator_positions = [(7, 7), (7, n-8), (n-8, 7)] - for horiz_ypos, vert_xpos in seperator_positions: - # Each side of the seperator is 8 bits long - for i in range(8): - QR_matrix[horiz_ypos][i] = 0 - - for i in range(8): - QR_matrix[i][vert_xpos] = 0 + # |_____________________________ SEPERATORS ____________________________________| + + # Add white seperators on edges of finder patterns that touch the inside of the QR code + # Each side of the seperator is 8 bits long + + # Fill seperators for topleft finder pattern + for i in range(8): + QR_matrix[7][i] = 0 + QR_matrix[i][7] = 0 + + # Seperators on bottomleft + for i in range(8): + QR_matrix[n-8][i] = 0 + QR_matrix[(n-8)+i][7] = 0 + + # Seperators on topright + for i in range(8): + QR_matrix[7][(n-8)+i] = 0 + QR_matrix[i][n-8] = 0 + + # |____________________________ TIMING PATTERNS ___________________________________| # Insert timing patterns # Horizontal timing pattern is on 7th row (idx 6) of matrix between the seperators (i.e. from columns (by index) 8 up to [not including] n-8) @@ -158,7 +175,9 @@ def make_matrix(code, version=1): # that is located at coordinate (8, [4*V]+9) for version V QR_matrix[(4*version)+9][8] = 1 - # Reserve format information area - set reserved modules as value 'F' + # |_______________________________ RESERVE FORMAT INFO _____________________________________| + + # Reserve format information area - set reserved modules as value 'R' # Reserve: # Near topleft finder pattern, a 1-module # Near topright finder pattern, a 1-module horizontal strip below the seperator @@ -174,23 +193,269 @@ def make_matrix(code, version=1): continue # Reserve horiz. strip - QR_matrix[8][i] = 'F' + QR_matrix[8][i] = 'R' # Reserve vertical strip - QR_matrix[i][8] = 'F' + QR_matrix[i][8] = 'R' # Reserve horizontal strip near topright finder # No dark modules in this area, so no need to check for i in range(n-8,n): - QR_matrix[8][i] = 'F' + QR_matrix[8][i] = 'R' # Reserve vertical strip near bottomleft finder # A single dark module is at the top of this strip, so start iteration at module below it for i in range(n-7,n): - QR_matrix[i][8] = 'F' + QR_matrix[i][8] = 'R' + + # |______________________________________ PLACE DATA BITS _________________________________________________| # Finally, place the data bits into the matrix # Bits are placed starting at the bottom right and proceeding upward in a 2-module wide column # When the column hits the top, the next 2-module column starts immediately to the left and continues downward. # If a reserved area or function pattern is encountered, data bits are placed in the next unused module + i = n-1 + j = n-1 + bit = 0 + going_up = True + while j > 0: + + # Skip over column 7 (idx 6) entirely - this contains the vertical timing pattern + if j == 6: + j -= 1 + continue + + # Change direction & shift to next column if hit top or bottom of matrix + if i < 0 or i > n-1: + if going_up: + going_up = False + i += 1 + else: + going_up = True + i -= 1 + j -= 2 + + if QR_matrix[i][j] == 'E': + QR_matrix[i][j] = code[bit] + QR_matrix[i][j-1] = code[bit+1] + bit += 2 + + if going_up: + i -= 1 + else: + i += 1 + + return QR_matrix + + +def mask_matrix(QR_matrix, version=1): + """ + Apply module masking (toggling) as appropriate to a given QR matrix + The input matrix should have patterns and reserved areas added as required, and data bits inserted. + + THIS FUNCTION ONLY WORKS FOR VERSIONS 1 AND 2 QR CODES + + Args: + QR_Matrix (int[][]): Binary matrix representing the QR code (0s represent a white module, 1s represent a black module, 'R' for any reserved modules) + version (int): Version of the QR code (version 1 by default) + + Returns: + int[][]: Binary matrix representing the QR code with masking applied + """ + + # Masking determined by applying 8 different masking patterns to the QR matrix + # and comparing them. The pattern that gives the lowest 'penalty score' is the one that will be used. + + masking_patterns = [ + lambda r, c: (r+c) % 2 == 0, + lambda r, c: r % 2 == 0, + lambda r, c: c % 3 == 0, + lambda r, c: (r+c) % 3 == 0, + lambda r, c: (r//2 + c//3) % 2 == 0, + lambda r, c: (r*c % 2) + (r*c % 3) == 0, + lambda r, c: (r*c % 2) + (r*c % 3) == 0, + lambda r, c: ((r*c % 2) + (r*c % 3)) % 2 == 0, + lambda r, c: (((r+c) % 2) + (r*c % 3)) % 2 == 0 + ] + + # Find pattern that incurs the minimum penalty + min_penalty = math.inf + min_penalty_matrix = None + for pattern in masking_patterns: + masked_mat = __apply_masking_pattern(pattern, QR_matrix) + pen = __calculate_mask_penalty(masked_mat) + + if pen < min_penalty: + min_penalty = pen + min_penalty_matrix = masked_mat + + + return min_penalty_matrix + + +def __apply_masking_pattern(pattern, QR_matrix, version=1): + """ + Apply, to a QR matrix, a masking pattern defined by a lambda function. + + Args: + pattern (Lambda(row, col): Boolean): The mask pattern - A Lambda that returns true if QR_matrix[row][col] is to be toggled, otherwise false. + QR_Matrix (int[][]): Binary matrix representing the QR code (0s represent a white module, 1s represent a black module, 'R' for any reserved modules) + version (int): Version of the QR code (version 1 by default) + + Returns: + int[][]: Binary matrix representing the QR code with masking pattern applied + """ + n = len(QR_matrix) + + for row in range(n): + for col in range(n): + + # Skip over timing patterns and reserved areas + if (row == 6 or col == 6) or (QR_matrix[row][col] == 'R'): + continue + # Skip over finder patterns & seperators + if (row < 8 and col < 8) or (row < 8 and col > n-9) or (row > n-9 and col < 8): + continue + # Skip over the dark module + if row == n-8 and col == 8: + continue + + if pattern(row, col): # Toggle modules that meet pattern condition + QR_matrix[row][col] = 1 if QR_matrix[row][col] == 0 else 0 + + return QR_matrix + + +def __calculate_mask_penalty(QR_matrix): + """ + Calculates a mask's overall 'penalty' score for a QR code matrix with a masking pattern applied. + + Args: + QR_Matrix (int[][]): Binary matrix representing the QR code (0s represent a white module, 1s represent a black module, 'R' for any reserved modules), with a mask pattern applied + + Returns: + int: total (summation of 4 individual scores) mask penalty score + """ + + # 4 Evaluation conditions need to be checked to compute the total penalty + # The final penalty is the sum of the penalties calculated for each condition - so we can just keep a running total as we resolve each condition + # + # All modules in matrix included - even those reserved or in finder patterns + total_penalty = 0 + n = len(QR_matrix) + + # |_______________________________ CONDITION 1 ________________________________| + + # Evaluation Condition #1 - penalize for each group of five or more same-colored modules in a row (or column). + # Examine this condition w.r.t rows + for row in QR_matrix: + curr_module = None + curr_module_occurences = 0 + for module in row: + + if module == curr_module and module != 'R': # Make sure we skip over modules marked as reserved + curr_module_occurences += 1 + else: + # If there are five consecutive modules of the same color, add 3 to the penalty + # If there are more modules of the same color after the first five, add 1 for each additional module of the same color + if curr_module_occurences >= 5: + total_penalty += curr_module_occurences-2 + curr_module = module + curr_module_occurences = 1 + + # Same thing, but this time with columns instead of rows + for c in range(n): + curr_module = None + curr_module_occurences = 0 + for r in range(n): + + module = QR_matrix[r][c] + if module == curr_module and module != 'R': # Make sure we skip over modules marked as reserved + curr_module_occurences += 1 + else: + if curr_module_occurences >= 5: + total_penalty += curr_module_occurences-2 + curr_module = module + curr_module_occurences = 1 + + # |_______________________________ CONDITION 2 ________________________________| + + # Look for areas of same colour that are are least 2x2 modules or larger + # For every 2x2 single-colour block , add 3 to the penalty score (also count overlapping 2x2 blocks) + for r in range(0, n-1, 2): # iterate in chunks of 2 in both directions - this way we can inspect 2x2 blocks + for c in range(0, n-1, 2): + # Check if all modules in a 2x2 block are the same color + if QR_matrix[r][c] == QR_matrix[r+1][c] == QR_matrix[r][c+1] == QR_matrix[r+1][c+1]: + total_penalty += 3 + + # |_______________________________ CONDITION 3 ________________________________| + + # This condition looks for sequences of modules that resemble the finder patterns at the corners of the matrix + # More specifically, patterns in form dark-light-dark-dark-dark-light-dark that have 4 light modules on either side. + # This is done both row-wise and column-wise. + # 40 (!) is added to the total penalty for every one of these kinds of pattern found. + + # Pattern can either have 4 white modules at left or right + pattern1 = ['1', '0', '1', '1', '1', '0', '1', '0', '0', '0', '0'] + pattern2 = ['0', '0', '0', '0', '1', '0', '1', '1', '1', '0', '1'] + + # Search row-wise first + for row in QR_matrix: + for i in range(n-10): + if (row[i:i+11] == pattern1) or (row[i:i+11] == pattern2): + total_penalty += 40 + + # Now search column-wise. To do this, can simply use numpy to + # transpose the matrix and apply the above row-search process. + mat_transpose = numpy.transpose(QR_matrix).tolist() + for row in mat_transpose: + for i in range(n-10): + if (row[i:i+11] == pattern1) or (row[i:i+11] == pattern2): + total_penalty += 40 + + # |_______________________________ CONDITION 4 ________________________________| + + # This condition is based on the ratio of dark to light modules + + # Count # of dark modules + num_dark_modules = 0 + for r in range(0, n, 2): + for c in range(0, n, 2): + if QR_matrix[r][c] == 1: + num_dark_modules += 1 + + # Calculate % of modules that are dark (total # of modules given by n^2) + # round to a whole integer for convenience + percentage_dark = round( (num_dark_modules/(n**2)) * 100.0 ) + + # Determine the previous and next multiple of 5 of this percentage + prev_multiple = (percentage_dark//5)*5 # Calculate previous multiple by floor dividing by 5 then multiplying back by 5 + next_multiple = prev_multiple + 5 + + # Subtract 50 from these mutliples and take the absolute values of the results + # Divide these by five, then take the smallest of the 2 results and multiply it by 10 + # for the penalty score #4 + penalty_score_4 = max( abs(next_multiple - 50)/5, abs(prev_multiple - 50)/5 ) + total_penalty += penalty_score_4 + + return total_penalty + + +def add_format_info(QR_matrix, version=1): + """ + Encodes formatting information into the reserved ('R') modules in the QR matrix. + The input matrix should have patterns and reserved areas added as required, data bits inserted, + and a masking pattern applied. + + THIS FUNCTION ONLY WORKS FOR VERSIONS 1 AND 2 QR CODES + + Args: + QR_Matrix (int[][]): Binary matrix representing the QR code (0s represent a white module, 1s represent a black module, 'R' for any reserved modules) + version (int): Version of the QR code (version 1 by default) + + Returns: + int[][]: Binary matrix representing the QR code with format info added + """ + + diff --git a/main.py b/main.py index 52a4619dce337d6ddf84a8d5d39ad6b3ca6783a8..d9e0faba84c1b7f884ff0094aa23949226c80d1a 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,8 @@ -from devin_32017267 import generate_code, make_matrix +from devin_32017267 import generate_code, make_matrix, mask_matrix code = generate_code("Hello world") matrix = make_matrix(code) +matrix = mask_matrix(matrix) -print(matrix) \ No newline at end of file +for row in matrix: + print([str(i) for i in row]) \ No newline at end of file