Skip to content

Instantly share code, notes, and snippets.

@lastforkbender
Created June 19, 2026 07:04
Show Gist options
  • Select an option

  • Save lastforkbender/b8fcdb41d79a6ae1fb37932417729001 to your computer and use it in GitHub Desktop.

Select an option

Save lastforkbender/b8fcdb41d79a6ae1fb37932417729001 to your computer and use it in GitHub Desktop.
Kariaian / IRS
# 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