
今回のお題は「画像の鮮明化」です。
弊社が展開する"てんかく忍者"サービスでは、走行する列車や自動車から、ビデオカメラを使って構造物や施設を撮影する手法を採っており、天候によって暗かったり、陰になったりで「もう少し見えんか?」というケースが多く発生します。
HDじゃなく2K、2Kじゃなく4K、高速なオートフォーカスで・・といったハードウェアのテクニカルな部分で"購入すれば解決できる"問題ならそのようにすればよいのですが、実は画像処理で結構なことが出来るので、今回はPythonの画像処理でどこまで出来ちゃうのかを見てみましょう。
・・・の前に、画像データを生成する部分で重要なことをまとめておきます。
・カメラハードウェアのポイント
解像度、シャッタースピード、フォーカススピード
各々がお互いに影響する事象です、受光部の解像度は高ければ高いほど精密ですが、光学的に解像度が高いかどうかは、光量があるか?ピントはばっちり合っているか?であり、高速移動しながらピントをばっちり合せるには、高速なオートフォーカス技術と高速なシャッタースピード、露光時間が短くても明るく写る高感度なセンサーが必要です。
・撮影時条件
暗所では照明を焚く、白飛びなく、階調を出来るだけ高いビット(分解能)で保存していること。
階調がきちんと残されていれば画像処理で明瞭化することができます。
「白飛びさせるくらいなら暗いくらいで良いです。」が
暗いとフォーカスが合っていないことが多いので、「フォーカスには特に気を配ってください」
・・とはいえ、天候に左右されながら、一度きりの撮影で、本番一発勝負の撮影現場に「もう少し何とかなりませんか?」というのもなかなか言い難い部分もありますね。
「画像処理技術でなんとかならんのか?」・・のその"何とか"をやってみましょう。
これ、AKAZEなどで特徴点を検出したり、テンプレートマッチ、YOLOで物体検出する際にもオールマイティに効くと思います。
見た目にも、「おお、ここまで見えるんけ」というくらい、鮮明化します。
最近のデジタルカメラや高画質を売りにしているスマホのカメラ機能では、HDRが良く使われている印象です。市販のムービーカメラでも使っているのがガンマ補正やAutoExplosureでしょうか。
1:露出違いの複数枚で合成する(HDR)
2:ガンマ補正 ※
3:露出補正(AutoExplosure)※
4:適用的ヒストグラム平たん化(CLAHE)※
一括処理が出来る画像処理ソフトは「"DxO PhotoLab(ViewPoint)"がいいよ」の一言で終わりそうな感じもしますが。
ともかく、今回は、上記の2-4を実装した、Pythonのプログラム"image_utils.py"をご紹介する次第です。このプログラムは最近はデフォルトで使っている位非常の良く効く写真屋さんです。
まずは、実際の元画像と処理画像でその効果を見てください。
・よりくっきりはっきり
![]() |
![]() |
・暗くてもそれなりに見せる
![]() |
![]() |
・細部がくっきり
![]() |
・質感が鮮明に
![]() |
・機能
・使い方
utils=ImageUtils(target_width,target_height) #写真屋さんを召喚
processed =utils.load_and_process_image(self.current_image)
・処理速度
2K動画(1920x1080)から切り出した静止画を処理した場合の例です。
🧪 処理時間の確認(合計:約1.29秒)
| 処理 | 時間(秒) | 備考 |
| resize | 0.18 | 画像サイズに依存。高速化余地は少ない |
| watermark | 0.00 | 影付きでも軽量。問題なし |
| clahe | 0.19 | tileGridSizeやclipLimitの見直しで改善可能 |
| gammma補正 | 0.18 | LUT使用済みなら限界近い |
| auto_explosure | 0.18 | NumPy処理の最適化が鍵 |
| compress | 0.19 | JPEG品質と処理順で調整可能 |
| progressive | 0.27 | UX向上のための犠牲。最も重い処理 |
・チューニング箇所
- CLAHEの `tileGridSize=(4,4)` → `(8,8)` にしてみる(処理軽くなるが粒度は粗くなる)
- progressive encoding をオフにしてみる(UXは下がるが処理は速くなる)
🧪 各処理のパラメータとチューニングポイント
| 処理 | 主なパラメータ | 意味・役割 | チューニングの勘所 |
・resize
`target_size`(例:1920×1080)、表示・処理の基準サイズに統一 、処理速度
とViewer体験のバランス。UI設計に合わせて最適化
・watermark
`font_scale`, `position`, `color`, `thickness`、 処理履歴の注釈を画像に埋め込む 、 Viewerの視線の終着点(左下など)に配置。サイズとコントラストを調整
<・CLAHE
`clipLimit`, `tileGridSize`(例:(8,8))
局所的なコントラスト強調
`tileGridSize`が大きすぎると処理が重くなる。`clipLimit`は1.5〜3.0が自然
<・Gamma補正
`gamma`(例:1.2〜1.8)
明暗のバランス調整 LUT(ルックアップテーブル)を使うと高速化。暗部強調なら1.2〜1.4が目安
<・Auto Exposure
`target_mean`, `gain_limit` など
平均輝度を基準に露出補正
ヒストグラム処理をNumPyで最適化。`target_mean=128`前後が自然な明るさ
<・JPEG圧縮
`quality`(例:85)
ファイルサイズと画質のトレードオフ
`quality=75〜85`がバランス良。低すぎると語りの焦点が潰れる
<・Progressive Encoding
`progressive=True`
段階的に画像を表示
Viewer体験を滑らかに。処理時間は若干増えるがUX向上には有効 |
🛠️ チューニングの実践例
・ CLAHEの軽量化
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(4,4))
・ Gamma補正の高速化(LUT使用)
invGamma = 1.0 / gamma
table = np.array([((i / 255.0) ** invGamma) * 255 for i in np.arange(256)]).astype("uint8")
adjusted = cv2.LUT(image, table)
・JPEG圧縮の最適化
cv2.imwrite("output.jpg", image, [cv2.IMWRITE_JPEG_QUALITY, 80])
いかがでしたでしょうか?
このCLAHE処理の威力。
何にでも塩を振って食べるのと同じように、"どんな画像にもCLAHE"がおいしく頂くコツな気がします。
image_util.pyをコピー・保存して、使ってみてください。
import io
import cv2
from PIL import Image
import numpy as np
import time
class ImageUtils:
def __init__(self, width=1920, height=1080):
self.width = width
self.height = height
#イメージ加工のラッパー (画像加工屋さん≠PhotoShop)
def load_and_process_image(self,img):
#処理速度に影響するので、printや時間計測はコメントアウトしています
#リサイズ
#start=time.time()
#print(type(img), img.shape, img.dtype)
try:
img = cv2.resize(img, (self.width, self.height), interpolation=cv2.INTER_AREA)
#print("🔧 resize完了")
#print(type(img), img.shape, img.dtype)
except Exception as e:
#print("resize失敗:",e)
print(type(img), img.shape, img.dtype)
#return img
#print(f"⏱️ resize: {time.time() - start:.2f} 秒")
#ウォーターマーク
#start=time.time()
#print(type(img), img.shape, img.dtype)
try:
img= self.embed_processing_watermark(img)
except Exception as e:
print("watermrk失敗:",e)
#print(type(img), img.shape, img.dtype)
#return img
#print(f"⏱️ watermark: {time.time() - start:.2f} 秒")
#CLAHE(局所ヒストグラム均等化)
#start=time.time()
try:
img = self.clahe(img)
#print("🎚️ clahe完了")
#print(type(img), img.shape, img.dtype)
except Exception as e:
print("clahe失敗:",e)
#print(type(img), img.shape, img.dtype)
# return img
#print(f"⏱️ clahe: {time.time() - start:.2f} 秒")
#ガンマ補正
#start=time.time()
try:
img = self.adjust_gamma(img)
#print("🎚️ adjust_gamma完了")
#print(type(img), img.shape, img.dtype)
except Exception as e:
print("adjust_gamma失敗:",e)
#print(type(img), img.shape, img.dtype)
# return img
#print(f"⏱️ adjast_gamma: {time.time() - start:.2f} 秒")
#start=time.time()
try:
img = self.auto_exposure(img)
#print("🌞 auto_exposure完了")
# #print(type(img), img.shape, img.dtype)
except Exception as e:
print("auto_exposure失敗:",e)
# #print(type(img), img.shape, img.dtype)
# return img
#print(f"⏱️ auto_exposure: {time.time() - start:.2f} 秒")
#start=time.time()
try:
img = self.compress_image(img) # 圧縮演出
#print("🗜️ compress_image完了")
#print(type(img), img.shape, img.dtype)
except Exception as e:
print("compress_image失敗:",e)
#print(type(img), img.shape, img.dtype)
#return img
#print(f"⏱️ compress: {time.time() - start:.2f} 秒")
#ProgressveJpeg処理は他に比べて遅い(0.27秒)なので割愛する
#start=time.time()
#try:
# img = self.apply_progressive_encoding(img) # プログレッシブ化
# print("📶 progressive_encoding完了")
# #print(type(img), img.shape, img.dtype)
#except Exception as e:
# print("progressive_encoding失敗:",e)
# #print(type(img), img.shape, img.dtype)
# return img
#print(f"⏱️ progressive: {time.time() - start:.2f} 秒")
#try:
#img = self.embed_exif_metadata(img, path) # EXIF追加
#except Exception as e:
# print("progressive_encoding失敗:",e)
# return img
return img
# プログレッシブJPGに加工
def apply_progressive_encoding(self, img):
# OpenCVのimgをPillowに変換
img_pil = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
# メモリバッファに保存(プログレッシブJPEG)
buffer = io.BytesIO()
img_pil.save(buffer, format="JPEG", progressive=True, quality=85)
# バッファから再読み込みして OpenCV形式に戻す(必要なら)
buffer.seek(0)
pil_img = Image.open(buffer)
#buffer.close()
return cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR)
# JPEG圧縮
def compress_image(self, img):
encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), 75]
result, encimg = cv2.imencode('.jpg', img, encode_param)
if result:
del img
return cv2.imdecode(encimg, 1)
else:
raise ValueError("JPEG圧縮に失敗しました")
def clahe(self,img):
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(4,4))
img_yuv = cv2.cvtColor(img, cv2.COLOR_BGR2YUV)
img_yuv[...,0] = clahe.apply(img_yuv[...,0])
img = cv2.cvtColor(img_yuv, cv2.COLOR_YUV2BGR)
return img
def adjust_gamma(self,image, gamma=1.05):
invGamma = 1.0 / gamma
table = np.array([
((i / 255.0) ** invGamma) * 255
for i in np.arange(256)
]).astype("uint8")
return cv2.LUT(image, table)
def auto_exposure(self,image, alpha=1.2, beta=20):
return cv2.convertScaleAbs(image, alpha=alpha, beta=beta)
def embed_processing_watermark(self, image, text="Clahe/Gamma/Exposure adjusted"):
font = cv2.FONT_HERSHEY_SIMPLEX
font_scale = 1.2
color = (255, 255, 255)
thickness = 2
shadow_color = (0, 0, 0)
# 画像サイズ取得
height, width = image.shape[:2]
# テキストサイズ取得
(text_width, text_height), _ = cv2.getTextSize(text, font, font_scale, thickness)
# 左下に配置(少し余白を持たせる)
position = (10, height - 10)
# 影付き描画
cv2.putText(image, text, (position[0]+2, position[1]+2), font, font_scale, shadow_color, thickness, cv2.LINE_AA)
cv2.putText(image, text, position, font, font_scale, color, thickness, cv2.LINE_AA)
return image
▼この記事を書いたひと

R&Dセンター 松井 良行
R&Dセンター 技術戦略担当部長。コンピュータと共に35年。そしてこれからも!
●富士見事務所 TEL : 052-228-8733 FAX : 052-323-3337
〒460-0014 愛知県名古屋市中区富士見町13−22 ファミール富士見711 地図
交通部 R&Dセンター


