Created
June 19, 2026 07:04
-
-
Save lastforkbender/b8fcdb41d79a6ae1fb37932417729001 to your computer and use it in GitHub Desktop.
Kariaian / IRS
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # kariaian.py | |
| import sys, os, struct, zlib, io, threading, hashlib, time, math, copy | |
| from pathlib import Path | |
| import numpy as np | |
| from PIL import Image, ImageFilter, ImageEnhance, ExifTags | |
| import cv2 | |
| from scipy import ndimage | |
| from scipy.ndimage import gaussian_filter, uniform_filter | |
| from skimage import restoration, filters, morphology, exposure | |
| from skimage.restoration import (inpaint_biharmonic, wiener, | |
| unsupervised_wiener, denoise_wavelet, | |
| denoise_bilateral) | |
| try: | |
| import torch, torchvision | |
| TORCH = True | |
| except ImportError: | |
| TORCH = False | |
| try: | |
| import rawpy | |
| RAW_SUPPORT = True | |
| except ImportError: | |
| RAW_SUPPORT = False | |
| from PyQt5.QtWidgets import ( | |
| QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, | |
| QSplitter, QTreeWidget, QTreeWidgetItem, QLabel, QPushButton, | |
| QFileDialog, QProgressBar, QTextEdit, QTabWidget, QGroupBox, | |
| QSlider, QSpinBox, QComboBox, QCheckBox, QScrollArea, | |
| QGridLayout, QFrame, QSizePolicy, QMessageBox, QAction, | |
| QToolBar, QStatusBar, QMenu, QDockWidget, QListWidget, | |
| QListWidgetItem, QAbstractItemView, QDialog, QFormLayout, QLineEdit) | |
| from PyQt5.QtCore import Qt, QThread, pyqtSignal, QSize, QTimer | |
| from PyQt5.QtGui import (QPixmap, QImage, QIcon, QColor, QPalette, | |
| QFont, QPainter, QPen, QBrush, QLinearGradient) | |
| P = {"bg": "#0D1117", "panel": "#161B22", "border": "#21262D", | |
| "accent": "#00D9FF", "accent2": "#7EE787", "warn": "#F0883E", | |
| "danger": "#F85149", "text": "#E6EDF3", "muted": "#8B949E", | |
| "hover": "#1C2128", "sel": "#1F6FEB",} | |
| QSS = f""" | |
| QMainWindow,QWidget{{background:{P['bg']};color:{P['text']};font-family:'Segoe UI','Inter',sans-serif;font-size:13px}} | |
| QTabWidget::pane{{border:1px solid {P['border']};background:{P['panel']}}} | |
| QTabBar::tab{{background:{P['bg']};color:{P['muted']};padding:8px 16px;border:1px solid {P['border']};border-bottom:none;border-radius:4px 4px 0 0}} | |
| QTabBar::tab:selected{{background:{P['panel']};color:{P['accent']};border-bottom:2px solid {P['accent']}}} | |
| QTabBar::tab:hover{{color:{P['text']};background:{P['hover']}}} | |
| QPushButton{{background:{P['panel']};color:{P['text']};border:1px solid {P['border']};border-radius:6px;padding:7px 16px;font-weight:500}} | |
| QPushButton:hover{{background:{P['hover']};border-color:{P['accent']};color:{P['accent']}}} | |
| QPushButton#primary{{background:{P['sel']};border-color:{P['sel']};color:#fff;font-weight:600}} | |
| QPushButton#primary:hover{{background:#2879F5}} | |
| QGroupBox{{border:1px solid {P['border']};border-radius:8px;margin-top:14px;padding:10px;color:{P['muted']};font-size:11px}} | |
| QGroupBox::title{{subcontrol-origin:margin;left:10px;padding:0 6px}} | |
| QTreeWidget,QListWidget{{background:{P['panel']};border:1px solid {P['border']};border-radius:6px;color:{P['text']}}} | |
| QTreeWidget::item:selected,QListWidget::item:selected{{background:{P['sel']}}} | |
| QTreeWidget::item:hover,QListWidget::item:hover{{background:{P['hover']}}} | |
| QTextEdit{{background:{P['panel']};border:1px solid {P['border']};border-radius:6px;color:{P['text']};font-family:'Consolas',monospace;font-size:12px}} | |
| QProgressBar{{background:{P['border']};border-radius:4px;height:8px;color:transparent}} | |
| QProgressBar::chunk{{background:qlineargradient(x1:0,y1:0,x2:1,y2:0,stop:0 {P['accent']},stop:1 {P['accent2']});border-radius:4px}} | |
| QSlider::groove:horizontal{{background:{P['border']};height:4px;border-radius:2px}} | |
| QSlider::handle:horizontal{{background:{P['accent']};width:14px;height:14px;border-radius:7px;margin:-5px 0}} | |
| QSlider::sub-page:horizontal{{background:{P['accent']};border-radius:2px}} | |
| QComboBox{{background:{P['panel']};border:1px solid {P['border']};border-radius:6px;padding:5px 10px;color:{P['text']}}} | |
| QComboBox QAbstractItemView{{background:{P['panel']};border:1px solid {P['border']};color:{P['text']};selection-background-color:{P['sel']}}} | |
| QScrollBar:vertical{{background:{P['bg']};width:8px}} | |
| QScrollBar::handle:vertical{{background:{P['border']};border-radius:4px;min-height:30px}} | |
| QScrollBar::add-line:vertical,QScrollBar::sub-line:vertical{{height:0}} | |
| QLabel#h{{color:{P['accent']};font-weight:700;font-size:14px}} | |
| QLabel#m{{color:{P['muted']};font-size:11px}} | |
| QStatusBar{{background:{P['panel']};color:{P['muted']};border-top:1px solid {P['border']}}} | |
| QToolBar{{background:{P['panel']};border-bottom:1px solid {P['border']};spacing:4px;padding:4px}} | |
| QMenuBar{{background:{P['panel']};color:{P['text']};border-bottom:1px solid {P['border']}}} | |
| QMenuBar::item:selected{{background:{P['hover']}}} | |
| QMenu{{background:{P['panel']};border:1px solid {P['border']};color:{P['text']}}} | |
| QMenu::item:selected{{background:{P['sel']}}} | |
| QCheckBox{{color:{P['text']};spacing:8px}} | |
| QCheckBox::indicator{{width:16px;height:16px;border:1px solid {P['border']};border-radius:3px;background:{P['panel']}}} | |
| QCheckBox::indicator:checked{{background:{P['sel']};border-color:{P['sel']}}} | |
| QSpinBox,QLineEdit{{background:{P['panel']};border:1px solid {P['border']};border-radius:6px;padding:5px 8px;color:{P['text']}}} | |
| """ | |
| SIGS = { | |
| "JPEG": (b"\xFF\xD8\xFF", b"\xFF\xD9", ".jpg", 8<<20), | |
| "PNG": (b"\x89PNG\r\n\x1a\n", b"\x00\x00\x00\x00IEND", ".png", 16<<20), | |
| "GIF": (b"GIF8", b"\x00;", ".gif", 4<<20), | |
| "BMP": (b"BM", b"", ".bmp", 8<<20), | |
| "TIFF": (b"II*\x00", b"", ".tif", 24<<20), | |
| "TIFF2":(b"MM\x00*", b"", ".tif", 24<<20), | |
| "WEBP": (b"RIFF", b"WEBP", ".webp",16<<20), | |
| "ICO": (b"\x00\x00\x01\x00", b"", ".ico", 1<<20), | |
| "PSD": (b"8BPS", b"", ".psd", 64<<20),} | |
| if RAW_SUPPORT: | |
| SIGS["CR2"] = (b"\xFF\xD8\xFF\xE1", b"", ".cr2", 48<<20) | |
| SIGS["NEF"] = (b"II*\x00\x10\x00\x00\x00CR", b"", ".nef", 48<<20) | |
| class Worker(QThread): | |
| progress = pyqtSignal(int) | |
| log = pyqtSignal(str) | |
| finished = pyqtSignal(object) | |
| error = pyqtSignal(str) | |
| class CarveWorker(Worker): | |
| def __init__(self, src, out, types, max_sz_mb): | |
| super().__init__() | |
| self.src, self.out, self.types = src, out, types | |
| self.max_sz = max_sz_mb << 20 | |
| def run(self): | |
| try: | |
| total = os.path.getsize(self.src) | |
| found, CHUNK, OVERLAP = [], 4<<20, 64 | |
| with open(self.src, "rb") as f: | |
| buf = b""; off = 0 | |
| while True: | |
| new = f.read(CHUNK) | |
| if not new: break | |
| buf = (buf[-OVERLAP:] if buf else b"") + new | |
| base = off - min(OVERLAP, off) | |
| for name, (hdr, ftr, ext, maxsz) in SIGS.items(): | |
| if name not in self.types: continue | |
| limit = min(maxsz, self.max_sz) | |
| i = 0 | |
| while True: | |
| i = buf.find(hdr, i) | |
| if i == -1: break | |
| abs_s = base + i | |
| end = abs_s + limit | |
| if ftr: | |
| j = buf.find(ftr, i + len(hdr)) | |
| if j != -1: end = base + j + len(ftr) | |
| data = self._slice(self.src, abs_s, end - abs_s) | |
| if data and len(data) > 64: | |
| fn = os.path.join(self.out, | |
| f"{name}_{abs_s:010X}{ext}") | |
| open(fn,"wb").write(data) | |
| found.append(fn) | |
| self.log.emit(f"✓ {name} @ 0x{abs_s:X} ({len(data)//1024} KB)") | |
| i += len(hdr) | |
| off += len(new) | |
| self.progress.emit(min(99, int(off/total*100))) | |
| self.progress.emit(100) | |
| self.finished.emit(found) | |
| except Exception as e: | |
| self.error.emit(str(e)) | |
| def _slice(self, path, start, length): | |
| try: | |
| with open(path,"rb") as f: | |
| f.seek(start); return f.read(min(length, self.max_sz)) | |
| except: return None | |
| class FSForensicsWorker(Worker): | |
| IMAGE_EXTS = {b'JPG',b'JPEG',b'PNG',b'GIF',b'BMP',b'TIF',b'TIFF',b'WEBP',b'PSD',b'CR2',b'NEF'} | |
| def __init__(self, src, out, do_ntfs, do_fat): | |
| super().__init__() | |
| self.src, self.out = src, out | |
| self.do_ntfs, self.do_fat = do_ntfs, do_fat | |
| def run(self): | |
| try: | |
| total = os.path.getsize(self.src) | |
| results = [] | |
| CHUNK = 4 << 20 | |
| OVERLAP = 4096 | |
| with open(self.src,"rb") as f: | |
| buf = b""; off = 0; p = 0 | |
| while True: | |
| new = f.read(CHUNK) | |
| if not new: break | |
| buf = (buf[-OVERLAP:] if buf else b"") + new | |
| base = off - min(OVERLAP, off) | |
| if self.do_ntfs: | |
| results += self._scan_mft(buf, base, f, total) | |
| if self.do_fat: | |
| results += self._scan_fat(buf, base, f, total) | |
| off += len(new) | |
| np_ = int(off/total*100) | |
| if np_ > p: p = np_; self.progress.emit(min(99, p)) | |
| self.progress.emit(100) | |
| self.finished.emit(results) | |
| except Exception as e: | |
| self.error.emit(str(e)) | |
| def _scan_mft(self, buf, base, f, total): | |
| found = []; i = 0 | |
| while True: | |
| i = buf.find(b"FILE", i) | |
| if i == -1: break | |
| rec = buf[i:i+1024] | |
| if len(rec) < 48: | |
| i += 4; continue | |
| if rec[4:6] != b"\x28\x00" and rec[4:6] != b"\x30\x00": | |
| i += 4; continue | |
| try: | |
| alloc = struct.unpack_from("<H", rec, 28)[0] | |
| flags = struct.unpack_from("<H", rec, 22)[0] | |
| in_use = bool(flags & 0x01) | |
| is_dir = bool(flags & 0x02) | |
| if not in_use and not is_dir: | |
| name, size = self._parse_mft_attrs(rec) | |
| if name: | |
| ext = Path(name).suffix.upper().lstrip(".").encode() | |
| if ext in self.IMAGE_EXTS or not ext: | |
| abs_off = base + i | |
| info = {"type":"NTFS-MFT","name":name, | |
| "size":size,"offset":abs_off} | |
| found.append(info) | |
| self.log.emit(f" MFT deleted: {name} ({size//1024 if size else '?'} KB)") | |
| except: pass | |
| i += 4 | |
| return found | |
| def _parse_mft_attrs(self, rec): | |
| name = ""; size = 0 | |
| try: | |
| off = struct.unpack_from("<H", rec, 20)[0] | |
| while off + 4 < len(rec): | |
| atype = struct.unpack_from("<I", rec, off)[0] | |
| if atype == 0xFFFFFFFF: break | |
| alen = struct.unpack_from("<I", rec, off+4)[0] | |
| if alen < 8 or alen > 1024: break | |
| if atype == 0x30: | |
| try: | |
| fnoff = struct.unpack_from("<H", rec, off+20)[0] | |
| flen = rec[off + fnoff + 64] | |
| raw = rec[off+fnoff+66 : off+fnoff+66+flen*2] | |
| name = raw.decode("utf-16-le", errors="replace") | |
| size = struct.unpack_from("<Q", rec, off+fnoff+48)[0] | |
| except: pass | |
| elif atype == 0x80: | |
| try: | |
| non_res = rec[off+8] | |
| if not non_res: | |
| dlen = struct.unpack_from("<I", rec, off+16)[0] | |
| size = max(size, dlen) | |
| except: pass | |
| off += alen | |
| except: pass | |
| return name, size | |
| def _scan_fat(self, buf, base, f, total): | |
| found = []; i = 0 | |
| while True: | |
| if i + 32 > len(buf): break | |
| b0 = buf[i] | |
| if b0 == 0xE5: | |
| attr = buf[i+11] | |
| if attr in (0x20, 0x21, 0x01): | |
| raw_name = buf[i+1:i+8].rstrip(b"\x20\x00") | |
| raw_ext = buf[i+8:i+11].rstrip(b"\x20\x00") | |
| if raw_ext in self.IMAGE_EXTS: | |
| try: | |
| size = struct.unpack_from("<I", buf, i+28)[0] | |
| name = (raw_name + b"." + raw_ext).decode("ascii","replace") | |
| info = {"type":"FAT-dirent","name":name, | |
| "size":size,"offset":base+i} | |
| found.append(info) | |
| self.log.emit(f" FAT deleted: {name} ({size//1024} KB)") | |
| except: pass | |
| i += 32 | |
| return found | |
| class RepairWorker(Worker): | |
| def __init__(self, path, out): | |
| super().__init__() | |
| self.path, self.out = path, out | |
| def run(self): | |
| try: | |
| ext = Path(self.path).suffix.lower() | |
| if ext == ".png": | |
| self.finished.emit(self._repair_png()) | |
| else: | |
| self.finished.emit(self._repair_jpeg()) | |
| except Exception as e: | |
| self.error.emit(str(e)) | |
| def _repair_png(self): | |
| self.progress.emit(10) | |
| data = bytearray(open(self.path,"rb").read()) | |
| if data[:8] != b"\x89PNG\r\n\x1a\n": | |
| data[:8] = b"\x89PNG\r\n\x1a\n" | |
| self.log.emit("Fixed PNG magic bytes") | |
| self.progress.emit(30) | |
| data = self._fix_png_chunks(data) | |
| self.progress.emit(70) | |
| out = os.path.join(self.out, "repaired_" + os.path.basename(self.path)) | |
| open(out,"wb").write(data) | |
| try: | |
| Image.open(out).verify() | |
| self.log.emit("✓ PNG verified") | |
| except Exception as e: | |
| self.log.emit(f"! Partial: {e}") | |
| self.progress.emit(100) | |
| return [out] | |
| def _fix_png_chunks(self, data): | |
| SIG = b"\x89PNG\r\n\x1a\n" | |
| pos = 8; out = bytearray(SIG); fixed = 0 | |
| while pos + 12 <= len(data): | |
| raw_len = data[pos:pos+4] | |
| length = struct.unpack(">I", raw_len)[0] | |
| if length > 1<<24: | |
| self.log.emit(f"Bad chunk length @ {pos:#x} — skipped") | |
| pos += 4; fixed += 1; continue | |
| ctype = data[pos+4:pos+8] | |
| cdata = data[pos+8:pos+8+length] | |
| real_crc = struct.pack(">I", zlib.crc32(ctype+cdata)&0xFFFFFFFF) | |
| if data[pos+8+length:pos+12+length] != real_crc: | |
| self.log.emit(f"CRC fixed in chunk {ctype}") | |
| fixed += 1 | |
| out += data[pos:pos+8+length] + real_crc | |
| pos += 12 + length | |
| if ctype == b"IEND": break | |
| if fixed: self.log.emit(f"Total chunks repaired: {fixed}") | |
| return bytes(out) | |
| def _repair_jpeg(self): | |
| self.progress.emit(10) | |
| data = bytearray(open(self.path,"rb").read()) | |
| if data[:3] != b"\xFF\xD8\xFF": | |
| data = bytearray(b"\xFF\xD8\xFF\xE0\x00\x10JFIF\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00") + data | |
| self.log.emit("Prepended JPEG SOI+APP0") | |
| self.progress.emit(20) | |
| if data[-2:] != b"\xFF\xD9": | |
| data += b"\xFF\xD9" | |
| self.log.emit("Appended JPEG EOI") | |
| self.progress.emit(40) | |
| sos_off = data.find(b"\xFF\xDA") | |
| if sos_off != -1: | |
| data = self._rescue_scan_stream(data, sos_off) | |
| self.progress.emit(80) | |
| out = os.path.join(self.out, "repaired_" + os.path.basename(self.path)) | |
| open(out,"wb").write(data) | |
| try: | |
| img = Image.open(out); img.load() | |
| self.log.emit(f"✓ JPEG loaded: {img.width}×{img.height}") | |
| except Exception as e: | |
| self.log.emit(f"! Partial: {e}") | |
| try: | |
| arr = np.array(Image.open(out).convert("RGB")) | |
| bgr = cv2.cvtColor(arr, cv2.COLOR_RGB2BGR) | |
| clean_out = os.path.join(self.out, | |
| "repaired_clean_" + Path(self.path).stem + ".jpg") | |
| cv2.imwrite(clean_out, bgr, [cv2.IMWRITE_JPEG_QUALITY, 92]) | |
| self.log.emit(f"✓ Clean re-encode saved") | |
| self.progress.emit(100) | |
| return [out, clean_out] | |
| except: | |
| pass | |
| self.progress.emit(100) | |
| return [out] | |
| def _rescue_scan_stream(self, data, sos_off): | |
| header_len = struct.unpack(">H", data[sos_off+2:sos_off+4])[0] | |
| scan_start = sos_off + 2 + header_len | |
| scan_data = bytearray(data[scan_start:]) | |
| fixed = 0 | |
| i = 0 | |
| while i < len(scan_data) - 1: | |
| if scan_data[i] == 0xFF: | |
| nxt = scan_data[i+1] | |
| if nxt not in (0x00,0xD9) and not (0xD0 <= nxt <= 0xD7): | |
| if nxt != 0xFF: | |
| scan_data[i+1] = 0x00 | |
| fixed += 1 | |
| i += 1 | |
| if fixed: | |
| self.log.emit(f"Scan-stream: {fixed} invalid marker(s) patched") | |
| return bytes(data[:scan_start]) + bytes(scan_data) | |
| class EnhanceWorker(Worker): | |
| def __init__(self, path, out, ops): | |
| super().__init__() | |
| self.path, self.out, self.ops = path, out, ops | |
| def run(self): | |
| try: | |
| self.progress.emit(5) | |
| pil = Image.open(self.path).convert("RGB") | |
| img = np.array(pil).astype(np.float32) / 255.0 | |
| step = max(1, 90 // len(self.ops)) if self.ops else 90 | |
| p = 5 | |
| for op, params in self.ops: | |
| img = self._apply(op, params, img) | |
| p = min(p + step, 95); self.progress.emit(p) | |
| out_name = "enhanced_" + Path(self.path).stem + ".png" | |
| out_path = os.path.join(self.out, out_name) | |
| Image.fromarray((img*255).clip(0,255).astype(np.uint8)).save(out_path) | |
| self.log.emit(f"✓ Saved → {out_name}") | |
| self.progress.emit(100) | |
| self.finished.emit([out_path]) | |
| except Exception as e: | |
| self.error.emit(str(e)) | |
| def _u8(self, img): | |
| return (img*255).clip(0,255).astype(np.uint8) | |
| def _apply(self, op, p, img): | |
| u8 = self._u8(img) | |
| bgr = cv2.cvtColor(u8, cv2.COLOR_RGB2BGR) | |
| if op == "denoise_nlm": | |
| self.log.emit("NLM denoising…") | |
| h = p.get("h", 10) | |
| bgr = cv2.fastNlMeansDenoisingColored(bgr, None, h, h, 7, 21) | |
| return cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB).astype(np.float32)/255 | |
| if op == "denoise_wavelet": | |
| self.log.emit("Wavelet denoising…") | |
| return denoise_wavelet(img, channel_axis=-1, rescale_sigma=True, | |
| sigma=p.get("sigma",0.05)).astype(np.float32) | |
| if op == "denoise_bilateral": | |
| self.log.emit("Bilateral denoising…") | |
| return denoise_bilateral(img, sigma_color=p.get("sc",0.1), | |
| sigma_spatial=p.get("ss",2), | |
| channel_axis=-1).astype(np.float32) | |
| if op == "sharpen": | |
| self.log.emit("Unsharp mask…") | |
| pil = Image.fromarray(u8) | |
| pil = pil.filter(ImageFilter.UnsharpMask( | |
| radius=p.get("sigma",1.0), | |
| percent=int(p.get("amount",1.5)*100), threshold=3)) | |
| return np.array(pil).astype(np.float32)/255 | |
| if op == "contrast_clahe": | |
| self.log.emit("CLAHE contrast…") | |
| lab = cv2.cvtColor(bgr, cv2.COLOR_BGR2LAB) | |
| lab[:,:,0] = cv2.createCLAHE(p.get("clip",2.0),(8,8)).apply(lab[:,:,0]) | |
| return cv2.cvtColor(lab, cv2.COLOR_LAB2RGB).astype(np.float32)/255 | |
| if op == "contrast_gamma": | |
| self.log.emit("Gamma correction…") | |
| g = p.get("gamma", 1.0) | |
| return np.power(img.clip(0,1), g).astype(np.float32) | |
| if op == "deblur_wiener": | |
| self.log.emit("Wiener deblur…") | |
| psz = max(3, int(p.get("psf",5))) | |
| psf = np.ones((psz,psz))/(psz**2) | |
| ch = [wiener(img[:,:,c], psf, p.get("balance",0.1)) for c in range(3)] | |
| return np.stack(ch,-1).clip(0,1).astype(np.float32) | |
| if op == "deblur_dft": | |
| self.log.emit("DFT-domain deblur (inverse filter)…") | |
| return self._dft_deblur(img, p) | |
| if op == "inpaint_telea": | |
| self.log.emit("Inpaint TELEA (dark-region mask)…") | |
| gray = cv2.cvtColor(bgr, cv2.COLOR_BGR2GRAY) | |
| _, mask = cv2.threshold(gray, p.get("thresh",10), 255, cv2.THRESH_BINARY_INV) | |
| mask = cv2.dilate(mask, np.ones((3,3),np.uint8), iterations=2) | |
| result = cv2.inpaint(bgr, mask, p.get("radius",5), cv2.INPAINT_TELEA) | |
| return cv2.cvtColor(result, cv2.COLOR_BGR2RGB).astype(np.float32)/255 | |
| if op == "inpaint_biharmonic": | |
| self.log.emit("Biharmonic inpaint (scratch mask)…") | |
| gray = cv2.cvtColor(u8, cv2.COLOR_RGB2GRAY) | |
| _, m = cv2.threshold(gray, p.get("thresh",10), 1, cv2.THRESH_BINARY_INV) | |
| m = cv2.dilate(m.astype(np.uint8), np.ones((3,3),np.uint8), iterations=1) | |
| return inpaint_biharmonic(img, m.astype(bool), | |
| channel_axis=-1).clip(0,1).astype(np.float32) | |
| if op == "color_restore": | |
| self.log.emit("Colour restoration…") | |
| pil = Image.fromarray(u8) | |
| pil = ImageEnhance.Color(pil).enhance(p.get("sat",1.5)) | |
| pil = ImageEnhance.Contrast(pil).enhance(p.get("con",1.2)) | |
| pil = ImageEnhance.Brightness(pil).enhance(p.get("bri",1.05)) | |
| return np.array(pil).astype(np.float32)/255 | |
| if op == "histogram_eq": | |
| self.log.emit("Adaptive histogram equalisation…") | |
| from skimage import exposure | |
| return exposure.equalize_adapthist(img, clip_limit=p.get("clip",0.03) | |
| ).astype(np.float32) | |
| if op == "denoise_tv": | |
| self.log.emit("Total-variation denoising…") | |
| from skimage.restoration import denoise_tv_chambolle | |
| return denoise_tv_chambolle(img, weight=p.get("weight",0.1), | |
| channel_axis=-1).astype(np.float32) | |
| return img | |
| def _dft_deblur(self, img, p): | |
| k = p.get("k", 0.01) | |
| psz = max(3, int(p.get("psf",5))) | |
| psf = np.ones((psz,psz))/(psz**2) | |
| out = np.zeros_like(img) | |
| h, w = img.shape[:2] | |
| for c in range(3): | |
| ch = img[:,:,c].astype(np.float32) | |
| P = np.zeros((h,w), np.float32) | |
| P[:psz,:psz] = psf | |
| P = np.roll(np.roll(P, -(psz//2), 0), -(psz//2), 1) | |
| H = np.fft.fft2(P) | |
| G = np.fft.fft2(ch) | |
| Hc = np.conj(H) | |
| denom = Hc*H + k | |
| F = Hc * G / denom | |
| out[:,:,c] = np.real(np.fft.ifft2(F)) | |
| return out.clip(0,1).astype(np.float32) | |
| class SuperResWorker(Worker): | |
| def __init__(self, path, out, scale, iters, denoise): | |
| super().__init__() | |
| self.path, self.out = path, out | |
| self.scale, self.iters, self.denoise = scale, iters, denoise | |
| def run(self): | |
| try: | |
| self.progress.emit(5) | |
| pil = Image.open(self.path).convert("RGB") | |
| img = np.array(pil).astype(np.float32) / 255.0 | |
| self.log.emit(f"Input: {img.shape[1]}×{img.shape[0]}") | |
| if TORCH: | |
| result = self._srcnn_upscale(img) | |
| else: | |
| result = self._iterative_upscale(img) | |
| out_name = f"superres_x{self.scale}_{Path(self.path).stem}.png" | |
| out_path = os.path.join(self.out, out_name) | |
| Image.fromarray((result*255).clip(0,255).astype(np.uint8)).save(out_path) | |
| self.log.emit(f"✓ Output: {result.shape[1]}×{result.shape[0]}") | |
| self.progress.emit(100) | |
| self.finished.emit([out_path]) | |
| except Exception as e: | |
| self.error.emit(str(e)) | |
| def _iterative_upscale(self, img): | |
| self.log.emit("Iterative edge-guided upscale (no GPU)…") | |
| u8 = (img*255).clip(0,255).astype(np.uint8) | |
| bgr = cv2.cvtColor(u8, cv2.COLOR_RGB2BGR) | |
| h, w = bgr.shape[:2] | |
| nh, nw = h*self.scale, w*self.scale | |
| up = cv2.resize(bgr, (nw,nh), interpolation=cv2.INTER_LANCZOS4) | |
| self.progress.emit(20) | |
| for i in range(self.iters): | |
| lab = cv2.cvtColor(up, cv2.COLOR_BGR2LAB) | |
| l = lab[:,:,0].astype(np.float32) | |
| edges = cv2.Laplacian(l.astype(np.uint8), cv2.CV_32F) | |
| edges = np.abs(edges) / (np.abs(edges).max() + 1e-8) | |
| blur = cv2.GaussianBlur(l, (0,0), 1.2) | |
| sharp = l + (0.6 + 0.4*edges) * (l - blur) | |
| lab[:,:,0] = np.clip(sharp, 0, 255).astype(np.uint8) | |
| up = cv2.cvtColor(lab, cv2.COLOR_LAB2BGR) | |
| up_f = up.astype(np.float32)/255 | |
| up_f = cv2.bilateralFilter(up_f, 5, 0.08, 1.5) | |
| up = (up_f*255).clip(0,255).astype(np.uint8) | |
| frac = 20 + int(60*(i+1)/self.iters) | |
| self.progress.emit(frac) | |
| self.log.emit(f" Pass {i+1}/{self.iters} done") | |
| if self.denoise: | |
| up = cv2.fastNlMeansDenoisingColored(up, None, 5, 5, 7, 21) | |
| self.log.emit(" Final NLM denoise applied") | |
| self.progress.emit(90) | |
| rgb = cv2.cvtColor(up, cv2.COLOR_BGR2RGB) | |
| return rgb.astype(np.float32)/255 | |
| def _srcnn_upscale(self, img): | |
| import torch, torch.nn as nn, torch.nn.functional as F | |
| self.log.emit("SRCNN-style upscale (PyTorch)…") | |
| class Net(nn.Module): | |
| def __init__(self): | |
| super().__init__() | |
| self.c1 = nn.Conv2d(3,64,9,padding=4) | |
| self.c2 = nn.Conv2d(64,32,1,padding=0) | |
| self.c3 = nn.Conv2d(32,3,5,padding=2) | |
| def forward(self,x): | |
| x = F.relu(self.c1(x)) | |
| x = F.relu(self.c2(x)) | |
| return self.c3(x) | |
| h, w = img.shape[:2] | |
| nh, nw = h*self.scale, w*self.scale | |
| bicubic = cv2.resize( | |
| (img*255).astype(np.uint8), (nw,nh), interpolation=cv2.INTER_CUBIC | |
| ).astype(np.float32)/255 | |
| t = torch.FloatTensor(bicubic).permute(2,0,1).unsqueeze(0) | |
| net = Net() | |
| with torch.no_grad(): | |
| out = net(t).squeeze(0).permute(1,2,0).numpy() | |
| result = (bicubic * 0.7 + out * 0.3).clip(0,1) | |
| self.progress.emit(80) | |
| self.log.emit(" SRCNN pass complete") | |
| return self._iterative_upscale(result) | |
| class MetaWorker(Worker): | |
| def __init__(self, path): | |
| super().__init__(); self.path = path | |
| def run(self): | |
| try: | |
| self.progress.emit(10) | |
| img = Image.open(self.path) | |
| info = {"Format": img.format or "Unknown", | |
| "Mode": img.mode, | |
| "Size": f"{img.width} × {img.height} px", | |
| "File size": f"{os.path.getsize(self.path):,} bytes"} | |
| exif = img._getexif() if hasattr(img,"_getexif") else None | |
| if exif: | |
| for tid, val in exif.items(): | |
| tag = ExifTags.TAGS.get(tid, tid) | |
| if isinstance(val, bytes): | |
| try: val = val.decode("utf-8","replace") | |
| except: val = repr(val) | |
| info[str(tag)] = str(val)[:300] | |
| self.progress.emit(60) | |
| raw = open(self.path,"rb").read() | |
| info["MD5"] = hashlib.md5(raw).hexdigest() | |
| info["SHA-256"]= hashlib.sha256(raw).hexdigest() | |
| arr = np.array(img.convert("RGB")) | |
| for i, ch in enumerate("RGB"): | |
| info[f"Mean {ch}"] = f"{arr[:,:,i].mean():.1f}" | |
| info[f"Std {ch}"] = f"{arr[:,:,i].std():.1f}" | |
| self.progress.emit(100) | |
| self.finished.emit(info) | |
| except Exception as e: | |
| self.error.emit(str(e)) | |
| class ImagePreview(QLabel): | |
| def __init__(self): | |
| super().__init__() | |
| self.setAlignment(Qt.AlignCenter) | |
| self.setMinimumSize(280, 220) | |
| self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) | |
| self._path = None | |
| self._placeholder() | |
| def _placeholder(self): | |
| self.setText("Drop image here\nor use Open") | |
| self.setStyleSheet(f"QLabel{{background:{P['panel']};border:2px dashed {P['border']};border-radius:10px;color:{P['muted']};font-size:13px}}") | |
| def load(self, path): | |
| self._path = path | |
| try: | |
| pix = QPixmap(path) | |
| if pix.isNull(): | |
| buf = io.BytesIO() | |
| Image.open(path).convert("RGB").save(buf, "PNG") | |
| buf.seek(0) | |
| qi = QImage(); qi.loadFromData(buf.read()) | |
| pix = QPixmap.fromImage(qi) | |
| if not pix.isNull(): | |
| self.setPixmap(pix.scaled(self.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)) | |
| self.setStyleSheet(f"QLabel{{background:{P['panel']};border:1px solid {P['border']};border-radius:10px}}") | |
| else: | |
| self.setText("Cannot preview") | |
| except Exception as e: | |
| self.setText(f"Preview error:\n{e}") | |
| def resizeEvent(self, e): | |
| if self._path: self.load(self._path) | |
| class LogPanel(QTextEdit): | |
| def __init__(self): | |
| super().__init__() | |
| self.setReadOnly(True); self.setMaximumHeight(120) | |
| self.setPlaceholderText("Operation log…") | |
| def msg(self, text): | |
| ts = time.strftime("%H:%M:%S") | |
| col = P["accent2"] if text.startswith("✓") else \ | |
| P["warn"] if text.startswith("!") else \ | |
| P["danger"] if text.startswith("✗") else P["text"] | |
| self.append(f'<span style="color:{P["muted"]}">[{ts}]</span> <span style="color:{col}">{text}</span>') | |
| def _slider_row(label, sl_id, mn, mx, default, scale=1.0): | |
| row = QHBoxLayout() | |
| lbl = QLabel(label); lbl.setFixedWidth(90); lbl.setObjectName("m") | |
| sl = QSlider(Qt.Horizontal); sl.setRange(mn, mx); sl.setValue(default) | |
| val = QLabel(str(default)); val.setFixedWidth(32) | |
| sl.valueChanged.connect(lambda v: val.setText(str(v))) | |
| row.addWidget(lbl); row.addWidget(sl); row.addWidget(val) | |
| return row, sl | |
| class CarveTab(QWidget): | |
| def __init__(self, log, status): | |
| super().__init__(); self._log = log; self._status = status | |
| self._worker = None; self._build() | |
| def _build(self): | |
| lay = QVBoxLayout(self); lay.setSpacing(8) | |
| g1 = QGroupBox("Source (file or raw disk image)") | |
| r1 = QHBoxLayout(g1) | |
| self.src = QLineEdit(); btn1 = QPushButton("Browse…") | |
| btn1.clicked.connect(self._pick_src) | |
| r1.addWidget(self.src); r1.addWidget(btn1) | |
| g2 = QGroupBox("Output Directory") | |
| r2 = QHBoxLayout(g2) | |
| self.out = QLineEdit(); btn2 = QPushButton("Browse…") | |
| btn2.clicked.connect(self._pick_out) | |
| r2.addWidget(self.out); r2.addWidget(btn2) | |
| g3 = QGroupBox("File Types") | |
| fl = QHBoxLayout(g3) | |
| self._cks = {} | |
| for n in SIGS: | |
| c = QCheckBox(n); c.setChecked(True) | |
| self._cks[n] = c; fl.addWidget(c) | |
| fl.addStretch() | |
| g4 = QGroupBox("Limits") | |
| gl = QHBoxLayout(g4) | |
| gl.addWidget(QLabel("Max file size (MB):")) | |
| self.max_mb = QSpinBox(); self.max_mb.setRange(1,512); self.max_mb.setValue(32) | |
| gl.addWidget(self.max_mb); gl.addStretch() | |
| self.prog = QProgressBar() | |
| btn_run = QPushButton("⛏ Start Carving"); btn_run.setObjectName("primary") | |
| btn_run.clicked.connect(self._run) | |
| g5 = QGroupBox("Recovered Files") | |
| r5 = QVBoxLayout(g5) | |
| self.lst = QListWidget() | |
| r5.addWidget(self.lst) | |
| for w in [g1,g2,g3,g4,self.prog,btn_run,g5]: lay.addWidget(w) | |
| def _pick_src(self): | |
| p,_ = QFileDialog.getOpenFileName(self,"Source","","All Files (*)") | |
| if p: self.src.setText(p) | |
| def _pick_out(self): | |
| p = QFileDialog.getExistingDirectory(self,"Output") | |
| if p: self.out.setText(p) | |
| def _run(self): | |
| src = self.src.text().strip() | |
| out = self.out.text().strip() | |
| if not src or not os.path.isfile(src): | |
| self._log.msg("✗ Invalid source"); return | |
| if not out: | |
| out = os.path.join(os.path.dirname(src),"carved_output") | |
| self.out.setText(out) | |
| os.makedirs(out, exist_ok=True) | |
| types = [n for n,c in self._cks.items() if c.isChecked()] | |
| if not types: self._log.msg("✗ No types selected"); return | |
| self.prog.setValue(0); self.lst.clear() | |
| self._worker = CarveWorker(src, out, types, self.max_mb.value()) | |
| self._worker.progress.connect(self.prog.setValue) | |
| self._worker.log.connect(self._log.msg) | |
| self._worker.finished.connect(lambda f: ( | |
| [self.lst.addItem(x) for x in f], | |
| self._status(f"Carved {len(f)} file(s)"), | |
| self._log.msg(f"✓ Done — {len(f)} file(s)"))) | |
| self._worker.error.connect(lambda e: self._log.msg(f"✗ {e}")) | |
| self._worker.start() | |
| class FSTab(QWidget): | |
| def __init__(self, log, status): | |
| super().__init__(); self._log = log; self._status = status | |
| self._worker = None; self._build() | |
| def _build(self): | |
| lay = QVBoxLayout(self); lay.setSpacing(8) | |
| g1 = QGroupBox("Disk Image / Drive") | |
| r1 = QHBoxLayout(g1) | |
| self.src = QLineEdit(); btn = QPushButton("Browse…") | |
| btn.clicked.connect(self._pick) | |
| r1.addWidget(self.src); r1.addWidget(btn) | |
| g2 = QGroupBox("Output Directory") | |
| r2 = QHBoxLayout(g2) | |
| self.out = QLineEdit(); btn2 = QPushButton("Browse…") | |
| btn2.clicked.connect(self._pick_out) | |
| r2.addWidget(self.out); r2.addWidget(btn2) | |
| g3 = QGroupBox("Filesystem Scanners") | |
| fl = QVBoxLayout(g3) | |
| self.ntfs = QCheckBox("NTFS — scan MFT FILE records (finds deleted entries with filenames)"); self.ntfs.setChecked(True) | |
| self.fat = QCheckBox("FAT12/16/32 — scan directory entries (0xE5 deleted marker)"); self.fat.setChecked(True) | |
| fl.addWidget(self.ntfs); fl.addWidget(self.fat) | |
| self.prog = QProgressBar() | |
| btn_run = QPushButton("@ Scan Filesystem"); btn_run.setObjectName("primary") | |
| btn_run.clicked.connect(self._run) | |
| g4 = QGroupBox("Found Entries") | |
| r4 = QVBoxLayout(g4) | |
| self.tree = QTreeWidget() | |
| self.tree.setHeaderLabels(["Type","Filename","Size","Offset"]) | |
| self.tree.setColumnWidth(0,80); self.tree.setColumnWidth(1,200) | |
| self.tree.setColumnWidth(2,80) | |
| r4.addWidget(self.tree) | |
| for w in [g1,g2,g3,self.prog,btn_run,g4]: lay.addWidget(w) | |
| def _pick(self): | |
| p,_ = QFileDialog.getOpenFileName(self,"Disk image","","All Files (*)") | |
| if p: self.src.setText(p) | |
| def _pick_out(self): | |
| p = QFileDialog.getExistingDirectory(self,"Output") | |
| if p: self.out.setText(p) | |
| def _run(self): | |
| src = self.src.text().strip() | |
| out = self.out.text().strip() | |
| if not src or not os.path.isfile(src): | |
| self._log.msg("✗ Invalid source"); return | |
| if not out: | |
| out = os.path.join(os.path.dirname(src),"fs_output") | |
| self.out.setText(out) | |
| os.makedirs(out, exist_ok=True) | |
| self.prog.setValue(0); self.tree.clear() | |
| self._worker = FSForensicsWorker(src, out, self.ntfs.isChecked(), self.fat.isChecked()) | |
| self._worker.progress.connect(self.prog.setValue) | |
| self._worker.log.connect(self._log.msg) | |
| self._worker.finished.connect(self._done) | |
| self._worker.error.connect(lambda e: self._log.msg(f"✗ {e}")) | |
| self._worker.start() | |
| def _done(self, entries): | |
| for e in entries: | |
| sz = f"{e['size']//1024} KB" if e.get("size") else "?" | |
| item = QTreeWidgetItem([e["type"], e.get("name","?"), sz, hex(e["offset"])]) | |
| item.setForeground(0, QColor(P["accent"])) | |
| self.tree.addTopLevelItem(item) | |
| self._log.msg(f"✓ Scan complete — {len(entries)} deleted image entries found") | |
| self._status(f"FS Forensics: {len(entries)} entries") | |
| class RepairTab(QWidget): | |
| def __init__(self, log, status, preview): | |
| super().__init__() | |
| self._log = log; self._status = status; self._preview = preview | |
| self._worker = None; self._build() | |
| def _build(self): | |
| lay = QVBoxLayout(self); lay.setSpacing(8) | |
| g1 = QGroupBox("Corrupted File") | |
| r1 = QHBoxLayout(g1) | |
| self.src = QLineEdit(); btn = QPushButton("Browse…") | |
| btn.clicked.connect(self._pick) | |
| r1.addWidget(self.src); r1.addWidget(btn) | |
| g2 = QGroupBox("Output Directory") | |
| r2 = QHBoxLayout(g2) | |
| self.out = QLineEdit(); btn2 = QPushButton("Browse…") | |
| btn2.clicked.connect(self._pick_out) | |
| r2.addWidget(self.out); r2.addWidget(btn2) | |
| g3 = QGroupBox("What gets repaired") | |
| fl = QVBoxLayout(g3) | |
| for t in ["PNG: magic bytes, chunk CRC repair, IEND reconstruction", | |
| "JPEG: SOI header, APP0 rebuild, EOI append", | |
| "JPEG: scan-stream rescue (bad restart markers patched)", | |
| "JPEG: clean re-encode via OpenCV DCT pipeline"]: | |
| l = QLabel(f"• {t}"); l.setObjectName("m"); fl.addWidget(l) | |
| self.prog = QProgressBar() | |
| btn_run = QPushButton("🔧 Repair File"); btn_run.setObjectName("primary") | |
| btn_run.clicked.connect(self._run) | |
| g4 = QGroupBox("Repaired Files") | |
| r4 = QVBoxLayout(g4) | |
| self.lst = QListWidget() | |
| self.lst.itemDoubleClicked.connect(lambda i: self._preview.load(i.text())) | |
| r4.addWidget(self.lst) | |
| for w in [g1,g2,g3,self.prog,btn_run,g4]: lay.addWidget(w) | |
| def _pick(self): | |
| p,_ = QFileDialog.getOpenFileName(self,"Corrupted file","", | |
| "Images (*.jpg *.jpeg *.png *.gif *.bmp *.tif *.webp);;All (*)") | |
| if p: | |
| self.src.setText(p) | |
| self.out.setText(os.path.join(os.path.dirname(p),"repaired")) | |
| def _pick_out(self): | |
| p = QFileDialog.getExistingDirectory(self,"Output") | |
| if p: self.out.setText(p) | |
| def _run(self): | |
| src = self.src.text().strip() | |
| out = self.out.text().strip() or os.path.join(os.path.dirname(src),"repaired") | |
| if not src or not os.path.isfile(src): | |
| self._log.msg("✗ Invalid source"); return | |
| os.makedirs(out, exist_ok=True) | |
| self.prog.setValue(0); self.lst.clear() | |
| self._worker = RepairWorker(src, out) | |
| self._worker.progress.connect(self.prog.setValue) | |
| self._worker.log.connect(self._log.msg) | |
| self._worker.finished.connect(self._done) | |
| self._worker.error.connect(lambda e: self._log.msg(f"✗ {e}")) | |
| self._worker.start() | |
| def _done(self, files): | |
| for f in files: self.lst.addItem(f) | |
| if files: self._preview.load(files[0]) | |
| self._status(f"Repair: {len(files)} file(s)") | |
| class EnhanceTab(QWidget): | |
| def __init__(self, log, status, preview): | |
| super().__init__() | |
| self._log = log; self._status = status; self._preview = preview | |
| self._worker = None; self._sliders = {}; self._checks = {} | |
| self._build() | |
| def _build(self): | |
| lay = QVBoxLayout(self); lay.setSpacing(8) | |
| g_io = QGroupBox("Files") | |
| fl = QFormLayout(g_io) | |
| self.src = QLineEdit(); b1 = QPushButton("…"); b1.setMaximumWidth(30) | |
| b1.clicked.connect(self._pick_src) | |
| r1 = QHBoxLayout(); r1.addWidget(self.src); r1.addWidget(b1) | |
| self.out = QLineEdit(); b2 = QPushButton("…"); b2.setMaximumWidth(30) | |
| b2.clicked.connect(self._pick_out) | |
| r2 = QHBoxLayout(); r2.addWidget(self.out); r2.addWidget(b2) | |
| fl.addRow("Source:", r1); fl.addRow("Output dir:", r2) | |
| ops_def = [ | |
| ("denoise_nlm", "NLM Denoise", [("Strength h", "h", 1, 40, 10)]), | |
| ("denoise_wavelet", "Wavelet Denoise", [("Sigma×100", "sigma_w", 1, 50, 5)]), | |
| ("denoise_bilateral","Bilateral Denoise", [("σ colour×100","sc",1,50,10)]), | |
| ("denoise_tv", "Total Variation", [("Weight×100","weight",1,50,10)]), | |
| ("sharpen", "Unsharp Mask", [("Amount×100","amount",50,400,150)]), | |
| ("contrast_clahe", "CLAHE Contrast", [("Clip×10","clip",5,80,20)]), | |
| ("contrast_gamma", "Gamma Correct", [("Gamma×100","gamma",20,300,100)]), | |
| ("deblur_wiener", "Wiener Deblur", [("PSF size","psf",3,15,5)]), | |
| ("deblur_dft", "DFT Inverse Filter", [("PSF size","psf_d",3,15,5),("K×1000","k_d",1,100,10)]), | |
| ("inpaint_telea", "Inpaint TELEA", [("Dark thresh","thresh_t",1,60,10)]), | |
| ("inpaint_biharmonic","Inpaint Biharmonic", [("Dark thresh","thresh_b",1,60,10)]), | |
| ("color_restore", "Colour Restore", [("Saturation×100","sat",50,300,150)]), | |
| ("histogram_eq", "Adaptive Hist EQ", [("Clip×100","clip_h",1,10,3)]),] | |
| scroll = QScrollArea(); scroll.setWidgetResizable(True) | |
| inner = QWidget(); grid = QGridLayout(inner); grid.setSpacing(6) | |
| scroll.setWidget(inner) | |
| for idx, (key, label, sls) in enumerate(ops_def): | |
| r, c = divmod(idx, 2) | |
| gb = QGroupBox(); v = QVBoxLayout(gb) | |
| cb = QCheckBox(label); self._checks[key] = cb; v.addWidget(cb) | |
| self._sliders[key] = {} | |
| for sname, sid, mn, mx, dflt in sls: | |
| row_l, sl = _slider_row(sname, sid, mn, mx, dflt) | |
| self._sliders[key][sid] = sl; v.addLayout(row_l) | |
| grid.addWidget(gb, r, c) | |
| self.prog = QProgressBar() | |
| btn_run = QPushButton("> Apply Operations"); btn_run.setObjectName("primary") | |
| btn_run.clicked.connect(self._run) | |
| g_res = QGroupBox("Output Files") | |
| r_res = QVBoxLayout(g_res) | |
| self.lst = QListWidget() | |
| self.lst.itemDoubleClicked.connect(lambda i: self._preview.load(i.text())) | |
| r_res.addWidget(self.lst) | |
| for w in [g_io, scroll, self.prog, btn_run, g_res]: lay.addWidget(w) | |
| def _pick_src(self): | |
| p,_ = QFileDialog.getOpenFileName(self,"Source","", | |
| "Images (*.jpg *.jpeg *.png *.bmp *.tif *.webp);;All (*)") | |
| if p: | |
| self.src.setText(p) | |
| self.out.setText(os.path.join(os.path.dirname(p),"enhanced")) | |
| self._preview.load(p) | |
| def _pick_out(self): | |
| p = QFileDialog.getExistingDirectory(self,"Output"); | |
| if p: self.out.setText(p) | |
| def _run(self): | |
| src = self.src.text().strip(); out = self.out.text().strip() | |
| if not src or not os.path.isfile(src): | |
| self._log.msg("✗ Select a valid source"); return | |
| os.makedirs(out, exist_ok=True) | |
| scale_map = { | |
| "sigma_w":0.01,"sc":0.01,"weight":0.01,"amount":0.01, | |
| "clip":0.1,"gamma":0.01,"k_d":0.001,"sat":0.01, | |
| "clip_h":0.01,} | |
| rename_map = {"sigma_w":"sigma","sc":"sc","weight":"weight", | |
| "amount":"amount","clip":"clip","gamma":"gamma", | |
| "psf_d":"psf","k_d":"k","thresh_t":"thresh", | |
| "thresh_b":"thresh","sat":"sat","clip_h":"clip"} | |
| ops = [] | |
| for key, cb in self._checks.items(): | |
| if not cb.isChecked(): continue | |
| params = {} | |
| for sid, sl in self._sliders[key].items(): | |
| pname = rename_map.get(sid, sid) | |
| scale = scale_map.get(sid, 1.0) | |
| params[pname] = sl.value() * scale | |
| ops.append((key, params)) | |
| if not ops: self._log.msg("✗ Select at least one operation"); return | |
| self.prog.setValue(0); self.lst.clear() | |
| self._worker = EnhanceWorker(src, out, ops) | |
| self._worker.progress.connect(self.prog.setValue) | |
| self._worker.log.connect(self._log.msg) | |
| self._worker.finished.connect(self._done) | |
| self._worker.error.connect(lambda e: self._log.msg(f"✗ {e}")) | |
| self._worker.start() | |
| def _done(self, files): | |
| for f in files: self.lst.addItem(f) | |
| if files: self._preview.load(files[0]) | |
| self._status(f"Enhancement: {len(files)} file(s)") | |
| class SuperResTab(QWidget): | |
| def __init__(self, log, status, preview): | |
| super().__init__() | |
| self._log = log; self._status = status; self._preview = preview | |
| self._worker = None; self._build() | |
| def _build(self): | |
| lay = QVBoxLayout(self); lay.setSpacing(8) | |
| g1 = QGroupBox("Source Image") | |
| r1 = QHBoxLayout(g1) | |
| self.src = QLineEdit(); btn = QPushButton("Browse…") | |
| btn.clicked.connect(self._pick) | |
| r1.addWidget(self.src); r1.addWidget(btn) | |
| g2 = QGroupBox("Output Directory") | |
| r2 = QHBoxLayout(g2) | |
| self.out = QLineEdit(); btn2 = QPushButton("Browse…") | |
| btn2.clicked.connect(self._pick_out) | |
| r2.addWidget(self.out); r2.addWidget(btn2) | |
| g3 = QGroupBox("Parameters") | |
| fl = QFormLayout(g3) | |
| self.scale = QComboBox(); self.scale.addItems(["2","3","4"]) | |
| self.iters = QSpinBox(); self.iters.setRange(1,8); self.iters.setValue(3) | |
| self.denoise_cb = QCheckBox("Final NLM denoise pass"); self.denoise_cb.setChecked(True) | |
| fl.addRow("Scale factor:", self.scale) | |
| fl.addRow("Sharpening passes:", self.iters) | |
| fl.addRow("", self.denoise_cb) | |
| torch_lbl = QLabel(f"PyTorch: {'✓ available (SRCNN-style)' if TORCH else '✗ not found — using iterative CPU method'}") | |
| torch_lbl.setObjectName("m") | |
| self.prog = QProgressBar() | |
| btn_run = QPushButton("^ Upscale Image"); btn_run.setObjectName("primary") | |
| btn_run.clicked.connect(self._run) | |
| g4 = QGroupBox("Output Files") | |
| r4 = QVBoxLayout(g4) | |
| self.lst = QListWidget() | |
| self.lst.itemDoubleClicked.connect(lambda i: self._preview.load(i.text())) | |
| r4.addWidget(self.lst) | |
| for w in [g1,g2,g3,torch_lbl,self.prog,btn_run,g4]: lay.addWidget(w) | |
| def _pick(self): | |
| p,_ = QFileDialog.getOpenFileName(self,"Source","", | |
| "Images (*.jpg *.jpeg *.png *.bmp *.tif *.webp);;All (*)") | |
| if p: | |
| self.src.setText(p) | |
| self.out.setText(os.path.join(os.path.dirname(p),"superres")) | |
| self._preview.load(p) | |
| def _pick_out(self): | |
| p = QFileDialog.getExistingDirectory(self,"Output") | |
| if p: self.out.setText(p) | |
| def _run(self): | |
| src = self.src.text().strip(); out = self.out.text().strip() | |
| if not src or not os.path.isfile(src): | |
| self._log.msg("✗ Select a valid source"); return | |
| os.makedirs(out, exist_ok=True) | |
| self.prog.setValue(0); self.lst.clear() | |
| self._worker = SuperResWorker(src, out, | |
| int(self.scale.currentText()), | |
| self.iters.value(), | |
| self.denoise_cb.isChecked()) | |
| self._worker.progress.connect(self.prog.setValue) | |
| self._worker.log.connect(self._log.msg) | |
| self._worker.finished.connect(self._done) | |
| self._worker.error.connect(lambda e: self._log.msg(f"✗ {e}")) | |
| self._worker.start() | |
| def _done(self, files): | |
| for f in files: self.lst.addItem(f) | |
| if files: self._preview.load(files[0]) | |
| self._status(f"Super-res complete") | |
| class MetaTab(QWidget): | |
| def __init__(self, log, status): | |
| super().__init__(); self._log = log; self._status = status; self._build() | |
| def _build(self): | |
| lay = QVBoxLayout(self); lay.setSpacing(8) | |
| g1 = QGroupBox("Image") | |
| r1 = QHBoxLayout(g1) | |
| self.src = QLineEdit(); btn = QPushButton("Browse…") | |
| btn.clicked.connect(self._pick) | |
| r1.addWidget(self.src); r1.addWidget(btn) | |
| self.prog = QProgressBar() | |
| btn_run = QPushButton("> Extract Metadata & Hashes"); btn_run.setObjectName("primary") | |
| btn_run.clicked.connect(self._run) | |
| self.tree = QTreeWidget() | |
| self.tree.setHeaderLabels(["Field","Value"]) | |
| self.tree.setColumnWidth(0,220) | |
| self.tree.header().setStretchLastSection(True) | |
| for w in [g1,self.prog,btn_run,self.tree]: lay.addWidget(w) | |
| def _pick(self): | |
| p,_ = QFileDialog.getOpenFileName(self,"Image","", | |
| "Images (*.jpg *.jpeg *.png *.tif *.webp *.bmp);;All (*)") | |
| if p: self.src.setText(p) | |
| def _run(self): | |
| src = self.src.text().strip() | |
| if not src or not os.path.isfile(src): | |
| self._log.msg("✗ Select a valid image"); return | |
| self.prog.setValue(0); self.tree.clear() | |
| w = MetaWorker(src) | |
| w.progress.connect(self.prog.setValue) | |
| w.log.connect(self._log.msg) | |
| w.finished.connect(self._done) | |
| w.error.connect(lambda e: self._log.msg(f"✗ {e}")) | |
| w.start() | |
| def _done(self, info): | |
| for k,v in info.items(): | |
| item = QTreeWidgetItem([str(k),str(v)]) | |
| item.setForeground(0, QColor(P["accent"])) | |
| self.tree.addTopLevelItem(item) | |
| self._status(f"Metadata: {len(info)} fields") | |
| class MainWindow(QMainWindow): | |
| def __init__(self): | |
| super().__init__() | |
| self.setWindowTitle("Kariaian v2") | |
| self.resize(1280, 860) | |
| self._build() | |
| def _build(self): | |
| c = QWidget(); self.setCentralWidget(c) | |
| main = QHBoxLayout(c); main.setContentsMargins(8,8,8,8); main.setSpacing(8) | |
| left = QWidget(); left.setFixedWidth(300) | |
| ll = QVBoxLayout(left); ll.setSpacing(6) | |
| h = QLabel("ImageRecovery Pro"); h.setObjectName("h"); h.setAlignment(Qt.AlignCenter) | |
| s = QLabel("v2 · Advanced Forensic Suite"); s.setObjectName("m"); s.setAlignment(Qt.AlignCenter) | |
| self.preview = ImagePreview() | |
| self.log = LogPanel() | |
| ll.addWidget(h); ll.addWidget(s) | |
| ll.addWidget(self.preview,1) | |
| ll.addWidget(QLabel("Log")); ll.addWidget(self.log) | |
| tabs = QTabWidget() | |
| tabs.addTab(CarveTab(self.log, self._st), "• Carver") | |
| tabs.addTab(FSTab(self.log, self._st), "• FS Forensics") | |
| tabs.addTab(RepairTab(self.log, self._st, self.preview),"• Repair") | |
| tabs.addTab(EnhanceTab(self.log, self._st, self.preview),"• Enhance") | |
| tabs.addTab(SuperResTab(self.log, self._st, self.preview),"• Super-Res") | |
| tabs.addTab(MetaTab(self.log, self._st), "• Metadata") | |
| main.addWidget(left); main.addWidget(tabs,1) | |
| mb = self.menuBar() | |
| fm = mb.addMenu("File") | |
| fm.addAction("Open Image…", self._open, "Ctrl+O") | |
| fm.addSeparator() | |
| fm.addAction("Exit", self.close, "Ctrl+Q") | |
| mb.addMenu("Help").addAction("About", self._about) | |
| self.statusBar().showMessage("Ready • Kariaian v2") | |
| def _st(self, msg): self.statusBar().showMessage(msg) | |
| def _open(self): | |
| p,_ = QFileDialog.getOpenFileName(self,"Open","", | |
| "Images (*.jpg *.jpeg *.png *.bmp *.tif *.webp *.gif);;All (*)") | |
| if p: self.preview.load(p); self.log.msg(f"Opened {os.path.basename(p)}") | |
| def _about(self): | |
| QMessageBox.about(self,"About", | |
| "<b>Kariaian v2</b><br>" | |
| "6-tab advanced img forensic suite.<br><br>" | |
| "<b>New in v2:</b><br>" | |
| "• NTFS MFT deleted-entry scanner<br>" | |
| "• FAT directory entry scanner<br>" | |
| "• JPEG DCT scan-stream rescue<br>" | |
| "• DFT inverse filter deblur<br>" | |
| "• Biharmonic inpainting<br>" | |
| "• Total-variation denoising<br>" | |
| "• Adaptive histogram equalisation<br>" | |
| "• AI iterative super-resolution (+ SRCNN if torch installed)<br>" | |
| "• SHA-256 + colour histogram in metadata<br><br>" | |
| f"<b>PyTorch:</b> {'available' if TORCH else 'not installed'}<br>" | |
| f"<b>RAW support:</b> {'available' if RAW_SUPPORT else 'install rawpy'}") | |
| def main(): | |
| app = QApplication(sys.argv) | |
| app.setStyleSheet(QSS) | |
| app.setApplicationName("Kariaian v2") | |
| MainWindow().show() | |
| sys.exit(app.exec_()) | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment