def read_pnm_image(fname, vlo, vhi): # Reads the PBM, PGM or PPM image {fname} and converts it to an array # of floats {G} with shape {(nx,ny,nc)} and elements in {[0 _ 1]}. # # The conversion maps integer samples in the range {vlo..vhi} affinely # to {[0_1]}. If {vhi} is zero, uses the {maxval} specified in the # file. Samples in the file that are outside that range are mapped to # 0.0 or 1.0. Before returning, writes to {sys.stderr} the number of # such overflows. # # Return the array {G} and the integer {m}. # Data for {data_error}: nread = 0 last_line = "" # Last line read from file def next_line(): nonlocal rd, nread, last_line line = rd.readline() if line == "": error("unexpected EOF in PNM image header") nread += 1 last_line = line line = re.sub(r"[#].*$", "", line) line = re.sub(r"[ ]+", " ", line) line = line.strip() return line # .................................................................... def error(msg): nonlocal fname, nread, last_line file_line_error(fname, nread, msg, last_line) assert False # Just in case # .................................................................... # To simplify things, convert to ASCII PGM or PPM # (not binary pixels, not PBM): # with one datum per line: tname = f"/tmp/{os.getpid()}-read_pnm_image.pnm" bash \ ( f"cat {fname}" + \ r" | pamtopnm -plain" + \ f" > {tname}" ) bimg = None with open(tname, "r") as rd: ny, nx, nc, maxval, bpp = read_pnm_header(next_line, error) assert bpp == 0 vhi = maxval if vhi == 0 else min(vhi, maxval) bimg = read_ascii_pnm_samples(next_line, error, ny, nx, nc, maxval, vlo, vhi) rd.close() return bimg # ---------------------------------------------------------------------- def read_pnm_header(next_line, error): # Reads the header of a PBM, PGM or PPM file. Returns the number of # rows {ny}, number of cols {nx}, number of channels {nc}, the max # sample value {maxval}, and the integer number {bpp} of bits per sample. # # The {bpp} will be 0 for an ASCII PBM,PGM, or PPM file (with samples # in ASCII decimal), 1 for a raw PBM file (with 1 bit samples packed 8 # ber byte), or 8 or 16 for a raw PGM or PPM file (with 1 or 2 bytes # per sample). # # Note that if the file is ASCI PBM there may be no spaces between # samples. Thus if {bpp} is zero, {nc} is 1, and {maxval} is 9 or less # one should skip blanks before each sample, but read only one digit # thereafter. Otherwise, if {bpp} is zero one should skip blanks # before each sample and then read any number of digits until # a space or end-of-line. # # The procedure fails if EOF occurs before the header is complete, or # if the header is somehow invalid. The {fname} should be the file # name, used only for error messages. It may be {None} or "-" if not # available, e.g. when reading from {sys.stdin}. # # The function {next_line()} function should provide the next line, # cleansed of '#'-comments and leading, trailing, or redundant blanks, # and fail on end-of-file. The funtion {error(msg)} should print {msg} # and abort. # fields = [] while len(fields) < 4: line = next_line() flds = line.split(r" ") fields += flds if len(flds) > 4: error("spurious fields in PNM image header") magic = fields[0]; raw = False if magic == "P4" or magic == "P5" or magic == "P6": magic = "P" + chr(ord(magic[1]) - 3); raw = True pbm = False; if magic == "P1": nc = 1; pbm = True; elif magic == "P2": nc = 1; elif magic == "P3": nc = 3 else: error(f"invalid magic '{magic}' in PNM header") ifields = [] for k in range(1,4): if not re.fullmatch(r"[0-9]+", fields[k]): error(f"invalid parameter '{fields[k]}' in PNM header") ival = int(fields[k]) if ival < 1 or ival > 65535: error(f"invalid number '{ival}' in PNM header") ifields.append(ival) nx, ny, maxval = ifields if not raw: bpp = 0 elif pbm: bpp = 1 if maxval > 1: error(f"invalid maxval '{maxval}' in PBM header") elif maxval <= 255: bpp = 8 else: bpp = 16 assert maxval < 65535 return ny, nx, nc, maxval, bpp # ---------------------------------------------------------------------- def read_ascii_pnm_samples(next_line, error, ny, nx, nc, maxval, vlo, vhi): # Reads the samples of an ASCII PGM or PBM image file # with {ny} rows, {nx} columns, and {nc} channels. # Returns them as a {numpy} array with shape {(ny,nx,nc)}. # # Each sample must be in the range {0..maxval}, and it is converted to # a float by affinely mapping {vlo..vhi} to {[0_1]}. Pixels out of # that range are counted and a report is written to {stderr} at the # end. # # The function {next_line()} function should provide the next line, # cleansed of '#'-comments and leading, trailing, or redundant blanks, # and fail on end-of-file. The funtion {error(msg)} should print {msg} # and abort. # assert 0 <= vlo and vlo < vhi, "bad {vlo,vhi}" bimg = numpy.zeros((ny, nx, nc)) nbig = 0; nsma = 0; true_vlo = 999999999; true_vhi = 0 smps = [ ]; ksmp = 0; for iy in range(ny): for ix in range(nx): for ic in range(nc): # Ensure that the buffer is not empty: while ksmp >= len(smps): line = next_line() if line == "": file_line_error(tname, nread, "unexpected EOF in PNM samples") smps = line.split(" ") ksmp = 0 smp = smps[ksmp] if not re.fullmatch(r"[0-9]+", smp): file_line_error(tname, nread, f"invalid sample '{smp}' in PNM file") ismp = int(smp) assert ismp >= 0 if ismp > maxval: file_line_error(tname, nread, f"sample '{smp}' too big in PNM file") true_vlo = min(true_vlo, ismp); true_vhi = max(true_vhi, ismp); if ismp < vlo: nsma += 1; ismp = vlo elif ismp > vhi: nbig += 1; ismp = vhi fval = float(ismp - vlo)/float(vhi - vlo) bimg[iy,ix,ic] = fval if nsma > 0: sys.stderr.write(f"{fname}: {nsma:5d} samples below {vlo:5d}\n") if nbig > 0: sys.stderr.write(f"{fname}: {nbig:5d} samples above {vhi:5d}\n") if nbig > 0 or nsma > 0: sys.stderr.write(f"true range = {{{true_vlo:d}..{true_vhi:d}}}\n") return bimg # ----------------------------------------------------------------------